Skip to content

feat(diagnostics): real v8 heap snapshot from GC heap walk + inspector/repl honesty (#4916) - #4979

Merged
proggeramlug merged 1 commit into
mainfrom
fix/4916-diagnostics-honesty
Jun 11, 2026
Merged

feat(diagnostics): real v8 heap snapshot from GC heap walk + inspector/repl honesty (#4916)#4979
proggeramlug merged 1 commit into
mainfrom
fix/4916-diagnostics-honesty

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Closes #4916.

What

The diagnostics audit had one SILENT-LIE (empty-but-valid heap snapshots) and two PARTIALs (inspector, repl). This PR makes the snapshot real and makes the rest honest.

Real heap snapshot (gc/heap_snapshot.rs, new)

v8.getHeapSnapshot() / v8.writeHeapSnapshot() now emit an actual object graph built from Perry's GC heap walk, in Node v26 / V8 13.x snapshot format:

  • Nodes: every arena + malloc GC allocation on the calling thread after a forced full collection — real self_size, type mapped from GC_TYPE_* (objects/arrays/strings/closures/bigints/native), class instances named via the class registry, string nodes carry their content (truncated at 128 chars).
  • Edges: enumerated with the same visit_gc_rewrite_slots walk the marker traces, so the edge set is definitionally what the GC sees. Object inline fields resolve to named property edges through the keys array, array elements get element edges, weak slots get weak edges, everything else (captures, Map/Set side tables, overflow) is an internal edge.
  • Reachability: a synthetic (GC roots) node points at every zero-indegree node plus any pure cycles, so DevTools (which drops unreachable nodes) shows the whole dump.
  • Stub flags + first-call warnings on both APIs removed; stub_inventory.rs now pins them must-not-be-stub.

Validated by loading Perry-produced snapshots in Node and checking: 0 invalid edge targets, 100% root reachability, named property edges present, class-instance nodes present (object:Widget ×5), marker strings/keys in the string table. test-files/test_gap_v8_heap_snapshot_4916.ts byte-matches node --experimental-strip-types (v26.3).

Found & sidestepped: pre-existing gc() liveness bug (#4977)

While validating, live top-level locals vanished from dumps and read back as garbage with plain explicit gc() too — in the default auto stack-scan mode a full collect skips native-stack roots and reclaims live objects (const k={nested:{deep:"s"}}; gc(); corrupts k.nested.deep; fine under PERRY_CONSERVATIVE_STACK_SCAN=full or PERRY_GEN_GC=0). Filed as #4977 with the bisection. The snapshot's forced collection runs under the existing per-thread set_conservative_stack_scan_override(Full) so the diagnostics API cannot inherit the corruption.

Heap stats provenance (acceptance item 2)

getHeapStatistics / getHeapSpaceStatistics / getHeapCodeStatistics / GCProfiler.stop get manifest stub_notes that state exactly which fields are Perry-arena numbers, which are RSS, and which are fixed/zero — rendered into reference.md / perry.d.ts (regenerated here).

Inspector (acceptance item 3)

  • inspector.url() → always undefined. It used to fabricate ws://host:port/uuid for an endpoint that never existed.
  • open() no longer prints the fake Debugger listening on ws://… banner; first-call stub warn; keeps ERR_INSPECTOR_ALREADY_ACTIVATED/close() semantics.
  • waitForDebugger() stub-warns (it returns immediately — blocking would hang forever since nothing can attach).
  • Session.post(): the complete supported subset (Runtime.enable + canned Runtime.evaluate) is documented at run_command and in the manifest; unsupported methods keep the spec'd -32601.

REPL verified (acceptance item 4)

Confirmed the suspicion: repl.start() builds the server object, but nothing ever reads the input stream and .write() routes through eval_simple_expression (numeric literals, context lookups, one +). Flagged stub: true with a first-call warning — a real eval loop needs an embedded interpreter, out of scope for AOT Perry.

Tests

  • RUST_TEST_THREADS=1 cargo test --release -p perry-runtime — 1021/1022; the one failure (date::tests::test_full_year_setters_revive_invalid_date_only) fails identically on a clean origin/main tree (pre-existing, macOS-local).
  • perry-api-manifest (incl. updated stub_inventory drift guard) and perry-codegen manifest_consistency — green.
  • Gap tests test_gap_node_v8_3137plus, test_gap_v8_2_3680plus, test_gap_v8_heap_snapshot_4916 — byte-identical to node v26.3.
  • Docs regenerated via ./scripts/regen_api_docs.sh (api-docs-drift clean).

Per contributor convention: no version bump / changelog — maintainer folds metadata at merge.

…r/repl honesty (#4916)

Heap snapshot — the SILENT-LIE half of #4916:
- NEW crates/perry-runtime/src/gc/heap_snapshot.rs: builds a real
  V8-format .heapsnapshot (Node v26 / V8 13.x field layout) from the
  GC heap walk — every arena+malloc GC allocation on the calling
  thread, real self_size, real reference edges via the collector's own
  visit_gc_rewrite_slots enumeration. Property edges are named through
  the object keys array, array elements get element indices, class
  instances carry their registered class name, string nodes carry
  content. A synthetic "(GC roots)" node guarantees full reachability
  so DevTools shows everything.
- v8.getHeapSnapshot()/writeHeapSnapshot() now emit it; stub flags and
  first-call warnings removed.
- The forced full collection runs under a per-thread
  conservative-stack-scan override: the default auto mode reclaims
  live top-level locals on explicit full collects (pre-existing gc()
  bug, filed #4977) and a diagnostics API must not inherit that.

Heap stats provenance (manifest stub_notes → reference.md/.d.ts):
- getHeapStatistics/getHeapSpaceStatistics/getHeapCodeStatistics and
  GCProfiler.stop document exactly which fields are Perry-arena
  numbers, which are RSS, and which are fixed/zero.

Inspector honesty:
- inspector.url() always returns undefined — it used to fabricate a
  ws:// URL for an endpoint that never existed; open() no longer
  prints the fake "Debugger listening" banner and stub-warns on first
  call; waitForDebugger() stub-warns instead of silently pretending.
- Session.post()'s complete supported protocol subset (Runtime.enable
  + canned Runtime.evaluate) is documented in code and manifest;
  everything else still gets the spec'd -32601 error.

REPL verified per the acceptance checklist:
- repl.start() builds the REPLServer shape but never reads the input
  stream and .write() only evaluates numeric literals / a single '+';
  flagged stub:true with a first-call warning (real eval needs an
  embedded interpreter — Perry is AOT).

Tests: gc::heap_snapshot unit test; test_gap_v8_heap_snapshot_4916.ts
byte-matches node v26.3; existing v8 gap tests pass; stub_inventory
drift guard updated (#4916 now 10 documented entries, snapshots
pinned must-NOT-be-stub).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Diagnostics fakes: v8 heap snapshot is an empty-but-valid graph; inspector/repl sessions look real but aren't

1 participant