UNPKG

@dudousxd/nestjs-telescope

Version:

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

157 lines 6.16 kB
// packages/core/src/profiling/profiler.service.ts import { EntryType } from '../entry/entry.js'; import { CpuProfiler } from './cpu-profiler.js'; /** The `type` of the entries this service records. */ export const CPU_PROFILE_ENTRY_TYPE = EntryType.CpuProfile; /** * Decides whether to profile a request and drives the capture lifecycle around * it. The integration calls {@link shouldProfile} (cheap gate), then * {@link begin}/{@link end} around the request body. * * OVERHEAD: while disabled, {@link shouldProfile} returns false and {@link begin} * returns null after a single boolean check — no profiler is ever constructed * and `node:inspector` is never required. While enabled but a request isn't * selected, the only added cost is the gate and (if sampling) one RNG call. */ export class ProfilerService { config; deps; active = 0; manual = []; random; profilerFactory; constructor(config, deps) { this.config = config; this.deps = deps; this.random = deps.random ?? Math.random; this.profilerFactory = deps.profilerFactory ?? (() => new CpuProfiler(undefined, { samplingIntervalMicros: config.samplingIntervalMicros })); } /** * Arm a manual capture for the next `count` requests (optionally only those * matching `label`). Returns the pending budget. No-op when disabled. */ arm(arm) { if (!this.config.enabled) return { pendingManual: 0 }; const count = Math.max(1, Math.floor(arm.count)); this.manual.push({ count, ...(arm.label ? { label: arm.label } : {}) }); return { pendingManual: this.pendingManual() }; } /** * Whether this request should be profiled. Consults the manual arm budget * first (targeted captures win), then sampling. Cheap and side-effect-free * EXCEPT it does not yet consume the manual budget — that happens in * {@link begin} so a request rejected by the concurrency cap doesn't waste it. */ shouldProfile(label) { if (!this.config.enabled) return false; if (this.matchingArm(label) !== null) return true; if (this.config.sampleRate <= 0) return false; return this.random() < this.config.sampleRate; } /** * Begin a capture for a request that {@link shouldProfile} accepted. Returns a * handle to pass to {@link end}, or `null` if the request should not be * profiled (disabled, concurrency cap reached, or not selected). The caller * MUST treat a null handle as "no profiling" and proceed untouched. */ begin(label) { if (!this.config.enabled) return null; const arm = this.matchingArm(label); const reason = arm !== null ? 'manual' : 'sampled'; if (reason === 'sampled') { if (this.config.sampleRate <= 0 || this.random() >= this.config.sampleRate) return null; } // Respect the concurrency cap BEFORE consuming the manual budget so a capped // request can be retried by a later arm. if (this.active >= this.config.maxConcurrent) return null; if (arm !== null) { arm.count -= 1; if (arm.count <= 0) this.manual.splice(this.manual.indexOf(arm), 1); } const profiler = this.profilerFactory(); this.active += 1; // Start asynchronously; the handle is valid immediately. A start failure // simply yields an empty/partial profile at stop — never throws into the host. // The promise is retained on the handle so `end` can await it before stopping. const started = profiler.start().catch(() => { }); return { profiler, startedAt: now(), reason, started }; } /** * Stop a capture and record it as a `cpu_profile` entry. Safe to call with a * null handle (no-op). Never throws into the host request path. Captures below * the `minDurationMs` floor are discarded. */ async end(handle, label) { if (handle === null) return; try { // Ensure `start()` settled so `stop()` sees a running profiler. (Real // requests run long enough that start is done; this guards the fast path.) await handle.started; const result = await handle.profiler.stop(); if (result === null) return; if (result.durationMs < this.config.minDurationMs) return; const content = { durationMs: result.durationMs, sampleCount: result.sampleCount, reason: handle.reason, label: label ?? null, tree: result.tree, hot: result.hot, }; this.deps.record({ type: CPU_PROFILE_ENTRY_TYPE, content, familyHash: label ?? null, tags: ['profile', handle.reason], durationMs: result.durationMs, }); } catch { // A failed capture must never affect the request it wrapped. } finally { this.active = Math.max(0, this.active - 1); } } status() { return { enabled: this.config.enabled, sampleRate: this.config.sampleRate, active: this.active, maxConcurrent: this.config.maxConcurrent, pendingManual: this.pendingManual(), }; } pendingManual() { return this.manual.reduce((sum, a) => sum + a.count, 0); } /** First arm matching this label (labelled arms require an exact match). */ matchingArm(label) { for (const arm of this.manual) { if (arm.count <= 0) continue; if (arm.label === undefined) return arm; if (label !== null && arm.label === label) return arm; } return null; } } function now() { return Date.now(); } //# sourceMappingURL=profiler.service.js.map