From 0a072fef1d9fb7321d21628a422404098b25a8e3 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Sun, 4 Oct 2026 02:01:20 -0700 Subject: [PATCH 1/3] bench(signals): benchmarks measure the built package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In benchmark mode the benches' src/index.js import is aliased to the dist entry of the selected tier (SIGNALS_TIER, default prod for benchmarks) and the dist is externalized so Node links it natively — the prod build preserves modules, and vite-node's SSR transform would turn its cross-module imports back into the namespace-object property loads the alias exists to take out of the measurement. SIGNALS_BENCH=source keeps a from-source loop for local iteration. A missing or stale dist fails the run with the rebuild instruction instead of timing code not under test. CodSpeed already builds before benching; its first run after this re-baselines every signals bench. Plan sec. 43.1. Co-authored-by: Cursor --- packages/signals/vite.config.ts | 119 +++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 24 deletions(-) diff --git a/packages/signals/vite.config.ts b/packages/signals/vite.config.ts index 376146287..ac0c8078a 100644 --- a/packages/signals/vite.config.ts +++ b/packages/signals/vite.config.ts @@ -1,30 +1,101 @@ +import { existsSync, readdirSync, statSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; import codspeedPlugin from "@codspeed/vitest-plugin"; -// Vitest sets mode to "benchmark" for `vitest bench`. Benchmarks measure dev -// semantics but must not pay the __TEST__-only invariant machinery -// (per-write tracking + quiescence sweeps) — that cost regressed the whole -// CodSpeed suite by 5-21% when it ran under the test defines. +const root = dirname(fileURLToPath(import.meta.url)); + +// Vitest sets mode to "benchmark" for `vitest bench`. +// +// Benchmarks measure the BUILT package. The source under vite-node is one +// ESM module per file with every cross-module import a live-binding +// namespace load; the shipped bundle is a single scope with those calls +// inlined and its properties mangled. The gap between the two is not a +// constant factor — it grows with the number of modules on a hot path — so +// numbers taken on the source neither track what users pay nor stay +// comparable across refactors that move code between files. The import +// alias below routes the benches' `src/index.js` import to the dist entry +// of the selected tier; `SIGNALS_BENCH=source` opts back into the source +// (handy for a quick local loop — not for recording numbers). // -// SIGNALS_TIER selects the build tier the source is compiled as (see -// src/globals.d.ts): "dev" (default — checks + wiring), "observe" (wiring -// only: what a production observability build pays with no hooks -// installed), "prod" (neither). Meant for `vitest bench`; the test suite -// asserts dev-tier behaviour and is not expected to pass under other tiers. -const tier = process.env.SIGNALS_TIER ?? "dev"; -if (tier !== "dev" && tier !== "observe" && tier !== "prod") - throw new Error(`SIGNALS_TIER must be dev | observe | prod, got "${tier}"`); +// SIGNALS_TIER selects the build tier (see src/globals.d.ts): "dev" (checks +// + wiring), "observe" (wiring only: what a production observability build +// pays with no hooks installed), "prod" (neither). Benchmarks default to +// "prod" — the artifact users ship. The test suite defaults to "dev" and +// asserts dev-tier behaviour; it is not expected to pass under other tiers. +// When the source is compiled (tests, or SIGNALS_BENCH=source) the tier is +// applied through the defines; __TEST__ (per-write tracking + quiescence +// sweeps) is never on for benchmarks — that machinery alone regressed the +// CodSpeed suite by 5-21% when it was. +const DIST_ENTRY = { + dev: "dist/dev.js", + observe: "dist/observe/index.js", + prod: "dist/prod/index.js" +} as const; + +function newestSource(dir: string): { path: string; mtimeMs: number } { + let newest = { path: dir, mtimeMs: 0 }; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + const found = entry.isDirectory() + ? newestSource(path) + : { path, mtimeMs: statSync(path).mtimeMs }; + if (found.mtimeMs > newest.mtimeMs) newest = found; + } + return newest; +} + +export default defineConfig(({ mode }) => { + const benchmark = mode === "benchmark"; + const dist = benchmark && process.env.SIGNALS_BENCH !== "source"; + const tier = process.env.SIGNALS_TIER ?? (dist ? "prod" : "dev"); + if (tier !== "dev" && tier !== "observe" && tier !== "prod") + throw new Error(`SIGNALS_TIER must be dev | observe | prod, got "${tier}"`); + if ( + benchmark && + process.env.SIGNALS_BENCH && + !["dist", "source"].includes(process.env.SIGNALS_BENCH) + ) + throw new Error(`SIGNALS_BENCH must be dist | source, got "${process.env.SIGNALS_BENCH}"`); -export default defineConfig(({ mode }) => ({ - plugins: [codspeedPlugin()], - define: { - __DEV__: String(tier === "dev"), - __OBSERVE__: String(tier !== "prod"), - __TEST__: mode === "benchmark" || tier !== "dev" ? "false" : "true" - }, - test: { - globals: true, - dir: "./tests", - pool: "threads" + let alias: { find: RegExp; replacement: string }[] = []; + if (dist) { + const entry = join(root, DIST_ENTRY[tier]); + if (!existsSync(entry)) + throw new Error( + `@solidjs/signals benchmarks measure the built package, but ${DIST_ENTRY[tier]} is missing. ` + + "Run `pnpm build:js` first (or set SIGNALS_BENCH=source to measure the source)." + ); + // A stale build silently produces numbers for code that is not under + // test — fail instead. (A fresh checkout bumps src mtimes; rebuild.) + const newest = newestSource(join(root, "src")); + if (newest.mtimeMs > statSync(entry).mtimeMs) + throw new Error( + `@solidjs/signals benchmarks measure the built package, but ${DIST_ENTRY[tier]} is older than ` + + `${relative(root, newest.path)}. Run \`pnpm build:js\` first (or set SIGNALS_BENCH=source).` + ); + alias = [{ find: /^(?:\.\.\/)+src\/index\.js$/, replacement: entry }]; } -})); + + return { + plugins: [codspeedPlugin()], + define: { + __DEV__: String(tier === "dev"), + __OBSERVE__: String(tier !== "prod"), + __TEST__: benchmark || tier !== "dev" ? "false" : "true" + }, + resolve: { alias }, + test: { + globals: true, + dir: "./tests", + pool: "threads", + // The dist is loaded by Node itself, not re-transformed by vite-node: + // the prod build preserves modules, and vite-node's SSR transform + // would turn every one of its cross-module imports back into a + // namespace-object property load — the very cost the alias exists + // to take out of the measurement. + server: { deps: { external: dist ? [/\/dist\//] : [] } } + } + }; +}); From 3ab88fd5ea4b8385a84e7214dcf1bbdfa456eb47 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Sun, 4 Oct 2026 02:01:20 -0700 Subject: [PATCH 2/3] store: the fold queue records the pre-batch backing beside the target The weak map keyed by target kept every container's last adopted-away backing alive until its next adoption: for a keyed reconcile the previous tick's whole tree promoted out of the nursery every tick (saturated listened-paths +15%, reconcile tree +10-13% over next on the dist, all of it GC); for a store adopted once, the old tree for the store's lifetime. The record is now foldOlds, parallel to foldList: t.v at queueFold for every queued target, released with the drain. That one record is the fold's base in every case (draft, eager adoption, mid-batch privatization, draft over an adopted raw), so privatizedOlds, draftedAdoptions and the adoption flag go. The target has no slot for it (ARRAY SHAPE RULE), so the by-target lookups (preBatch: a node born onto an open staging, a committed-frame read of a node-less staging, a park) index the list through a map built on first use per batch; the drain never builds it. After, on the dist vs next: tree reverse +3%, shuffle +4%, saturated +1.5%, sparse parity; fresh stores / owned backings / enumerate / storebench unchanged; GC totals identical to the head. Signals 4894/0, web 1132/0, solid 819/0; every size cap holds. Plan sec. 43.2. Co-authored-by: Cursor --- .../store-fold-queue-prebatch-record.md | 5 + .../plans/size-reduction-carve-step1.md | 95 +++++++++++++++++++ packages/signals/src/store/store.ts | 79 ++++++++------- 3 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 .changeset/store-fold-queue-prebatch-record.md diff --git a/.changeset/store-fold-queue-prebatch-record.md b/.changeset/store-fold-queue-prebatch-record.md new file mode 100644 index 000000000..f09552704 --- /dev/null +++ b/.changeset/store-fold-queue-prebatch-record.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +Store fold queue: the pre-batch committed backing is recorded beside the queued target and released with the drain. The weak map it replaces kept every container's last adopted-away backing alive until its next adoption — for a keyed reconcile, the previous tick's whole tree promoted out of the nursery every tick (the saturated listened-paths and reconcile-tree shapes ran 10–15% over `next` on that alone); for a store adopted once, the old tree for the store's lifetime. The per-batch side maps for mid-batch privatizations and drafts over adoptions fold into the same record. diff --git a/documentation/plans/size-reduction-carve-step1.md b/documentation/plans/size-reduction-carve-step1.md index f358a0b08..d2d62fd79 100644 --- a/documentation/plans/size-reduction-carve-step1.md +++ b/documentation/plans/size-reduction-carve-step1.md @@ -5461,6 +5461,101 @@ A-rules carry `Mechanism (L2, 2026-10-04)` against the running code; the pre-L2 paragraphs are under History verbatim. The branch's fate: **push and open one PR against `next`**, this document the design record. +## 43. After the merge — CodSpeed against the dist; the fold queue's record (2026-10-04, 1:30–2:10 AM) + +#3774 merged as `1b9ceb679`. CodSpeed (not a required check) listed 14 +regressions on it; §41.1 had already shown its method — vite-node, one ESM +module per source file, every cross-module call a namespace-object +property load — penalizes the carve's layout and contradicts the dist on +the core suite. Four store shapes had not been dist-measured. This section +is both halves: the audit, and making the next report mean something. + +### 43.1 Benchmarks measure the built package + +`packages/signals/vite.config.ts`: in benchmark mode the benches' +`src/index.js` import is aliased to the dist entry of the tier +(`SIGNALS_TIER`, default **prod** for benchmarks — the artifact users ship; +the test suite keeps dev), and the dist is **externalized** so Node links it +natively — the prod build preserves modules, and vite-node's SSR transform +would have turned its cross-module imports straight back into the namespace +loads the alias exists to remove. `SIGNALS_BENCH=source` keeps a from-source +loop for local iteration. A stale or missing dist fails the run with the +rebuild instruction instead of silently timing code not under test (a fresh +checkout bumps `src` mtimes — rebuild). The CodSpeed workflow already +builds before benching; no workflow change. Web's benches already measured +signals through the package `exports` (its `development` condition → +`dist/dev.js`); signals' own were the only ones on source. + +Consequence for CodSpeed: the first run after this lands re-baselines every +signals bench (a different series — bundled prod instead of per-module dev +source). The 14 "regressions" on `1b9ceb679` are superseded by that +re-baseline; the four shapes below are the only ones that were real, and +43.2 is their fix. + +### 43.2 The four unmeasured shapes, on the dist + +Harness `/tmp/carve/cs3.mjs`: the CodSpeed benches reproduced exactly (one +`createEffect` walking all 1111 tree nodes; one effect reading all ~12k +leaves), prod dist, Node 26, fork (`next` @ `8950bb7b7`) vs L2, interleaved. + +| shape (CodSpeed name) | fork | L2 @ `1b9ceb679` | | +|---|---|---|---| +| store reconcile tree reverse: 1111 keyed nodes | 1.21–1.25 ms | 1.36–1.38 | +11% | +| store reconcile tree shuffle: 1111 keyed nodes | 1.02–1.04 | 1.12–1.18 | +10–13% | +| reconcile: deep tree, all ~12k paths subscribed | 3.79 | 4.37–4.41 | +15% | +| reconcile: deep tree, 10 of ~12k paths subscribed | 0.37 | 0.22–0.38 | parity/ahead | +| dbmon shallow full tick (1000 rows; `shapes4.mjs`) | 0.89–0.93 | 0.67–0.69 | L2 −25% | + +So two of CodSpeed's four were real (the tree pair is one shape), one was +the harness (dbmon shallow — L2 is a quarter faster on the dist), one was +noise. + +**Cause.** The profile's self-time differences were small (`queueFold`, +`drainFolds`, `preBatch`, `foldTarget`, `parentSlotKey` ≈ 7–9%) but +`(garbage collector)` was 16% against 6%, and removing the two weak-map +operations alone took the saturated shape *under* the fork. §41.5's fold +queue kept each adopted container's pre-batch backing in a `WeakMap` +keyed by target, "written in place batch after batch — no churn". No +churn, but **retention**: an entry lives as long as its target, so every +container held its last adopted-away backing until its next adoption — for +a keyed reconcile, the previous tick's entire tree (5,400 containers, +12k strings) stayed reachable through the scavenges, was promoted, and +became old-generation garbage every tick; for a store adopted once, the +old tree is retained for the store's lifetime. A memory bug as much as a +speed one. + +**Fix.** The record moves beside the target: `foldOlds`, parallel to +`foldList`, set to `t.v` at `queueFold` for **every** queued target +(adoption or draft) and cleared with the drain. That one record is the +fold's base in every case — a draft's backing does not move before the +fold (the record is `t.v` still), an adoption's swaps eagerly (the record +is the frame committed readers keep), a mid-batch privatization or a draft +over an adopted raw moves it again (the record is what the batch started +from) — so `privatizedOlds`, `draftedAdoptions` and the adoption flag on +`queueFold` go too. The target has no slot for the record (ARRAY SHAPE RULE: +20 named fields, 21 ≡ 0 mod 3 normalizes array targets to dictionary +mode), so the by-target lookups (`preBatch`: a container node born onto an +open staging, a committed-frame read of a node-less staging, `_storePark`) +go through an index map built on first use per batch and kept in step by +`queueFold`; the drain walks the two lists together and never builds it. +The arrays' `length = 0` releases the entries — nothing outlives the batch. + +**After** (same harness, interleaved, 3 rounds, medians): tree reverse +1.232 → **1.267** (+3%), shuffle 1.196 → **1.245** (+4%, 1.137 vs 1.153 in +the profiled run), saturated 3.166 → **3.212** (+1.5%), sparse 0.333 → +0.330. The residual is the queue itself — `drainFolds` + `foldTarget` + +`parentSlotKey` ≈ 3.5% of a tick — the price of a staging per adopted +container under the hold model. `adoptPB` at 21% self is the per-adopted-raw +`storeLookup.set` registration, the same in both arms (the lever listed with the deep-tick ruling, §42). +No regression elsewhere: fresh stores 1.00–1.27 ms (fork 1.16), 2000 owned +backings 0.30–0.32 (fork 0.35), enumerate 7.3/7.9 (fork 8.5/8.1), +storebench parity; GC totals over the store battery identical to the head +(40 scavenges 87 ms, 4 major 94 ms; fork 38/108 + 5/98). Size: within every +cap (`+ createStore` 14519, every-store 28755, page base 44029 — 1 B under +its cap — live 47654); a few bytes over the head per scenario, the arrays +and the lazy index against the three collections. Signals **4894 / 0**, +web 1132 / 0, solid 819 / 0. + --- ## Appendix — ledger (verbatim) diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 400e131e0..396c9552d 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -720,12 +720,11 @@ function ensurePB(target: StoreTarget): Record { // adoption and the draft is a mainline layer above it (#3612/#3688: a // mainline setter mid-hold publishes mainline; the held keys stay held). pb = target.pb = cloneRaw(pb, target); - draftedAdoptions.add(target); const k = target.k; if (k !== null && !(k._config & CONFIG_HELD)) k._pendingValue = pb; } else if (pb === null) { const v = target.v; - if (!directCommit) queueFold(target, false); + if (!directCommit) queueFold(target); if ( !target.ch && !Array.isArray(v) && @@ -747,10 +746,8 @@ function privatizeCommitted(target: StoreTarget): void { if (isOwned(target.v)) return; const before = target.v; const clone = cloneRaw(before, target); - // Mid-batch (a staging is open on this target): the fold must still see - // the backing the batch started from (`preBatch`). - if (target.pb !== null && target.pb !== before && !draftedAdoptions.has(target)) - privatizedOlds.set(target, before); + // Mid-batch (a staging is open on this target) the fold still sees the + // backing the batch started from: the queue recorded it (`foldOlds`). target.v = clone; target.ch = false; if (target.u) { @@ -809,7 +806,7 @@ export function adoptPB( incoming: Record, notify = true ): void { - if (!directCommit) queueFold(target, true); + if (!directCommit) queueFold(target); let base: Record; if (target.pb !== null) { if (target.ovl) materializePB(target); @@ -852,35 +849,48 @@ export function adoptPB( * of the tick's young-generation GC, and the whole of its gap to `next` * (plan sec. 41.5). * - * The pre-batch committed backing (`preBatch`): a DRAFT's is `t.v` itself — - * the backing does not move until the fold — so nothing is recorded for - * it (2000 fresh one-key stores per flush recorded 2000 weak-map entries - * for nothing, 2× the fork); an ADOPTION swaps the backing eagerly, so its - * pre-batch backing is kept in a weak map written in place batch after - * batch (the containers of a keyed reconcile are reused — no churn); the - * two rare cases that move a draft's backing mid-batch — a privatization - * (a child's path copy repointed the parent), a draft over an adopted raw - * (cloned) — are kept in small per-batch maps. */ + * Beside each target, its PRE-BATCH committed backing — `t.v` as the batch + * opened on it (`foldOlds`, parallel to `foldList`). That one record is the + * fold's base in every case: a draft's backing does not move until the fold + * (so the record is `t.v` still), an adoption swaps it eagerly (the record + * is the frame committed readers keep), a mid-batch privatization or a + * draft over an adopted raw moves it again (the record is what the batch + * started from, which is all the fold wants to know). The record lives + * exactly as long as the batch: a weak map keyed by target kept every + * container's LAST adopted-away backing alive until its next adoption — + * for a keyed reconcile, the whole previous tree promoted out of the + * nursery every tick (the saturated listened-paths shape ran 15–25% over + * the fork, all of it this; plan sec. 43.2) — and for a store adopted + * once, for its lifetime. + * + * The target carries no slot for its record (ARRAY SHAPE RULE, target.ts), + * so a lookup by target (`preBatch`: a container node born onto an open + * staging, a committed-frame read of a node-less staging, a park) indexes + * the list through a map built on first use per batch — the drain itself + * walks the two lists in step and never builds it. */ let foldList: StoreTarget[] = []; +let foldOlds: Record[] = []; let foldSpare: StoreTarget[] = []; -const adoptOlds = new WeakMap>(); -const privatizedOlds = new Map>(); -const draftedAdoptions = new Set(); +let foldOldsSpare: Record[] = []; +let foldIndex: Map | null = null; /** Before the staging is set and before an adoption swaps the backing. */ -function queueFold(target: StoreTarget, adoption: boolean): void { +function queueFold(target: StoreTarget): void { if (target.pb !== null) return; schedule(); - if (adoption) adoptOlds.set(target, target.v); + if (foldIndex !== null) foldIndex.set(target, foldList.length); foldList.push(target); + foldOlds.push(target.v); } -/** The committed backing before this batch (valid while `pb !== null`). An - * eager adoption nothing wrote after is `pb === v`; one a draft wrote over - * is listed; a privatized draft's is listed; any other draft's is `v`. */ +/** The committed backing before this batch (valid while `pb !== null`). */ function preBatch(t: StoreTarget): Record { - if (t.pb === t.v || draftedAdoptions.has(t)) return adoptOlds.get(t) ?? t.v; - return privatizedOlds.get(t) ?? t.v; + if (foldIndex === null) { + foldIndex = new Map(); + for (let i = 0; i < foldList.length; i++) foldIndex.set(foldList[i], i); + } + const at = foldIndex.get(t); + return at === undefined ? t.v : foldOlds[at]; } /** The flush committed its pending nodes: fold every pending backing whose @@ -891,21 +901,24 @@ function preBatch(t: StoreTarget): Record { function drainFolds(): void { if (foldList.length !== 0) { const list = foldList; + const olds = foldOlds; foldList = foldSpare; + foldOlds = foldOldsSpare; foldSpare = list; + foldOldsSpare = olds; + foldIndex = null; for (let i = 0; i < list.length; i++) { const t = list[i]; if (t.pb === null) continue; // folded already (a direct commit) const k = t.k; - if (k !== null && k._pendingValue !== NOT_PENDING) - foldList.push(t); // held: commits with its hold - else foldTarget(t, preBatch(t)); + if (k !== null && k._pendingValue !== NOT_PENDING) { + // held: commits with its hold + foldList.push(t); + foldOlds.push(olds[i]); + } else foldTarget(t, olds[i]); } list.length = 0; - if (foldList.length === 0) { - if (privatizedOlds.size !== 0) privatizedOlds.clear(); - if (draftedAdoptions.size !== 0) draftedAdoptions.clear(); - } + olds.length = 0; } if (deferredReleases.size !== 0) { for (const node of deferredReleases) { From 04dc66e37ae72327d9580fdde73fb921bbfff500 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Sun, 4 Oct 2026 02:33:51 -0700 Subject: [PATCH 3/3] bench(signals): projection-root-write loads under the dev tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The warm-up store write sat inside createRoot, where the dev tier's owned-scope write check throws — the file failed to load under SIGNALS_TIER=dev, so CodSpeed compared its prod-dist values against a stale base (two of the four 'regressions' on #3776). Moved outside the root. Plan sec. 43.1 records the first dist-mode CodSpeed run. Co-authored-by: Cursor --- documentation/plans/size-reduction-carve-step1.md | 15 +++++++++++++++ .../tests/store/projection-root-write.bench.ts | 12 ++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/documentation/plans/size-reduction-carve-step1.md b/documentation/plans/size-reduction-carve-step1.md index d2d62fd79..e2fb2c4d8 100644 --- a/documentation/plans/size-reduction-carve-step1.md +++ b/documentation/plans/size-reduction-carve-step1.md @@ -5492,6 +5492,21 @@ source). The 14 "regressions" on `1b9ceb679` are superseded by that re-baseline; the four shapes below are the only ones that were real, and 43.2 is their fix. +**The PR's first CodSpeed run** (#3776, `3ab88fd5e` vs `next`): 84 +improved (×2–5 — the layout cost, now gone from the measurement: dbmon +full tick 750 → 337 ms of simulated time, createOwners ×5), 100 untouched, +4 "regressed", flagged by CodSpeed itself as compared across different +runtime environments. All four are the instrument: `merge-merge-mixed(deep +0, 100, 3, 2)` 46 → 819 µs is a GC landing inside a ~75 ns operation's +measured window (`--predictable-gc-schedule`; the operation itself is 72–98 +ns on prod and dev dist alike, JIT or `--no-opt`, every variant); +`merge-static(deep)` −7% is the same window's small cousin; and the two +`projection-root-write` rows had no live base at all — that file **fails to +load under the dev tier** (a store write inside `createRoot` trips the +owned-scope check), so its base was stale while the prod run has values +(20 µs each, consistent with the file's own reference line). The warm-up +write moved outside the root so the file runs in every tier. + ### 43.2 The four unmeasured shapes, on the dist Harness `/tmp/carve/cs3.mjs`: the CodSpeed benches reproduced exactly (one diff --git a/packages/signals/tests/store/projection-root-write.bench.ts b/packages/signals/tests/store/projection-root-write.bench.ts index cc657a539..d37bc4821 100644 --- a/packages/signals/tests/store/projection-root-write.bench.ts +++ b/packages/signals/tests/store/projection-root-write.bench.ts @@ -63,12 +63,16 @@ describe(`one root key per commit, ${KEYS}-key record`, () => { const [store, set] = createStore>(seed()); void store.k0; - set(d => { - d.k0 = { n: -1 }; - }); - flush(); setStore = set; }); + // The warm-up write happens outside the root: inside it, the dev tier's + // owned-scope write check throws and the whole file fails to load under + // `SIGNALS_TIER=dev` — leaving CodSpeed to compare a prod run against a + // stale base. + setStore(d => { + d.k0 = { n: -1 }; + }); + flush(); bench("projection derive: delete + set one ROOT key (#3352)", () => { for (let c = 0; c < COMMITS; c++) {