Skip to content

🤖 perf: keep the config snapshot while it still matches the file - #5750

Merged
ThomasK33 merged 2 commits into
mainfrom
thomask33/perf-config-keep-snapshot
Oct 6, 2026
Merged

ThomasK33 merged 2 commits into
mainfrom
thomask33/perf-config-keep-snapshot

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

An edit no longer throws away a config snapshot that still matches config.json. With a reader on every event-loop turn (the startup tombstone heal sweep does this), one edit now parses config.json 2 times instead of 4. At 4,712 workspaces, that edit is 66 ms faster (205 ms to 138 ms, 95% CI -40.1% to -23.9%). In the startup scenario at 4,700 workspaces, the heal sweep now does 2 parses per save instead of 4.

Refs #5727

Background

The edit's own fresh read (readConfigOrDefault inside enqueueConfigEditEffect) cleared the shared snapshot, and the save cleared it again after the rename. Each clear forced the next reader to parse the same bytes again. So with a reader on every turn, one edit cost 4 parses:

  1. The edit's own read under the lock.
  2. A reader during the save, because the edit's read had cleared the snapshot.
  3. A reader between the rename and the end of the save.
  4. The first reader after the save, because the save cleared the snapshot that parse 3 stored.

This change removes parses 2 and 4. The floor is 2: the edit's fresh read, and one read that publishes the saved file.

Implementation

All in src/node/config/index.ts (9 changed code lines plus comments):

  1. enqueueConfigEditEffect: the transform reads with readConfigOrDefault({ keepSnapshot: true }).
  2. readConfigOrDefault: with keepSnapshot, it clears the snapshot as before, then gives it back only after a successful, cacheable read. A failed, missing or lenient read leaves it cleared. Otherwise the edit gate's loadConfigOrDefault would hit the old snapshot, never retry the read that clears a recorded load failure, and refuse every edit until a restart.
  3. saveConfigEffect: after the write, it keeps a snapshot only if the snapshot's stat key equals the current stat key of config.json. That snapshot came from a reader that parsed the real bytes on disk, whoever wrote them. The cost is one statSync per save.

The bench file src/node/config/configScale.bench.ts adds live-shaped synthetic rows (no real data). It is the file from the unmerged bench branch, unchanged.

Invariants and the tests that guard them

All tests are in src/node/config/snapshot.test.ts. The existing cases are unchanged.

Invariant Test
With a reader on every event-loop turn, one edit reads config.json exactly 2 times (main: 4) parses config.json twice per edit while a reader runs on every event-loop turn (new)
A reader during an edit gets the committed pre-edit snapshot, never the transform's object serves the committed snapshot, never the transform's object, to readers during an edit (new)
Only a successful read gives the snapshot back: after a transient EIO in the edit's read, the next edit succeeds without a file change or restart accepts the next edit after the edit's own read fails once on a warm snapshot (new)
A failed save clears the snapshot, so readers never see an unsaved in-memory change drops the snapshot when a save fails (new)
A snapshot is served only for the file it was parsed from, and another writer's rename between our rename and the save's end wins invalidates on atomic external replacement, including same-size bytes and mtime, isolates edits and reloads the saved snapshot once, sees an external atomic replacement between our rename and save completion (existing, unchanged)
Corrupt and lenient files behave as before does not reuse a lenient structurally invalid load for a strict read, does not cache structurally invalid entries after an unrelated save, treats stat failures as cache misses, does not reuse a cached snapshot after the file disappears (existing, unchanged)

Mutation check, run locally against snapshot.test.ts (each mutant fails only the expected test):

Mutation of index.ts Failing test
Both changes reverted (main) reader-per-turn test (reads 4 times) and committed-snapshot test
The transform does not keep the snapshot reader-per-turn test (3 reads) and committed-snapshot test
The save always clears the snapshot reader-per-turn test (3 reads)
The kept snapshot comes back even after a failed read EIO recovery test
No clear on a failed save failed-save test (reader sees "Unsaved")
The transform's object is stored as the snapshot committed-snapshot test

Measurements

Host: shared Coder host (AMD EPYC 9454P), Node 22.19, make bench-compare ROUNDS=10 (interleaved base and head processes, paired 95% CI on per-round log ratio).

  • Base 0c5e35d81638e85ed420c4b7c943d86ed502a2f7 (origin/main merge-base), head 4fa387b2e78bf32f810d7c21bc1f488bd33ce265.

A/A noise floor: make bench-compare BENCH=config BASE=HEAD ROUNDS=10

Both sides are the same commit (712637f68e, the product code of this PR). Result: 0 faster or slower verdicts out of 42 rows.

