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
Tombstone deletes shipped default-on (#9029 → #9038): bench_populated_delete went 2030 → ~315 ms on the Linux box (6.5×) and ~2100 → 332 ms on the Mac mini. The remaining gap vs node is ~14× (node 21-24 ms), and it is structural, not constant-factor — this issue records where the time is and the ranked options, so the next campaign doesn't re-derive it.
Cross-engine, Mac mini, min-of-7 interleaved, self-timed: node 23 · perry 332 · porffor 113 · scriptc 24. The scriptc number is the data point that matters: an AOT competitor does this at node parity, almost certainly via dictionary-style storage — parity is achievable in an AOT setting.
Where the ~315 ms goes (perf, symbolized, flag-on, Linux)
The profile is a flat tail — after the tombstone work no symbol is above ~20%:
shape_slot_lookup_verdict ~20% total, split by caller (DWARF callgraph): 8.5% the delete's own slot-find (js_object_delete_field → keys_find_slot_by_key_ptr), 3.8% read-path misses, 3.5% write-path misses. Per call it's TLS + RefCell borrow + two hash lookups (~35-40 ns) — call count × fixed cost, not list length.
Publish machinery per delete: remove_descriptor_id_from_facts_index 2.8%, shape_descriptor_by_id 2.8%, shape_descriptor_ensure_with_holes 1.7%, plus FastKeyHasher 2.8% and from_utf8 2.8% (SSO decode during candidate validation).
The rest: get_field_by_name tails, js_put_value_set, js_object_delete_field, accessor probes — each 1-3%.
The structural cause
Every delete mints a fresh shape token (this is forced: per-site dyn-IC ways live in generated-code globals the runtime cannot reach, so retiring a deleted key's cached (token, key) → slot entries requires changing the token — see #9029's description). Consequence: after each delete, EVERY inline cache on that receiver misses once — the loop's read and write both take their miss paths every iteration. Node doesn't pay this: V8's dictionary-mode ICs validate against the dictionary, not a shape, so an unrelated delete invalidates nothing.
Ranked options
Per-key invalidation instead of whole-token retirement (the real fix, larger design). Keep the shape token STABLE across tombstone deletes; make IC hits validate the cached slot's key content (one load+compare — the slot still holds the key unless tombstoned, and a tombstoned slot holds TAG_HOLE, which never content-matches) or check a per-object delete epoch only on the deleted key's hash class. Either kills the miss-per-delete for the 499 untouched keys. Risk surface: every IC hit gains a compare; measure on the write/read benches that currently BEAT node before accepting.
Cheapen the per-delete publish: the mint+sweep+facts-index dance is ~8-10% combined. With (1) in place most of it disappears entirely (no successor id needed per delete — only the hole write + epoch bump).
Verdict-lookup fixed cost (~20%): one-entry TLS memo (keys_id → *mut ShapeIndex) with Boxed ShapeIndex values for address stability, invalidated on any indices insert/remove. Saves the borrow + first hash lookup on the 3-4 lookups per op. Bounded win (~40-60 ms of 315).
Where we are
Tombstone deletes shipped default-on (#9029 → #9038):
bench_populated_deletewent 2030 → ~315 ms on the Linux box (6.5×) and ~2100 → 332 ms on the Mac mini. The remaining gap vs node is ~14× (node 21-24 ms), and it is structural, not constant-factor — this issue records where the time is and the ranked options, so the next campaign doesn't re-derive it.Self-contained repro
Cross-engine, Mac mini, min-of-7 interleaved, self-timed: node 23 · perry 332 · porffor 113 · scriptc 24. The scriptc number is the data point that matters: an AOT competitor does this at node parity, almost certainly via dictionary-style storage — parity is achievable in an AOT setting.
Where the ~315 ms goes (perf, symbolized, flag-on, Linux)
The profile is a flat tail — after the tombstone work no symbol is above ~20%:
shape_slot_lookup_verdict~20% total, split by caller (DWARF callgraph): 8.5% the delete's own slot-find (js_object_delete_field → keys_find_slot_by_key_ptr), 3.8% read-path misses, 3.5% write-path misses. Per call it's TLS +RefCellborrow + two hash lookups (~35-40 ns) — call count × fixed cost, not list length.remove_descriptor_id_from_facts_index2.8%,shape_descriptor_by_id2.8%,shape_descriptor_ensure_with_holes1.7%, plusFastKeyHasher2.8% andfrom_utf82.8% (SSO decode during candidate validation).get_field_by_nametails,js_put_value_set,js_object_delete_field, accessor probes — each 1-3%.The structural cause
Every delete mints a fresh shape token (this is forced: per-site dyn-IC ways live in generated-code globals the runtime cannot reach, so retiring a deleted key's cached
(token, key) → slotentries requires changing the token — see #9029's description). Consequence: after each delete, EVERY inline cache on that receiver misses once — the loop's read and write both take their miss paths every iteration. Node doesn't pay this: V8's dictionary-mode ICs validate against the dictionary, not a shape, so an unrelated delete invalidates nothing.Ranked options
TAG_HOLE, which never content-matches) or check a per-object delete epoch only on the deleted key's hash class. Either kills the miss-per-delete for the 499 untouched keys. Risk surface: every IC hit gains a compare; measure on the write/read benches that currently BEAT node before accepting.(keys_id → *mut ShapeIndex)withBoxedShapeIndexvalues for address stability, invalidated on any indices insert/remove. Saves the borrow + first hash lookup on the 3-4 lookups per op. Bounded win (~40-60 ms of 315).DELETE_TOMBSTONES_DESIGN.md§3/§7).Acceptance
bench_populated_delete≤ 3× node on the mini AND the Linux box, min-of-7 interleaved vs the exact main tip.