UNPKG

@dudousxd/nestjs-telescope

Version:

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

124 lines 4.91 kB
/** How many hottest-by-self frames to precompute for the dashboard. */ const HOT_FRAME_LIMIT = 12; /** The synthetic root name V8 uses; we keep/normalize to it. */ const ROOT_NAME = '(root)'; /** * Aggregates a raw V8 CPU profile into a flamegraph frame tree with self/total * time in milliseconds. Pure and deterministic — no inspector dependency, so it * is trivially unit-testable and runs OUTSIDE any hot path (only when a capture * actually completes). * * Method: each sample's `timeDelta` (microseconds) is charged to the SELF time * of the node it landed on. Total time is then the sum of a node's self time and * its descendants', computed by a single post-order walk. The flame tree mirrors * V8's `children` call-graph so callers nest exactly as they did at runtime. */ export function aggregateCpuProfile(profile) { const byId = new Map(); for (const node of profile.nodes) byId.set(node.id, node); // 1. Charge each sample's time delta to its node's self time (microseconds). const selfMicros = new Map(); const selfSamples = new Map(); const samples = profile.samples ?? []; const deltas = profile.timeDeltas ?? []; let totalMicros = 0; for (let i = 0; i < samples.length; i++) { const id = samples[i]; if (id === undefined) continue; // V8 emits one delta per sample (the time elapsed before that sample). Fall // back to 0 if the arrays are mismatched rather than guessing. const delta = deltas[i] ?? 0; totalMicros += delta; selfMicros.set(id, (selfMicros.get(id) ?? 0) + delta); selfSamples.set(id, (selfSamples.get(id) ?? 0) + 1); } // 2. Find (or synthesize) the root. V8 puts the (root) node first; tolerate // its absence by stitching all top-level nodes under a synthetic root. const rootNode = profile.nodes.find((n) => n.callFrame.functionName === ROOT_NAME); const build = (node) => { const childIds = node.children ?? []; const children = []; for (const childId of childIds) { const child = byId.get(childId); if (child) children.push(build(child)); } const self = (selfMicros.get(node.id) ?? 0) / 1000; const selfCount = selfSamples.get(node.id) ?? 0; const childTotal = children.reduce((sum, c) => sum + c.totalMs, 0); const childSamples = children.reduce((sum, c) => sum + c.totalSamples, 0); return { name: normalizeName(node.callFrame.functionName), file: frameFile(node), selfMs: self, totalMs: self + childTotal, totalSamples: selfCount + childSamples, children, }; }; let tree; if (rootNode) { tree = build(rootNode); } else { // No explicit root: every node referenced as a child belongs to a parent; // top-level nodes are those never referenced. Stitch them under a synthetic root. const referenced = new Set(); for (const node of profile.nodes) { for (const childId of node.children ?? []) referenced.add(childId); } const tops = profile.nodes.filter((n) => !referenced.has(n.id)).map(build); const childTotal = tops.reduce((sum, c) => sum + c.totalMs, 0); tree = { name: ROOT_NAME, file: '', selfMs: 0, totalMs: childTotal, totalSamples: tops.reduce((sum, c) => sum + c.totalSamples, 0), children: tops, }; } // 3. Hot frames by self time (flatten, sort desc, take N). const totalMs = totalMicros / 1000; const flat = []; const visit = (node) => { if (node.selfMs > 0) { flat.push({ name: node.name, file: node.file, selfMs: node.selfMs, selfPct: totalMs > 0 ? (node.selfMs / totalMs) * 100 : 0, }); } for (const child of node.children) visit(child); }; visit(tree); flat.sort((a, b) => b.selfMs - a.selfMs); return { tree, durationMs: durationFor(profile, totalMicros), sampleCount: samples.length, hot: flat.slice(0, HOT_FRAME_LIMIT), }; } /** Prefer the summed sample deltas; fall back to start/end when samples absent. */ function durationFor(profile, totalMicros) { if (totalMicros > 0) return totalMicros / 1000; const wall = profile.endTime - profile.startTime; return wall > 0 ? wall / 1000 : 0; } function normalizeName(name) { return name === '' ? '(anonymous)' : name; } function frameFile(node) { const { url, lineNumber } = node.callFrame; if (url === '') return ''; return lineNumber >= 0 ? `${url}:${lineNumber + 1}` : url; } //# sourceMappingURL=aggregate-profile.js.map