Skip to content

Avoid closure allocation in destroy() - #21600

Open
NullVoxPopuli wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli:nvp/perf/destroyable-iterate
Open

Avoid closure allocation in destroy()#21600
NullVoxPopuli wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli:nvp/perf/destroyable-iterate

Conversation

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

iterate in @glimmer/destroyable now takes an extra argument that is forwarded to the callback, so destroy() uses module-level functions instead of allocating three closures per destroyable. removeChildFromParent swaps its parameter order to fit that shape.

🤖 Generated with Claude Code

Split out of nvp/simplify-some-vm-hot-paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@NullVoxPopuli NullVoxPopuli changed the title [PERF] Avoid closure allocation in destroy() Avoid closure allocation in destroy() Sep 9, 2026
@NullVoxPopuli

Copy link
Copy Markdown
Contributor Author

Benchmark: main (bdb2580) vs nvp/perf/destroyable-iterate (20551cf)

Ran via pnpm bench (the krausest js-framework-benchmark scenario in smoke-tests/benchmark-app, tracerbench, 20 samples each, 8x CPU throttle). Negative delta means the PR is faster. Only rows tracerbench marks significant (p < 0.05) are meaningful.

Phase main median PR median Δ (estimator) 95% CI Significant
Total duration 10,359ms 10,501ms 138ms (+1.3%) -110ms .. 324ms
render 160ms 157ms -3ms (-1.9%) -9ms .. 4ms
render1000Items1 375ms 372ms -1ms (-0.4%) -13ms .. 8ms
clearItems1 40ms 39ms -1ms (-3.1%) -3ms .. 0ms ✅
render1000Items2 312ms 315ms 3ms (+1.0%) -5ms .. 12ms
clearItems2 138ms 136ms -2ms (-1.6%) -4ms .. -1ms ✅
render10000Items1 2,926ms 2,889ms -28ms (-0.9%) -129ms .. 17ms
clearManyItems1 385ms 376ms -9ms (-2.4%) -14ms .. -5ms ✅
render10000Items2 2,917ms 2,845ms -79ms (-2.7%) -122ms .. -21ms ✅
clearManyItems2 275ms 266ms -11ms (-4.0%) -16ms .. -7ms ✅
render1000Items3 289ms 457ms 167ms (+57.7%) 152ms .. 176ms ✅
append1000Items1 380ms 359ms -31ms (-8.1%) -46ms .. -5ms ✅
append1000Items2 358ms 373ms 11ms (+3.1%) -7ms .. 35ms
updateEvery10thItem1 316ms 363ms 47ms (+14.8%) 38ms .. 56ms ✅
updateEvery10thItem2 356ms 376ms 22ms (+6.1%) 4ms .. 33ms ✅
selectFirstRow1 88ms 73ms -15ms (-16.6%) -20ms .. -9ms ✅
selectSecondRow1 67ms 71ms 2ms (+3.4%) -2ms .. 6ms
removeFirstRow1 223ms 257ms 35ms (+15.7%) 19ms .. 48ms ✅
removeSecondRow1 215ms 249ms 30ms (+14.1%) 16ms .. 40ms ✅
swapRows1 153ms 169ms 16ms (+10.2%) 5ms .. 25ms ✅
swapRows2 153ms 180ms 25ms (+16.5%) 6ms .. 41ms ✅
clearItems4 111ms 127ms 16ms (+14.5%) 13ms .. 19ms ✅
paint 8ms 7ms -1ms (-8.3%) -2ms .. 1ms

@NullVoxPopuli

Copy link
Copy Markdown
Contributor Author

Caveat on the table above: a +58% swing on render1000Items3 (and +10 to 16% on the update phases) is not plausible for a change that only touches teardown, and the benchmark machine had a device-management daemon pinned at 100% CPU during this run. Treat these numbers as noise for now. A rerun on a quieter machine state is queued and will be posted as a follow-up.

