Skip to content

Devirtualize ForEachChild and IterChildren#4395

Open
jakebailey wants to merge 2 commits into
mainfrom
jabaile/ast-method-dispatch-opt
Open

Devirtualize ForEachChild and IterChildren#4395
jakebailey wants to merge 2 commits into
mainfrom
jabaile/ast-method-dispatch-opt

Conversation

@jakebailey

Copy link
Copy Markdown
Member

Now that we have code gen, generating a function to do the mapping instead of relying on interface calls is now possible. Doing this lets escape analysis see the actual functions being called, which then allows for the functions to not escape and potentially even inline.

(*Node).ForEachChild previously dispatched through the nodeData interface
(`n.data.ForEachChild(v)`). An interface call is opaque to escape analysis,
which must then assume the visitor `v` escapes; this forced every caller's
visitor closure -- and any locals it captured -- onto the heap.

Generate a (*Node).ForEachChild that instead switches on n.Kind and calls
the concrete node type's method directly (n.data.(*T).ForEachChild(v)). The
statically-resolved call lets escape analysis prove the visitor does not
escape, keeping caller closures on the stack, and the integer switch compiles
to a jump table that is cheaper than the interface call it replaces.

A recursive walk of checker.ts (the language-server traversal pattern, passing
a fresh closure per node) drops from ~596k allocations to 0 and runs ~4.2x
faster. Sites that already cached their visitor (e.g. the binder's bindFunc)
are unaffected; this makes that optimization automatic everywhere.
Node.IterChildren was doubly affected by escape analysis being unable to see
through dynamic calls. (*Node).IterChildren() dispatched through the nodeData
interface (`n.data.IterChildren()`), returning a closure, and each iteration
allocated two closures: the bound method value `forEachChildIter` plus the
`invert(yield)` wrapper used to reconcile TS-visitor early-return semantics
(`true` stops) with Go iterator semantics (`false` stops).

Implement IterChildren directly on *Node, using the now-devirtualized
ForEachChild and folding the inversion into the yield. Because ForEachChild no
longer leaks its visitor, the iterator and its inner closure stay on the stack.
Remove the now-unused nodeData.IterChildren interface method,
NodeDefault.IterChildren, forEachChildIter, and invert.

A recursive walk of checker.ts via IterChildren drops from ~1.19M allocations
to 0 and runs ~8x faster.
Copilot AI review requested due to automatic review settings June 22, 2026 15:49
@jakebailey

Copy link
Copy Markdown
Member Author

@typescript-bot perf test this

@typescript-automation

typescript-automation Bot commented Jun 22, 2026

Copy link
Copy Markdown

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
perf test this ✅ Started 👀 Results

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR devirtualizes AST child traversal by moving (*Node).ForEachChild from an interface dispatch (node.data.ForEachChild) to a generated switch n.Kind dispatcher, and by implementing IterChildren directly on Node. The goal is to make the concrete ForEachChild call visible to Go escape analysis so visitor/yield closures are less likely to escape to the heap (and may inline more often).

Changes:

  • Implement (*Node).IterChildren() directly in ast.go, removing IterChildren from nodeData / NodeDefault.
  • Add a generated (*Node).ForEachChild(v Visitor) bool kind-switch dispatcher in ast_generated.go.
  • Update _scripts/generate-go-ast.ts to generate the new ForEachChild dispatch block.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

File Description
internal/ast/ast.go Implements IterChildren directly on Node and removes IterChildren from the nodeData interface/default implementation.
internal/ast/ast_generated.go Adds the generated (*Node).ForEachChild kind-switch dispatcher that calls concrete node ForEachChild methods.
_scripts/generate-go-ast.ts Generates the new ForEachChild dispatcher based on schema nodes that have children (or are hand-written like SourceFile).
Files not reviewed (1)
  • internal/ast/ast_generated.go: Generated file

@typescript-automation

Copy link
Copy Markdown

@jakebailey
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - native
Errors 4 4 ~ ~ ~ p=1.000 n=6
Symbols 81,777 (± 0.04%) 81,773 (± 0.05%) ~ 81,699 81,804 p=0.810 n=6
Types 98,821 98,821 ~ ~ ~ p=1.000 n=6
Memory Used 180,343k (± 0.39%) 180,303k (± 0.34%) ~ 179,245k 180,953k p=0.810 n=6
Memory Allocs 2,546,204 (± 0.01%) 2,538,455 (± 0.01%) -7,750 (- 0.30%) 2,538,084 2,538,818 p=0.005 n=6
Config Time 0.001s (±77.38%) 0.001s (±109.43%) ~ 0.000s 0.001s p=0.640 n=6
Parse Time 0.061s (± 3.33%) 0.063s (± 5.33%) ~ 0.060s 0.068s p=0.466 n=6
Bind Time 0.019s (±20.43%) 0.020s (± 8.02%) ~ 0.018s 0.023s p=0.466 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 1.059s (± 2.38%) 1.052s (± 1.12%) ~ 1.037s 1.066s p=0.630 n=6
Total Time 1.141s (± 2.21%) 1.137s (± 0.96%) ~ 1.125s 1.151s p=0.748 n=6
angular-1 - native
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 876,497 (± 0.07%) 876,531 (± 0.07%) ~ 875,946 877,304 p=1.000 n=6
Types 263,750 (± 0.00%) 263,750 (± 0.00%) ~ 263,748 263,752 p=0.491 n=6
Memory Used 830,457k (± 0.07%) 831,153k (± 0.05%) ~ 830,666k 831,685k p=0.066 n=6
Memory Allocs 12,598,280 (± 0.32%) 12,455,246 (± 0.09%) -143,035 (- 1.14%) 12,442,013 12,467,677 p=0.005 n=6
Config Time 0.027s (±15.32%) 0.027s (± 5.23%) ~ 0.025s 0.029s p=0.872 n=6
Parse Time 0.272s (± 1.90%) 0.269s (± 5.04%) ~ 0.245s 0.283s p=1.000 n=6
Bind Time 0.042s (±15.36%) 0.052s (±26.98%) ~ 0.042s 0.080s p=0.092 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 2.091s (± 1.19%) 2.094s (± 1.12%) ~ 2.067s 2.129s p=1.000 n=6
Total Time 2.447s (± 0.65%) 2.454s (± 1.02%) ~ 2.425s 2.495s p=0.689 n=6
mui-docs - native
Errors 11,275 (± 0.08%) 11,276 (± 0.11%) ~ 11,251 11,283 p=0.677 n=6
Symbols 4,214,832 4,214,832 ~ ~ ~ p=1.000 n=6
Types 1,534,275 1,534,275 ~ ~ ~ p=1.000 n=6
Memory Used 4,964,813k (± 0.02%) 4,966,842k (± 0.05%) ~ 4,963,880k 4,971,013k p=0.128 n=6
Memory Allocs 82,334,533 (±12.22%) 93,677,774 (±20.34%) ~ 76,248,888 115,819,666 p=0.471 n=6
Config Time 0.027s (±13.20%) 0.026s (± 8.87%) ~ 0.023s 0.029s p=0.466 n=6
Parse Time 1.105s (±18.32%) 1.278s (±21.16%) ~ 1.024s 1.601s p=0.471 n=6
Bind Time 0.002s 0.002s (±18.82%) ~ 0.002s 0.003s p=0.405 n=6
Check Time 19.717s (± 0.41%) 19.867s (± 0.74%) ~ 19.706s 20.056s p=0.066 n=6
Emit Time 0.533s (± 4.92%) 0.534s (± 4.99%) ~ 0.514s 0.569s p=0.873 n=6
Total Time 22.195s (± 1.51%) 22.579s (± 1.30%) ~ 22.187s 22.986s p=0.093 n=6
self-build-src - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,394,138 1,394,138 ~ ~ ~ p=1.000 n=6
Types 442,153 442,153 ~ ~ ~ p=1.000 n=6
Memory Used 1,650,064k (± 0.32%) 1,653,488k (± 0.38%) ~ 1,644,730k 1,660,882k p=0.378 n=6
Memory Allocs 97,840,068 (± 0.07%) 96,883,166 (± 0.10%) -956,902 (- 0.98%) 96,786,194 97,033,186 p=0.005 n=6
Config Time 0.024s (± 9.85%) 0.022s (± 8.62%) ~ 0.020s 0.025s p=0.126 n=6
Parse Time 0.261s (± 3.23%) 0.262s (± 2.63%) ~ 0.254s 0.272s p=0.748 n=6
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=6
Check Time 2.702s (± 0.66%) 2.711s (± 0.46%) ~ 2.691s 2.723s p=0.173 n=6
Emit Time 0.339s (± 2.79%) 0.344s (± 4.37%) ~ 0.320s 0.364s p=0.378 n=6
Total Time 34.833s (± 0.26%) 34.894s (± 0.41%) ~ 34.735s 35.047s p=0.471 n=6
self-compiler - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 337,634 337,634 ~ ~ ~ p=1.000 n=6
Types 199,520 199,520 ~ ~ ~ p=1.000 n=6
Memory Used 332,353k (± 0.03%) 332,370k (± 0.03%) ~ 332,291k 332,485k p=0.936 n=6
Memory Allocs 4,725,318 (± 0.01%) 4,677,791 (± 0.01%) -47,527 (- 1.01%) 4,677,257 4,678,555 p=0.005 n=6
Config Time 0.001s 0.001s ~ ~ ~ p=1.000 n=6
Parse Time 0.130s (± 2.50%) 0.129s (± 3.61%) ~ 0.124s 0.137s p=0.745 n=6
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=6
Check Time 1.389s (± 0.96%) 1.385s (± 1.01%) ~ 1.369s 1.404s p=0.748 n=6
Emit Time 0.124s (± 6.46%) 0.126s (± 7.94%) ~ 0.109s 0.136s p=0.748 n=6
Total Time 1.701s (± 0.90%) 1.697s (± 0.78%) ~ 1.680s 1.716s p=0.630 n=6
ts-pre-modules - native
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 97,488 97,488 ~ ~ ~ p=1.000 n=6
Types 356 356 ~ ~ ~ p=1.000 n=6
Memory Used 133,735k (± 0.02%) 133,713k (± 0.03%) ~ 133,651k 133,755k p=0.298 n=6
Memory Allocs 183,032 (± 0.21%) 180,759 (± 0.21%) -2,273 (- 1.24%) 180,325 181,188 p=0.005 n=6
Config Time 0.001s 0.001s ~ ~ ~ p=1.000 n=6
Parse Time 0.113s (± 5.67%) 0.115s (± 2.85%) ~ 0.109s 0.118s p=0.687 n=6
Bind Time 0.040s (±20.35%) 0.039s (±11.98%) ~ 0.032s 0.046s p=0.747 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=6
Total Time 0.157s (± 6.83%) 0.158s (± 1.64%) ~ 0.153s 0.161s p=1.000 n=6
vscode - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 6,697,507 6,697,507 ~ ~ ~ p=1.000 n=6
Types 2,368,100 2,368,100 ~ ~ ~ p=1.000 n=6
Memory Used 4,555,621k (± 0.05%) 4,554,574k (± 0.04%) ~ 4,552,495k 4,557,376k p=0.298 n=6
Memory Allocs 32,086,968 (± 0.10%) 31,138,522 (± 0.05%) -948,446 (- 2.96%) 31,123,560 31,159,363 p=0.005 n=6
Config Time 0.074s (± 9.38%) 0.073s (±12.79%) ~ 0.059s 0.082s p=0.936 n=6
Parse Time 0.879s (± 3.73%) 0.887s (± 4.74%) ~ 0.828s 0.934s p=0.810 n=6
Bind Time 0.173s (±27.16%) 0.171s (±22.44%) ~ 0.145s 0.232s p=0.689 n=6
Check Time 8.872s (± 0.80%) 8.888s (± 0.64%) ~ 8.830s 8.970s p=0.810 n=6
Emit Time 2.236s (±11.63%) 2.323s (±10.63%) ~ 1.986s 2.522s p=0.689 n=6
Total Time 12.264s (± 1.38%) 12.364s (± 1.57%) ~ 12.076s 12.538s p=0.230 n=6
webpack - native
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 182,877 182,877 ~ ~ ~ p=1.000 n=6
Types 340 340 ~ ~ ~ p=1.000 n=6
Memory Used 221,478k (± 0.10%) 221,574k (± 0.07%) ~ 221,356k 221,832k p=0.230 n=6
Memory Allocs 1,062,781 (± 0.29%) 921,747 (± 0.16%) 🟩-141,034 (-13.27%) 919,436 922,936 p=0.005 n=6
Config Time 0.010s (±41.77%) 0.008s (±33.64%) ~ 0.006s 0.011s p=0.346 n=6
Parse Time 0.155s (± 5.21%) 0.146s (± 8.36%) ~ 0.133s 0.164s p=0.199 n=6
Bind Time 0s 0s ~ ~ ~ p=1.000 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.034s (±26.95%) 0.038s (±24.23%) ~ 0.029s 0.050s p=0.572 n=6
Total Time 0.200s (± 5.81%) 0.192s (± 3.26%) ~ 0.185s 0.201s p=0.295 n=6
xstate-main - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,065,023 1,065,023 ~ ~ ~ p=1.000 n=6
Types 389,185 389,185 ~ ~ ~ p=1.000 n=6
Memory Used 643,141k (± 0.01%) 643,159k (± 0.03%) ~ 642,988k 643,356k p=1.000 n=6
Memory Allocs 5,061,955 (± 0.16%) 4,959,932 (± 0.11%) -102,023 (- 2.02%) 4,951,542 4,965,947 p=0.005 n=6
Config Time 0.005s (±11.05%) 0.005s (± 8.44%) ~ 0.004s 0.005s p=0.595 n=6
Parse Time 0.139s (± 3.26%) 0.133s (± 1.92%) 🟩-0.006s (- 4.43%) 0.130s 0.136s p=0.029 n=6
Bind Time 0.035s (±24.96%) 0.032s (±18.83%) ~ 0.028s 0.043s p=0.744 n=6
Check Time 1.313s (± 0.38%) 1.310s (± 1.00%) ~ 1.291s 1.327s p=0.748 n=6
Emit Time 0.001s 0.001s ~ ~ ~ p=1.000 n=6
Total Time 1.496s (± 0.56%) 1.485s (± 0.72%) ~ 1.476s 1.503s p=0.128 n=6
System info unknown
Hosts
  • native
Scenarios
  • Compiler-Unions - native
  • angular-1 - native
  • mui-docs - native
  • self-build-src - native
  • self-compiler - native
  • ts-pre-modules - native
  • vscode - native
  • webpack - native
  • xstate-main - native
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

lsp

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsLSP - native
Req 1 - updateOpen 10ms (± 5.34%) 10ms (± 6.32%) ~ 9ms 11ms p=0.386 n=6
Req 2 - geterr 981ms (± 0.51%) 977ms (± 1.17%) ~ 969ms 1,000ms p=0.065 n=6
Req 3 - references 38ms (± 6.06%) 39ms (±10.97%) ~ 37ms 48ms p=0.608 n=6
Req 4 - navto 18ms (±14.60%) 20ms (±19.10%) ~ 17ms 26ms p=0.118 n=6
Req 5 - completionInfo count 1,357 1,357 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 19ms (±21.85%) 19ms (±22.11%) ~ 15ms 24ms p=0.934 n=6
CompilerLSP - native
Req 1 - updateOpen 11ms (± 4.84%) 11ms (± 3.76%) ~ 10ms 11ms p=0.595 n=6
Req 2 - geterr 473ms (± 1.03%) 467ms (± 3.07%) ~ 453ms 490ms p=0.467 n=6
Req 3 - references 45ms (± 4.73%) 49ms (± 3.94%) 🔻+4ms (+ 8.86%) 48ms 53ms p=0.004 n=6
Req 4 - navto 17ms (± 5.95%) 19ms (± 8.15%) 🔻+2ms (+ 9.62%) 18ms 22ms p=0.047 n=6
Req 5 - completionInfo count 1,519 1,519 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 23ms (±13.67%) 24ms (±19.38%) ~ 21ms 33ms p=0.622 n=6
System info unknown
Hosts
  • native
Scenarios
  • CompilerLSP - native
  • Compiler-UnionsLSP - native
  • xstate-main-1-LSP - native
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
lsp-startup - native
Execution time 4.90ms (±516.54%) 4.90ms (±516.54%) -0.00ms (- 0.10%) 0.00ms 491.68ms p=0.000 n=600
tsgo-startup - native
Execution time 6.07ms (±516.55%) 6.06ms (±516.54%) -0.01ms (- 0.18%) 0.00ms 608.44ms p=0.000 n=600
System info unknown
Hosts
  • native
Scenarios
  • lsp-startup - native
  • tsgo-startup - native
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

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.

2 participants