UNPKG

@hirosystems/api-toolkit

Version:
186 lines 8.07 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildProfilerServer = buildProfilerServer; const events_1 = require("events"); const os = require("os"); const path = require("path"); const fs = require("fs"); const helpers_1 = require("../helpers"); const promises_1 = require("node:stream/promises"); const logger_1 = require("../logger"); const fastify_1 = require("fastify"); const type_provider_typebox_1 = require("@fastify/type-provider-typebox"); const inspector_util_1 = require("./inspector-util"); const DurationSchema = type_provider_typebox_1.Type.Number({ minimum: 0 }); const SamplingIntervalSchema = type_provider_typebox_1.Type.Optional(type_provider_typebox_1.Type.Number({ minimum: 0 })); const CpuProfiler = (fastify, options, done) => { let existingSession; fastify.get('/profile/cpu', { schema: { querystring: type_provider_typebox_1.Type.Object({ duration: DurationSchema, sampling_interval: SamplingIntervalSchema, }), }, }, async (req, res) => { if (existingSession) { await res.status(409).send({ error: 'Profile session already in progress' }); return; } const seconds = req.query.duration; const samplingInterval = req.query.sampling_interval; const cpuProfiler = (0, inspector_util_1.initCpuProfiling)(samplingInterval); existingSession = { instance: cpuProfiler, response: res }; try { const filename = `cpu_${Math.round(Date.now() / 1000)}_${seconds}-seconds.cpuprofile`; await cpuProfiler.start(); const ac = new AbortController(); const timeoutPromise = (0, helpers_1.timeout)(seconds * 1000, ac); await Promise.race([timeoutPromise, (0, events_1.once)(res.raw, 'close')]); if (res.raw.writableEnded || res.raw.destroyed) { // session was cancelled ac.abort(); return; } const result = await cpuProfiler.stop(); const resultString = JSON.stringify(result); logger_1.logger.info(`[CpuProfiler] Completed, total profile report JSON string length: ${resultString.length}`); await res .headers({ 'Cache-Control': 'no-store', 'Transfer-Encoding': 'chunked', 'Content-Disposition': `attachment; filename="${filename}"`, 'Content-Type': 'application/json; charset=utf-8', }) .send(resultString); } finally { const session = existingSession; existingSession = undefined; await session?.instance.dispose().catch(); } }); fastify.get('/profile/cpu/start', { schema: { querystring: type_provider_typebox_1.Type.Object({ sampling_interval: SamplingIntervalSchema, }), }, }, async (req, res) => { if (existingSession) { await res.status(409).send({ error: 'Profile session already in progress' }); return; } const samplingInterval = req.query.sampling_interval; const cpuProfiler = (0, inspector_util_1.initCpuProfiling)(samplingInterval); existingSession = { instance: cpuProfiler, response: res }; await cpuProfiler.start(); const profilerRunningLogger = setInterval(() => { if (existingSession) { logger_1.logger.error(`CPU profiler has been enabled for a long time`); } else { clearInterval(profilerRunningLogger); } }, 10_000).unref(); await res.send('CPU profiler started'); }); fastify.get('/profile/cpu/stop', async (req, res) => { if (!existingSession) { await res.status(409).send({ error: 'No profile session in progress' }); return; } if (existingSession.instance.sessionType !== 'cpu') { await res.status(409).send({ error: 'No CPU profile session in progress' }); return; } try { const elapsedSeconds = existingSession.instance.stopwatch.getElapsedSeconds(); const timestampSeconds = Math.round(Date.now() / 1000); const filename = `cpu_${timestampSeconds}_${elapsedSeconds}-seconds.cpuprofile`; const result = await existingSession.instance.stop(); const resultString = JSON.stringify(result); logger_1.logger.info(`[CpuProfiler] Completed, total profile report JSON string length: ${resultString.length}`); await res .headers({ 'Cache-Control': 'no-store', 'Transfer-Encoding': 'chunked', 'Content-Disposition': `attachment; filename="${filename}"`, 'Content-Type': 'application/json; charset=utf-8', }) .send(resultString); } finally { const session = existingSession; existingSession = undefined; await session?.instance.dispose().catch(); } }); fastify.get('/profile/heap_snapshot', async (req, res) => { if (existingSession) { await res.status(409).send({ error: 'Profile session already in progress' }); return; } const filename = `heap_${Math.round(Date.now() / 1000)}.heapsnapshot`; const tmpFile = path.join(os.tmpdir(), filename); const fileWriteStream = fs.createWriteStream(tmpFile); const heapProfiler = (0, inspector_util_1.initHeapSnapshot)(fileWriteStream); existingSession = { instance: heapProfiler, response: res }; try { // Taking a heap snapshot (with current implementation) is a one-shot process ran to get the // applications current heap memory usage, rather than something done over time. So start and // stop without waiting. await heapProfiler.start(); const result = await heapProfiler.stop(); logger_1.logger.info(`[HeapProfiler] Completed, total snapshot byte size: ${result.totalSnapshotByteSize}`); await (0, promises_1.pipeline)(fs.createReadStream(tmpFile), res.raw); await res.headers({ 'Cache-Control': 'no-store', 'Transfer-Encoding': 'chunked', 'Content-Disposition': `attachment; filename="${filename}"`, 'Content-Type': 'application/json; charset=utf-8', }); } finally { const session = existingSession; existingSession = undefined; await session?.instance.dispose().catch(); try { fileWriteStream.destroy(); } catch (_) { } try { logger_1.logger.info(`[HeapProfiler] Cleaning up tmp file ${tmpFile}`); fs.unlinkSync(tmpFile); } catch (_) { } } }); fastify.get('/profile/cancel', async (req, res) => { if (!existingSession) { await res.status(409).send({ error: 'No existing profile session is exists to cancel' }); return; } const session = existingSession; await session.instance.stop().catch(); await session.instance.dispose().catch(); await session.response.status(500).send('cancelled'); existingSession = undefined; await Promise.resolve(); await res.send({ ok: 'existing profile session stopped' }); }); done(); }; /** * Creates a Fastify server that controls a CPU profiler. * @returns Fastify instance */ async function buildProfilerServer() { const fastify = (0, fastify_1.default)({ trustProxy: true, logger: logger_1.PINO_LOGGER_CONFIG, }).withTypeProvider(); await fastify.register(CpuProfiler); return fastify; } //# sourceMappingURL=server.js.map