Skip to content

keep a v8.serialize sidecar next to ParsedFile and parse-cache JSON shards — JSON.parse + reviver is ~5× slower than v8.deserialize on the same object graph #3089

Description

@BecauseY

Area

gitnexus (CLI / core / indexing / MCP server)

Problem or opportunity

Both on-disk stores that feed scope-resolution round-trip through JSON with a custom reviver: ParsedFile shards (parsedfile-store.ts, makeInterningReviver — string interning plus per-shard def dedup) and parse-cache chunk results (parse-cache.ts, mapReviver for Scope.bindings / typeBindings). Every warm run pays the full deserialization cost again.

Measured on a ~6.3k-file Java-dominant monorepo (same setup as #3086):

  • ParsedFile store, ~480 MiB of JSON: JSON.parse + reviver 32.8 s vs v8.deserialize of the same object graph 6.35 s.
  • parse-cache, 17 worker-result shards, ~218 MiB: JSON.parse + mapReviver 9.36 s vs v8.deserialize 1.79 s.

End-to-end on our warm-path matrix (other local cache changes applied): removing the ParsedFile sidecar costs +27.4 s (63.3 s → 90.7 s); removing the parse-cache sidecar +4.7 s (63.3 s → 68.1 s). Graph output identical in both cases.

Proposed solution

Best-effort binary sidecar, JSON stays canonical:

  • After a shard's object graph exists (worker write for ParsedFiles; saveParseCache for chunk results), also write <shard>.json.v8 = v8.serialize(graph) with a small header carrying the Node major / process.versions.v8.
  • Readers try the sidecar first; on a missing file, header mismatch, size mismatch or any deserialization error they silently fall back to the JSON + reviver path (the current code, unchanged).
  • The durable ParsedFile restore copies sidecars together with the JSON; parse-cache save/prune carry them in lockstep with the surviving keys, so a sidecar can never outlive its JSON.
  • Because the interning reviver also dedups strings, the v8 path should serialize the revived graph (post-intern), so retained memory after load is the same as today.

Any failure in the binary path only loses speed; cache hits, content addressing and graph semantics are unaffected.

Alternatives considered

  • MessagePack / CBOR: adds a dependency and still needs custom Map/Set handling; v8 is built in and already understands Map/Set.
  • Replacing JSON entirely with v8: loses cross-Node-version compatibility and the human-readable recovery format; the sidecar keeps JSON as the source of truth.
  • Making the reviver cheaper: the interning is what keeps the heap bounded (gitnexus analyze linux kernel source JavaScript heap out of memory OOM. #1983), so the per-token cost is inherent to JSON.

Acceptance criteria

  • Byte-identical graph output with and without sidecars (we verified node/edge hashes on our matrix).
  • Warm run with sidecars present measurably faster on a large repo; sidecar missing/corrupt/mismatched → same result as today, at most a debug log line.
  • Sidecars pruned/copied exactly alongside their JSON; no orphan .v8 files after prune.

Constraints

  • Disk: roughly doubles the size of the two stores (the v8 payload is about the size of the JSON).
  • v8.serialize wire format is not stable across Node majors — the header check is mandatory, and a mismatch must be a silent fallback rather than an error.

Contribution

  • I am willing to open a PR for this (may need design discussion first).

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions