Skip to content

project: keep a full build's loader data in regions freed with the program - #678

Draft
maschwenk wants to merge 1 commit into
mainfrom
max/build-arena-residue
Draft

maschwenk wants to merge 1 commit into
mainfrom
max/build-arena-residue

Conversation

@maschwenk

@maschwenk maschwenk commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

the bigger of the two leaks #670 found. the ~41 KiB per full build that #677 couldnt explain is the file loader's worker-side data, and the lsp never gave it a region

why

new_program runs the file loader on the rayon pool. watch mode gives each program ProgramOptions::regions (one per thread) so what the loader allocates on workers is freed with the program. the lsp and the api never set it, so every full build (createSnapshot, a tsconfig change, an edit that cant be reused in place) left that data in the workers' own arenas forever

what changed

  • Project::create_program gives every full build loader regions (256 KiB first chunk per thread) and programOwner holds them, dropped last. a version reused in place inherits the old build's, like the base region
  • the in-place update path calls reuse_program and builds the fallback itself with fresh regions through a new Program::rebuild_options, which update_program now uses too. nothing changes for watch or build mode
  • ProgramRegions::new(first_chunk), default stays 1 MiB for the watcher, allocated_bytes for the region log
  • docs/LSP.md memory plan row

how i found it

a temporary per-allocation backtrace into thread arenas, 50 vs 200 api-snapshot cycles, not committed. per cycle on main: 23.5 KiB from filesparser / prefetch_resolutions / create_resolved_module on workers, 7.8 KiB of package.json cache entries that go to the thread arena on purpose, 3.2 KiB the config load (#677)

numbers

400 cycles, 50 warmup, --split

scenario rss/cycle before after arena used/cycle before after heap live/cycle before after
api-snapshot 355.2 KiB 279.6 KiB 46.4 KiB 18.8 KiB 14.0 KiB 12.3 KiB
lsp-reload 64.9 KiB 38.6 KiB 46.4 KiB 15.7 KiB 16.0 KiB 15.4 KiB
api-update 196 B 196 B 0 B 0 B 0 B 0 B

poison 200 cycles: 0 wrong answers, regions balanced. whats left per full build is the config load (#677) and ~8 KiB of package.json cache entries, which is an arena routing change and not this PR

tradeoff

one region per worker thread that touched a full build, 256 KiB reserved each (18 threads here, 4.5 MiB reserved per program chain, resident only as used). versions reused in place keep the loader regions alive like they keep the base region. touches the same two files as #677 and the return types #668 changes, so whichever lands second rebases with a trivial conflict

…ogram

The file loader runs on the worker pool, and what it allocates there
(resolutions, include reasons, metadata) went to the workers' own arenas in
the language server and the API, which never set ProgramOptions::regions
(watch mode does): every full build left about 25 KiB of a 40-module
project's loader data in thread arenas for the life of the process, which
notes/lifecycle-leaks.md read as part of the config load.

Project::create_program now gives every full build loader regions, held by
the program owner and dropped last; a version reused in place inherits
them through its configuration, as it does the base region, and the
fallback of an update that cannot reuse the program is built by the
project with regions of its own (Program::rebuild_options, which
update_program uses too). ProgramRegions::new takes the first chunk size
(256 KiB here, 1 MiB for the watcher).

Measured with tools/lsp-mem/lifecycle_leaks.py (PR 670), 400 cycles:
thread arena bytes used per cycle 46.4 -> 18.8 KiB for api-snapshot and
46.4 -> 15.7 KiB for lsp-reload; api-update unchanged at 0. What remains
is the config load (PR 677) and the package.json cache entries that
enter_table_owner routes to the thread arena.
maschwenk added a commit that referenced this pull request Oct 21, 2026
The 41 KiB per full build is mostly the file loader's worker-side
resolutions: the language server and the API never set
ProgramOptions::regions, so every full build left them in thread arenas.
@github-actions

Copy link
Copy Markdown
Contributor

Performance change vs main

Workflow run · main 357d1d3ec231 → PR merge 92da10b6eb03 (head d1a120856e17)

Clef gate: Clef estimated 49.0% wall-time, 74.8% peak-RSS, and 50.8% instruction-count impact probability.

Release builds, default checker count; -p <project> --noEmit --incremental false --pretty false. Ten alternating main/PR timing pairs and three RSS runs per binary on each project's runner. Instructions are user-space instructions retired across all threads in three separate alternating pairs at the default checker count.

Project Wall time Δ Peak RSS Δ Instructions Δ
vscode -1.7% +0.0% -0.0%
xstate-main +2.4% -0.5% -0.5%
webpack -3.0% +1.0% +0.3%
mui-docs -0.6% -1.1% +0.3%
Compiler -0.8% +0.4% +0.1%
Compiler-Unions +5.7% +1.4% +0.1%
cal-diy -5.5% +0.7% -1.7%
formbricks-web -0.1% -0.3% -0.7%
supabase-studio -1.8% +0.1% +0.3%
t3code-server +0.7% +0.2% -0.9%
mikro-orm -4.9% -0.9% +0.2%
next-packages-next -0.4% +0.3% -0.5%
next-root +1.6% +0.1% -0.1%
storybook -0.7% -0.0% -0.1%
nuxt -2.9% +0.0% -0.2%
playwright +0.8% +1.0% -1.3%
drizzle-orm +0.4% +0.5% -0.3%
typeorm +1.5% +0.0% -0.2%
date-fns -0.1% -0.5% +0.0%

Negative means the PR used less; positive means it used more. Deltas are medians of paired PR/main ratios for wall time and instructions. Shared CI machines and parallel scheduling add noise to small changes.

@codspeed

codspeed Bot commented Oct 21, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 12.1%

1 warning

⚠️ 4 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚡ 2 improved benchmarks
✅ 10 untouched benchmarks
⏩ 4 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
⚡ Walltime drizzle-orm 1.3 s 1.2 s +15.15%
⚡ Walltime webpack 1,019.2 ms 933.9 ms +9.14%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing max/build-arena-residue (d1a1208) with main (357d1d3)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

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

Development

Successfully merging this pull request may close these issues.

1 participant