Repository navigation
Description
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
function deleteHeavy(n: number): number {
const o: Record<string, number> = {};
let s = 0;
for (let i = 0; i < n; i++) {
const k = "k" + (i % 500);
o[k] = i;
s += o[k];
delete o[k]; // the object never holds more than one live key
}
return s;
}
// 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_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
- Stable-token deletes + per-key IC validation — perf: populated delete is ~14x node after tombstones — the residual is IC retirement per delete, not the delete itself #9064's option (1). For THIS bench it removes the delete-side retirement; the read IC then survives the delete and only the append still moves identity.
- 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 eachk(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_msstays ≤ node (currently 13 vs 19 — must not regress).- delete_penalty back under ~2× (node's is 1.6×).
- feat(runtime): O(1) object deletes via tombstones (flag-gated; populated delete 6.5x on, -11% off) #9029 differential battery byte-identical, both tombstone flag states.
Activity
proggeramlug commentedon Aug 30, 2026
Implementer handoff notes for the Codex sessions are on #9064 (state-of-main since filing, coordination with in-flight latch work, measurement discipline, the correctness battery locations, and bench baselines) — they apply to this issue identically since the two share a root cause. One addition specific here: when rewriting the bench header comment as the issue asks, keep BOTH columns' history (the 2026-08-28 numbers it cites were real at the time — the point is the ledger inverted, not that it was wrong).