@NullVoxPopuli

Copy link
Copy Markdown
Contributor Author

Rerun of the benchmark above, same build, same settings, with the machine quieter (load average around 1.7 versus 4 to 5 during the first run). Where this table and the first one disagree, trust the rows that are significant in both.

Benchmark: main (bdb2580) vs nvp/perf/destroyable-iterate (20551cf)

Ran via pnpm bench (the krausest js-framework-benchmark scenario in smoke-tests/benchmark-app, tracerbench, 20 samples each, 8x CPU throttle). Negative delta means the PR is faster. Only rows tracerbench marks significant (p < 0.05) are meaningful.

Phase main median PR median Δ (estimator) 95% CI Significant
Total duration 8,893ms 8,968ms 186ms (+2.1%) 12ms .. 364ms ✅
render 137ms 137ms -1ms (-0.8%) -7ms .. 3ms
render1000Items1 323ms 326ms 2ms (+0.5%) -6ms .. 7ms
clearItems1 36ms 35ms 0ms (-1.1%) -2ms .. 1ms
render1000Items2 273ms 273ms 0ms (+0.1%) -5ms .. 9ms
clearItems2 134ms 131ms -3ms (-1.9%) -5ms .. 0ms ✅
render10000Items1 2,552ms 2,590ms 2ms (+0.1%) -39ms .. 50ms
clearManyItems1 358ms 350ms -7ms (-1.9%) -13ms .. -4ms ✅
render10000Items2 2,528ms 2,467ms -64ms (-2.5%) -88ms .. -10ms ✅
clearManyItems2 254ms 241ms -9ms (-3.6%) -16ms .. -5ms ✅
render1000Items3 230ms 386ms 157ms (+68.2%) 150ms .. 164ms ✅
append1000Items1 364ms 298ms -60ms (-16.4%) -90ms .. -29ms ✅
append1000Items2 309ms 305ms 10ms (+3.2%) -11ms .. 20ms
updateEvery10thItem1 266ms 317ms 52ms (+19.8%) 44ms .. 65ms ✅
updateEvery10thItem2 285ms 301ms 14ms (+4.9%) 2ms .. 27ms ✅
selectFirstRow1 71ms 59ms -10ms (-14.4%) -15ms .. -5ms ✅
selectSecondRow1 52ms 60ms 6ms (+11.6%) 2ms .. 9ms ✅
removeFirstRow1 177ms 200ms 27ms (+15.0%) 18ms .. 37ms ✅
removeSecondRow1 165ms 192ms 28ms (+16.8%) 19ms .. 35ms ✅
swapRows1 111ms 131ms 19ms (+17.4%) 15ms .. 24ms ✅
swapRows2 114ms 133ms 20ms (+17.8%) 15ms .. 25ms ✅
clearItems4 101ms 115ms 16ms (+15.5%) 13ms .. 18ms ✅
paint 10ms 10ms 0ms (+1.3%) -2ms .. 4ms

@NullVoxPopuli

Copy link
Copy Markdown
Contributor Author

Correction to my caveat above: the quiet rerun reproduces the first run closely (render1000Items3 +68% vs +58%, remove/swap rows +15 to 18% both times, clear phases 2 to 4% faster both times), so this is a real effect of the change and not machine noise.

The shape is odd though. Total duration barely moves (+1.3% not significant, then +2.1%), while adjacent phases swing hard in opposite directions: selectFirstRow1 is 14% faster and selectSecondRow1 is 12% slower, every clear phase is faster and the phase right after each clear is slower. That pattern usually means the change moved when work happens rather than how much: fewer closure allocations in destroy() shift GC pauses out of the clear phases and into whatever runs next. It could also be that the fn(item, arg) call site in iterate now sees four distinct targets and an arg that alternates between undefined and an object, so it no longer inlines. I have not confirmed either. The tracerbench traces from both runs are available if someone wants to compare GC events per phase before this merges.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant