Using the dynamic wasm allocator (new_wasm_dynamic_allocator, i.e. WasmGrowAndClaim) in a long-running module, I measured it holding a lot more linear memory than WasmGrowAndExtend on the same workload. Since wasm memory only ever grows, that high-water mark is permanent, so I wanted to put the numbers in front of you and ask whether the default should change.
Pages of linear memory grown (1 page = 64 KiB), each workload run against a fresh allocator per source:
| workload |
Claim |
Extend |
ratio |
| ratchet |
1705 |
171 |
10.0x |
| random_actions |
1516 |
720 |
2.1x |
| page_churn |
3 |
3 |
1.0x |
ratchet is a buffer that grows by about a page each step, freeing the previous one; random_actions is the random alloc/dealloc/realloc churn from the regression suite; page_churn is a 65520-byte alloc/free loop with one small pinned allocation.
The difference is that WasmGrowAndClaim::acquire always claims a fresh heap, even though memory.grow returns a region contiguous with the previous heap end, so a freed heap can never coalesce with a later grow. WasmGrowAndExtend checks old_end == new_base and extends, so freed tail space merges with the new pages. The two are equal when allocations are uniform and fully freed (page_churn) and diverge as soon as the live set ratchets up or fragments.
The cost of Extend is code size: the optimized dynamic module is +150 bytes (1627 to 1777, about +9%), which I assume is why Claim is the documented default. So it's a real tradeoff, 150 bytes of code against 2-10x linear memory for long-running modules, and your call which side the default lands on.
A few ways to go: flip new_wasm_dynamic_allocator to WasmGrowAndExtend; fold the two sources into one (extend-when-contiguous, which is what Extend already does, falling back to claim otherwise); or keep both and just call out the tradeoff more loudly for long-running use. One thing I'd argue against is geometric or over-provisioned growth: since wasm can't shrink, that only raises the permanent high-water mark, so the lever here is coalescing, not growth policy.
I measured this with a small wasm-bindgen test that runs each workload under a fresh allocator and reads the memory_size() delta. Happy to send it as a PR or fold it into the wasm regression tests if useful.
Using the dynamic wasm allocator (
new_wasm_dynamic_allocator, i.e.WasmGrowAndClaim) in a long-running module, I measured it holding a lot more linear memory thanWasmGrowAndExtendon the same workload. Since wasm memory only ever grows, that high-water mark is permanent, so I wanted to put the numbers in front of you and ask whether the default should change.Pages of linear memory grown (1 page = 64 KiB), each workload run against a fresh allocator per source:
ratchetis a buffer that grows by about a page each step, freeing the previous one;random_actionsis the random alloc/dealloc/realloc churn from the regression suite;page_churnis a 65520-byte alloc/free loop with one small pinned allocation.The difference is that
WasmGrowAndClaim::acquirealwaysclaims a fresh heap, even thoughmemory.growreturns a region contiguous with the previous heap end, so a freed heap can never coalesce with a later grow.WasmGrowAndExtendchecksold_end == new_baseandextends, so freed tail space merges with the new pages. The two are equal when allocations are uniform and fully freed (page_churn) and diverge as soon as the live set ratchets up or fragments.The cost of Extend is code size: the optimized dynamic module is +150 bytes (1627 to 1777, about +9%), which I assume is why Claim is the documented default. So it's a real tradeoff, 150 bytes of code against 2-10x linear memory for long-running modules, and your call which side the default lands on.
A few ways to go: flip
new_wasm_dynamic_allocatortoWasmGrowAndExtend; fold the two sources into one (extend-when-contiguous, which is what Extend already does, falling back to claim otherwise); or keep both and just call out the tradeoff more loudly for long-running use. One thing I'd argue against is geometric or over-provisioned growth: since wasm can't shrink, that only raises the permanent high-water mark, so the lever here is coalescing, not growth policy.I measured this with a small wasm-bindgen test that runs each workload under a fresh allocator and reads the
memory_size()delta. Happy to send it as a PR or fold it into the wasm regression tests if useful.