Skip to content

perf(call-graph): walk the raw AST when indexing call expressions - #142

Merged
rahlk merged 1 commit into
mainfrom
perf/issue-141-bound-ast-residency
Sep 3, 2026
Merged

perf(call-graph): walk the raw AST when indexing call expressions#142
rahlk merged 1 commit into
mainfrom
perf/issue-141-bound-ast-residency

Conversation

@rahlk

@rahlk rahlk commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closes #141.

indexCallExpressions traversed with ts-morph's forEachDescendant, which wraps every visited
node in a JS object and caches it on the SourceFile for the lifetime of the program. Indexing
call sites therefore kept the entire AST of every file resident, for the sake of the ~471k
call-like nodes it actually keeps. This recurses the raw compiler nodes and wraps only the matches.

Measured

End-to-end on vscode/src (6,758 files, 470,973 call sites), same machine, same flags
(--no-build -a 4 --graphs cfg,dfg,pdg,sdg):

build peak RSS
origin/main 29.69 GB
this branch 28.75 GB

−0.94 GB, about 3.2% of peak. The index phase itself goes from +3.23 GB to +2.52 GB.

Output is unchanged: analysis.json is byte-identical at -a 4 with --graphs cfg,dfg,pdg,sdg
on dataflow-app, sample-app and multi-tsconfig-app, and the vscode/src run above produces
the same 6,758 modules and 1,053,108 call-graph edges as origin/main.

A differential test pins the raw walk against the wrapper walk it replaces. It was mutation-checked:
dropping tagged templates from the kind filter, and dropping the recursion, each make it fail.

Scope, and what was dropped

This PR originally also carried a per-program "interleave solve, extract, release" commit. Measured
separately, that commit added 0.90 GB — 28.75 GB with the raw walk alone, 29.65 GB with both —
so it has been dropped rather than merged on its description. It is preserved on
perf/interleave-dispose if anyone wants to take it further.

What this does not do

It does not make whole-vscode -a 4 fit. Per #141, 21.33 GB is already committed before the
interprocedural phase begins, and that phase's entire retained state is 0.48 GB. This is a 0.94 GB
reduction against a ~29.7 GB peak, not a change to the ceiling. Whole-vscode -a 3 completes today
(19m41s, 28.4 GB); -a 4 needs #112 Step 3 or 4.

Wall time across single runs was 9m37s (main), 10m34s (this branch) — within the run-to-run spread
I saw on this machine, and not something I would claim either direction from one sample each.

`indexCallExpressions` traversed with ts-morph's `forEachDescendant`, which wraps
every visited node in a JS object and caches it on the SourceFile for the lifetime
of the program. Indexing call sites therefore materialised the entire AST of every
source file, not just the call-like nodes it keeps.

Recurse the raw compiler nodes instead and wrap only the matches. Measured on
vscode/src (6,758 files, 470,973 indexed call sites): the phase drops from +3.23GB
to +2.52GB and runs 1.1s faster, with byte-identical analysis.json output.

A differential test pins the raw walk against the wrapper walk it replaced.
@rahlk
rahlk marked this pull request as draft September 3, 2026 01:21
@rahlk

rahlk commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Marking this draft. The end-to-end A/B does not support the memory claim in the description.

Same machine, same input, same flags (--input vscode/src --no-build -a 4 --graphs cfg,dfg,pdg,sdg):

build peak RSS wall
origin/main 29.69 GB 9m37s
this branch 29.65 GB 10m26s

0.04 GB apart is noise, and this branch is ~49s slower.

The phase-level reductions I measured are real — the call-site index goes from +3.23 GB to
+2.52 GB — but they do not move peak RSS, because the peak is set later in the run. That is
consistent with the other measurement in #141: 21.33 GB is already committed before the
interprocedural phase begins, so trimming an earlier phase's residency changes the shape of the
curve without changing its maximum. I should have drawn that conclusion from my own number before
opening this.

The 27.0 GB target in #141's definition of done is also wrong. It came from an earlier run whose
exact invocation I did not record, and it does not reproduce — origin/main itself measures
29.69 GB under these conditions.

What still stands on its own:

  • the raw AST walk is a strictly better implementation of the same function (wraps ~471k matches
    instead of every node), is byte-identical in output, and is pinned by a mutation-checked
    differential test
  • the interleave is the more likely source of the 49s regression, since extracting each program
    while it is warm gives up batching across programs

Next step is to attribute the two commits separately rather than defend them together. Not merging
until that is done.

@rahlk
rahlk force-pushed the perf/issue-141-bound-ast-residency branch from 8a58686 to 46f9357 Compare September 3, 2026 01:33
@rahlk rahlk changed the title perf(call-graph): bound AST residency in the solve — raw call index + per-program release perf(call-graph): walk the raw AST when indexing call expressions Sep 3, 2026
@rahlk
rahlk marked this pull request as ready for review September 3, 2026 01:33
@rahlk

rahlk commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Attribution run done. Reduced this PR to the raw walk alone.

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

The interleave commit costs 0.90 GB rather than saving anything, so it is dropped from this branch (kept on perf/interleave-dispose). Description rewritten to the measurement that actually holds.

@rahlk
rahlk merged commit b53f729 into main Sep 3, 2026
1 check passed
@rahlk
rahlk deleted the perf/issue-141-bound-ast-residency branch September 3, 2026 10:41
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.

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

1 participant