You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
perf: add/delete churn on a small object is ~33x node — shape identity turns over on EVERY op (and the bench's own header now argues the opposite of reality) #9065
bench_dynamic_property_keys.ts's delete-heavy loop: perry 981 ms vs node 30 ms (Mac mini, min-of-7, current main with #9038 default-on). Cross-engine: scriptc 29 ms (node parity), porffor fails to compile it. Sibling issue for the large-object case: #9064 — same root cause, different scale and different dominant term.
Self-contained repro
functiondeleteHeavy(n: number): number{consto: Record<string,number>={};lets=0;for(leti=0;i<n;i++){constk="k"+(i%500);o[k]=i;s+=o[k];deleteo[k];// the object never holds more than one live key}returns;}// N = 300_000 → perry ~981 ms, node ~30 ms.
Unlike #9064's stable 500-key receiver, this object oscillates around one live key. Per iteration: an append (new shape id), a read (IC primed against it), a delete (successor id minted, every cache retired). ~3.3 µs/op vs node's ~0.1 µs — the shape-identity machinery IS the loop.
What tombstones already do here (and why it's not enough)
The tombstone threshold (key_count >= 16 before squeezing) means the array grows to 16 slots, squeezes back to ~1, repeats — amortized ~1 slot-copy per delete. Fine. What remains is that all three ops per iteration change or chase shape identity:
the append publishes a successor id (lineage mint + facts-index insert),
the delete publishes another one + sweeps stale ids for the owned address,
and both retirements force the read's and write's ICs to miss every single iteration (js_put_value_set_dyn_ic_miss and the get_field_by_name tail dominate the old profile of this binary, and the descriptor mint/lookup pair — shape_descriptor_by_id + ensure_with_holes — runs multiple times per op).
Append-side identity churn on re-add: a re-added key appends and mints an id per append. For churn shapes, a small transition cache keyed (predecessor_id, key) → successor id already exists for first-time builds — verify it HITS on the delete→re-add cycle (the tombstone delete's fresh generation may make every re-add transition a cold miss forever). If each k(i%500) cycle re-mints instead of reusing 500 cached transitions, that's the memory-and-time leak to close.
Small-object fast path: below the 16-key threshold the per-delete publish could collapse to hole-write + epoch bump with NO descriptor machinery at all once (1) lands — a one-live-key object needs none of it.
Housekeeping in the same change
The bench's header comment (dated 2026-08-28) now argues the inverse of reality and should be rewritten when this is touched: it says the delete penalty is 1.1× ('LOWER than node's — dictionary mode refuted') and overwrite is the real 60× gap. After the write campaigns, overwrite is 13 ms and beats node (19 ms); the delete penalty is 75×. The ratio that used to refute the dictionary-mode argument is now the strongest evidence for #9064's per-key-invalidation work.
Acceptance
delete_heavy_ms ≤ 3× node on the mini; overwrite_ms stays ≤ node (currently 13 vs 19 — must not regress).
The number
bench_dynamic_property_keys.ts's delete-heavy loop: perry 981 ms vs node 30 ms (Mac mini, min-of-7, current main with #9038 default-on). Cross-engine: scriptc 29 ms (node parity), porffor fails to compile it. Sibling issue for the large-object case: #9064 — same root cause, different scale and different dominant term.Self-contained repro
Unlike #9064's stable 500-key receiver, this object oscillates around one live key. Per iteration: an append (new shape id), a read (IC primed against it), a delete (successor id minted, every cache retired). ~3.3 µs/op vs node's ~0.1 µs — the shape-identity machinery IS the loop.
What tombstones already do here (and why it's not enough)
The tombstone threshold (
key_count >= 16before squeezing) means the array grows to 16 slots, squeezes back to ~1, repeats — amortized ~1 slot-copy per delete. Fine. What remains is that all three ops per iteration change or chase shape identity:js_put_value_set_dyn_ic_missand theget_field_by_nametail dominate the old profile of this binary, and the descriptor mint/lookup pair —shape_descriptor_by_id+ensure_with_holes— runs multiple times per op).What to optimize, in order
(predecessor_id, key)→ successor id already exists for first-time builds — verify it HITS on the delete→re-add cycle (the tombstone delete's fresh generation may make every re-add transition a cold miss forever). If eachk(i%500)cycle re-mints instead of reusing 500 cached transitions, that's the memory-and-time leak to close.Housekeeping in the same change
The bench's header comment (dated 2026-08-28) now argues the inverse of reality and should be rewritten when this is touched: it says the delete penalty is 1.1× ('LOWER than node's — dictionary mode refuted') and overwrite is the real 60× gap. After the write campaigns, overwrite is 13 ms and beats node (19 ms); the delete penalty is 75×. The ratio that used to refute the dictionary-mode argument is now the strongest evidence for #9064's per-key-invalidation work.
Acceptance
delete_heavy_ms≤ 3× node on the mini;overwrite_msstays ≤ node (currently 13 vs 19 — must not regress).