@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
92 lines • 3.52 kB
JavaScript
// packages/core/src/profiling/cpu-profiler.ts
import { createRequire } from 'node:module';
import { aggregateCpuProfile } from './aggregate-profile.js';
const DEFAULT_SAMPLING_INTERVAL_MICROS = 1000;
/**
* Wraps a V8 CPU profiling session over the inspector protocol. A single
* profiler instance captures ONE profile at a time (`start` then `stop`).
*
* OVERHEAD GUARANTEE: constructing a profiler does no work and creates no
* session. The inspector session is created (and `node:inspector` is required)
* only inside `start()`. When profiling is disabled the controller never
* constructs one, so there is provably zero inspector activity.
*/
export class CpuProfiler {
sessionFactory;
session = null;
running = false;
intervalMicros;
constructor(sessionFactory = defaultSessionFactory, options = {}) {
this.sessionFactory = sessionFactory;
this.intervalMicros = options.samplingIntervalMicros ?? DEFAULT_SAMPLING_INTERVAL_MICROS;
}
get isRunning() {
return this.running;
}
/** Begin a CPU profile. No-op if one is already running. */
async start() {
if (this.running)
return;
const session = this.sessionFactory();
session.connect();
this.session = session;
await this.postAsync(session, 'Profiler.enable');
await this.postAsync(session, 'Profiler.setSamplingInterval', {
interval: this.intervalMicros,
});
await this.postAsync(session, 'Profiler.start');
this.running = true;
}
/**
* Stop the profile and aggregate it into a flame tree. Returns `null` when no
* profile is running. Always disconnects the session, even on error.
*/
async stop() {
const session = this.session;
if (!this.running || session === null)
return null;
try {
const result = (await this.postAsync(session, 'Profiler.stop'));
return aggregateCpuProfile(result.profile);
}
finally {
this.running = false;
this.session = null;
try {
session.disconnect();
}
catch {
// A disconnect failure must never mask the captured profile or throw.
}
}
}
postAsync(session, method, params) {
return new Promise((resolve, reject) => {
const cb = (err, result) => {
if (err)
reject(err);
else
resolve(result);
};
if (params === undefined)
session.post(method, cb);
else
session.post(method, params, cb);
});
}
}
/**
* Lazily require `node:inspector` and return a fresh Session. Kept out of the
* module's import graph so that importing this file (or the whole package) does
* NOT load the inspector binding — that only happens the first time a capture
* starts, i.e. only when profiling is enabled AND a request is sampled.
*/
export function defaultSessionFactory() {
// `createRequire` performs a SYNCHRONOUS require of the built-in without it
// appearing in this module's static import graph, so loading the package never
// pulls in the inspector binding. Built only on first capture.
const requireFn = createRequire(import.meta.url);
const inspector = requireFn('node:inspector');
return new inspector.Session();
}
//# sourceMappingURL=cpu-profiler.js.map