Skip to content

fix: TypeScript-at-scale indexing cascade + parallel-only crash recovery#930

Merged
DeusData merged 1 commit into
mainfrom
fix/ts-scale-parallel
Jul 7, 2026
Merged

fix: TypeScript-at-scale indexing cascade + parallel-only crash recovery#930
DeusData merged 1 commit into
mainfrom
fix/ts-scale-parallel

Conversation

@DeusData

@DeusData DeusData commented Jul 7, 2026

Copy link
Copy Markdown
Owner

An 81k-file TypeScript corpus previously never finished indexing (single core pinned for hours, four innocent files quarantined by the crash supervisor along the way). Root-caused via stack sampling and the supervisor's own records; three stacked fixes plus one hardening:

  • Linear child traversal in walk_defs — index-based ts_node_child(i) is O(i) per call, quadratic on wide nodes (580k-sibling file ≈ 1.7e11 steps). TSTreeCursor collection at all six descent sites; 400k-sibling guard test bounded at 30s.
  • One cross-LSP dispatch for both pipelines — the sequential driver rebuilt a full registry per file (O(files×defs)); it now shares the parallel worker's dispatch (module filter → shared registries → filtered fallback), with the registry arena owned by the pipeline driver (fixes an ASan use-after-free in pass_calls) and rust exempt from the def filter (Cargo cross-crate resolution needs other-crate defs; the starvation predated this change on the parallel path — Cross-crate call graphs stop at boundaries #56 repro guards it).
  • Parallel-only crash recovery — recovery re-runs are no longer single-threaded; the marker is an append journal written by extraction and both resolve drivers, suspects are the in-flight set, and quarantine requires recurrence across two consecutive failed runs (one file per round, oldest open S first). Innocents can no longer be quarantined by a stale marker. Guard asserts zero single-threaded spawns (verified RED against the old loop).
  • Dynamic type-parse budget — parse_ts_type_text work bounded proportional to input size (env-overridable), warn-once + graceful degrade.

Measured: microsoft/TypeScript 81,398 files — infinite → 33s, 295k nodes / 779k edges, 0 dangling, 0 skipped. Full 9-language sweep (incl. linux kernel: 390s / 8.5M nodes / 0 dangling) green with no quality flags. Local gates: full suite 5925/0, MT determinism guard byte-identical, lint-ci clean.

…overy

An 81k-file TypeScript corpus previously never finished indexing. Three
stacked defects, found by stack-sampling the live grind and reading the
supervisor's quarantine records:

1) walk_defs pushed children via index-based ts_node_child(i), which is
   O(i) per call in tree-sitter — O(n^2) per wide node. A 3.5 MB file
   with ~580k flat comment siblings needed ~1.7e11 iterator steps and
   hung extraction past the supervisor's quiet-timeout. All six descent
   sites (including the default loop) now collect children linearly with
   a TSTreeCursor (wd_collect_children); small nodes keep the direct
   index path. Guard: extract_wide_flat_file_is_linear (400k comment
   siblings — the monster's exact shape — bounded at 30s).

2) The sequential cross-LSP driver handed the FULL def list to a full
   per-file registry build+finalize — O(files x defs); 74% of samples
   sat in build_qn_index. The parallel resolve worker's dispatch
   (module-def filter -> shared prebuilt registries -> filtered
   fallback) is now extracted into cbm_pxc_dispatch_file and drives
   BOTH pipelines — one path, one semantics. The shared registries
   live in a caller-owned arena that outlives the calls pass
   (resolved_calls borrow registry strings; freeing at pass end was a
   use-after-free ASan caught in pass_calls). Rust is exempt from the
   def filter: Cargo-manifest cross-crate resolution needs defs from
   OTHER workspace crates that the own+imports filter drops (this
   starvation predated the unification on the parallel path). Guard:
   pipeline_seq_ts_cross_uses_shared_registry (full builds 40 -> 0,
   cross-file edge preserved).

3) The crash supervisor's recovery re-ran the worker SINGLE-THREADED to
   keep one exact marker. At scale that fell into the sequential crawl,
   was killed as a hang mid-pass, and the stale extraction marker got
   four innocent files quarantined, one 15-minute retry at a time.
   Recovery (and the terminal partial run) now re-run PARALLEL; the
   marker is an append journal ("S <rel>" / "D <rel>", written by
   extraction and both resolve drivers), the suspect set is the open-S
   in-flight set, and a file is quarantined only when it recurs across
   two consecutive failed runs — one per round, oldest open S first;
   disjoint consecutive sets stop the loop rather than blame an
   innocent. Guard: index_recovery_parallel_quarantines_crasher
   (single-threaded spawn count must stay zero; verified RED against
   the old loop).

Also: parse_ts_type_text gains a dynamic work budget proportional to
the input (1M + 64x source bytes, CBM_TS_TYPE_BUDGET override, warn
once then degrade to unknown) replacing unbounded recursion cost.

Measured on microsoft/TypeScript (81,398 files): infinite -> 33s cold
full index, 295k nodes / 779k edges, zero dangling edges, zero files
skipped — the monster file itself now indexes cleanly. Full 9-language
sweep green with no quality flags; kernel unchanged at 390s / 8.5M
nodes / 0 dangling.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData merged commit 63ec1a7 into main Jul 7, 2026
11 of 12 checks passed
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.

1 participant