Repository navigation
Conversation
- Cap each worker's young generation at 32MB. V8 sizes it as if each worker had the whole machine, so it was over half of a worker's heap. - With one thread, process chunks one at a time, on a copy of their input as a worker gets. All chunks used to start at once, holding all their work together, and `jsx-ast` changed the metadata `legacy-json` reads, which only worked because `legacy-json` happened to finish first. - Let go of the pages' code once `html` compiled their programs: the generator and its caller both held it until every page was rendered. - Move `DEFAULT_MAX_THREADS` to the threading constants. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Vite's native bundler started a thread per CPU in rolldown's runtime and in the Rayon pool it shares with lightningcss, each holding memory of its own, and its allocator (mimalloc) kept what it freed: it only purges on its next call past a delay, which never comes once bundling ends, and with transparent huge pages it only returns whole free 2MB pages. On the Node.js docs that left ~800MB resident through rendering. The pools now get the threads doc-kit was given (`ROLLDOWN_WORKER_THREADS`, `RAYON_NUM_THREADS`; `TOKIO_WORKER_THREADS` and `ROLLDOWN_MAX_BLOCKING_THREADS` made no difference), and mimalloc returns memory as it's freed (`MIMALLOC_PURGE_DELAY=0`, `MIMALLOC_ALLOW_THP=0`). They're read when the bundler loads, so they're set just before it is, unless the environment already sets them. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🚀 Deploying Preview to Cloudflare 🚀Preview Deployments by commit
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1179 +/- ##
==========================================
+ Coverage 93.44% 93.66% +0.21%
==========================================
Files 278 280 +2
Lines 27820 28054 +234
Branches 2790 2820 +30
==========================================
+ Hits 25997 26276 +279
+ Misses 1801 1756 -45
Partials 22 22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
|
| File | Main | PR | Change |
|---|---|---|---|
orama-db.json |
29.24 MB | 29.24 MB | -3.00 B (-0.0%) |
Performance estimate (single CI run)
- Generation time: 8.8% faster (7.35 s → 6.70 s)
- Peak memory: 20.9% lower (1.28 GB → 1.01 GB)
web Generator
Performance estimate (single CI run)
- Generation time: 14.7% faster (22.42 s → 19.13 s)
- Peak memory: 18.2% lower (1.90 GB → 1.56 GB)
Copying every chunk's input with one thread cost ~2.5s of a ~24s build, while only `jsx-ast` needs it: building a page changes its entries, and on the main thread those are the metadata other generators read. It now copies its own input there, and workers keep using theirs as is. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
At 32MB, workers collected their young generation often enough to cost 6-8% at 4 threads. 64MB keeps most of the memory saved (26-34% at 4 threads) within 1-2.5% of `main`'s build time. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
On 4 CPUs, like CI's runners, 64MB and 96MB keep the same memory (19% less across json, legacy-json, legacy-html, orama-db and web) and 96MB was marginally faster. It's also what V8 gives a worker with a 512MB old generation on Node.js 26, so the cap only changes Node.js 24. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
ovflowd
marked this pull request as ready for review
October 26, 2026 12:21
Building a page splices the heading out of its entries' content and replaces the stability nodes, and on the main thread those entries are the metadata other generators read. With one thread, building `legacy-html` with other targets crashed (`Cannot read properties of undefined (reading 'tagName')`), on `main` too. It now works on a copy there, as it already does in a worker. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
- With one thread, chunks are processed one at a time, in order. - Workers get both the old and young generation limits. - jsx-ast and legacy-html leave their input unchanged on the main thread. - resolveBundler sets the bundler's thread and allocator variables from `threads`, keeps any already set, and leaves a custom bundler alone. Each fails against `main`. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR keeps doc-kit's memory use down, most of all at low thread counts, where
-p 1couldn't build the Node.js API docs under--max-old-space-size=512:WORKER_GEN_SIZE, passed asresourceLimits.maxYoungGenerationSizeMb). With a 512MB old generation, V8 gives a worker 192MB of young generation on Node.js 24 and 96MB on Node.js 26, so the cap only changes Node.js 24. Node.js's--max-semi-space-sizestill replaces it.-p 1, every chunk used to start at once, so all their work was held together. Chunks now run one after another.jsx-astandlegacy-htmlrewrites its entries' content, and on the main thread those are the metadata other generators read. A worker already gets its own copy, so they only copy there. With one thread, buildinglegacy-htmlwith other targets crashed onmain(Cannot read properties of undefined (reading 'tagName')), andlegacy-jsononly worked because it happened to finish beforejsx-ast. Copying every generator's input instead cost ~2.5s at-p 1.htmlcompiled the page programs, both it and the scheduler still held every page's code until the last page was rendered.buildAllPagenow runs insidebuildPrograms,generate()clears its input, andgenerators.mjsreturnsgenerate()instead of awaiting it.ROLLDOWN_WORKER_THREADSandRAYON_NUM_THREADS(documented by rolldown;TOKIO_WORKER_THREADSandROLLDOWN_MAX_BLOCKING_THREADSmade no difference), and mimalloc returns memory as it's freed (MIMALLOC_PURGE_DELAY=0,MIMALLOC_ALLOW_THP=0). mimalloc only purges on its next call past the delay, which never comes once bundling ends, so a shorter delay like 250ms doesn't help. These are read when the bundler loads, so they're set just before it is, unless the environment already sets them.DEFAULT_MAX_THREADSmoves to the threading constants, next toWORKER_HEAP_SIZE.V8 flags (
--optimize-for-size,--memory-saver-mode,--heap-growing-percent,--max-semi-space-sizeand Node.js 26's memory pool flags) were measured too and left out: they were slower or saved nothing on top of the above.Validation
node --run test(754/754),node --run lintandnode --run format:checkare clean. New tests cover each change and fail againstmain: one chunk at a time with a single thread, both worker generation limits,jsx-astandlegacy-htmlleaving their input unchanged on the main thread, andresolveBundlersetting the bundler's variables (keeping any already set).The output is the same as
main. On a fullwebbuild with section pages, with asset hashes normalized, only 3 of 918 files differ (two chunks andsitemap.xml), the same 3 that differ between twomainruns.all.jsonandorama-db.jsononly differ in order, which already varies betweenmainruns (orama's index changes size with insertion order, hence the -84B in CI). A-p 1build oflegacy-html,legacy-jsonandwebtogether matches the-p 4one file for file.CI's command and default threads, Node.js 24, pinned to 4 CPUs like CI's runners, medians of 4 runs:
mainlegacy-htmljsonorama-dblegacy-jsonwebA regular
webbuild (all CPUs, default threads), 4 runs:main-p 1with--max-old-space-size=512finishes 6/6 on this PR (1071–1181MB peak RSS on Node.js 24 and 26), whilemainfinished 0/3 on Node.js 26 and 1/3 on Node.js 24.CI's comparison comment pits a single run against an older
mainrun, so its timings swing a lot; pinned to 4 CPUs locally, the difference stays within ~0–5%.Related Issues
Follows #1156.
Check List
node --run testand all tests passed.node --run format:check&node --run lint.