fix(allocator): fixed-size allocators use VirtualAlloc on Windows#22124
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR updates oxc_allocator’s Windows fixed-size arena implementation to avoid immediate OOMs by switching from System allocations to a VirtualAlloc-backed reserve/commit strategy, allowing address-space reservation without eagerly committing physical memory.
Changes:
- Implement Windows fixed-size
Arena::new_fixed_sizeusingVirtualAlloc(MEM_RESERVE)+ incrementalMEM_COMMITgrowth. - Add Windows-only dependency on
windows-sysand adjust internal visibility/utilities to support the new implementation. - Modify CI workflow to run Windows NAPI jobs/steps unconditionally by commenting out
if:guards.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/oxc_allocator/src/from_raw_parts.rs | Updates safety/docs to reflect platform-dependent deallocation behavior. |
| crates/oxc_allocator/src/arena/utils.rs | Makes round_down_to a const fn. |
| crates/oxc_allocator/src/arena/from_raw_parts.rs | Updates safety/docs to reflect platform-dependent deallocation behavior. |
| crates/oxc_allocator/src/arena/fixed_size/windows.rs | New Windows VirtualAlloc reserve/commit allocator + growth + tests + VirtualFree deallocation path. |
| crates/oxc_allocator/src/arena/create.rs | Exposes FIRST_ALLOCATION_GOAL to the module for Windows allocator use. |
| crates/oxc_allocator/Cargo.toml / Cargo.toml / Cargo.lock | Adds windows-sys (Windows-only dep for oxc_allocator, version pinned in workspace). |
| .github/workflows/ci.yml | Comments out if: conditions for Windows NAPI jobs/steps, changing CI execution behavior. |
8b384b5 to
31e4778
Compare
91be1c5 to
2098199
Compare
|
@overlookmotel With this version, I could not produce a panic after 20 minutes. Almost removed jQuery from an 800 line file. Modified other heavy files too, and it worked like a charm. I can test it on Monday with a longer session. But it looks like it fixes the issue in windows for me 👍 |
Hahaha brilliant! You really went to town! Would love to have seen your machine rocking under that load. "HD videos and more" is the crowning touch! Thanks loads for putting it through its paces. I got cold feet about merging it before today's release. I also want to put it through its paces on Linux/Mac to make sure the changes don't accidentally "bleed" into other platforms. Please excuse my excess caution - destabilizing the allocator could destabilize everything. I intend to look at it again later this week, do more testing, and hopefully get it into next Monday's release. |
|
This has taken the "Private Bytes" for oxlint from 88GB to ~100MB, a 99.9% reduction in committed memory 🎉 . |
|
@overlookmotel could you sync the graphite stack? |
31e4778 to
cd1cde5
Compare
2098199 to
01d8cdd
Compare
|
Stack now rebased on main. However, I'm going to do a fix for #22339 tomorrow, and then will have to alter this stack too to go on top of that fix. |
|
Could not crash the LSP server today 🥳 |
cd1cde5 to
fac47f6
Compare
01d8cdd to
cdc43b7
Compare
fac47f6 to
21e5847
Compare
cdc43b7 to
9e53a66
Compare
21e5847 to
98ade91
Compare
9e53a66 to
c08e350
Compare
98ade91 to
053c1c6
Compare
053c1c6 to
74a046f
Compare
c08e350 to
dd5ead0
Compare
74a046f to
60033f6
Compare
|
Have tested this branch on Mac OS linting Kibana repo with JS plugins + Finally going to merge this thing! |
Merge activity
|
…22124) Fixes #19395 - OOM errors in Oxlint JS plugins on Windows. ### The problem Previously, on all platforms each fixed-size (raw transfer-enabled) allocator obtained a large chunk of memory from the `System` allocator (6 GiB in the case of Windows). This is fine on MacOS and most Linux distros, as they use overcommit allocation accounting, so these allocations consume only virtual memory (address space), not actual physical memory. The available address space is enormous (~128 TiB), so it's hard to exhaust it. Physical memory is only consumed when pages of the allocation are actually touched, which the vast majority never are. Windows behaves differently. Allocations obtained from `System` allocator immediately count towards the memory limit. So, on memory-constrained machines, allocating 6 GiB can immediately fail - OOM just from linting the first file. ### This PR [#22088](#22088), #22122, and #22123 introduced platform-specific code for handling allocation and deallocation of fixed-size `Arena`s, which are used in raw transfer/Oxlint JS plugins. This PR switches to a different allocation method on Windows for fixed-size `Arena`s. It switches to using [VirtualAlloc](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc). This Windows-only API allows reserving address space and committing pages of that reservation as separate operations (similar to `mmap`). It works like this: - Fixed-size allocators now still reserve 6 GiB, but that only consumes address space, and does not count towards max memory limit. - Of that 6 GiB reservation, a 2 GiB "container", aligned on a 4 GiB boundary, will be used as the active part of the memory for storing the AST and other data. - Initially only the last 16 KiB of the container is committed (made into usable memory, with physical pages backing it). - If AST does not fit in the committed region, the region grows downwards, committing further pages to accomodate allocation requests, up to the maximum of 2 GiB. Essentially, this replicates something similar to Linux-style allocation, but on Windows. The only difference is that on Windows it's necessary to explicitly commit pages before touching them, whereas Linux/MacOS commit on demand automatically. ### Implementation details and testing This PR uses the battle-tested [windows-sys](https://crates.io/crates/windows-sys) crate for `VirtualAlloc` and `VirtualFree`. The allocation implementation is written from scratch, but while I was researching to validate that I'd not missed any edge cases, I discovered that it's almost identical to [wasmtime's implementation](https://github.com/bytecodealliance/wasmtime/blob/386a3280dee61f5c4120ce7cde621c1039e383d5/crates/wasmtime/src/runtime/vm/sys/windows/mmap.rs) which uses the same APIs. Comments in the code go over in some detail considerations of Rust's memory model and pointer provenance. As far as I can see, the implementation is fairly watertight. There are tests for the essential functionality and edge cases, and #22121 made these tests run under Miri on Windows. However, I don't have access to a Windows machine to fully test this by running Oxlint on a massive repo. I am hoping members of the community who do can put this through its paces a bit before we merge. ### What's missing In enormous projects, when linting with both JS plugins and `import` plugin, it's possible to end up with a huge number of ASTs in memory simultaneously. With each AST consuming 6 GiB of address space, it's possible to exhaust the entire 128 TiB usable address space, and get OOM on _virtual_ memory. At present we have a workaround to prevent too many fixed-size allocators existing concurrently, so avoiding OOM, but the workaround is a drag on performance. Future work will aim to address this.
…e chunks (#22122) Add per-platform implementations of `dealloc_fixed_size_arena_chunk`, and delegate to them when dropping an `Arena` which has fixed-size chunks. Currently, all the implementations are identical - they just deallocate the memory with `System` allocator. But this prepares the way for #22124, which alters the Windows impl to use `VirtualFree` instead.
Add a private method `Arena::grow_fixed_size_chunk` which attempts to grow a fixed-size `Arena` chunk in place. Similar to #22122, there's multiple implementations for different platforms, but all are currently the same - all return `None` to indicate "could not grow". This is preparatory work for #22124, which adds a Windows-specific implementation.
dd5ead0 to
e216a84
Compare
60033f6 to
4ab57eb
Compare
### 🚀 Features - bc91a17 codegen: Expose `Codegen::with_source_type` method (#22432) (camc314) ### 🐛 Bug Fixes - 5ac7e79 minifier: Drop unused-var-init pure IIFEs and preserve annotation for downstream (#22349) (Dunqing) - 4ab57eb allocator: Fixed-size allocators use `VirtualAlloc` on Windows (#22124) (overlookmotel) - 66d77eb allocator: Fix segfault on Linux MUSL with fixed-size allocators (#22388) (overlookmotel) - b8fbc1f transformer/object-rest-spread: Correct scope id when moving bindings (#22419) (camc314) - 18edc2c codegen: Keep `Object.defineProperty` property name as plain string in minify (#22400) (Dunqing) - dda33de transformer/explicit-resource-management: Align lexical binding scopes (#22320) (camc314) - 8e79de8 transformer: Preserve for-await statement bodies (#22361) (camc314) - 0cba210 transformer/class: Replace `new.target` in static blocks (#22360) (camc314) - 67ab1c9 transformer/es2018/for-await: Hoist for-await generated bindings (#22355) (camc314) - c3ceb4a transformer/object-rest-spread: Use hoisted scope for `for-of` temp refs (#22347) (camc314) ### ⚡ Performance - 73a9043 allocator/bitset: Avoid temp heap `String` allocation (#22403) (camc314) - 8b2f4f9 transformer/object-rest-spread: Collect `Vec<SymbolId` over `Vec<BindingIdentifier>` (#22418) (camc314) - 83679ea parser: Split TriviaBuilder::handle_token hot/cold paths (#22415) (Boshen) - 2c7d781 codegen: Inline identifier-name accessors (#22411) (Boshen) - 618bc76 diagnostics: Inline `OxcDiagnosticInner` to avoid heap allocation (#22406) (Boshen) - 0b4e158 parser: Reserve cap `2` for sequence expressions vec (#22374) (camc314) - 5f3bdd0 codegen: Add `#[inline]` to `code`, `code_len` (#22373) (camc314) Co-authored-by: overlookmotel <557937+overlookmotel@users.noreply.github.com>

Fixes #19395 - OOM errors in Oxlint JS plugins on Windows.
The problem
Previously, on all platforms each fixed-size (raw transfer-enabled) allocator obtained a large chunk of memory from the
Systemallocator (6 GiB in the case of Windows). This is fine on MacOS and most Linux distros, as they use overcommit allocation accounting, so these allocations consume only virtual memory (address space), not actual physical memory. The available address space is enormous (~128 TiB), so it's hard to exhaust it. Physical memory is only consumed when pages of the allocation are actually touched, which the vast majority never are.Windows behaves differently. Allocations obtained from
Systemallocator immediately count towards the memory limit. So, on memory-constrained machines, allocating 6 GiB can immediately fail - OOM just from linting the first file.This PR
#22088, #22122, and #22123 introduced platform-specific code for handling allocation and deallocation of fixed-size
Arenas, which are used in raw transfer/Oxlint JS plugins.This PR switches to a different allocation method on Windows for fixed-size
Arenas.It switches to using VirtualAlloc. This Windows-only API allows reserving address space and committing pages of that reservation as separate operations (similar to
mmap).It works like this:
Essentially, this replicates something similar to Linux-style allocation, but on Windows. The only difference is that on Windows it's necessary to explicitly commit pages before touching them, whereas Linux/MacOS commit on demand automatically.
Implementation details and testing
This PR uses the battle-tested windows-sys crate for
VirtualAllocandVirtualFree. The allocation implementation is written from scratch, but while I was researching to validate that I'd not missed any edge cases, I discovered that it's almost identical to wasmtime's implementation which uses the same APIs. Comments in the code go over in some detail considerations of Rust's memory model and pointer provenance. As far as I can see, the implementation is fairly watertight.There are tests for the essential functionality and edge cases, and #22121 made these tests run under Miri on Windows.
However, I don't have access to a Windows machine to fully test this by running Oxlint on a massive repo. I am hoping members of the community who do can put this through its paces a bit before we merge.
What's missing
In enormous projects, when linting with both JS plugins and
importplugin, it's possible to end up with a huge number of ASTs in memory simultaneously. With each AST consuming 6 GiB of address space, it's possible to exhaust the entire 128 TiB usable address space, and get OOM on virtual memory.At present we have a workaround to prevent too many fixed-size allocators existing concurrently, so avoiding OOM, but the workaround is a drag on performance. Future work will aim to address this.