runtime node | rounds 10 | base 712637f68e4d | head 712637f68e4d
loadavg start 18.98 30.03 35.74 | end 13.57 17.40 22.90
A/A table (42 rows)
benchmark                                                                      base       head   delta            95% CI  verdict
config-a28db80a: loadConfigOrDefault (fresh Config, 4500 workspaces)       11.82 ms   12.02 ms   +1.4%    [-1.6%, +4.6%]        ~
config-a28db80a: loadConfigOrDefault (snapshot hit)                         3.29 µs    3.27 µs   -2.1%    [-5.8%, +1.7%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (50)              510.54 µs  510.23 µs  +13.1%  [-14.4%, +49.4%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (500)               3.11 ms    3.13 ms   +5.1%   [-5.0%, +16.4%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (5000)             32.37 ms   32.28 ms   -0.2%    [-2.2%, +1.7%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (4712)             30.26 ms   30.70 ms   +1.0%    [-1.9%, +4.0%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (50)                3.01 µs    3.02 µs   +0.7%    [-0.4%, +1.8%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (500)               3.03 µs    3.06 µs   +0.9%    [-1.2%, +3.0%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (5000)              3.03 µs    3.04 µs   +0.4%    [-1.2%, +2.0%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (4712)              3.02 µs    3.04 µs   +0.6%    [-1.2%, +2.5%]        ~
configScale-334200bf: findWorkspace(last id) (50)                           3.12 µs    3.13 µs   -0.7%    [-2.5%, +1.1%]        ~
configScale-334200bf: findWorkspace(last id) (500)                          3.14 µs    3.12 µs   -0.5%    [-1.6%, +0.6%]        ~
configScale-334200bf: findWorkspace(last id) (5000)                         3.27 µs    3.30 µs   +0.2%    [-2.9%, +3.3%]        ~
configScale-334200bf: findWorkspace(last id) (4712)                         3.32 µs    3.33 µs   +0.8%    [-1.3%, +2.9%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (50)              1.36 ms    1.34 ms   -2.5%    [-6.9%, +2.1%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (500)            12.77 ms   12.67 ms   -1.5%    [-6.9%, +4.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (5000)          136.93 ms  134.83 ms   -5.4%   [-14.0%, +4.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (4712)          128.04 ms  126.63 ms   -2.5%    [-7.7%, +3.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (50)     671.20 µs  670.10 µs   +1.2%    [-3.8%, +6.4%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (500)      6.52 ms    6.65 ms   +2.6%    [-1.7%, +7.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (5000)    69.80 ms   71.89 ms   -6.0%  [-23.8%, +16.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (4712)    66.47 ms   68.29 ms   +2.4%    [-2.9%, +7.9%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (50)           3.54 µs    3.52 µs   -1.9%    [-5.3%, +1.5%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (500)          3.90 µs    3.84 µs   -0.9%    [-3.0%, +1.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (5000)         5.67 µs    5.52 µs   -1.0%    [-3.9%, +1.9%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (4712)         5.51 µs    5.49 µs   +0.4%    [-2.4%, +3.2%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (50)               78.58 µs   79.11 µs   +0.9%    [-3.2%, +5.3%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (500)              76.70 µs   74.88 µs   -4.6%   [-10.9%, +2.1%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (5000)             72.46 µs   73.24 µs  -16.1%  [-45.2%, +28.3%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (4712)             70.61 µs   72.19 µs   -5.1%  [-20.4%, +13.1%]        ~
configScale-334200bf: editConfig, same-value edit (50)                      5.47 ms    5.69 ms  +11.5%   [-2.0%, +26.9%]        ~
configScale-334200bf: editConfig, same-value edit (500)                    17.49 ms   17.66 ms   +4.1%   [-1.6%, +10.1%]        ~
configScale-334200bf: editConfig, same-value edit (5000)                  151.67 ms  153.64 ms   +1.8%  [-14.1%, +20.6%]        ~
configScale-334200bf: editConfig, same-value edit (4712)                  139.34 ms  140.59 ms  +13.7%  [-12.5%, +47.7%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (50)             5.63 ms    5.58 ms   -0.6%    [-6.1%, +5.3%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (500)           17.75 ms   17.35 ms   -0.5%    [-3.1%, +2.1%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (5000)         148.56 ms  145.67 ms   -1.4%    [-7.5%, +5.1%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (4712)         138.72 ms  138.13 ms   -6.7%   [-17.6%, +5.8%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (50)      5.41 ms    5.61 ms  +16.7%  [-15.6%, +61.2%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (500)    17.34 ms   17.41 ms   +0.0%  [-20.5%, +25.8%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (5000)  151.02 ms  148.86 ms   -7.3%  [-26.1%, +16.2%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (4712)  137.19 ms  139.04 ms   -2.9%   [-12.8%, +8.1%]        ~

Base vs head: make bench-compare BENCH=config ROUNDS=10

Result: 4 faster verdicts, all on the reader-per-turn row. 0 slower verdicts. At the live-shaped size (4,712 workspaces) the reader-per-turn edit takes 66 ms less (budget: at least 35 ms). Plain edits without a concurrent reader do not change, as expected.

runtime node | rounds 10 | base 0c5e35d81638 | head 4fa387b2e78b
loadavg start 14.26 17.36 22.80 | end 17.83 31.33 29.39
benchmark                                                                      base       head   delta            95% CI  verdict
configScale-334200bf: loadConfigOrDefault, fresh Config (4712)             31.70 ms   31.62 ms   -8.9%  [-25.5%, +11.5%]        ~
configScale-334200bf: editConfig, same-value edit (4712)                  139.47 ms  139.37 ms   -0.6%    [-6.8%, +5.9%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (4712)         142.68 ms  137.67 ms  -13.0%   [-24.7%, +0.5%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (50)      7.02 ms    5.63 ms  -23.0%   [-34.2%, -9.9%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (500)    23.62 ms   17.14 ms  -39.0%  [-54.9%, -17.6%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (5000)  220.55 ms  149.48 ms  -34.4%  [-38.5%, -30.0%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (4712)  204.65 ms  138.40 ms  -32.5%  [-40.1%, -23.9%]   faster
Full base-vs-head table (42 rows)
benchmark                                                                      base       head   delta            95% CI  verdict
config-a28db80a: loadConfigOrDefault (fresh Config, 4500 workspaces)       12.59 ms   12.90 ms   +0.9%    [-2.9%, +5.0%]        ~
config-a28db80a: loadConfigOrDefault (snapshot hit)                         3.37 µs    3.36 µs   -0.5%    [-2.2%, +1.4%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (50)              532.11 µs  536.67 µs   +0.7%    [-2.7%, +4.2%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (500)               3.20 ms    3.14 ms   -0.4%    [-4.8%, +4.2%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (5000)             33.11 ms   31.97 ms   -2.5%    [-6.2%, +1.3%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (4712)             31.70 ms   31.62 ms   -8.9%  [-25.5%, +11.5%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (50)                3.11 µs    3.09 µs   -0.1%    [-2.4%, +2.2%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (500)               3.11 µs    3.10 µs   -0.7%    [-3.9%, +2.5%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (5000)              3.11 µs    3.04 µs   -1.6%    [-4.5%, +1.3%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (4712)              3.11 µs    3.09 µs   -3.2%    [-7.1%, +0.9%]        ~
configScale-334200bf: findWorkspace(last id) (50)                           3.22 µs    3.19 µs   -1.5%    [-5.4%, +2.5%]        ~
configScale-334200bf: findWorkspace(last id) (500)                          3.21 µs    3.22 µs   +0.9%    [-3.0%, +5.0%]        ~
configScale-334200bf: findWorkspace(last id) (5000)                         3.33 µs    3.38 µs   -7.0%  [-22.7%, +11.8%]        ~
configScale-334200bf: findWorkspace(last id) (4712)                         3.41 µs    3.36 µs   +4.7%  [-10.0%, +21.9%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (50)              1.37 ms    1.32 ms  +17.7%  [-26.0%, +87.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (500)            13.17 ms   12.28 ms   -8.7%  [-32.9%, +24.2%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (5000)          138.94 ms  135.98 ms   +1.1%   [-9.6%, +13.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (4712)          128.80 ms  130.51 ms   -2.9%   [-10.9%, +5.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (50)     663.84 µs  659.88 µs   -0.5%    [-3.9%, +3.2%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (500)      6.59 ms    6.56 ms   -1.4%    [-4.4%, +1.6%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (5000)    69.84 ms   72.30 ms   -0.3%    [-4.6%, +4.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (4712)    68.58 ms   67.88 ms   -1.2%    [-5.5%, +3.4%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (50)           3.61 µs    3.60 µs   -1.5%    [-6.1%, +3.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (500)          4.03 µs    3.87 µs   -3.3%    [-6.9%, +0.5%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (5000)         6.12 µs    5.85 µs   -9.0%  [-28.4%, +15.7%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (4712)         6.00 µs    5.73 µs   -8.9%  [-26.0%, +12.3%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (50)               80.81 µs   78.16 µs   -5.7%   [-16.9%, +6.9%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (500)              77.07 µs   77.58 µs   -5.8%   [-18.4%, +8.7%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (5000)             75.45 µs   74.15 µs   -5.8%   [-13.1%, +2.0%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (4712)             76.16 µs   73.88 µs   -4.5%   [-11.0%, +2.3%]        ~
configScale-334200bf: editConfig, same-value edit (50)                      6.17 ms    5.76 ms  -17.9%  [-43.1%, +18.5%]        ~
configScale-334200bf: editConfig, same-value edit (500)                    18.02 ms   17.55 ms  -19.0%   [-40.1%, +9.5%]        ~
configScale-334200bf: editConfig, same-value edit (5000)                  151.06 ms  151.14 ms   -2.6%  [-14.7%, +11.2%]        ~
configScale-334200bf: editConfig, same-value edit (4712)                  139.47 ms  139.37 ms   -0.6%    [-6.8%, +5.9%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (50)             5.86 ms    5.75 ms   -9.0%   [-21.2%, +5.1%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (500)           17.65 ms   17.19 ms   -2.6%    [-7.3%, +2.4%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (5000)         149.01 ms  147.88 ms   -2.9%    [-7.2%, +1.6%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (4712)         142.68 ms  137.67 ms  -13.0%   [-24.7%, +0.5%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (50)      7.02 ms    5.63 ms  -23.0%   [-34.2%, -9.9%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (500)    23.62 ms   17.14 ms  -39.0%  [-54.9%, -17.6%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (5000)  220.55 ms  149.48 ms  -34.4%  [-38.5%, -30.0%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (4712)  204.65 ms  138.40 ms  -32.5%  [-40.1%, -23.9%]   faster

Parses per save in the startup heal-sweep scenario (counting harness, 4,700 workspaces)

Setup: a synthetic fixture from scripts/perf/workspace-scale/generate-fixture.ts (4,700 workspaces, 19 projects, 57% archived, realistic profile, 8.5 MB config.json). Before start, the driver writes 2,734 one-day-old tombstones for unregistered ids, 2 one-day-old tombstones for registered ids, and 1 fresh tombstone for a registered id. It starts node --require counter.cjs dist/cli/index.js server --no-auth with an isolated XUM_ROOT and XUM_MOCK_AI=1. As soon as the port opens, it sends 20 workspace.updateTitle calls. The preload counts readConfigOrDefault and saveConfig calls while healRemovalTombstonesForRegisteredWorkspaces runs. Counts are exact. Timings come from single runs on a shared host (load average 11-32) and are noisy. Run 1 used a build of 712637f68e; runs 2, 3 and the control used the head build. Both have the same product code (the later commit changes only a test).

Run Build Parses during sweep Saves during sweep Parses per save Sweep ms Start to housekeeping settled (s)
1 base 85 21 4.05 17619 31.49
1 head 43 21 2.05 13499 26.54
2 base 85 21 4.05 17271 30.74
2 head 43 21 2.05 13872 27.71
3 base 85 21 4.05 17458 31.53
3 head 43 21 2.05 15617 29.87
control, 0 edits base 5 1 - 5475 16.61
control, 0 edits head 3 1 - 5335 17.11
  • Per save, after taking away the one cold read that the 0-edit control shows: base (85 - 1) / 21 = 4.0, head (43 - 1) / 21 = 2.0. The plan projected 4.4 on base from live captures; this harness measures 4.0.
  • On both builds, the sweep healed exactly the 2 old registered tombstones ("Healed a removal tombstone" logged 2 times), and the other 2,735 tombstones stayed.

Dogfood (head 4fa387b2e78bf32f810d7c21bc1f488bd33ce265, synthetic 4,700-workspace root, no real data)

The same driver ran a cross-process check after each startup run, on base and head:

  1. Rename a workspace through workspace.updateTitle: workspace.getInfo returns API title.
  2. A script writes a changed title into config.json through a temp file and a rename, as another backend does: workspace.getInfo returns External atomic title.
  3. A script edits config.json in place with a different length: workspace.getInfo returns In-place edit, longer title.

All 3 steps passed on base and head in all 3 runs. The harness source and raw results are in a PR comment.

Risks

Low. The change only stops clearing a snapshot whose stat key still matches the file, and every snapshot read compares keys first, as before. The remaining risk is the existing stat-key limit (an in-place rewrite that keeps inode, mtime and size), which this PR does not change. A save still keeps no snapshot of the file it replaced, and a failed save still clears it.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

An edit's fresh read no longer drops the snapshot of the unchanged file, and
a save keeps a snapshot only when a reader already parsed the file that is on
disk after the save. With a reader on every event-loop turn (the startup heal
sweep), one edit parsed config.json 4 times; now it parses it twice.

Adds configScale.bench.ts (live-shaped synthetic config rows).
…hot mutation

The test asserted only snapshot identity and a read count after a failed
setUpdateChannel save. With a fresh transform input, a kept snapshot still
matches the unchanged file, so that assertion pinned an extra read, not a
behavior. The clear exists for an edit that returns the shared snapshot after
mutating it (removeWorkspaceFromTestConfig does): without it, readers see the
unsaved mutation. The test now runs that edit and asserts readers get the file
content.

Signed-off-by: Thomas Kosiewski <tk@coder.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T13:33:33.186790Z 4fa387b PR opened
🔒 Security Review ✅ Completed 2026-10-06T13:37:22.895797Z 4fa387b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33

Copy link
Copy Markdown
Member Author

Dogfood evidence for head 4fa387b2e78bf32f810d7c21bc1f488bd33ce265 (synthetic 4,700-workspace fixture, no real data).

Each line below is the driver's raw JSON for one run: startup heal-sweep counts, then the cross-process check (afterApi, afterAtomic, afterInPlace are workspace.getInfo titles), then the tombstone checks.

run1 {"label": "base", "workspaces": 4700, "portOpenS": 2.32, "titlesDoneS": 16.26, "settledS": 31.49, "sweep": {"parses": 85, "saves": 21, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 17619.175097000003, "sweepRuns": 1}, "sweepParsesPerSave": 4.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 89, "saves": 22, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 17619.175097000003, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
run1 {"label": "head", "workspaces": 4700, "portOpenS": 2.16, "titlesDoneS": 12.21, "settledS": 26.54, "sweep": {"parses": 43, "saves": 21, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 13499.029835000001, "sweepRuns": 1}, "sweepParsesPerSave": 2.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 47, "saves": 22, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 13499.029835000001, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
run2 {"label": "base", "workspaces": 4700, "portOpenS": 2.37, "titlesDoneS": 16.11, "settledS": 30.74, "sweep": {"parses": 85, "saves": 21, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 17270.552028000002, "sweepRuns": 1}, "sweepParsesPerSave": 4.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 89, "saves": 22, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 17270.552028000002, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
run2 {"label": "head", "workspaces": 4700, "portOpenS": 2.2, "titlesDoneS": 12.58, "settledS": 27.71, "sweep": {"parses": 43, "saves": 21, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 13871.772016, "sweepRuns": 1}, "sweepParsesPerSave": 2.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 47, "saves": 22, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 13871.772016, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
run3 {"label": "base", "workspaces": 4700, "portOpenS": 2.16, "titlesDoneS": 15.89, "settledS": 31.53, "sweep": {"parses": 85, "saves": 21, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 17458.416162, "sweepRuns": 1}, "sweepParsesPerSave": 4.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 89, "saves": 22, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 17458.416162, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
run3 {"label": "head", "workspaces": 4700, "portOpenS": 2.18, "titlesDoneS": 12.4, "settledS": 29.87, "sweep": {"parses": 43, "saves": 21, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 15616.672260000001, "sweepRuns": 1}, "sweepParsesPerSave": 2.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 47, "saves": 22, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 15616.672260000001, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
run0 {"label": "base", "workspaces": 4700, "portOpenS": 2.18, "titlesDoneS": 2.18, "settledS": 16.61, "sweep": {"parses": 5, "saves": 1, "sweepParses": 5, "sweepSaves": 1, "sweepMs": 5474.685412999999, "sweepRuns": 1}, "sweepParsesPerSave": 5.0, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 9, "saves": 2, "sweepParses": 5, "sweepSaves": 1, "sweepMs": 5474.685412999999, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
run0 {"label": "head", "workspaces": 4700, "portOpenS": 2.18, "titlesDoneS": 2.18, "settledS": 17.11, "sweep": {"parses": 3, "saves": 1, "sweepParses": 3, "sweepSaves": 1, "sweepMs": 5335.1228790000005, "sweepRuns": 1}, "sweepParsesPerSave": 3.0, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 7, "saves": 2, "sweepParses": 3, "sweepSaves": 1, "sweepMs": 5335.1228790000005, "sweepRuns": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
counter.cjs (preload)
// Preload: count Config parses and saves overall and while the startup heal sweep runs.
const { isMainThread } = require("worker_threads");
const fs = require("fs");
if (isMainThread) {
  const repo = process.env.F2_REPO;
  const out = process.env.F2_COUNTS;
  const { Config } = require(repo + "/dist/node/config/index.js");
  const removal = require(repo + "/dist/node/services/workspaceRemoval.js");
  const c = { parses: 0, saves: 0, sweepParses: 0, sweepSaves: 0, sweepMs: null, sweepRuns: 0 };
  let inSweep = false;
  const P = Config.prototype;
  const origRead = P.readConfigOrDefault;
  P.readConfigOrDefault = function (...a) { c.parses++; if (inSweep) c.sweepParses++; return origRead.apply(this, a); };
  const origSave = P.saveConfig;
  P.saveConfig = function (...a) { c.saves++; if (inSweep) c.sweepSaves++; return origSave.apply(this, a); };
  const origHeal = removal.healRemovalTombstonesForRegisteredWorkspaces;
  if (typeof origHeal !== "function") throw new Error("heal sweep export missing");
  removal.healRemovalTombstonesForRegisteredWorkspaces = async function (...a) {
    c.sweepRuns++; inSweep = true; const t0 = performance.now();
    try { return await origHeal.apply(this, a); }
    finally {
      inSweep = false; c.sweepMs = performance.now() - t0;
      fs.writeFileSync(out + ".sweep", JSON.stringify(c));
    }
  };
  process.on("SIGUSR2", () => fs.writeFileSync(out, JSON.stringify(c)));
}
driver.py
"""F2 startup + cross-process dogfood driver. Synthetic fixture only; never real ~/.xum data."""
import hashlib, json, os, shutil, signal, socket, subprocess, sys, time, urllib.request, random

repo, label, template, port, outdir = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4]), sys.argv[5]
root = os.path.join(outdir, f"root-{label}")
assert root.startswith(os.environ["XUM_SCRATCH_DIR"] + "/") and os.path.realpath(root) != os.path.realpath(os.path.expanduser("~/.xum")), root
shutil.rmtree(root, ignore_errors=True)
shutil.copytree(template, root, symlinks=True)
cfg_path = os.path.join(root, "config.json")
cfg = json.load(open(cfg_path))
ws = [(p, w) for p, proj in cfg["projects"] for w in proj["workspaces"]]
active = [w for _, w in ws if not w.get("archivedAt")]
assert len(ws) >= 4700, len(ws)

def tomb(wid):
    d = hashlib.sha256(wid.encode()).hexdigest()[:32]
    return os.path.join(root, "locks", f"workspace-removed-{d}.json")
os.makedirs(os.path.join(root, "locks"), exist_ok=True)
old = time.time() - 86400
rng = random.Random(7)
for i in range(2734):
    f = tomb(f"gone-{rng.getrandbits(64):016x}")
    json.dump({"workspaceId": os.path.basename(f), "removedAt": 1}, open(f, "w"))
    os.utime(f, (old, old))
healed = [active[1]["id"], active[2]["id"]]
for wid in healed:
    f = tomb(wid); json.dump({"workspaceId": wid, "removedAt": 1}, open(f, "w")); os.utime(f, (old, old))
fresh = active[3]["id"]
json.dump({"workspaceId": fresh, "removedAt": 1}, open(tomb(fresh), "w"))
# The unregistered markers name ids that are not in config (their own file name).
for f in os.listdir(os.path.join(root, "locks")):
    pass
counts = os.path.join(outdir, f"counts-{label}.json")
for p in (counts, counts + ".sweep"):
    if os.path.exists(p): os.remove(p)
env = {
    "PATH": os.environ["PATH"], "HOME": os.path.join(root, "home"), "USERPROFILE": os.path.join(root, "home"),
    "XDG_CONFIG_HOME": os.path.join(root, "home", ".config"), "XDG_CACHE_HOME": os.path.join(root, "home", ".cache"),
    "XUM_ROOT": root, "XUM_LOG_LEVEL": "info", "NODE_ENV": "production", "XUM_MOCK_AI": "1",
    "XUM_DISABLE_TELEMETRY": "1", "F2_REPO": repo, "F2_COUNTS": counts,
}
log = open(os.path.join(outdir, f"server-{label}.log"), "w")
t0 = time.monotonic()
proc = subprocess.Popen(["node", "--require", os.path.join(os.path.dirname(__file__), "counter.cjs"),
                         os.path.join(repo, "dist/cli/index.js"), "server", "--host", "127.0.0.1",
                         "--port", str(port), "--no-auth"], env=env, stdout=log, stderr=subprocess.STDOUT,
                        stdin=subprocess.DEVNULL, start_new_session=True)
open(os.path.join(outdir, f"server-{label}.pid"), "w").write(str(proc.pid))

def api(route, body):
    req = urllib.request.Request(f"http://127.0.0.1:{port}/api/{route}", data=json.dumps(body).encode(),
                                 headers={"content-type": "application/json"}, method="POST")
    with urllib.request.urlopen(req, timeout=120) as r:
        raw = r.read()
        return json.loads(raw) if raw else None

result = {"label": label, "workspaces": len(ws)}
try:
    while True:
        assert proc.poll() is None, "server exited"
        try:
            socket.create_connection(("127.0.0.1", port), timeout=0.2).close(); break
        except OSError:
            time.sleep(0.02)
    result["portOpenS"] = round(time.monotonic() - t0, 2)
    titles = []
    for i in range(int(os.environ.get("F2_TITLES", "20"))):
        r = api("workspace/updateTitle", {"workspaceId": active[10 + i]["id"], "title": f"F2 startup title {i}"})
        assert r is not None and r.get("success") is True, r
    result["titlesDoneS"] = round(time.monotonic() - t0, 2)
    deadline = time.monotonic() + 600
    while not (os.path.exists(counts + ".sweep") and "housekeeping settled" in open(log.name).read()):
        assert proc.poll() is None, "server exited"; assert time.monotonic() < deadline, "timeout"
        time.sleep(0.1)
    result["settledS"] = round(time.monotonic() - t0, 2)
    sweep = json.load(open(counts + ".sweep"))
    result["sweep"] = sweep
    result["sweepParsesPerSave"] = round(sweep["sweepParses"] / sweep["sweepSaves"], 2) if sweep["sweepSaves"] else None
    # Cross-process check: our rename, then another writer's atomic replace, then an in-place edit.
    target = active[40]["id"]
    assert api("workspace/updateTitle", {"workspaceId": target, "title": "API title"})["success"]
    info = api("workspace/getInfo", {"workspaceId": target}); result["afterApi"] = info["title"]
    data = json.load(open(cfg_path))
    for _, proj in data["projects"]:
        for w in proj["workspaces"]:
            if w["id"] == target: w["title"] = "External atomic title"
    tmp = cfg_path + ".ext"; json.dump(data, open(tmp, "w"), indent=2); os.replace(tmp, cfg_path)
    result["afterAtomic"] = api("workspace/getInfo", {"workspaceId": target})["title"]
    text = open(cfg_path).read().replace('"External atomic title"', '"In-place edit, longer title"')
    with open(cfg_path, "r+") as f: f.write(text); f.truncate()
    result["afterInPlace"] = api("workspace/getInfo", {"workspaceId": target})["title"]
    os.kill(proc.pid, signal.SIGUSR2); time.sleep(0.5)
    result["totals"] = json.load(open(counts))
finally:
    if proc.poll() is None:
        os.killpg(proc.pid, signal.SIGTERM)
        try: proc.wait(20)
        except subprocess.TimeoutExpired: os.killpg(proc.pid, signal.SIGKILL); proc.wait()
locks = os.listdir(os.path.join(root, "locks"))
names = set(locks)
result["tombstonesLeft"] = sum(1 for n in locks if n.startswith("workspace-removed-"))
result["healedGone"] = all(os.path.basename(tomb(w)) not in names for w in healed)
result["freshKept"] = os.path.basename(tomb(fresh)) in names
result["healWarnings"] = open(log.name).read().count("Healed a removal tombstone")
print(json.dumps(result))

@ThomasK33 ThomasK33 added backlog and removed backlog labels Oct 6, 2026
@ThomasK33

Copy link
Copy Markdown
Member Author

Remote UAT (Coder Agents on dogfood, coder-prototypes template) on head 4fa387b2e78bf32f810d7c21bc1f488bd33ce265: PASS. The base was 0c5e35d81638e85ed420c4b7c943d86ed502a2f7. Synthetic 4,700-workspace fixture, mock AI, no real data.

  1. bun test src/node/config/ on head: 63 pass, 0 fail.
  2. Startup heal sweep, 3 alternating base/head pairs: head 21 saves, 21 edit reads, 22 reader parses (1 cold read before the first save), 2.05 parses per save. Base: 21 saves, 21 edit reads, 64 reader parses (2 before the first save), 4.05 per save. All runs healed exactly the 2 stale registered tombstones and kept the other 2,735.
  3. Cross-process reads: API rename, another writer's atomic rename, and an in-place edit of a different length all showed up in workspace.getInfo on both builds.
  4. chmod 000 config.json: the edit was refused and the file did not change. After chmod 600, the next edit succeeded without a restart. Same on base.
  5. Corrupt file: the edit was refused, a config.json.corrupt-* backup was created, and no defaults were written. After the restore, the next edit succeeded without a restart. Same on base.
  6. External writer renaming config.json in a loop during 50 edits: getInfo matched the disk in every check, on the live server and after a restart. A read-freshness probe found 0 stale reads on both builds.

The remote also found older behavior that this PR does not change (same on base): getInfo returns a null body while config.json is unreadable or corrupt, an EACCES read reports "no confirmed backup yet", and an in-place edit that keeps size and mtime stays invisible until the next save.

startup-results.txt
base-1 build=base sha=0c5e35d81638e85ed420c4b7c943d86ed502a2f7 titles=20 rc=0 {"label": "base-1", "workspaces": 4700, "portOpenS": 7.76, "titlesDoneS": 33.32, "settledS": 174.08, "sweep": {"parses": 85, "saves": 21, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 49090.142915000004, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 64, "sweepReaderParsesBeforeFirstSave": 2}, "sweepParsesPerSave": 4.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 89, "saves": 22, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 49090.142915000004, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 64, "sweepReaderParsesBeforeFirstSave": 2}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
head-1 build=head sha=4fa387b2e78bf32f810d7c21bc1f488bd33ce265 titles=20 rc=0 {"label": "head-1", "workspaces": 4700, "portOpenS": 2.39, "titlesDoneS": 21.51, "settledS": 161.57, "sweep": {"parses": 43, "saves": 21, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 48918.627538, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 22, "sweepReaderParsesBeforeFirstSave": 1}, "sweepParsesPerSave": 2.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 47, "saves": 22, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 48918.627538, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 22, "sweepReaderParsesBeforeFirstSave": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
base-2 build=base sha=0c5e35d81638e85ed420c4b7c943d86ed502a2f7 titles=20 rc=0 {"label": "base-2", "workspaces": 4700, "portOpenS": 8.1, "titlesDoneS": 32.34, "settledS": 79.94, "sweep": {"parses": 85, "saves": 21, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 43479.307466, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 64, "sweepReaderParsesBeforeFirstSave": 2}, "sweepParsesPerSave": 4.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 89, "saves": 22, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 43479.307466, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 64, "sweepReaderParsesBeforeFirstSave": 2}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
head-2 build=head sha=4fa387b2e78bf32f810d7c21bc1f488bd33ce265 titles=20 rc=0 {"label": "head-2", "workspaces": 4700, "portOpenS": 1.69, "titlesDoneS": 13.0, "settledS": 59.34, "sweep": {"parses": 43, "saves": 21, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 17547.606755, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 22, "sweepReaderParsesBeforeFirstSave": 1}, "sweepParsesPerSave": 2.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 47, "saves": 22, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 17547.606755, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 22, "sweepReaderParsesBeforeFirstSave": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
base-ctl build=base sha=0c5e35d81638e85ed420c4b7c943d86ed502a2f7 titles=0 rc=0 {"label": "base-ctl", "workspaces": 4700, "portOpenS": 1.68, "titlesDoneS": 1.68, "settledS": 23.52, "sweep": {"parses": 5, "saves": 1, "sweepParses": 5, "sweepSaves": 1, "sweepMs": 10138.557294999999, "sweepRuns": 1, "sweepTransformParses": 1, "sweepReaderParses": 4, "sweepReaderParsesBeforeFirstSave": 2}, "sweepParsesPerSave": 5.0, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 9, "saves": 2, "sweepParses": 5, "sweepSaves": 1, "sweepMs": 10138.557294999999, "sweepRuns": 1, "sweepTransformParses": 1, "sweepReaderParses": 4, "sweepReaderParsesBeforeFirstSave": 2}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
head-ctl build=head sha=4fa387b2e78bf32f810d7c21bc1f488bd33ce265 titles=0 rc=0 {"label": "head-ctl", "workspaces": 4700, "portOpenS": 1.45, "titlesDoneS": 1.45, "settledS": 17.5, "sweep": {"parses": 3, "saves": 1, "sweepParses": 3, "sweepSaves": 1, "sweepMs": 3188.4682430000003, "sweepRuns": 1, "sweepTransformParses": 1, "sweepReaderParses": 2, "sweepReaderParsesBeforeFirstSave": 1}, "sweepParsesPerSave": 3.0, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 7, "saves": 2, "sweepParses": 3, "sweepSaves": 1, "sweepMs": 3188.4682430000003, "sweepRuns": 1, "sweepTransformParses": 1, "sweepReaderParses": 2, "sweepReaderParsesBeforeFirstSave": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
DONE
base-3 build=base sha=0c5e35d81638e85ed420c4b7c943d86ed502a2f7 titles=20 rc=0 {"label": "base-3", "workspaces": 4700, "portOpenS": 1.51, "titlesDoneS": 10.85, "settledS": 21.77, "sweep": {"parses": 85, "saves": 21, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 12144.875042, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 64, "sweepReaderParsesBeforeFirstSave": 2}, "sweepParsesPerSave": 4.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 89, "saves": 22, "sweepParses": 85, "sweepSaves": 21, "sweepMs": 12144.875042, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 64, "sweepReaderParsesBeforeFirstSave": 2}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
head-3 build=head sha=4fa387b2e78bf32f810d7c21bc1f488bd33ce265 titles=20 rc=0 {"label": "head-3", "workspaces": 4700, "portOpenS": 1.27, "titlesDoneS": 7.78, "settledS": 18.4, "sweep": {"parses": 43, "saves": 21, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 9108.323377, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 22, "sweepReaderParsesBeforeFirstSave": 1}, "sweepParsesPerSave": 2.05, "afterApi": "API title", "afterAtomic": "External atomic title", "afterInPlace": "In-place edit, longer title", "totals": {"parses": 47, "saves": 22, "sweepParses": 43, "sweepSaves": 21, "sweepMs": 9108.323377, "sweepRuns": 1, "sweepTransformParses": 21, "sweepReaderParses": 22, "sweepReaderParsesBeforeFirstSave": 1}, "tombstonesLeft": 2735, "healedGone": true, "freshKept": true, "healWarnings": 2}
DONE3
label     build sha       saves parses transform reader readerBeforeFirstSave readerAfterFirstSave parsesPerSave tombstonesLeft healedGone freshKept healWarnings afterApi/afterAtomic/afterInPlace
base-1    base  0c5e35d81638    21     85        21     64                     2                   62          4.05           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'
head-1    head  4fa387b2e78b    21     43        21     22                     1                   21          2.05           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'
base-2    base  0c5e35d81638    21     85        21     64                     2                   62          4.05           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'
head-2    head  4fa387b2e78b    21     43        21     22                     1                   21          2.05           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'
base-ctl  base  0c5e35d81638     1      5         1      4                     2                    2           5.0           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'
head-ctl  head  4fa387b2e78b     1      3         1      2                     1                    1           3.0           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'
base-3    base  0c5e35d81638    21     85        21     64                     2                   62          4.05           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'
head-3    head  4fa387b2e78b    21     43        21     22                     1                   21          2.05           2735 True       True                 2 'API title'/'External atomic title'/'In-place edit, longer title'

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit a4e2d5c Oct 6, 2026
32 of 33 checks passed
@ThomasK33
ThomasK33 deleted the thomask33/perf-config-keep-snapshot branch October 6, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant