Skip to content

🤖 perf: skip the config save clone when no slot holds Cyber - #5770

Merged
ThomasK33 merged 4 commits into
mainfrom
perf/config-save-skip-cyber-clone
Oct 6, 2026
Merged

ThomasK33 merged 4 commits into
mainfrom
perf/config-save-skip-cyber-clone

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Refs #5727

Summary

Every config save cloned the whole config with structuredClone before it encoded Cyber reasoning modes for disk. The clone keeps the encoder from changing runtime state. This PR skips the clone and the encode when no reasoning-mode slot holds "cyber", so the encode would change nothing anyway. A new hasCyberReasoningMode(doc) in cyberReasoningModeDisk.ts walks the same collectReasoningModeSlots list as the encoder, so the check and the encoder cannot disagree about which slots exist. Configs with a Cyber slot still take the clone-and-encode path, as before.

The original 20 ms target was not met.

This PR ships under a budget exception that the perf owner declared in advance and recorded on #5727:

Results

The plan's budget was "editConfig at least 20 ms faster at the live-shaped size (4,712 workspaces)". The primary row is editConfig, same-value edit (4712). It saved 17.8 ms, 18.7 ms and 16.6 ms in the three base-vs-head runs (run 1, run 2, confirmation), so the target was not met. Run 2's CI for that row includes 0. The confirmation run's CI excludes 0, which is the exception's gate.

Row (4,712 workspaces) Run Base Head Saved Delta 95% CI Verdict
editConfig, same-value edit (primary) Run 1 (head de7e1c35d8) 142.67 ms 124.82 ms 17.8 ms -12.4% [-16.2%, -8.5%] faster
editConfig, same-value edit (primary) Run 2 (head de7e1c35d8) 144.59 ms 125.93 ms 18.7 ms -9.3% [-18.6%, +1.1%] ~
editConfig, same-value edit (primary) Confirmation (final head c14e682ec3) 140.99 ms 124.37 ms 16.6 ms -12.6% [-14.7%, -10.4%] faster
editConfig, then loadConfigOrDefault Run 1 (head de7e1c35d8) 140.83 ms 110.31 ms 30.5 ms -21.0% [-23.2%, -18.6%] faster
editConfig, then loadConfigOrDefault Run 2 (head de7e1c35d8) 139.71 ms 108.64 ms 31.1 ms -26.3% [-34.3%, -17.4%] faster
editConfig, then loadConfigOrDefault Confirmation (final head c14e682ec3) 139.57 ms 109.92 ms 29.6 ms -25.3% [-34.0%, -15.4%] faster
editConfig, reader on every event-loop turn Run 1 (head de7e1c35d8) 139.88 ms 113.01 ms 26.9 ms -21.7% [-27.7%, -15.1%] faster
editConfig, reader on every event-loop turn Run 2 (head de7e1c35d8) 138.40 ms 111.34 ms 27.1 ms -14.3% [-23.5%, -3.9%] faster
editConfig, reader on every event-loop turn Confirmation (final head c14e682ec3) 139.46 ms 111.96 ms 27.5 ms -19.8% [-20.9%, -18.6%] faster

All runs: Node, make bench-compare ROUNDS=10, base a4e2d5c44933b17bbf01e455fc42346999bdaa47 (merge base with origin/main), the same synthetic fixtures (configScale.bench.ts, never real data).

  • Removing the clone confirms the mechanism, not the amount saved. The remote CPU profile in the Dogfood section shows structuredClone gone from the save path on head.
  • The two rows that read after the edit save about 27–31 ms, more than the plain edit row. One possible reason is that the bench runs garbage collection before each iteration (.gc("inner")), which can hide part of the clone's cost from the plain edit row. This is an unverified guess.
  • The cost of the new check: hasCyberReasoningMode takes 926.77 µs at 4,712 workspaces and 1.02 ms at 5,000 (A/A run). The limit was 2 ms.
  • Other rows: the confirmation run marks getWorkspaceMetadataById(last id) (4712) +2.5% slower (CI [+0.5%, +4.4%]). Run 1 marked two getAllWorkspaceMetadata, last-known probes rows +4.1% and +2.5% slower, and run 2 did not. This PR does not touch that code. The A/A run, which compares one commit with itself, marked 4 rows slower at +1% to +2.7%, so a gap of that size can come from noise on this shared host. I did not prove that it does. I did not rerun: the exception allowed one confirmation run.
  • The bench filter is BENCH='src/node/config/config*.bench.ts', not plain BENCH=config. bench-compare runs the head's bench files on both sides, and the new cyberReasoningModeDisk.bench.ts imports hasCyberReasoningMode, which does not exist on base. The glob selects the same config.bench.ts and configScale.bench.ts rows in all three runs.

