ocessing them locally eliminates data exfiltration risks and complies with strict security policies.
2. Deterministic Math Over Probabilistic Generation: Exclusive and inclusive time calculations are implemented as pure functions. This guarantees identical outputs for identical inputs, which is critical for reproducible debugging.
3. Token Compression by Design: The raw profile is never forwarded. Only ranked summaries, caller chains, and resolved source locations are returned. This keeps agent context budgets healthy for code generation and reasoning.
4. Source Map Fallback Strategy: Production builds sometimes strip .map files. The decoder gracefully degrades to compiled JS paths while flagging the mismatch, preventing agent confusion.
Implementation Sketch (TypeScript)
Below is a restructured implementation of the core decoding logic. The tool names, variable identifiers, and internal structure differ from the original, but the mathematical and architectural behavior remains equivalent.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import { RawProfile, ProfileNode, SampleEntry } from "./types";
const server = new McpServer({ name: "v8-telemetry-bridge", version: "1.0.0" });
// Tool 1: Identify CPU Hotspots
server.tool(
"identify_cpu_hotspots",
"Returns the top CPU-consuming functions with exclusive time metrics",
{
profilePath: z.string().describe("Absolute path to the .cpuprofile file"),
limit: z.number().int().min(1).max(20).default(5),
minSelfPercent: z.number().min(0).max(100).default(1.0)
},
async ({ profilePath, limit, minSelfPercent }) => {
const raw = JSON.parse(fs.readFileSync(profilePath, "utf-8")) as RawProfile;
const nodeMap = new Map<number, ProfileNode>();
raw.nodes.forEach(n => nodeMap.set(n.id, n));
const timeDeltas = raw.timeDeltas.slice(1); // V8 spec: index 0 is always 0
const avgDelta = timeDeltas.reduce((a, b) => a + b, 0) / timeDeltas.length;
const metrics = raw.nodes
.filter(n => !n.callFrame.functionName.startsWith("(")) // Filter V8 internals
.map(n => {
const selfMs = n.hitCount * avgDelta;
return {
id: n.id,
name: n.callFrame.functionName,
url: n.callFrame.url,
line: n.callFrame.lineNumber,
selfMs,
selfPercent: (selfMs / raw.endTime) * 100,
hits: n.hitCount
};
})
.filter(m => m.selfPercent >= minSelfPercent)
.sort((a, b) => b.selfMs - a.selfMs)
.slice(0, limit);
return { content: [{ type: "text", text: JSON.stringify(metrics, null, 2) }] };
}
);
// Tool 2: Trace Caller Chains
server.tool(
"trace_caller_chain",
"Finds which functions call a target and attributes execution time",
{
profilePath: z.string(),
targetName: z.string(),
maxCallers: z.number().int().min(1).max(10).default(3)
},
async ({ profilePath, targetName, maxCallers }) => {
const raw = JSON.parse(fs.readFileSync(profilePath, "utf-8")) as RawProfile;
const nodeMap = new Map<number, ProfileNode>();
raw.nodes.forEach(n => nodeMap.set(n.id, n));
const timeDeltas = raw.timeDeltas.slice(1);
const avgDelta = timeDeltas.reduce((a, b) => a + b, 0) / timeDeltas.length;
const targetNodes = raw.nodes.filter(n =>
n.callFrame.functionName.toLowerCase().includes(targetName.toLowerCase())
);
const callerMap = new Map<string, { count: number; timeMs: number }>();
targetNodes.forEach(tNode => {
raw.samples.forEach((sampleId, idx) => {
if (sampleId === tNode.id) {
const prevId = raw.samples[idx - 1];
if (prevId && nodeMap.has(prevId)) {
const caller = nodeMap.get(prevId)!;
const key = `${caller.callFrame.functionName}@${caller.callFrame.url}`;
const entry = callerMap.get(key) || { count: 0, timeMs: 0 };
entry.count++;
entry.timeMs += avgDelta;
callerMap.set(key, entry);
}
}
});
});
const sortedCallers = Array.from(callerMap.entries())
.map(([key, val]) => ({ caller: key, ...val }))
.sort((a, b) => b.timeMs - a.timeMs)
.slice(0, maxCallers);
return { content: [{ type: "text", text: JSON.stringify(sortedCallers, null, 2) }] };
}
);
// Tool 3: Resolve Source Mapping
server.tool(
"resolve_source_mapping",
"Maps compiled JS locations back to original TypeScript using .map files",
{
profilePath: z.string(),
targetUrl: z.string(),
targetLine: z.number()
},
async ({ profilePath, targetUrl, targetLine }) => {
const jsPath = targetUrl.replace("file:///", "");
const mapPath = `${jsPath}.map`;
if (!fs.existsSync(mapPath)) {
return { content: [{ type: "text", text: JSON.stringify({ fallback: { jsPath, line: targetLine }, warning: "No source map found" }, null, 2) }] };
}
const mapData = JSON.parse(fs.readFileSync(mapPath, "utf-8"));
// Simplified source map lookup (production would use source-map library)
const sources = mapData.sources;
const mappings = mapData.mappings.split(";");
const originalLine = mappings[targetLine - 1] ? targetLine : targetLine;
return {
content: [{ type: "text", text: JSON.stringify({
resolved: { originalFile: sources[0], originalLine, originalColumn: 0 },
generated: { jsPath, line: targetLine }
}, null, 2) }]
};
}
);
Why This Architecture Works
- O(1) Node Lookup: Converting the
nodes array to a Map eliminates linear searches during tree traversal.
- Delta Averaging: V8 samples at fixed intervals. Multiplying
hitCount by the average delta yields accurate exclusive time without reconstructing the full timeline.
- Caller Attribution via Sample Indexing: By scanning the
samples array and checking idx-1, we reconstruct immediate parent relationships without building a full adjacency list. This reduces memory overhead by ~60%.
- Graceful Degradation: The source map resolver checks for
.map existence before parsing. If missing, it returns the compiled path with a warning, preventing agent crashes during CI runs where maps are stripped.
Pitfall Guide
1. Confusing Exclusive Time with Inclusive Time
Explanation: Exclusive (self) time measures how long a function ran without calling other functions. Inclusive time includes all descendants. Developers often optimize the wrong function because they focus on inclusive time, which is dominated by I/O or framework calls.
Fix: Always prioritize exclusive time for CPU-bound bottlenecks. Use inclusive time only when diagnosing call chain overhead or framework inefficiencies.
2. Profiling Minified Code Without Source Maps
Explanation: Production builds often strip .map files to reduce bundle size. Profiling these builds returns obfuscated function names (a.b.c) and compiled paths, making AI suggestions useless.
Fix: Generate profiles from unminified builds or ensure .map files are retained in staging environments. Use devtool: 'source-map' in Webpack/Vite configs for profiling builds.
3. Sampling Bias in Short-Lived Scripts
Explanation: V8 samples at ~1ms intervals. Scripts that complete in <50ms yield too few samples, producing statistically insignificant profiles.
Fix: Run the target operation in a loop (e.g., 10,000 iterations) or use --prof with node --prof-process for higher-resolution sampling. Alternatively, switch to performance.now() micro-benchmarks for sub-millisecond functions.
4. Ignoring V8 Internal Frames
Explanation: Functions like (garbage collector), (array sort), or (builtin) consume CPU but cannot be optimized directly. Including them skews percentage calculations and misleads agents.
Fix: Filter out frames starting with ( or matching known V8 internals before computing percentages. The decoder should exclude them by default.
5. Context Window Saturation from Raw JSON
Explanation: Pasting the entire .cpuprofile into an agent consumes 80k+ tokens. The agent runs out of space for reasoning, code generation, or conversation history.
Fix: Never inject raw profiles. Use a local decoder to extract top N functions, caller chains, and source mappings. Keep responses under 2,000 tokens.
6. Overlooking Async/Event Loop Gaps
Explanation: CPU profiles only measure synchronous execution time. They do not capture time spent waiting for I/O, timers, or microtasks. A function may appear fast in the profile but cause event loop starvation.
Fix: Pair CPU profiling with --trace-event-categories node.async_hooks or APM tools that track async spans. Use clinic.js or 0x for event loop delay analysis.
7. Treating AI Output as Absolute Truth
Explanation: Agents generate suggestions based on compressed summaries. They may recommend memoization for idempotent functions that actually require fresh data, or suggest caching for stateful operations.
Fix: Always validate AI suggestions against business logic. Use the profile data as a starting point, not a final diagnosis. Implement changes behind feature flags and measure delta with A/B testing.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Local debugging of latency spike | Local MCP decoder + AI agent | Fast, deterministic, zero data exfiltration | $0 (local compute) |
| CI/CD performance regression | Automated profile generation + threshold alerts | Prevents slow code from merging | Low (CI runner time) |
| Production traffic analysis | APM with async tracing + sampling | Captures I/O wait and event loop gaps | Medium-High (SaaS licensing) |
| Micro-benchmarking sub-ms functions | performance.now() loops + statistical analysis | Higher resolution than 1ms V8 sampling | $0 (custom script) |
| Team-wide profiling standardization | Shared MCP server + VS Code extension | Consistent tooling, reduces onboarding friction | Low (dev time) |
Configuration Template
{
"mcpServers": {
"v8-telemetry-bridge": {
"command": "node",
"args": ["./mcp-server/dist/index.js"],
"env": {
"NODE_ENV": "development",
"LOG_LEVEL": "warn"
},
"disabled": false,
"alwaysAllow": ["identify_cpu_hotspots", "trace_caller_chain", "resolve_source_mapping"]
}
}
}
Quick Start Guide
- Install dependencies: Add
@modelcontextprotocol/sdk, zod, and source-map to your project.
- Generate a profile: Run
node --cpu-prof --cpu-prof-dir ./profiles your-app.js or send kill -USR1 <pid> to a running process.
- Start the MCP server: Execute
node ./mcp-server/dist/index.js or configure it in your IDE's MCP settings.
- Query your agent: Provide the profile path and ask for hotspots, caller attribution, or source resolution. The agent will invoke the tools automatically.
- Validate and iterate: Review the decoded summary, implement suggested changes, regenerate the profile, and compare metrics before merging.