Skip to content

perf(call-graph): bound AST residency in the solve — raw call index + per-program release #141

Description

@rahlk

Problem

Two places in the call-graph phase hold more of the AST than the work requires, and both are
measurable on vscode/src (6,758 files, one program).

1. indexCallExpressions materialises every AST node. It traverses with ts-morph's
forEachDescendant, which wraps each visited node in a JS object and caches it on the
SourceFile for the lifetime of the program. Indexing call sites therefore keeps the entire AST
of every file resident, when only the ~471k call-like nodes are ever used. Measured: +5.09 GB
for the index alone.

2. Every program stays materialised through the whole solve. #137 released programs but ran
the entire solve first, so all 92 of vscode's programs were live before disposal began; #127
interleaved but never released, so they stayed live too. Neither bounded anything on its own —
both measured ~30.6 GB on vscode and were killed. vscode/src is ONE program and completes;
the whole repo is 92 and does not.

Scope boundary

This is #112 Step 2 ("bound the resident program set") plus the indexing half of ceiling 1.

Not in scope: #112 Step 3 (incremental reuse) or Step 4 (sharding). Explicitly not a fix for
whole-vscode -a 4 — see Caveats.

Goals

  • Walk the raw compiler AST in indexCallExpressions, wrapping only the matches
  • Interleave solve → extract → release per programdropped, measured at +0.90 GB (see below)
  • Differential test pinning the raw walk against the wrapper walk it replaces
  • Byte-identical analysis.json on the fixture apps at -a 4 with every graph selector

Caveats and known risks

This does not make whole-vscode -a 4 fit, and the issue should not be read as claiming it
does.
Measured on vscode/src at -a 4, RSS is already 21.33 GB when the interprocedural
phase begins
, and the entire retained state of that phase is 0.48 GB:

structure size
datas (126,236 callables, 991k CFG nodes, 1.3M CFG edges) 0.34 GB
ddg (1,691,626 edges) 0.09 GB
summaries (126,236 entries) 0.05 GB

The remaining ~21 GB is tsc's own parse/bind/check state, which the analyzer does not own. That
rules out restructuring L4 — including per-program or chunked L4, which would additionally lose
every cross-program summary edge and so silently degrade the level rather than chunk it.

Current status of the ceiling, for the record:

run result
whole vscode -a 3 completes — 19m41s, 28.4 GB, 1.41 GB output
whole vscode -a 4 killed at 28.0-31.7 GB across six configurations
vscode/src -a 4 completes — 11m48s, 27.0 GB

Reaching whole-vscode -a 4 needs #112 Step 3 or 4, not further trimming of this phase.

Risk on the raw walk: it must key call sites identically to the wrapper walk, or edge provenance
shifts. Pinned by a differential test that was mutation-checked (dropping tagged templates, and
dropping the recursion, both fail it).

Risk on the interleave: extraction must be the last reader of a program before release. The root
program is deliberately spared, because finalizeAnalysis still needs it for the config-use
dataflow tier.

Definition of done

  • bun test green, including the new differential test
  • analysis.json byte-identical at -a 4 (all graph selectors) on dataflow-app,
    multi-tsconfig-app and sample-app
  • vscode/src -a 4 still completes, at a peak no worse than an origin/main run measured
    under identical conditions in the same session (do not carry a number over from an
    earlier run: the 27.0 GB target originally written here came from a run whose invocation
    was not recorded and does not reproduce — origin/main itself measures 29.69 GB here)
  • The caveat above is carried into Scaling L1-L4 past vscode: three memory ceilings and a sequenced path #112 so the remaining ceiling is not mis-attributed

Measured outcome (correction)

End-to-end A/B on vscode/src -a 4, same machine and flags:

build peak RSS wall
origin/main 29.69 GB 9m37s
#142 29.65 GB 10m26s

The phase-level reductions do not move peak RSS. They are real where measured (call-site
index +3.23 GB to +2.52 GB) but the peak is set later in the run, which the 21.33 GB
pre-wavefront floor above already implied. #142 is in draft pending per-commit attribution.

Per-commit attribution

build peak RSS
origin/main 29.69 GB
raw walk only 28.75 GB
raw walk + interleave 29.65 GB

The raw walk is worth 0.94 GB. The interleave gives 0.90 GB of it back — releasing source
files does not free the tsc state that dominates, and extracting per program gives up the
batched path's sharing. #142 carries the raw walk only; the interleave stays on
perf/interleave-dispose unmerged.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions