Skip to content

tools: measure memory across repeated program lifecycles - #670

Draft
maschwenk wants to merge 4 commits into
mainfrom
max/lifecycle-leak-tracking
Draft

maschwenk wants to merge 4 commits into
mainfrom
max/lifecycle-leak-tracking

Conversation

@maschwenk

@maschwenk maschwenk commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

a tool that cycles the servers through program lifecycles and says what grows per cycle. it found the two leaks #677 and #678 fix

why

#646 asks for observed leaks across repeated build and lsp cycles. lsp_mem.py did one long editing session. nothing cycled programs (rebuilds, snapshots, reloads, builds) or separated kept data from memory the allocator just holds

what changed

  • tools/lsp-mem/lifecycle_leaks.py: 8 scenarios (watch, watch-root, lsp-edit, lsp-open-close, lsp-reload, api-snapshot, api-update, api-build). rss after every cycle with a slope fit. --poison for crashes, wrong answers and region balance. --split diffs the servers' TSRS_MEM_SPLIT exit reports between a short and a long run, which gives bytes per cycle that stay used in thread arenas vs live heap vs merely resident. thats the number to trust
  • the --lsp and --api servers print the exit memory split like the compile path does (a few lines in main.rs)
  • notes/lifecycle-leaks.md: method, tables, the two leaks with their code paths, a draft issue (not filed)

what it found

six scenarios are steady. api-snapshot and lsp-reload grow forever, roughly 45-50 KiB of thread arena per full build. two causes, each with a fix PR: every tsconfig load lands in the thread arena outside any region (#677), and the lsp and api never set loader regions so every full build leaves the workers' resolutions in thread arenas for the life of the process (#678). 0 wrong answers in 2,520 normal and 1,260 poison cycles

tradeoffs

rss slopes are noisy, use --split. --vmmap is macos only. synthetic 40 module project, real tsconfigs keep more per load. the watch runs are short (60 / 30 cycles) because my machine's fseventsd was delivering events ~13s late to every client, tsrs's own path is under 30ms

tools/lsp-mem/lifecycle_leaks.py drives one tsrs process through N cycles
that create and release a program (watch rebuilds, language-server edits,
open/close and tsconfig reloads, API snapshots, updates and builds), samples
its resident memory after each cycle and fits bytes per cycle. --poison
checks crashes, answers and the region log's balance; --split divides the
difference of two TSRS_MEM_SPLIT exit reports by the extra cycles into what
stays used in the thread arenas and live in the heap; --vmmap attributes the
growth on macOS. The --lsp and --api servers now print the exit split.

notes/lifecycle-leaks.md records the runs at 357d1d3: six scenarios are
steady; createSnapshot in the API and a tsconfig change in the language
server keep every loaded config (about 50 KiB in a thread arena plus its
heap members), because the config load happens outside any region.
@codspeed

codspeed Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 7.6%

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 +8.96%
⚡ Walltime webpack 1,019.2 ms 959.2 ms +6.26%

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/lifecycle-leak-tracking (27a8584) 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. ↩

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Performance change vs main

Workflow run · main 357d1d3ec231 → PR merge 00c0484e0c16 (head 27a85845ee39)

Clef gate: Running benchmarks because the decision was unavailable (ValueError).

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 -0.3% -0.2% +0.0%
xstate-main +1.0% -0.2% +0.0%
webpack -1.9% -0.7% -0.5%
mui-docs +0.5% +2.2% -0.0%
Compiler -0.5% +0.1% +0.2%
Compiler-Unions +0.8% -0.3% -0.2%
cal-diy -0.9% +0.7% +0.4%
formbricks-web +1.3% +0.1% -0.3%
supabase-studio +2.9% -0.6% -0.7%
t3code-server +1.4% +1.3% +0.4%
mikro-orm -0.2% -2.1% +1.4%
next-packages-next -0.4% -0.9% +0.6%
next-root +1.3% +1.8% +1.3%
storybook -3.2% -0.6% +0.6%
nuxt +0.9% +0.1% -0.2%
playwright +2.7% +0.1% -1.9%
drizzle-orm -0.3% -0.3% +0.3%
typeorm +1.6% +0.5% -0.2%
date-fns +5.6% +1.1% +0.1%

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.

#677 moves the config load into a region; measured with the tool, the
command line's heap members and the config text are freed, but about
41 KiB per full program build stays in a thread arena and is not the
config load. The note said the whole figure was the load.
Every FSEvents client on the measuring machine saw file events about
13 s late (fseventsd at 99.7% CPU, 44 GiB resident); tsrs's own path from
the callback to "File change detected" and the rebuild is under 30 ms.
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.
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