5bfd250c perf(router-core): skip symbol enumeration for plain replaceEqualDeep inputs
Every object pair `replaceEqualDeep` visited paid for three native key
lookups per side: `Object.keys`, `Object.getOwnPropertyNames` (to detect
non-enumerable props) and `Object.getOwnPropertySymbols`. Measured on a
4-key literal the first two cost 7.6 ns and 10 ns, the symbol lookup
34 ns — 31 ns even on `{}` — so two thirds of the enumeration cost went
into a key type that router data never carries. (`Reflect.ownKeys` as a
single replacement call was tried and is 100 ns.)
Symbol keys and non-enumerable keys still have to be handled correctly:
Apollo's `preloadQuery` refs carry symbol keys and broke under
`defaultStructuralSharing` when copies dropped them (#4237). Instead of
comparing and copying such objects key by key, they are now opaque: an
object with non-enumerable own keys on either side, or symbol keys on
`next`, makes `next` pass through untouched. That keeps both guarantees
that matter — no partial copy, no stale `prev` returned for it — and only
gives up structural sharing for those objects.
`prev` is not scanned for symbols on purpose. It is normally an earlier
`next`, so a symbol-carrying `prev` meets a symbol-carrying `next` and the
`next` check fires; a symbol-carrying `prev` with a symbol-free, otherwise
equal `next` returns `prev` (covered by a test). Plain objects are now
compared and copied by `Object.keys` alone, and `getEnumerableOwnKeys`
is gone.
Paired runs against the previous version (hz, 1000 calls per iteration):
equal empty objects 14.8k -> 18.3k (+24%)
equal empty null-proto objects 5.1k -> 6.3k (+24%)
equal flat search 7.3k -> 8.2k (+12%)
flat, one changed leaf 6.1k -> 6.9k (+12%)
equal nested search 3.0k -> 3.1k (+4%)
nested, sharing subtrees 2.6k -> 2.7k (+7%)
equal array of objects 609 -> 729 (+20%)
array, one changed item 551 -> 640 (+16%)
wide (12 keys) changed, null-proto 1.8k -> 2.0k (+9%)
Tests now document the pass-through contract for symbol-keyed objects,
including the deliberate `prev`-only asymmetry. 9f3e6472 perf(router-core): skip symbol enumeration for plain replaceEqualDeep inputs
Every object pair `replaceEqualDeep` visited paid for three native key
lookups per side: `Object.keys`, `Object.getOwnPropertyNames` (to detect
non-enumerable props) and `Object.getOwnPropertySymbols`. Measured on a
4-key literal the first two cost 7.6 ns and 10 ns, the symbol lookup
34 ns — 31 ns even on `{}` — so two thirds of the enumeration cost went
into a key type that router data never carries. (`Reflect.ownKeys` as a
single replacement call was tried and is 100 ns.)
Symbol keys and non-enumerable keys still have to be handled correctly:
Apollo's `preloadQuery` refs carry symbol keys and broke under
`defaultStructuralSharing` when copies dropped them (#4237). Instead of
comparing and copying such objects key by key, they are now opaque: an
object with non-enumerable own keys on either side, or symbol keys on
`next`, makes `next` pass through untouched. That keeps both guarantees
that matter — no partial copy, no stale `prev` returned for it — and only
gives up structural sharing for those objects.
`prev` is not scanned for symbols on purpose. It is normally an earlier
`next`, so a symbol-carrying `prev` meets a symbol-carrying `next` and the
`next` check fires; a symbol-carrying `prev` with a symbol-free, otherwise
equal `next` returns `prev` (covered by a test). Plain objects are now
compared and copied by `Object.keys` alone, and `getEnumerableOwnKeys`
is gone.
Paired runs against the previous version (hz, 1000 calls per iteration):
equal empty objects 14.8k -> 18.3k (+24%)
equal empty null-proto objects 5.1k -> 6.3k (+24%)
equal flat search 7.3k -> 8.2k (+12%)
flat, one changed leaf 6.1k -> 6.9k (+12%)
equal nested search 3.0k -> 3.1k (+4%)
nested, sharing subtrees 2.6k -> 2.7k (+7%)
equal array of objects 609 -> 729 (+20%)
array, one changed item 551 -> 640 (+16%)
wide (12 keys) changed, null-proto 1.8k -> 2.0k (+9%)
Tests now document the pass-through contract for symbol-keyed objects,
including the deliberate `prev`-only asymmetry. 8cfbaa05 perf(router-core): skip symbol enumeration for plain replaceEqualDeep inputs
Every object pair `replaceEqualDeep` visited paid for three native key
lookups per side: `Object.keys`, `Object.getOwnPropertyNames` (to detect
non-enumerable props) and `Object.getOwnPropertySymbols`. Measured on a
4-key literal the first two cost 7.6 ns and 10 ns, the symbol lookup
34 ns — 31 ns even on `{}` — so two thirds of the enumeration cost went
into a key type that router data never carries. (`Reflect.ownKeys` as a
single replacement call was tried and is 100 ns.)
Symbol keys and non-enumerable keys still have to be handled correctly:
Apollo's `preloadQuery` refs carry symbol keys and broke under
`defaultStructuralSharing` when copies dropped them (#4237). Instead of
comparing and copying such objects key by key, they are now opaque: an
object with non-enumerable own keys on either side, or symbol keys on
`next`, makes `next` pass through untouched. That keeps both guarantees
that matter — no partial copy, no stale `prev` returned for it — and only
gives up structural sharing for those objects.
`prev` is not scanned for symbols on purpose. It is normally an earlier
`next`, so a symbol-carrying `prev` meets a symbol-carrying `next` and the
`next` check fires; a symbol-carrying `prev` with a symbol-free, otherwise
equal `next` returns `prev` (covered by a test). Plain objects are now
compared and copied by `Object.keys` alone, and `getEnumerableOwnKeys`
is gone.
Paired runs against the previous version (hz, 1000 calls per iteration):
equal empty objects 14.8k -> 18.3k (+24%)
equal empty null-proto objects 5.1k -> 6.3k (+24%)
equal flat search 7.3k -> 8.2k (+12%)
flat, one changed leaf 6.1k -> 6.9k (+12%)
equal nested search 3.0k -> 3.1k (+4%)
nested, sharing subtrees 2.6k -> 2.7k (+7%)
equal array of objects 609 -> 729 (+20%)
array, one changed item 551 -> 640 (+16%)
wide (12 keys) changed, null-proto 1.8k -> 2.0k (+9%)
Tests now document the pass-through contract for symbol-keyed objects,
including the deliberate `prev`-only asymmetry. ddc6c0b7 perf(router-core): skip symbol enumeration for plain replaceEqualDeep inputs
Every object pair `replaceEqualDeep` visited paid for three native key
lookups per side: `Object.keys`, `Object.getOwnPropertyNames` (to detect
non-enumerable props) and `Object.getOwnPropertySymbols`. Measured on a
4-key literal the first two cost 7.6 ns and 10 ns, the symbol lookup
34 ns — 31 ns even on `{}` — so two thirds of the enumeration cost went
into a key type that router data never carries. (`Reflect.ownKeys` as a
single replacement call was tried and is 100 ns.)
Symbol keys and non-enumerable keys still have to be handled correctly:
Apollo's `preloadQuery` refs carry symbol keys and broke under
`defaultStructuralSharing` when copies dropped them (#4237). Instead of
comparing and copying such objects key by key, they are now opaque: an
object with non-enumerable own keys on either side, or symbol keys on
`next`, makes `next` pass through untouched. That keeps both guarantees
that matter — no partial copy, no stale `prev` returned for it — and only
gives up structural sharing for those objects.
`prev` is not scanned for symbols on purpose. It is normally an earlier
`next`, so a symbol-carrying `prev` meets a symbol-carrying `next` and the
`next` check fires; a symbol-carrying `prev` with a symbol-free, otherwise
equal `next` returns `prev` (covered by a test). Plain objects are now
compared and copied by `Object.keys` alone, and `getEnumerableOwnKeys`
is gone.
Paired runs against the previous version (hz, 1000 calls per iteration):
equal empty objects 14.8k -> 18.3k (+24%)
equal empty null-proto objects 5.1k -> 6.3k (+24%)
equal flat search 7.3k -> 8.2k (+12%)
flat, one changed leaf 6.1k -> 6.9k (+12%)
equal nested search 3.0k -> 3.1k (+4%)
nested, sharing subtrees 2.6k -> 2.7k (+7%)
equal array of objects 609 -> 729 (+20%)
array, one changed item 551 -> 640 (+16%)
wide (12 keys) changed, null-proto 1.8k -> 2.0k (+9%)
Tests now document the pass-through contract for symbol-keyed objects,
including the deliberate `prev`-only asymmetry.