From b81b57c4c881aa5817e076a4ace3f7c55f082b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 1 Sep 2026 00:22:31 +0200 Subject: [PATCH] =?UTF-8?q?perf(runtime):=20tombstone=20deletes=20default-?= =?UTF-8?q?on=20again=20=E2=80=94=20#9317=20fixed=20the=20#9200=20corrupti?= =?UTF-8?q?on=20that=20rolled=20them=20back?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #9038 shipped O(1) tombstone deletes default-on; #9212 returned them to opt-in because #9200 let an evacuating minor sweep a deleted receiver's live keys array (an unarmed successor descriptor was the only root). #9317 fixed that structurally: every post-birth ShapeId publish routes through stamp_object_shape_id_with_carrier_note, which arms old_carrier for any non-nursery receiver, so the descriptor and its keys array are rooted by construction. The default flips back to ON; PERRY_OBJECT_TOMBSTONES=0 remains the kill switch (the same switch that attributed #9108, #9110 and #9200 each in one command). Verified on this build: both #9200 fixtures (tower + oldgen) x five configurations (default, kill, HL=8+FORCE_EVACUATE, HL=4+FORCE+VERIFY, evac+kill) all byte-identical to node, 3/3 runs each; tombstone unit suite 27/27; populated-delete bench (500 keys, 200k delete/re-add rounds, interleaved): default 82-89 ms vs kill-switch 2137-2352 ms — the flip restores a ~26x improvement (node: 24-25 ms on the same box). Claude-Session: https://claude.ai/code/session_01TE3JXAYXtdnKcLu8TCFWR6 --- changelog.d/9330-tombstones-default-on.md | 9 +++++++++ crates/perry-runtime/src/object/delete_rest.rs | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 changelog.d/9330-tombstones-default-on.md diff --git a/changelog.d/9330-tombstones-default-on.md b/changelog.d/9330-tombstones-default-on.md new file mode 100644 index 0000000000..b24a3b21d7 --- /dev/null +++ b/changelog.d/9330-tombstones-default-on.md @@ -0,0 +1,9 @@ +### Performance + +- **Tombstone deletes are default-on again.** #9038's O(1) delete (6.5× on + populated delete, ~15× vs node at the time) was rolled back to opt-in by + #9212 because of #9200 — an evacuating minor could sweep a deleted + receiver's live keys array, leaving it shapeless. #9317 fixed that + structurally (every post-birth ShapeId publish arms `old_carrier` through a + single funnel), so the win returns to the default configuration. + `PERRY_OBJECT_TOMBSTONES=0` remains the kill switch. diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index 3507a09cee..ddde8fd752 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -1500,13 +1500,19 @@ fn object_tombstone_deletes_enabled() -> bool { } static ON: std::sync::OnceLock = std::sync::OnceLock::new(); *ON.get_or_init(|| { - // #9200: keep tombstones opt-in until an evacuating-GC interaction - // with class dispatch is fixed. The default-on route can restore a - // deleted receiver to its canonical class shape after relocation, - // making Object.keys() empty and fixed-slot reads return wrong data. - matches!( + // DEFAULT-ON again (#9038's 6.5x populated-delete win). #9212 made + // this opt-in because of #9200 — an unarmed successor descriptor let + // an evacuating minor sweep a deleted receiver's live keys array — + // and #9317 fixed that structurally: every post-birth ShapeId publish + // now routes through `stamp_object_shape_id_with_carrier_note`, which + // arms `old_carrier` for any non-nursery receiver, so the descriptor + // (and the keys array only it reaches) is rooted by construction. + // `PERRY_OBJECT_TOMBSTONES=0` remains the kill switch for A/B and + // attribution — the same switch that isolated #9108, #9110 and #9200 + // each in one command. + !matches!( std::env::var("PERRY_OBJECT_TOMBSTONES").as_deref(), - Ok("1") | Ok("on") | Ok("true") + Ok("0") | Ok("off") | Ok("false") ) }) }