monitor.line_heatmap.v1
Status: Implemented (E3.1,
profiler.BuildHeatmap, theheat.Buildthis document originally drafted; E3.2 added live<pid|service>resolution on top of the same document).monitor hotserves this schema from--file,<pid>, and<service>alike. The fullmonitor hot <pid|service|--file>command shape is specified by the internal naming ADR (see the repo's AGENTS.md), not this document.
Why this exists
Monitor already captures per-line data in two profilers — V8's positionTicks (Node/Deno CPU profiles) and the github.com/google/pprof proto (Go CPU/heap/goroutine) — but today's CLI only ever prints function-level, sampled-symbol summaries; the per-line detail is discarded after being read. heat.Build combines both sources into one line-level model instead of adding a second, format-specific renderer per profiler:
FromCDP: builds the model from a V8/Bun.cpuprofile'spositionTicks. Call-path variants of the same (function, file, line) are merged;(idle),(program), and(garbage collector)pseudo-frames are excluded from the percentage denominator and reported separately asidle_pct/gc_pctinstead of diluting real lines.FromPprof: builds the model from a parsed pprof proto (cpu,heap, orgoroutine), rolling upflat/cumper (function, line) directly from the proto instead of shelling out togo tool pprof.
A function's line range comes from codemap symbol-at, when codemap is healthy; range_source: "observed" marks a range inferred from the sample data itself when codemap is unavailable (or, for a function with zero attributed lines — a pure ancestor whose only signal is its callees — falls back to its own generated declaration line as both start and end), so a heatmap can still render with an honest caveat instead of failing outright.
FromCDP also excludes a JS runtime's own bootstrap/module-loader frames (Node's node:internal/... built-ins, and the analogous ext:/deno: schemes Deno's isolate uses) from functions entirely, rather than listing them as zero-self wrapper rows: every sample's stack passes through a runtime's own module-loading machinery, so including it would bury real user functions under a wall of (anonymous) bootstrap wrappers. Their real CPU time still counts toward active_samples — it's excluded from the function list, not from the honesty accounting.
A location-less native builtin (no url, lineNumber: -1 — Bun's own JSON.stringify/String.prototype.repeat and similar) is excluded from functions the same way, for the same reason there's no real source line to report: it would otherwise render as a fabricated "line 0 of an empty file". Unlike the runtime-bootstrap case, though, its real cost is significant enough to be worth surfacing — it still appears in its caller's own callees, by name, with its real cumulative weight.
Shape
{
"schema": "monitor.line_heatmap.v1",
"profile_type": "cpu", // "cpu" | "heap_inuse" | "heap_alloc" | "goroutine"
"unit": "samples", // "samples" (CDP) | "nanoseconds" (pprof cpu — see the note below) | "bytes" (pprof heap) | "count" (pprof goroutine, or an unrecognized column)
"method": "v8_position_ticks", // "v8_position_ticks" | "pprof_proto" | "cpuprofile_file" | "darwin_sample"
"runtime": "unknown", // "node" | "deno" | "bun" | "go" | "python" | "ruby" | "unknown"
"samples": 12000,
"active_samples": 11840, // excludes (idle)/(program)/(GC) pseudo-frames
"idle_pct": 1.3,
"gc_pct": 4.1,
"functions": [
{
"name": "heavyStringify",
"file": "examples/polyglot/js/workload.js",
"start_line": 11,
"end_line": 22,
"range_source": "codemap", // "codemap" | "observed"
"self_pct": 71.2,
"cum_pct": 88.0,
"lines": [
{
"line": 17, // the dogfood hot line: 99.9% of heavyStringify's own ticks land here, not on the line-11 declaration
"self": 8120,
"cum": 8120,
"pct_of_function": 91.4, // this line's share of its OWN function's weight
"code": " s += JSON.stringify({ i, item, doubled, pad: 'x'.repeat(64) });",
"mapping": "exact", // "exact" | "ambiguous" | "transpiled" | "inferred" | ""
"stale": false, // true when the .map file is older than the generated file it maps by more than a small tolerance (internal/sourcemap's mtime check — not a content hash)
"issues": [
// additive, v1.17 E3.4: errors × heat cross-reference,
// read from the issues store, not computed here
{ "short_id": "A07E", "count": 3, "status": "open" }
]
}
],
"callees": [
{ "func": "JSON.stringify", "cum": 7600 }
] // sorted by cum descending, capped at 5
}
],
"warnings": [
"mostly idle: slowness is off-CPU", // idle_pct > 50
"diffuse: the hottest function (f) has only 3.2% of active samples; this profile may be too spread out to point at one line", // the top function's own self (or cum, for an all-wrapper profile) share is under 5%
"one-line/minified bundle (dist/app.js): V8 positionTicks carry no column; per-line attribution not possible, only ambiguous", // a generated line's mapped segments span more than one original line
"source map older than dist/app.js: resolved lines may be wrong" // the .map is older than the generated file it maps (a stale build)
],
"limitations": [
"V8 positionTicks are self-time; a callee's own time is listed under callees, not folded into the caller's line"
]
}Notes on specific fields
runtimedefaults to"unknown"for a file-loaded.cpuprofile(monitor hot --file): Node, Deno, and Bun all write the identical CDP wire shape, so it is genuinely not determinable from the file's content alone — degrading honestly to"unknown"rather than guessing"node". A live capture (monitor hot <pid|service>) sets it fromprocbind's own runtime detection."go"is hard-coded for a pprof-sourced heatmap: every pprof protomonitor hot --fileis actually handed, in this codebase, comes frommonitor profile/net/http/pprofcapturing a Go process — butLoadFileitself accepts any spec-shaped pprof proto, Go-produced or not, so this is an assumption about how the CLI is used, not a factheat.Buildcan verify from the proto's own bytes.samples/unitare measured in whateverunitsays, never literally "a count of samples" regardless of the field's name: a CDP source is a genuine sample count (unit: "samples"), but a pprof CPU source'ssamplesis a nanosecond total across the selected value column (unit: "nanoseconds") — the same figureactive_samplesandidle_pctare derived from when the proto carries a capture duration — a pprof heap source's is bytes, and goroutine's is a plain count.functionsis sorted bycum_pctdescending (thenself_pctdescending, thenname) and capped at 25 entries by default (monitor hot --top Noverrides the cap;--func NAMEfilters to one function by exact name instead). Sorting bycum_pct— notself_pct— matches a thin wrapper's cumulative total inheriting almost entirely from a callee it does no real work of its own on;monitor hot's own default CodeFrame target is still chosen by highestself_pct, not this sort order, since a wrapper's own line is never the interesting one to expand. That default-target pick, likewarnings' own "diffuse" check, is always computed from the FULL function list, before--func/--topfilter or capfunctions— so a small--topcan never silently swap in some other, cooler function's CodeFrame just because it truncated the real hot one out of the visible table.lines[].pct_of_functionis always relative to the function's own weight (self/cumroll-up within that function), not the profile total — this is what letsmonitor hot --file v8-hot.cpuprofilename "the hot line within the hot function" instead of only "the hot function".linesis always aggregated on ORIGINAL (source-mapped) coordinates, not generated ones: when a source map sends more than one GENERATED line to the same original line — common in a bundler's transpiled output —heat.Buildsums theirself/cuminto onelinesentry rather than emitting duplicate rows for the same original line. That entry'smappingis the LEAST certain of the group's own mappings (claiming the group's best-case certainty would overstate how precisely it was actually located), and itsstaleis the OR of the group's.mappingis the same enum as a stackFrame'sMappingfield (defined once and reused here rather than redeclared):exact(source map resolved this line precisely),ambiguous(multiple candidate mappings),transpiled(mapped through a build step without a source map),inferred(no source map at all; the location was guessed), or""when no source map applies (plain Go, or a.gofile profiled directly).heat.Builditself only ever producesexact,ambiguous, or""— the three outcomesinternal/sourcemap.Resolver.Resolveitself can return, given only a line (no column: V8positionTicks/pprof lines carry no column, soResolveis queried at the column of the generated line's first non-blank character rather than column 0, which several bundlers' output maps to the tail of the previous statement instead of the one actually on that line — verified live against a realbun build --sourcemap=externaloutput, seeinternal/profiler/testdata/tssrc).transpiled/inferreddescribe a guess made in the absence of a map entirely, which isstacktrace.Frame's business, notheat.Build's. Staleness is a separate boolean,lines[].stale, not a fifthmappingvalue: a profiled line can go stale after profiling (the file changed since), which is a signal a per-crash stackFramehas no equivalent for, so it does not belong inside the shared enum.lines[].issuesis additive (v1.17, E3.4) and always read from the issues store at render time —heat.Buildnever computes or caches issue membership itself, so a heatmap and the issues store never disagree about an issue's current status.warningsexists for exactly the honesty problem in the "which part of a function" reality check: an idle target (idle_pctover roughly 50%) gets an explicit"mostly idle"warning instead of silently pointing at whatever line happened to be running when the sampler fired, and a profile whose hottest function barely dominates (under roughly 5% of active samples, computed from the FULL function list — before--func/--topfilter or cap it — so neither flag can hide a real concentration or manufacture a false one) gets a"diffuse"warning instead of a confident-looking pick among many similarly-cold functions. A source-map warning is added per affected generated file (not per line) when a generated line's mapped segments span more than one original line — V8positionTickscarry no column, so this can't honestly be called an exact attribution — or when the.mapis older than the generated file it maps.
Compatibility
monitor profile --jsonis unaffected: it keeps its currenttextandsymbolsfields exactly as-is, becauseglyphrun'sprocmonstep savesprofile --json'stextverbatim. Onlyinvestigate --jsonand MCP omitprofile.textby default (opt back in with--include-raw/keep: true).monitor hotis a new command with no prior JSON shape to preserve, somonitor.line_heatmap.v1has no back-compat constraint of its own yet — but once it ships, later additive fields (such aslines[].issuesabove) must follow the same "new field with an explicit status, never a rename" rule asissue-context-v1.