@hirosystems/api-toolkit
Version:
API development toolkit
186 lines • 8.07 kB
JavaScript
;
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