perf(parser): build AST lists in thread-local scratch buffers#24539
perf(parser): build AST lists in thread-local scratch buffers#24539Boshen wants to merge 1 commit into
Conversation
2a7bd08 to
9991374
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
9991374 to
0fee28e
Compare
c2d05ea to
fa30393
Compare
0fee28e to
7589f35
Compare
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
69068b0 to
6573f83
Compare
7589f35 to
1b647b5
Compare
906b58c to
f83d4e0
Compare
f83d4e0 to
073e5a0
Compare
1b647b5 to
6f3dda8
Compare
6f3dda8 to
81cc312
Compare
81cc312 to
5e49c55
Compare
Building a list by pushing into an `ArenaVec` reallocates within the arena as the vec grows. Child AST nodes are allocated between pushes, so the growing buffer is almost never the arena's most recent allocation - every doubling allocates a new buffer and abandons the old one as permanent dead space in the arena. Instead, build every list in a scratch `std::Vec` and move it into the arena with a single exact-sized allocation (memcpy) once complete. There is one scratch vec per list element type (`ScratchBuffers`); nested lists with the same element type share one vec stack-like via watermarks: record the length at list start, push, drain your own tail at completion. List parsing is strictly LIFO (errors are stored, not thrown), so a list always drains its own elements before its enclosing list resumes. This is the scratch pattern used by Zig's `std.zig` parser, split per element type. The buffers are checked out of a thread-local cache when the parser is created and returned emptied when it is dropped, so they are never freed and stay grown to high-water capacity across parses on the same thread. Once a thread is warm, list building performs no heap allocation at all. Arena reallocs during parsing drop by 98-99.7% (checker.ts 22860 -> 59, antd.js 55355 -> 1285); arena allocs and system allocs are unchanged. Parser benchmarks improve by up to ~10% (App.tsx, react.development.js) and regress nowhere. Conformance output is byte-identical. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
5e49c55 to
e1c7432
Compare

Building a list by pushing into an
ArenaVecreallocates within the arena as the vec grows. Child AST nodes are allocated between pushes, so the growing buffer is almost never the arena's most recent allocation — every doubling allocates a new buffer and abandons the old one as permanent dead space in the arena. A list of N elements costs ~⌈log2 N⌉ grow events and ~2-4× its final size in arena bytes.This PR builds every list in a scratch
std::Vecinstead, and moves the completed list into the arena with a single exact-sized allocation (a memcpy).Design
ScratchBuffers, ~27 fields). Nested lists with the same element type (statements of nested blocks, arguments of nested calls) share one vec, used like a stack via watermarks: record the vec's length at list start, push elements, drain your own tail at completion. List parsing is strictly LIFO — parse errors are stored rather than thrown, and element closures cannot early-exit their caller — so a list always drains its own elements before its enclosing list resumes. (This is the scratch pattern used by Zig'sstd.zigparser, split per element type since our lists are heterogeneous.)parse_separated_list/parse_separated_list_fromhelpers. A debug assert at parse end catches any future site that pushes without draining (checked by every conformance run, which usesdebug-assertions = true).Results
just allocs(Arena allocs,Sys allocs,Sys reallocsare all unchanged):The residue is mostly lexer
StringBuilderescape paths and the comments vec, which are not converted.Parser benchmarks improve up to ~10% (
parser/App.tsx,parser/react.development.js,estree/checker.ts) and regress nowhere; conformance output is byte-identical across all suites.🤖 Generated with Claude Code