Skip to content

[PERF] Store destroyable meta on the destroyable - #21565

Draft
NullVoxPopuli-ai-agent wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli-ai-agent:perf/destroyable-meta-on-object
Draft

[PERF] Store destroyable meta on the destroyable#21565
NullVoxPopuli-ai-agent wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli-ai-agent:perf/destroyable-meta-on-object

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor

Breaking. Filed for the numbers and the discussion, not to merge as is. Split out of #21564 at @NullVoxPopuli's request.

getDestroyableMeta is the hottest Glimmer function in a CPU profile of smoke-tests/benchmark-app, at 1.7% self time. Every associated destroyable costs a WeakMap.get, and the first use of an object as a WeakMap key forces an identity hash onto it. Rendering a list associates one per item.

Storing meta on the destroyable under a symbol instead is worth 23% of VM time.

Numbers

Measured in Node against SimpleDOM, so DOM cost does not mask the VM. 15 interleaved pairs, medians in ms:

phase main branch delta
create 32.60 25.11 -24.4%
update every 10th 11.14 8.87 -24.4%
select row 5.90 4.73 -17.7%
swap rows 5.87 5.21 -13.5%
remove row 5.40 4.79 -4.1%
append 1000 53.08 39.48 -25.6%
clear 13.93 10.16 -26.1%
total 132.73 103.40 -23.1%

I have not measured this one alone through pnpm bench. On that harness, browser DOM work dominates and an A/A run reports false positives of about 7% on individual phases, so a single phase number there would not mean much.

Why it is breaking

main:   registerDestructor(Object.freeze({}), fn)  ->  OK
branch: registerDestructor(Object.freeze({}), fn)  ->  TypeError: Cannot add property
                                                       Symbol(DESTROYABLE_META),
                                                       object is not extensible

registerDestructor, destroy and associateDestroyableChild are public through @ember/destroyable, and they accept frozen and sealed objects today. A symbol property is also visible to Reflect.ownKeys and Object.getOwnPropertySymbols.

Nothing in the framework freezes a destroyable and no test covers it, so the suite passes either way. That is the decision this PR exists to raise.

If the answer is that Ember cannot break it, one option is a symbol with a WeakMap fallback for non-extensible objects. That keeps the fast path and never throws, at the cost of an Object.isExtensible check when meta is first created. I have not measured that variant.

Also in here

Destroyable tracking needs its own registry, because meta is no longer enumerable. It is a Set populated only between enableDestroyableTracking() and assertDestroyablesDestroyed().

Testing

9449 tests, 9432 pass, 17 skip, 0 fail. Identical to main on the same machine. tsc, eslint and prettier are clean.

Conflicts textually with #21564, which also touches destroy.

BREAKING. Opening this for the numbers and the discussion, not to merge as is.

`getDestroyableMeta` is the hottest Glimmer function in a CPU profile of
`smoke-tests/benchmark-app`, at 1.7% self time. Every associated destroyable
costs a `WeakMap.get`, and the first use of an object as a `WeakMap` key also
forces an identity hash onto it. Rendering a list associates one per item.

Moving meta onto the destroyable under a symbol is worth 23% of VM time on that
workload. Measured in Node against SimpleDOM so DOM cost does not mask it,
15 interleaved pairs, medians in ms:

| phase             | main   | branch | delta  |
| ----------------- | -----: | -----: | -----: |
| create            | 32.60  | 25.11  | -24.4% |
| update every 10th | 11.14  |  8.87  | -24.4% |
| select row        |  5.90  |  4.73  | -17.7% |
| swap rows         |  5.87  |  5.21  | -13.5% |
| remove row        |  5.40  |  4.79  |  -4.1% |
| append 1000       | 53.08  | 39.48  | -25.6% |
| clear             | 13.93  | 10.16  | -26.1% |
| total             | 132.73 | 103.40 | -23.1% |

Why it is breaking:

    main:   registerDestructor(Object.freeze({}), fn)  ->  OK
    branch: registerDestructor(Object.freeze({}), fn)  ->  TypeError

`registerDestructor`, `destroy` and `associateDestroyableChild` are public
through `@ember/destroyable`, and they accept frozen and sealed objects today.
A symbol property is also visible to `Reflect.ownKeys` and
`Object.getOwnPropertySymbols`.

Destroyable tracking needs its own registry, because meta is no longer
enumerable. It is a `Set` populated only between `enableDestroyableTracking()`
and `assertDestroyablesDestroyed()`.

Tests: 9449 tests, 9432 pass, 17 skip, 0 fail. Identical to main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor Author

Benchmarked against main with pnpm bench, fidelity 20, 8x CPU throttle.

Total: -3.66% [-6.26% to -1.41%]. No phase regresses.

phase delta
duration (total) -3.66% [-6.26%, -1.41%]
selectSecondRow1 -14.49%
clearItems4 -10.33%
clearManyItems2 -9.96%
append1000Items1 -8.94%
selectFirstRow1 -6.94%
clearManyItems1 -5.85%
render10000Items2 -5.10%
clearItems2 -2.54%
the other 14 phases no difference

For scale, I ran the same harness from a main worktree against itself. That reported duration as no difference across an interval of -1368ms to +739ms, so a total whose whole interval sits below zero is a real result rather than the harness moving on its own. That A/A run also produced two falsely significant phase results at about 7%, so treat individual phases here as secondary to the total.

Every clear phase improves. That is where getDestroyableMeta is called most, and this removes a WeakMap lookup from each call.

Worth comparing against #21564, which keeps the WeakMap. There clearItems2 and clearManyItems2 regress reproducibly. Here the same two phases improve by 2.54% and 9.96%.

So the ordering is clear: this is the destroyable win, and it is the largest single change measured in this batch. It still throws on frozen and sealed destroyables, which is the open question at the top of the description.

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.

2 participants