Load averages (1, 5 and 15 min):

  • A/A: loadavg start 11.43 12.92 14.18 | end 30.02 26.21 20.79
  • Run 1: loadavg start 28.35 26.02 20.82 | end 16.52 15.43 16.40
  • Run 2: loadavg start 13.54 14.79 16.16 | end 16.27 32.64 37.32
  • Confirmation: loadavg start 11.00 11.51 12.20 | end 4.30 5.89 8.05
Full bench-compare tables (A/A, run 1, run 2, confirmation)

A/A (make bench-compare BENCH=config BASE=HEAD, on 6e526e29af, which has the same product change as head, compared with itself):

runtime node | rounds 10 | base 6e526e29affe | head 6e526e29affe
loadavg start 11.43 12.92 14.18 | end 30.02 26.21 20.79
base/head: median over rounds of each run's mean time
benchmark                                                                          base       head   delta            95% CI  verdict
config-a28db80a: loadConfigOrDefault (fresh Config, 4500 workspaces)           12.24 ms   12.10 ms   -0.8%    [-3.4%, +1.9%]        ~
config-a28db80a: loadConfigOrDefault (snapshot hit)                             3.23 µs    3.23 µs   +0.7%    [-0.6%, +2.0%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (50)                  509.87 µs  523.58 µs   +2.3%    [-2.9%, +7.8%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (500)                   3.08 ms    3.18 ms   +2.7%    [-1.7%, +7.2%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (5000)                 32.93 ms   33.24 ms   +2.0%    [-1.5%, +5.7%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (4712)                 31.32 ms   31.66 ms   +2.8%    [-0.7%, +6.4%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (50)                    2.96 µs    2.99 µs   +1.9%    [-0.2%, +4.2%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (500)                   2.95 µs    2.97 µs   +2.2%    [-0.4%, +4.8%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (5000)                  2.95 µs    3.04 µs   +2.5%    [+0.1%, +5.0%]   slower
configScale-334200bf: loadConfigOrDefault, snapshot hit (4712)                  2.95 µs    3.04 µs   +2.6%    [+0.1%, +5.2%]   slower
configScale-334200bf: findWorkspace(last id) (50)                               3.05 µs    3.15 µs   +2.7%    [+0.3%, +5.2%]   slower
configScale-334200bf: findWorkspace(last id) (500)                              3.04 µs    3.13 µs   +1.4%    [-0.2%, +3.0%]        ~
configScale-334200bf: findWorkspace(last id) (5000)                             3.20 µs    3.25 µs   +1.0%    [+0.0%, +2.1%]   slower
configScale-334200bf: findWorkspace(last id) (4712)                             3.26 µs    3.30 µs   +1.2%    [-0.3%, +2.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (50)                  1.39 ms    1.40 ms   +1.4%    [-2.9%, +6.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (500)                12.81 ms   12.91 ms  +15.9%  [-15.0%, +58.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (5000)              135.25 ms  135.89 ms   +3.2%    [-2.7%, +9.4%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (4712)              131.05 ms  129.75 ms   -1.0%    [-5.4%, +3.6%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (50)         669.84 µs  673.46 µs   -0.9%    [-4.1%, +2.4%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (500)          6.63 ms    6.71 ms   +0.1%    [-2.9%, +3.1%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (5000)        72.67 ms   71.80 ms   -5.0%   [-13.6%, +4.5%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (4712)        67.96 ms   71.10 ms   +2.3%    [-1.1%, +5.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (50)               3.49 µs    3.56 µs   +1.2%    [-1.2%, +3.6%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (500)              3.76 µs    3.79 µs   +0.5%    [-2.3%, +3.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (5000)             5.59 µs    5.67 µs   +1.3%    [-3.4%, +6.2%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (4712)             5.56 µs    5.51 µs   -0.4%    [-4.5%, +3.9%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (50)                   78.50 µs   79.30 µs   +1.5%    [-4.7%, +8.2%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (500)                  74.40 µs   76.14 µs   +2.0%    [-2.5%, +6.7%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (5000)                 72.72 µs   73.17 µs   +1.4%    [-1.3%, +4.1%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (4712)                 71.21 µs   72.51 µs   +2.0%    [-1.6%, +5.7%]        ~
configScale-334200bf: editConfig, same-value edit (50)                          5.53 ms    5.27 ms   -4.2%  [-18.1%, +12.1%]        ~
configScale-334200bf: editConfig, same-value edit (500)                        15.14 ms   15.13 ms   +5.6%   [-8.1%, +21.4%]        ~
configScale-334200bf: editConfig, same-value edit (5000)                      124.24 ms  122.70 ms   -6.0%   [-16.7%, +6.1%]        ~
configScale-334200bf: editConfig, same-value edit (4712)                      121.02 ms  121.59 ms   -1.2%    [-6.0%, +3.8%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (50)                 5.40 ms    5.21 ms   -3.2%   [-11.6%, +6.0%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (500)               15.37 ms   15.39 ms   +1.9%    [-5.0%, +9.2%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (5000)             119.24 ms  119.93 ms   -0.5%    [-2.7%, +1.8%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (4712)             111.56 ms  112.97 ms   +4.0%   [-8.2%, +17.8%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (50)          5.30 ms    5.10 ms   -3.7%    [-8.1%, +0.8%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (500)        15.04 ms   15.80 ms   +1.8%    [-2.2%, +6.0%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (5000)      117.13 ms  122.54 ms   +3.1%    [-0.7%, +7.2%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (4712)      111.08 ms  112.42 ms   +2.1%    [-1.5%, +5.7%]        ~
cyberReasoningModeDisk-87e45990: hasCyberReasoningMode, no Cyber slot (50)      8.22 µs    8.35 µs   -0.7%    [-7.2%, +6.2%]        ~
cyberReasoningModeDisk-87e45990: hasCyberReasoningMode, no Cyber slot (500)    80.92 µs   83.24 µs   +0.7%    [-4.2%, +5.9%]        ~
cyberReasoningModeDisk-87e45990: hasCyberReasoningMode, no Cyber slot (5000)    1.01 ms    1.02 ms   -0.0%    [-4.0%, +4.1%]        ~
cyberReasoningModeDisk-87e45990: hasCyberReasoningMode, no Cyber slot (4712)  953.27 µs  926.77 µs   -0.4%    [-6.2%, +5.7%]        ~

Run 1:

runtime node | rounds 10 | base a4e2d5c44933 | head de7e1c35d823
loadavg start 28.35 26.02 20.82 | end 16.52 15.43 16.40
base/head: median over rounds of each run's mean time
benchmark                                                                      base       head   delta            95% CI  verdict
config-a28db80a: loadConfigOrDefault (fresh Config, 4500 workspaces)       12.04 ms   12.17 ms   +0.2%    [-2.6%, +3.1%]        ~
config-a28db80a: loadConfigOrDefault (snapshot hit)                         3.17 µs    3.21 µs   +1.4%    [-0.4%, +3.2%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (50)              511.51 µs  523.07 µs   +1.9%    [-4.0%, +8.2%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (500)               3.10 ms    3.09 ms   -0.2%    [-2.3%, +1.9%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (5000)             33.61 ms   33.32 ms   -1.1%    [-4.8%, +2.7%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (4712)             31.52 ms   31.05 ms   -1.7%    [-5.4%, +2.3%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (50)                2.95 µs    2.93 µs   -2.1%    [-4.7%, +0.5%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (500)               2.95 µs    2.94 µs   -0.3%    [-2.7%, +2.1%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (5000)              2.94 µs    2.93 µs   -0.5%    [-2.9%, +2.0%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (4712)              2.95 µs    2.95 µs   -1.0%    [-3.5%, +1.4%]        ~
configScale-334200bf: findWorkspace(last id) (50)                           3.08 µs    3.06 µs   -1.0%    [-2.6%, +0.6%]        ~
configScale-334200bf: findWorkspace(last id) (500)                          3.07 µs    3.04 µs   -1.6%    [-2.8%, -0.5%]   faster
configScale-334200bf: findWorkspace(last id) (5000)                         3.26 µs    3.22 µs   -1.1%    [-3.7%, +1.5%]        ~
configScale-334200bf: findWorkspace(last id) (4712)                         3.23 µs    3.23 µs   -1.0%    [-2.6%, +0.7%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (50)              1.35 ms    1.39 ms   +2.2%    [-1.5%, +6.1%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (500)            12.82 ms   12.95 ms   +1.5%    [-3.7%, +7.1%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (5000)          132.34 ms  134.61 ms   +2.0%    [-2.0%, +6.1%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (4712)          128.45 ms  127.46 ms   -0.5%    [-3.3%, +2.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (50)     678.63 µs  687.43 µs   +1.8%    [-2.9%, +6.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (500)      6.55 ms    6.90 ms   +4.1%    [+0.2%, +8.2%]   slower
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (5000)    72.02 ms   72.60 ms   +1.3%    [-2.5%, +5.2%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (4712)    67.06 ms   68.94 ms   +2.5%    [+0.3%, +4.8%]   slower
configScale-334200bf: getAllWorkspaceMetadata, registry memo (50)           3.46 µs    3.46 µs   +0.4%    [-1.7%, +2.6%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (500)          3.73 µs    3.77 µs   -0.1%    [-3.9%, +3.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (5000)         5.36 µs    5.45 µs   +0.3%    [-3.6%, +4.4%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (4712)         5.27 µs    5.35 µs   +0.8%    [-1.5%, +3.2%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (50)               79.89 µs   78.51 µs   +1.5%    [-2.9%, +6.0%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (500)              74.65 µs   75.02 µs   -1.6%    [-7.9%, +5.1%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (5000)             74.43 µs   72.75 µs   -2.9%    [-7.0%, +1.4%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (4712)             71.73 µs   72.01 µs   +1.5%    [-4.7%, +8.1%]        ~
configScale-334200bf: editConfig, same-value edit (50)                      5.88 ms    5.33 ms   -9.3%   [-16.2%, -1.8%]   faster
configScale-334200bf: editConfig, same-value edit (500)                    17.93 ms   15.77 ms  -12.2%   [-14.8%, -9.5%]   faster
configScale-334200bf: editConfig, same-value edit (5000)                  154.94 ms  123.40 ms  -19.8%  [-21.4%, -18.2%]   faster
configScale-334200bf: editConfig, same-value edit (4712)                  142.67 ms  124.82 ms  -12.4%   [-16.2%, -8.5%]   faster
configScale-334200bf: editConfig, then loadConfigOrDefault (50)             5.79 ms    5.48 ms   -4.2%   [-11.7%, +3.9%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (500)           17.67 ms   15.57 ms  -14.0%   [-18.7%, -9.0%]   faster
configScale-334200bf: editConfig, then loadConfigOrDefault (5000)         151.12 ms  118.81 ms  -22.5%  [-25.5%, -19.3%]   faster
configScale-334200bf: editConfig, then loadConfigOrDefault (4712)         140.83 ms  110.31 ms  -21.0%  [-23.2%, -18.6%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (50)      5.66 ms    5.44 ms   -7.9%   [-16.1%, +1.0%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (500)    17.32 ms   15.25 ms  -10.4%   [-14.1%, -6.6%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (5000)  150.68 ms  118.85 ms  -22.1%  [-25.0%, -19.2%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (4712)  139.88 ms  113.01 ms  -21.7%  [-27.7%, -15.1%]   faster

Run 2:

runtime node | rounds 10 | base a4e2d5c44933 | head de7e1c35d823
loadavg start 13.54 14.79 16.16 | end 16.27 32.64 37.32
base/head: median over rounds of each run's mean time
benchmark                                                                      base       head   delta             95% CI  verdict
config-a28db80a: loadConfigOrDefault (fresh Config, 4500 workspaces)       11.94 ms   12.24 ms   +2.0%     [-1.2%, +5.4%]        ~
config-a28db80a: loadConfigOrDefault (snapshot hit)                         3.22 µs    3.28 µs   +1.2%     [-0.8%, +3.4%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (50)              508.36 µs  524.06 µs   +0.5%     [-2.5%, +3.6%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (500)               3.08 ms    3.11 ms   -0.3%     [-2.7%, +2.2%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (5000)             32.97 ms   33.03 ms   +0.6%     [-1.6%, +2.8%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (4712)             31.03 ms   30.71 ms   +0.3%     [-2.1%, +2.8%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (50)                2.96 µs    2.98 µs   +0.3%     [-1.5%, +2.0%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (500)               2.98 µs    2.94 µs   -0.2%     [-2.6%, +2.1%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (5000)              2.98 µs    2.96 µs   -0.6%     [-2.5%, +1.4%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (4712)              2.96 µs    2.94 µs   -0.3%     [-2.2%, +1.6%]        ~
configScale-334200bf: findWorkspace(last id) (50)                           3.06 µs    3.05 µs   -0.1%     [-1.9%, +1.7%]        ~
configScale-334200bf: findWorkspace(last id) (500)                          3.09 µs    3.06 µs   -0.4%     [-1.7%, +0.9%]        ~
configScale-334200bf: findWorkspace(last id) (5000)                         3.25 µs    3.27 µs   +1.1%     [-1.3%, +3.6%]        ~
configScale-334200bf: findWorkspace(last id) (4712)                         3.27 µs    3.26 µs   +0.9%     [-2.3%, +4.1%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (50)              1.37 ms    1.42 ms   +1.5%     [-3.7%, +7.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (500)            12.76 ms   12.79 ms   +0.4%     [-6.1%, +7.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (5000)          136.56 ms  135.98 ms   +0.8%     [-5.2%, +7.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (4712)          127.50 ms  128.17 ms   -0.3%     [-6.5%, +6.2%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (50)     669.69 µs  676.99 µs   +1.5%     [-5.4%, +8.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (500)      6.76 ms    6.79 ms   +0.5%     [-6.1%, +7.6%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (5000)    71.70 ms   72.46 ms   -2.2%    [-11.8%, +8.4%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (4712)    68.71 ms   68.36 ms   -4.9%    [-16.8%, +8.6%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (50)           3.47 µs    3.44 µs   -0.4%     [-4.4%, +3.7%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (500)          3.82 µs    3.71 µs   +0.6%     [-3.5%, +4.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (5000)         5.58 µs    5.46 µs   +1.3%     [-3.0%, +5.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (4712)         5.51 µs    5.53 µs   +1.1%     [-3.3%, +5.7%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (50)               78.24 µs   80.17 µs   +5.0%    [-5.7%, +17.0%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (500)              75.25 µs   75.15 µs   +2.2%    [-5.0%, +10.0%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (5000)             74.92 µs   75.07 µs   +0.9%    [-8.8%, +11.8%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (4712)             72.08 µs   72.12 µs   +4.2%    [-6.1%, +15.5%]        ~
configScale-334200bf: editConfig, same-value edit (50)                      6.08 ms    5.50 ms  -18.6%   [-45.1%, +20.8%]        ~
configScale-334200bf: editConfig, same-value edit (500)                    18.08 ms   15.72 ms   -3.9%   [-48.0%, +77.5%]        ~
configScale-334200bf: editConfig, same-value edit (5000)                  154.53 ms  124.84 ms  -19.8%   [-27.1%, -11.7%]   faster
configScale-334200bf: editConfig, same-value edit (4712)                  144.59 ms  125.93 ms   -9.3%    [-18.6%, +1.1%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (50)             5.76 ms    5.49 ms  +26.8%   [-14.6%, +88.3%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (500)           17.60 ms   15.51 ms  -11.5%    [-24.9%, +4.2%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (5000)         147.53 ms  116.87 ms  -21.1%   [-29.1%, -12.2%]   faster
configScale-334200bf: editConfig, then loadConfigOrDefault (4712)         139.71 ms  108.64 ms  -26.3%   [-34.3%, -17.4%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (50)      5.63 ms    5.34 ms  +37.6%  [-27.5%, +161.0%]        ~
configScale-334200bf: editConfig, reader on every event-loop turn (500)    17.48 ms   15.48 ms   -8.2%    [-15.2%, -0.6%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (5000)  151.69 ms  118.14 ms  -19.0%   [-24.4%, -13.3%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (4712)  138.40 ms  111.34 ms  -14.3%    [-23.5%, -3.9%]   faster

Confirmation run on the final head:

runtime node | rounds 10 | base a4e2d5c44933 | head c14e682ec314
loadavg start 11.00 11.51 12.20 | end 4.30 5.89 8.05
base/head: median over rounds of each run's mean time
benchmark                                                                      base       head   delta            95% CI  verdict
config-a28db80a: loadConfigOrDefault (fresh Config, 4500 workspaces)       11.86 ms   11.71 ms   -0.6%    [-1.5%, +0.4%]        ~
config-a28db80a: loadConfigOrDefault (snapshot hit)                         3.18 µs    3.20 µs   +0.4%    [-0.8%, +1.7%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (50)              507.53 µs  506.12 µs   -0.9%    [-3.5%, +1.9%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (500)               3.07 ms    3.07 ms   -0.7%    [-2.9%, +1.6%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (5000)             32.89 ms   33.45 ms   +0.8%    [-0.9%, +2.6%]        ~
configScale-334200bf: loadConfigOrDefault, fresh Config (4712)             31.06 ms   31.40 ms   +0.7%    [-1.6%, +2.9%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (50)                2.93 µs    2.94 µs   +0.0%    [-0.5%, +0.6%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (500)               2.94 µs    2.93 µs   -0.2%    [-1.4%, +0.9%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (5000)              2.94 µs    2.93 µs   -0.5%    [-1.3%, +0.4%]        ~
configScale-334200bf: loadConfigOrDefault, snapshot hit (4712)              2.93 µs    2.91 µs   -0.4%    [-1.1%, +0.2%]        ~
configScale-334200bf: findWorkspace(last id) (50)                           3.03 µs    3.03 µs   -0.5%    [-1.1%, +0.2%]        ~
configScale-334200bf: findWorkspace(last id) (500)                          3.05 µs    3.03 µs   -0.2%    [-0.9%, +0.5%]        ~
configScale-334200bf: findWorkspace(last id) (5000)                         3.22 µs    3.21 µs   -0.3%    [-1.2%, +0.6%]        ~
configScale-334200bf: findWorkspace(last id) (4712)                         3.24 µs    3.23 µs   -0.2%    [-1.1%, +0.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (50)              1.42 ms    1.41 ms   -0.6%    [-3.4%, +2.2%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (500)            13.31 ms   13.38 ms   -0.4%    [-4.3%, +3.6%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (5000)          138.75 ms  141.60 ms   +1.7%    [-1.5%, +5.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, full build (4712)          129.71 ms  130.84 ms   -0.1%    [-3.5%, +3.5%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (50)     694.28 µs  691.28 µs   +1.0%    [-1.9%, +3.9%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (500)      6.85 ms    6.97 ms   +1.9%    [-1.1%, +5.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (5000)    73.67 ms   74.01 ms   +1.8%    [-1.1%, +4.8%]        ~
configScale-334200bf: getAllWorkspaceMetadata, last-known probes (4712)    70.49 ms   70.31 ms   +0.3%    [-2.3%, +2.9%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (50)           3.45 µs    3.43 µs   -0.8%    [-1.9%, +0.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (500)          3.69 µs    3.69 µs   +0.6%    [-2.1%, +3.3%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (5000)         5.25 µs    5.25 µs   -0.3%    [-1.6%, +1.0%]        ~
configScale-334200bf: getAllWorkspaceMetadata, registry memo (4712)         5.12 µs    5.15 µs   +0.3%    [-1.8%, +2.4%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (50)               77.42 µs   80.32 µs   +2.1%    [-1.3%, +5.5%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (500)              73.98 µs   73.99 µs   +1.8%    [-1.4%, +5.1%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (5000)             70.85 µs   74.00 µs   +3.6%    [-0.7%, +8.2%]        ~
configScale-334200bf: getWorkspaceMetadataById(last id) (4712)             70.58 µs   72.50 µs   +2.5%    [+0.5%, +4.4%]   slower
configScale-334200bf: editConfig, same-value edit (50)                      5.56 ms    5.29 ms   -4.3%    [-7.2%, -1.4%]   faster
configScale-334200bf: editConfig, same-value edit (500)                    17.64 ms   15.81 ms  -10.9%   [-13.4%, -8.4%]   faster
configScale-334200bf: editConfig, same-value edit (5000)                  156.77 ms  124.93 ms  -19.6%  [-21.0%, -18.2%]   faster
configScale-334200bf: editConfig, same-value edit (4712)                  140.99 ms  124.37 ms  -12.6%  [-14.7%, -10.4%]   faster
configScale-334200bf: editConfig, then loadConfigOrDefault (50)             5.44 ms    5.35 ms   -2.6%    [-7.5%, +2.5%]        ~
configScale-334200bf: editConfig, then loadConfigOrDefault (500)           17.48 ms   15.74 ms  -10.8%   [-12.3%, -9.2%]   faster
configScale-334200bf: editConfig, then loadConfigOrDefault (5000)         147.82 ms  116.47 ms  -21.3%  [-22.5%, -20.2%]   faster
configScale-334200bf: editConfig, then loadConfigOrDefault (4712)         139.57 ms  109.92 ms  -25.3%  [-34.0%, -15.4%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (50)      5.50 ms    5.16 ms   -7.4%   [-12.6%, -1.9%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (500)    17.14 ms   15.32 ms  -13.6%   [-20.6%, -6.0%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (5000)  150.93 ms  117.27 ms  -22.5%  [-24.1%, -20.9%]   faster
configScale-334200bf: editConfig, reader on every event-loop turn (4712)  139.46 ms  111.96 ms  -19.8%  [-20.9%, -18.6%]   faster

Same bytes

For a config without Cyber modes, head writes exactly the bytes base writes. A throwaway script wrote the configScale.bench.ts fixtures through the public Config API on each tree and hashed config.json after each save. writeId is a random UUID per save, so the script pins it to a constant. Base ran twice with identical results.

Workspaces Bytes Save Base a4e2d5c449 sha256 Head c14e682ec3 sha256 Equal
50 77,199 first save (in-memory objects) cb15542f45d5c2be05d277bf25a1f4920d128140a2813a828f444c3328ecc661 cb15542f45d5c2be05d277bf25a1f4920d128140a2813a828f444c3328ecc661 yes
50 77,199 same-value edit (parsed file) 33726d53306b6d3763f3f53aa155bddc0ac906c93f696053a8a6cb8f58911d06 33726d53306b6d3763f3f53aa155bddc0ac906c93f696053a8a6cb8f58911d06 yes
50 77,199 retitle last workspace 19ce7d11a08fbb9119d60896d2ff51d3ca6d7d63cd4fb333701927ceb66b09eb 19ce7d11a08fbb9119d60896d2ff51d3ca6d7d63cd4fb333701927ceb66b09eb yes
500 730,782 first save (in-memory objects) 3fb0327948522139320526089cd2dd563183ab5ef6887b8526257a4bbb87fb19 3fb0327948522139320526089cd2dd563183ab5ef6887b8526257a4bbb87fb19 yes
500 730,782 same-value edit (parsed file) 3ae7bf134a606d68bdd75884c2ac94d88433d2ad5d6895c6729c1a6dc7490967 3ae7bf134a606d68bdd75884c2ac94d88433d2ad5d6895c6729c1a6dc7490967 yes
500 730,782 retitle last workspace e40731874a44d008f71684bed2606f17eb5469b8f07e155720bd8c372dc5ac83 e40731874a44d008f71684bed2606f17eb5469b8f07e155720bd8c372dc5ac83 yes
5,000 7,314,006 first save (in-memory objects) 92684d58994d187bb0227b29aa87975d9ef81b937c617f13f3ac63ffb009d090 92684d58994d187bb0227b29aa87975d9ef81b937c617f13f3ac63ffb009d090 yes
5,000 7,314,006 same-value edit (parsed file) 49e8f35463e58137bacd99b1f885aaa298d687f6ea7a939e5dc3212563cee37e 49e8f35463e58137bacd99b1f885aaa298d687f6ea7a939e5dc3212563cee37e yes
5,000 7,314,006 retitle last workspace bd2fe5fced05263852450a48ebdbad9672acb85de0556ee954e48dd6f90564db bd2fe5fced05263852450a48ebdbad9672acb85de0556ee954e48dd6f90564db yes
4,712 6,893,026 first save (in-memory objects) a7ee746a21d270e5d11c72dd294949d2208c2a595c55a7f76d6078941e940c0b a7ee746a21d270e5d11c72dd294949d2208c2a595c55a7f76d6078941e940c0b yes
4,712 6,893,026 same-value edit (parsed file) 94b2cc60bfc68937a7742435c891ee93d8c69ace47d7c6a3c261e3c0554b3240 94b2cc60bfc68937a7742435c891ee93d8c69ace47d7c6a3c261e3c0554b3240 yes
4,712 6,893,026 retitle last workspace cf8e078a4bc898b93e050689da656d0c20e75b5a9dd96b27c269706de3d1d090 cf8e078a4bc898b93e050689da656d0c20e75b5a9dd96b27c269706de3d1d090 yes

Serialization-point test

Without the clone, the save works on objects it shares with runtime state. So the save must finish JSON.stringify before its first suspension, or a change made during the write could reach disk. de7e1c35d8 makes the existing serialization order explicit: it moves JSON.stringify out of the Effect.tryPromise thunk to right after the Cyber check. It does not fix a gap. In effect 4.0.0-rc.112, tryPromise already calls its thunk in the same synchronous step, so the payload was already serialized before the first await on 6e526e29af and on main.

The new test, "writes the returned state even if settings change mid-save" (both "no Cyber" and "Cyber" cases), pauses the save at the atomic write's first await: a spy on the CommonJS fs.realpath, held by a deferred promise, with no timers. It then changes the shared settings object (reasoningMode: "cyber" and a new model), resumes, and checks that the file holds the state the edit returned.

These mutant results show that the test catches a future regression. They do not prove that an earlier commit was broken.

Tree Runtime-state test Mid-save (no Cyber) Mid-save (Cyber)
origin/main 111a6a1339 with head's test file pass pass pass
final head pass pass pass
mutant A: one await (fs.realpath on config.json) added before JSON.stringify not run fail (the file got the changed model) pass (the clone protects it)
mutant B: Cyber path keeps diskData = data (no clone) fail (runtime lost "cyber") pass pass
Raw step 1 output
== origin/main 111a6a1339154faeaad0c81f854c641c6c6e5ae9 + head test file
(pass) Config > downgrade-safe Cyber reasoning mode > keeps Cyber in runtime state after a save that encodes it [33.00ms]
(pass) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (no Cyber) [3.00ms]
(pass) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (Cyber) [2.00ms]
 3 pass
 0 fail
== head c14e682ec3144f41916703e5f52b28f6b588ee25
(pass) Config > downgrade-safe Cyber reasoning mode > keeps Cyber in runtime state after a save that encodes it [37.00ms]
(pass) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (no Cyber) [4.00ms]
(pass) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (Cyber) [3.00ms]
 3 pass
 0 fail
== mutant A (await before stringify) on c14e682ec3144f41916703e5f52b28f6b588ee25:  1 file changed, 2 insertions(+)
+import cjsFsMut from "fs";
+      yield* Effect.promise(() => new Promise<void>((r) => cjsFsMut.realpath(self.configFile, () => r())));
(fail) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (no Cyber) [36.00ms]
(pass) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (Cyber) [4.00ms]
 1 pass
 1 fail
== mutant B (Cyber path without clone) on c14e682ec3144f41916703e5f52b28f6b588ee25:  1 file changed, 1 insertion(+), 1 deletion(-)
-        diskData = structuredClone(data);
+        diskData = data;
(fail) Config > downgrade-safe Cyber reasoning mode > keeps Cyber in runtime state after a save that encodes it [20.00ms]
(pass) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (no Cyber) [3.00ms]
(pass) Config > downgrade-safe Cyber reasoning mode > writes the returned state even if settings change mid-save (Cyber) [3.00ms]
 2 pass
 1 fail
head worktree restored

The independent test auditor also checked two more mutations. When the check misses nested slots, the runtime-state test and the Cyber mid-save case fail. When an await runs between the check and structuredClone, the Cyber mid-save case fails. The auditor made one test-only change (c14e682ec3): the test now races the pause against the edit, so a save that never reaches the paused realpath fails at once instead of hanging until the timeout.

Invariants and the tests that guard them

  1. Same bytes for configs without Cyber modes: the sha256 table above (every bench size, three kinds of save).
  2. A save never changes runtime state: "keeps Cyber in runtime state after a save that encodes it" (new, src/node/config.test.ts). Its only Cyber slot is nested (taskAiPins), so it also fails when the check misses that slot. No earlier test checked the in-memory object after a save: "never writes reasoning mode cyber to disk and restores it on reload" reloads through a new Config.
  3. The save serializes before its first suspension: "writes the returned state even if settings change mid-save (no Cyber)" and "(Cyber)" (new).
  4. Disk form and decode: the existing "never writes reasoning mode cyber to disk and restores it on reload" and "lets a mode written by an older build win over a stale Cyber marker" tests, unchanged.
  5. The check costs at most 2 ms at 4,712 workspaces: the hasCyberReasoningMode, no Cyber slot rows in the new cyberReasoningModeDisk.bench.ts.

Dogfood

Remote dogfood through Coder Agents on the exact final head c14e682ec3: PASS. Chat: https://dogfood.cdr.dev/agents/23706166-482e-4b69-bdb6-1c9b2bf41759

  • Setup: a new workspace and fresh clones, a synthetic root with 4,712 workspaces in 19 projects (54% tasks, 57% archived, no checkouts), and built servers on 127.0.0.1 only. Every server ran with XUM_DISABLE_TELEMETRY=1 from its first start, and its logs have 0 PostHog matches. The agent printed no tokens, and the attached logs are redacted.
  • No-Cyber save: the title and a pro mode reach disk and the API, with 0 "cyber" strings and 0 markers.
  • Cyber save: the slot is stored on disk as cyberReasoningMode: true with no reasoningMode key, and the API keeps returning "cyber". That survives an unrelated edit, a restart, 20 sequential and 5 concurrent edits, and a switch back to standard or pro. Cyber only in agentAiDefaults.exec.subagent, agentAiDefaults.plan or advisorReasoningMode is encoded and decoded the same way.
  • Same bytes through the HTTP API: the same input and the same 5 edits on base and head give files that differ only in the random writeId. With it replaced by a constant, the sha256 values match.
  • CPU profile of 5 title edits each (sampled at about 1 ms, so the times are estimates):
Build structuredClone under the save Save body total
Base, no Cyber present, 77.65 ms 103.91 ms
Head, no Cyber absent 54.6 ms
Head, one Cyber slot present, 87.58 ms 162.18 ms

This run supersedes round 1 (on 6e526e29af). In round 1 the first server ran with telemetry on, and the agent printed its own loopback server's token in the chat. That server was stopped, and no token from it was reused.

SHAs

  • Base: a4e2d5c44933b17bbf01e455fc42346999bdaa47 (merge base with origin/main)
  • Head: c14e682ec3144f41916703e5f52b28f6b588ee25
  • Production code is unchanged since de7e1c35d8. The commits after it touch only src/node/config.test.ts.

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

Every config save cloned the whole config before encoding Cyber reasoning
modes for disk. Without a Cyber slot the encode is a no-op, so check the
same slot list first and stringify the config directly (about 27 ms per
save at ~4,700 workspaces). The check costs about 0.9 ms at 4,712
workspaces (new cyberReasoningModeDisk bench).

Refs #5727

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The mid-save tests awaited the paused realpath alone, so a save that stops
calling the CommonJS fs.realpath hung until the 5 s test timeout and
`expect(pausedOnce).toBe(true)` could never fail. Race the pause against the
edit so that case fails fast on the pausedOnce assertion.

Refs #5727
@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-06T17:42:47.739396Z c14e682 PR opened
🔒 Security Review ✅ Completed 2026-10-06T17:44:52.480955Z c14e682 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
ThomasK33 added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit fa5e58b Oct 6, 2026
33 checks passed
@ThomasK33
ThomasK33 deleted the perf/config-save-skip-cyber-clone branch October 6, 2026 18:19
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