feat(diagnostics): real v8 heap snapshot from GC heap walk + inspector/repl honesty (#4916) - #4979
Merged
Merged
Conversation
…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).
This was referenced Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:self_size, type mapped fromGC_TYPE_*(objects/arrays/strings/closures/bigints/native), class instances named via the class registry, string nodes carry their content (truncated at 128 chars).visit_gc_rewrite_slotswalk 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 aninternaledge.(GC roots)node points at every zero-indegree node plus any pure cycles, so DevTools (which drops unreachable nodes) shows the whole dump.stub_inventory.rsnow 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.tsbyte-matchesnode --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 defaultautostack-scan mode a full collect skips native-stack roots and reclaims live objects (const k={nested:{deep:"s"}}; gc();corruptsk.nested.deep; fine underPERRY_CONSERVATIVE_STACK_SCAN=fullorPERRY_GEN_GC=0). Filed as #4977 with the bisection. The snapshot's forced collection runs under the existing per-threadset_conservative_stack_scan_override(Full)so the diagnostics API cannot inherit the corruption.Heap stats provenance (acceptance item 2)
getHeapStatistics/getHeapSpaceStatistics/getHeapCodeStatistics/GCProfiler.stopget manifeststub_notes that state exactly which fields are Perry-arena numbers, which are RSS, and which are fixed/zero — rendered intoreference.md/perry.d.ts(regenerated here).Inspector (acceptance item 3)
inspector.url()→ alwaysundefined. It used to fabricatews://host:port/uuidfor an endpoint that never existed.open()no longer prints the fakeDebugger listening on ws://…banner; first-call stub warn; keepsERR_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+ cannedRuntime.evaluate) is documented atrun_commandand 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 througheval_simple_expression(numeric literals, context lookups, one+). Flaggedstub: truewith 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. updatedstub_inventorydrift guard) andperry-codegen manifest_consistency— green.test_gap_node_v8_3137plus,test_gap_v8_2_3680plus,test_gap_v8_heap_snapshot_4916— byte-identical to node v26.3../scripts/regen_api_docs.sh(api-docs-drift clean).Per contributor convention: no version bump / changelog — maintainer folds metadata at merge.