Skip to content

ClosingPanes: the springing-close rules are Rust's (cmux_closing_panes_*); the Swift rule copy goes (step B of #35886) - #35934

Open
lawrencecchen wants to merge 5 commits into
feat-cmux-nextfrom
swift-logic/closing-panes-step-b
Open

lawrencecchen wants to merge 5 commits into
feat-cmux-nextfrom
swift-logic/closing-panes-step-b

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Takes back #35886: step B replaces the Swift rule copy that #35886 recorded the corpus from with calls into the Rust port that #35886 added (planned two-step move; same answers, checked by the corpus through the FFI).

Summary

Step B of moving ClosingPanes (FRAME OWNER S3c/S3d springing-close rules) into Rust. Step A #35886 (merged as e241a67) added the rules to cmux-layout-reducer (closing_panes) and the typed C calls to cmux-layout-reducer-ffi; their app FFI release is pinned on feat (#35911, cmux-app-ffi-728ed6cc8d33).

  • ClosingPanesRules (ClosingPanes+Rules.swift) calls cmux_closing_panes_* (one handle per screen; rects, the per-frame call, allocates nothing in Rust) and only maps the screen's geometry and live frames into the C structs and the answers back. The Swift copy of the rules is deleted.
  • CmuxNextLayout depends on CCmuxAppFFI (Package.swift).
  • ClosingPanesCorpusTests now checks the 160 recorded scripts (1,767 calls; each answer and a whole-state hash) through the FFI.
  • Table rows (lead rule: Swift that only calls the FFI): ClosingPanesRules PLATFORM, the reason names the calls; ClosingPanes MIXED 1, because its live-frame "moving" test stays in Swift.

Owner: PL-motion-core (layout motion). Swift-logic plan move 3.

Numbers

scripts/cmux-next/swift-logic-lines.py, head a9a0b09 (this branch with feat ce9bdbc merged in) and its base ce9bdbc:

Revision Table swift_logic_lines mirror_lines missing invalid
base ce9bdbc its own 420,966 72,057 21 0
head a9a0b09 the base's 420,771 72,057 21 0
head a9a0b09 its own 420,399 72,057 21 0
base ce9bdbc the head's 420,399 72,057 21 0

The move is 420,966 to 420,399 (-567): -195 Swift lines deleted (the rule bodies less the new glue, same table), -372 reclassified by the two new rows (ClosingPanesRules PLATFORM, ClosingPanes MIXED 1: FFI calls and data mapping, lead rule (c)). Before the merge the same split held at d2a578b / f6e8522 (417,557, 417,362, 416,990).

Copy safety (review of #35886)

ClosingPanesRules holds a final class Handle that owns the Rust handle (cmux_closing_panes_new in init, cmux_closing_panes_free in deinit), so copies of the value share one Rust state. No code copies it: ClosingPanes keeps it as private let rules, and the only ClosingPanes value is ScreenContentView.closingPanes, which is mutated in place and replaced whole (closingPanes = ClosingPanes() in ScreenContentView+ClosingPanes.swift), never assigned to another variable (grep of Sources and Tests at this head). The corpus test makes a new ClosingPanesRules() per script.

Changelog

none

Observed / Not run

Observed (fleet, at the pre-merge head f6e8522; at the merge head a9a0b09 the compile check is job 9a8fa1b707bd42d6172671ea and the 5 suites are step 27b4140d30b5ddb79e7ffe71, results to follow):

Bench (motion-frame rows, scripts/measure/split-spring.sh --suite=pane-perf, split, close and New Column in 2, 8 and 30 terminal layouts, base d2a578b vs this head on one worker, cmux-austin-mini-1):

  • First pair, base step 55a6f3e7fec4d493e6f77d8d and head step a3b6cbf2d2fb819c20e0c04a: NOT valid frame evidence. The report says window_on_screen: no on both sides (a 1920x1080 display cannot hold the 1600x1000 STRESS window; Finder was frontmost) and frames run at 60 Hz, so missed frames on both sides are noise. Layout work per close frame, the one number that does not need a shown window, is equal: 4x2 p95 2.26 vs 2.20 ms, 6x5 p95 2.70 vs 2.94 ms.
  • Rerun with the report's remedy --vdisplay=120 (a 1920x1200 virtual display at 120 Hz, window on screen on both sides), 3 rounds each, terminal rows: base step 3af0e9399da8d242ad1aabaa, head step 7acbff8501ad2603a82cec89, both on cmux-austin-mini-1.
Layout Action Base frames p50/p95/p99/max ms Base missed Base layout p95/max ms Head frames p50/p95/p99/max ms Head missed Head layout p95/max ms
1x2 split 8.33/8.33/16.67/25.51 2 2.67/2.94 8.33/8.33/27.02/52.17 5 4.07/9.62
1x2 close 8.33/8.33/8.33/16.67 1 1.65/2.06 8.33/8.33/17.25/39.50 5 2.63/6.35
1x2 column 8.33/8.33/18.25/22.26 6 1.34/1.85 8.33/8.33/26.47/37.86 4 1.38/6.37
1x2 column-close 8.33/8.33/16.67/16.67 2 1.02/2.18 8.33/8.33/16.67/16.67 2 1.20/2.06
4x2 split 8.33/8.33/19.17/33.33 6 4.60/7.80 8.33/8.33/17.44/33.54 4 3.25/3.93
4x2 close 8.33/8.33/8.33/16.67 2 2.20/2.80 8.33/8.33/16.67/16.67 3 2.19/3.20
4x2 column 8.33/8.33/20.08/58.49 3 2.01/2.41 8.33/8.33/30.14/38.94 5 1.90/3.76
4x2 column-close 8.33/8.33/8.33/16.67 1 2.02/3.19 8.33/8.33/23.25/27.32 4 1.95/2.95
6x5 split 8.33/8.33/39.53/41.29 5 5.12/15.11 8.33/8.33/17.00/28.25 4 4.25/8.48
6x5 close 8.33/8.33/17.54/37.17 5 3.38/4.25 8.33/8.33/18.81/25.00 6 3.05/4.44
6x5 column 8.33/16.67/25.90/71.82 8 4.83/5.34 8.33/10.18/23.02/74.69 7 6.09/6.38
6x5 column-close 8.33/12.98/39.03/43.66 8 21.72/34.02 8.33/16.67/22.26/36.07 10 4.05/6.75
  • Reading: neither side meets the motion budget (0 missed frames at 8.33 ms): base 49 missed frames over the 12 rows, head 59. The rows that run ClosingPanes (close, column-close) miss 19 frames on base and 30 on head; split and column, which do not close a pane, miss 30 and 29. With 3 rounds this is not yet a measured regression, but it is not shown to be noise either. A repeat of close and New Column with more rounds on the same worker is next; this PR must not land on the numbers above alone.
  • Chromium rows (--browser, the top-left pane a Chromium page), same worker and display, 3 rounds each: base step adc5b0ffd3ef472b2451d832, head step 7166664787583cc83e6ddecf.
Layout Action Base frames p50/p95/p99/max ms Base missed Base layout p95 ms Head frames p50/p95/p99/max ms Head missed Head layout p95 ms
1x2 split 8.33/12.10/21.92/24.87 7 7.09 8.33/8.33/17.50/24.76 7 6.05
1x2 close 8.33/12.04/17.05/20.76 8 6.04 8.33/8.33/17.38/21.98 7 5.34
1x2 column 8.33/13.54/38.27/44.45 9 3.47 8.33/8.33/16.67/16.67 3 2.20
1x2 column-close 8.33/10.31/20.65/27.37 6 3.11 8.33/8.33/8.33/12.10 0 2.16
4x2 split 8.33/16.67/54.84/66.93 9 6.45 8.33/8.33/16.67/30.44 7 6.73
4x2 close 8.33/9.25/32.41/57.26 7 9.46 8.33/8.33/22.33/26.84 6 6.35
4x2 column 8.33/8.33/26.91/55.08 5 2.64 8.33/8.33/32.36/52.01 5 2.08
4x2 column-close 8.33/8.33/20.09/58.66 5 3.56 8.33/8.33/21.29/62.73 6 5.99
6x5 split 8.33/8.33/26.55/57.12 7 8.75 8.33/8.33/28.27/38.72 5 7.51
6x5 close 8.33/8.33/16.79/24.51 5 6.76 8.33/8.33/20.09/21.01 6 7.16
6x5 column 8.33/8.33/53.57/63.41 7 7.40 8.33/8.33/40.76/59.66 7 6.01
6x5 column-close 8.33/16.67/35.00/58.17 10 7.82 8.33/16.67/30.33/37.56 11 12.18
  • Reading of the Chromium pair: the head misses FEWER frames, 70 against 85 over the 12 rows; on the close rows 36 against 41. Together with the terminal pair (close rows 30 against 19), the direction changes from one pair to the next: 3-round pairs on cmux-austin-mini-1 do not decide.

Paired set 1 (lead order): cmux21, base and head alternated job by job

Base d2a578b, head f6e8522 (before the feat merge; the merge changed no ClosingPanes, corpus or cmux-layout-reducer file). split-spring.sh --suite=pane-perf --config=bench --app-cache --vdisplay=120 --layouts=1x2,4x2,6x5, all on cmux21 (perf pool), in this order: terminal base e7ebea5dfef11aaf2573a580, head 3853057454b6205e9025ebf6, base a3fd69935e7aa7e9b745f4b5, head d10b15a7c919fa36d08eca4f (4 rounds each, close and New Column); Chromium (--browser, split, close and New Column, 3 rounds) base 17d1a1139a1ce276e638ef7a, head 9c058d5a89cd67e1c210e0f3. Every report: window_on_screen: yes, refresh 8.33 ms (1920x1200 virtual display at 120 Hz), no run error, nothing unmeasured. A pair is round i of a base job and round i of the head job after it. These refs predate #36027, so no round carries background daemon CPU and the "over 1 s dropped" table is the same as the table over all pairs.

Missed frames per pair (head - base):

term: all pairs (pair = job/round)

Layout Action Pairs Base missed Head missed Head - base per pair
1x2 close 8 2 6 +1 +1 +0 +1 +0 -1 +2 +0 (median +0.5)
1x2 column 8 5 5 +0 +0 +0 +0 +0 +1 -2 +1 (median +0)
1x2 column-close 8 3 1 +0 +0 -1 +0 +0 +0 +0 -1 (median +0)
1x2 split 8 13 11 -1 +0 +0 +0 +0 +0 -2 +1 (median +0)
4x2 close 8 2 2 +0 +0 +0 -1 +1 -1 +0 +1 (median +0)
4x2 column 8 6 6 +0 +1 -1 +0 +0 +0 +0 +0 (median +0)
4x2 column-close 8 5 1 +0 +0 +0 +0 -1 -1 -1 -1 (median -0.5)
4x2 split 8 7 9 +0 +0 +1 +1 -1 +1 +0 +0 (median +0)
6x5 close 8 15 15 -1 +1 +1 +0 -1 +1 +0 -1 (median +0)
6x5 column 8 26 20 -1 -1 -2 -2 +1 +0 -1 +0 (median -1)
6x5 column-close 8 23 17 +0 -2 +0 -1 -2 -1 +0 +0 (median -0.5)
6x5 split 8 15 15 +0 +0 +1 +0 +1 -1 -1 +0 (median +0)

chrome: all pairs (pair = job/round)

Layout Action Pairs Base missed Head missed Head - base per pair
1x2 close 3 3 4 -1 +2 +0 (median +0)
1x2 column 3 1 1 +1 -1 +0 (median +0)
1x2 column-close 3 2 6 +2 +1 +1 (median +1)
1x2 split 3 1 3 +0 +2 +0 (median +0)
4x2 close 3 5 6 +1 +0 +0 (median +0)
4x2 column 3 5 2 -1 -1 -1 (median -1)
4x2 column-close 3 7 5 -1 +0 -1 (median -1)
4x2 split 3 2 6 +2 +2 +0 (median +2)
6x5 close 3 5 6 +1 +0 +0 (median +0)
6x5 column 3 12 9 -1 +1 -3 (median -1)
6x5 column-close 3 8 6 +0 -1 -1 (median -1)
6x5 split 3 6 6 +0 -1 +1 (median +0)

Per row over all rounds, with the review's three block tests (light rows only; frames p50/p95/p99 are the median of the rounds, max is over all rounds, layout is the per-frame layout pass, which holds the rects call):

term: frames p50/p95/p99 (median of rounds) / max (all rounds), missed (sum), layout p95 (median) / max, ms

Layout Action Rounds Base frames Base missed Base layout Head frames Head missed Head layout Block tests
1x2 close 8/8 8.33/8.33/8.33/16.67 2 0.99/3.67 8.33/8.33/8.33/16.67 6 0.91/3.56 pass
1x2 column 8/8 8.33/8.33/8.33/16.67 5 0.66/1.02 8.33/8.33/8.33/21.78 5 0.65/1.01 pass
1x2 column-close 8/8 8.33/8.33/8.33/16.67 3 0.50/6.16 8.33/8.33/8.33/16.67 1 0.51/6.20 pass
1x2 split 8/8 8.33/8.33/8.33/16.72 13 1.35/1.91 8.33/8.33/8.33/23.67 11 1.35/2.11 pass
4x2 close 8/8 8.33/8.33/8.33/16.67 2 1.08/3.63 8.33/8.33/8.33/16.67 2 0.96/3.64 pass
4x2 column 8/8 8.33/8.33/8.33/21.51 6 0.58/1.64 8.33/8.33/8.33/24.45 6 0.85/1.79 layout p95 +0.27
4x2 column-close 8/8 8.33/8.33/8.33/16.67 5 1.05/4.24 8.33/8.33/8.33/13.76 1 1.06/4.34 pass
4x2 split 8/8 8.33/8.33/8.33/31.50 7 1.10/2.14 8.33/8.33/8.33/16.67 9 1.50/1.93 layout p95 +0.40
6x5 close 8/8 8.33/8.33/8.33/29.13 15 1.29/4.25 8.33/8.33/8.33/17.11 15 1.29/4.22 pass
6x5 column 8/8 8.33/8.33/16.67/37.29 26 1.45/4.60 8.33/8.33/16.67/25.74 20 1.30/4.83 pass
6x5 column-close 8/8 8.33/8.33/15.52/19.12 23 2.56/5.98 8.33/8.33/8.33/20.04 17 2.47/6.36 pass
6x5 split 8/8 8.33/8.33/8.33/30.40 15 1.23/2.01 8.33/8.33/8.33/30.87 15 1.19/1.92 pass

chrome: frames p50/p95/p99 (median of rounds) / max (all rounds), missed (sum), layout p95 (median) / max, ms

Layout Action Rounds Base frames Base missed Base layout Head frames Head missed Head layout Block tests
1x2 close 3/3 8.33/8.33/8.33/16.67 3 1.92/3.54 8.33/8.33/8.33/36.67 4 2.13/2.97 layout p95 +0.21
1x2 column 3/3 8.33/8.33/8.33/47.25 1 0.97/3.72 8.33/8.33/8.33/22.58 1 1.05/3.25 pass
1x2 column-close 3/3 8.33/8.33/8.33/31.29 2 1.27/4.44 8.33/8.33/10.52/33.05 6 1.21/5.13 p99 over refresh, base under
1x2 split 3/3 8.33/8.33/8.33/16.37 1 1.61/4.46 8.33/8.33/8.33/46.44 3 1.51/5.43 pass
4x2 close 3/3 8.33/8.33/11.06/32.70 5 1.97/3.80 8.33/8.33/11.96/38.04 6 1.65/8.59 pass
4x2 column 3/3 8.33/8.33/10.52/22.81 5 1.30/3.77 8.33/8.33/8.33/22.30 2 1.52/3.39 layout p95 +0.22
4x2 column-close 3/3 8.33/8.33/12.98/49.88 7 2.06/16.33 8.33/8.33/8.33/66.67 5 4.28/13.18 layout p95 +2.22
4x2 split 3/3 8.33/8.33/8.33/18.71 2 1.90/2.63 8.33/8.33/8.33/38.34 6 1.64/3.09 pass
6x5 close 3/3 8.33/8.33/8.33/33.38 5 2.04/4.36 8.33/8.33/8.54/50.00 6 1.69/7.54 p99 over refresh, base under
6x5 column 3/3 8.33/8.33/20.12/48.05 12 1.77/5.25 8.33/8.33/16.68/37.28 9 1.71/5.48 pass
6x5 column-close 3/3 8.33/8.33/8.33/50.19 8 2.41/13.40 8.33/8.33/9.87/49.82 6 2.35/12.32 p99 over refresh, base under
6x5 split 3/3 8.33/8.33/8.33/46.06 6 1.98/2.99 8.33/8.33/8.33/25.00 6 1.66/4.88 pass
  • Reading of set 1:
    • Terminal, all 12 rows: the per-pair median is 0 or below in 11 rows, +0.5 in 1x2 close (6 against 2 missed frames over 8 pairs). Total missed frames: head 108, base 122; close rows head 42, base 50. No terminal row that runs ClosingPanes (close, column-close) fails a block test. The two terminal rows flagged for layout p95 (4x2 split +0.40 ms, 4x2 column +0.27 ms) close no pane, so ClosingPanes does not run in them: that is the same-worker spread.
    • Chromium: head 60, base 57 missed frames. Flagged close rows: 4x2 column-close layout p95 4.20/4.28/4.45 ms on the head against 1.99/3.43/2.06 ms on the base, every round (+2.22 ms median; layout max is lower on the head, 13.18 against 16.33 ms; missed frames 5 against 7); and p99 above one refresh where the base median is at it in 1x2 column-close (10.52 against 8.33 ms), 6x5 close (8.54) and 6x5 column-close (9.87). Each of those is one job per arm, so a shift of the whole job cannot be told from the code.
    • Verdict on set 1: NOT a pass of the review's block tests. The terminal rows pass; the Chromium close rows are not decided, and STRESS rows were not run. This PR does not land on set 1.

Paired set 2 (running): Chromium repeat and STRESS

Base ce9bdbc (feat), head a9a0b09 (this branch with that feat merged), cmux21, alternated: Chromium light 2 more jobs per arm (split, close, New Column; 3 rounds), STRESS terminal 1 job per arm and STRESS Chromium (--layouts=stress --stress-selected=chromium) 1 job per arm, 3 rounds each. These refs carry #36027, so each round records background daemon CPU and the second table drops rounds over 1 s. The verdict uses the review's block tests on the Chromium close rows over 3 job pairs and on the STRESS rows.

  • The per-frame cost of mapping Swift values to the C structs is inside "layout p95/max" above (the rects call runs in the layout pass): close rows layout p95 base 1.65, 2.20, 3.38 ms; head 2.63, 2.19, 3.05 ms (1x2, 4x2, 6x5).

Not run: no live dogfood of a close on a person's Mac.

…s_*); the Swift rule copy goes (step B)

ClosingPanesRules keeps its API and now holds one cmux-layout-reducer
closing_panes handle per screen: each call maps the gathered values into the
C structs and the answer back. ClosingPanesCorpusTests replays the corpus the
Swift rules recorded (answers and the whole state hash after each call)
through the C calls. CmuxNextLayout links CCmuxAppFFI.
…_* call per method) and ClosingPanes MIXED 1 (the gather; its live-frame moving test stays Swift), rule (c)

swift-logic-lines.py: swift_logic_lines 415,963 at e60197e -> 415,646 at this head (-317).
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 817d809: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr35934 -f ref=swift-logic/closing-panes-step-b -f platform=macos -f backend_mode=local

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent): FIX FIRST at f6e8522

The code is correct. Two items block landing, and neither needs a push: a red caused by this PR, fixed by a line in the description, and the motion-frame bench, which has not finished. When both are done, this head can get SHIP if the bench shows nothing in the blocking list below.

Blocking

  1. risk evidence is red because of this PR (run 41935278880, job 126942063832): held: takes back #35886's change from the last 7 days: ClosingPanes+Rules.swift. This is the planned step B, not a revert, but the gate needs the line. Add a line like this to the description: Takes back #35886: step B replaces the Swift rule copy that #35886 recorded the corpus from with calls into the Rust port that #35886 added (planned two-step move; same answers, checked by the corpus through the FFI). Then rerun risk evidence on this SHA.
  2. The motion-frame bench has not finished, so SHIP depends on it. When I checked, cpb-bench/b3-chain.log was empty, b3-term-base.log was still compiling the base app (no numbers), and the head side had not started. Step A's review (6643086790) asked for terminal and Chromium motion-frame rows, light and STRESS, base and step-B head on one worker. The plan in the description (split-spring.sh --suite=pane-perf, terminal and --browser) does not name STRESS or the anim-perf rows: add them, or say why pane-perf covers them. For each row (split, close, New Column x 2/8/30 panes x terminal/Chromium x light/STRESS), any of these blocks:
    • a missed frame on the head in a row where the base has 0;
    • p95 main-thread time per frame higher than the base by more than 10% and by more than 0.1 ms (the same-worker noise PaneMotionBench showed in step A);
    • p99 or max above one refresh interval (8.33 ms at 120 Hz) where the base is below it.
      Put p50, p95, p99, max and missed frames for base and head in the table.

Checks

  1. Same behavior as step A's Swift rules on every call: yes.
    • Field mapping, checked against the header and the Rust #[repr(C)] structs:
      • Edges.c: min_x/max_x/min_y/max_y.
      • CmuxClosingPane: id, area, row_column, edges, scrolls 0/1, reserved[7].
      • CmuxClosingHandle: hit, mid_x, mid_y, area, column, kind = HandleKind.rawValue (0/1/2 = CMUX_CLOSING_HANDLE_*), horizontal, kept, fixed_split, docked, reserved[3].
      • CmuxClosingFrame: id, now, target, vx, vy, vw, vh, moving, reserved[7].
      • CmuxClosingRow: column, now, target.
      • Answers: CmuxClosingRect maps to Placed. CmuxClosingTaken maps to Taken by name (vx, vw, vy, vh, in the order Swift's remove used).
      • Units: CGFloat and Double are the same on arm64. viewShift: CGFloat? maps to has_view_shift 0/1 plus the value, which Rust reads only when the flag is 1.
    • Return codes:
      • begin and remove check == 1, so an error code (negative) reads as "no answer". That is the old false/nil.
      • settle and rects check == 0. On an error they return empty.
      • The unit-returning calls ignore their code, which is safe: their only codes are ERR_NULL (impossible here, the handle is never null) and ERR_PANIC.
      • ERR_INVALID cannot happen, because HandleKind has only 0, 1 and 2.
    • Empty arrays: baseAddress is nil with count 0, and Rust slice() takes (null, 0) as &[]. Good.
    • Borrowed answers are copied before the next call:
      • settle builds a Set from the UnsafeBufferPointer, and rects .maps the buffer into a Swift array, both before they return.
      • No pointer escapes the method.
      • out is a local var that never overlaps the input buffers or the handle.
      • &cShown is a local inout, valid for the call.
    • Order and sets: still comes back ascending and goes into a Set, as before. rects goes into a dictionary, as before. In the wrapper, entries changes only when the rules answer: begin sets the entry only on true, and drop and remove clear it in the same places as at the base. So the Swift isEmpty and the Rust state agree.
    • Corpus test: it now runs the FFI path. line(op, rules) calls ClosingPanesRules, and the state is cmux_closing_panes_state_hash. That is the same FNV-1a over the same string that the Swift rules built when they recorded the corpus, and the test compares each answer and the hash after each call.
  2. Shared handle: safe as used.
    • Copies: I grepped Sources and Tests at this head. ClosingPanesRules() is created only in ClosingPanes (private let rules) and in the corpus test (one per script). The only ClosingPanes value is ScreenContentView.closingPanes. It is changed in place and replaced whole (closingPanes = ClosingPanes() in releaseClosing). Nothing assigns it to another variable. PaneContentPolicyTests reads screen.closingPanes.isEmpty only. No copy is changed on its own.
    • Threads: the owner is an NSView, so the main actor. ClosingPanesRules holds a non-Sendable class, so strict concurrency stops it from crossing isolation. deinit runs on the thread of the last release, which is main in the app, and cmux_closing_panes_free has no thread affinity.
    • Use after free: none. Every call reads handle.pointer while self (and so handle) is held for the call. The old handle is freed only when the replaced value goes.
  3. Allocations per frame in rects (perf-hot motion-frame).
    • Swift heap allocations per frame at the head:
      • frameFacts array: 1.
      • The [frame.x, frame.y, frame.width, frame.height] literal: up to F, one per live frame, unless the optimizer promotes it to the stack.
      • rowTravels: the compactMap, the Set and the result. 3 when a closing pane has a row column, 0 otherwise.
      • c(frames): 1.
      • c(rows): 1 when there are rows.
      • The result array of rules.rects: 1.
      • The [PaneID: CGRect] dictionary: 1.
      • Total: 4 + F with no row columns, up to 8 + F with them.
    • Rust: 0 once its buffers have grown.
    • Base: the same gather (frameFacts, rowTravels, the result dictionary), plus the Swift rules' shown and rests dictionaries, the placed array and 8 compactMap arrays per closing pane. So the head allocates fewer than the base for each closing pane. This is acceptable under the 4 ms mark, subject to the bench.
    • If the bench rows move, change this (not blocking now): build [CmuxClosingFrame] directly in frameFacts (no FrameFact middle copy), keep it and the rows in reused buffers on Handle (removeAll(keepingCapacity: true)), write moving as four || tests instead of the array literal, and fill the result dictionary with reserveCapacity. That leaves 1 allocation per frame (the dictionary).
  4. Table rows (lead rule (c)).
    • ClosingPanesRules PLATFORM: correct. I read every line. It has the value structs, Edges.c, the Handle class, one cmux_closing_panes_* call per method and the answer mapping (code == 1, code == 0, nil maps to the flag). No line decides anything.
    • ClosingPanes MIXED 1: correct. The one Swift rule left is let moving = frame.rect != frame.targetRect || [...].contains { $0.velocity != 0 } in frameFacts. Everything else is gather or mapping:
      • the number maps;
      • handles(of:);
      • the handle facts from ScreenGeometry;
      • the row dedupe;
      • AnimatedFrame built from Taken.
    • Follow-up: FrameFact already sends now, target and the 4 velocities, so Rust could compute moving itself. That would make ClosingPanes PLATFORM on the next FFI repin.
  5. Count: I ran swift-logic-lines.py --json and the numbers match.
    • Base, its own table: 417,557.
    • Head, its own table: 416,990.
    • Head with the base's table: 417,362.
    • Base with the head's table: 416,990.
    • The honest statement of the move is 417,557 to 416,990 (-567). The two new rows are right, so the reclassification is earned. Of the 567, 195 are Swift lines deleted (the rule bodies less the new glue: 417,557 to 417,362 with one table), and 372 are the glue and gather that stay, now classed by rule (c).
    • The description's "With the same table the code change is 0 lines" is true only with the head's table, and it hides the 195 deleted lines. Please add the 417,362 row and say "-195 deleted, -372 reclassified".
  6. Fleet evidence at this head: verified.
    • Compile job e6c8e6258fdc475bef80858b is done. Its receipts are ok:true for CmuxNext, app and cmux-tui at f6e8522.
    • Step 77d85b5c414ef521ef70fef1 on aws-m4pro-7 exited 0, with 5 suites passed:
      • ClosingPanesCorpusTests: corpusMatchesTheFixture passed; recordCorpus was skipped (record=false).
      • PaneCloseKeptHandleTests: 7 tests.
      • ClosedPaneContentStaysUntilLandingTests: 2.
      • PaneContentPolicyTests: 17.
      • PaneGroundCoversPaneTests: 4.
  7. CI reds at this head:
    • Red: only risk evidence, which this PR causes (blocking item 1).
    • Green: cmux-next checks (the app FFI pin passes now), cmux-next fast tier (cargo) and the portable swift tests.
    • Still pending when I checked: cmux-next swift test, cmux-next fast tier and cmux-next daemon tests.

Not blocking

  • A Rust panic in rects empties every closing pane on screen. The result is [], and presentClosingPanes then drops every closing entry without hiding its host, so the grounds freeze at their last frame until the landing. This happens only after a Rust panic, and the failure is mild.
  • The dependency map comment is out of date. The comment at the top of Package.swift (the line CmuxNextTabs, Sidebar, Layout, Browser -> CmuxNextDesign) does not name Layout -> CCmuxAppFFI, but the other FFI users are listed. Nit.

Not run by me: no build, no test, no bench. I read the diff, the base Swift rules, the Rust closing_panes.rs (the crate and the FFI), the C header, the fleet logs of both jobs, the CI checks and the risk evidence log, and I ran the line counter.

…/closing-panes-step-b

# Conflicts:
#	Packages/macOS/CmuxNext/Package.swift
#	plans/cmux-next/swift-logic-classes.tsv
…nswer (red)

#35934 paired A/B on cmux21: the 4x2 Chromium column-close row's layout p95 was
4.20/4.28/4.45 ms on the head against 1.99/3.43/2.06 ms on the base. rects
builds four fresh arrays and a dictionary per frame. The bound is the answer's
dictionary, counted in Release.
…d allocates only its answer

The live frames and row travels go into C arrays the rules handle keeps from
frame to frame, the answer is read in place, `moving` is four comparisons
instead of an array literal per live frame, and the row columns are deduplicated
without a Set. Per frame this removes the FrameFact array, the CmuxClosingFrame
array, the answer array and one array per live frame; the dictionary the screen
reads is reserved once. Same calls into cmux-layout-reducer, same order: the
corpus (ClosingPanesCorpusTests) replays through the same load path.

Red first: ClosingPanesFrameAllocationTests (688add4).

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 817d809c Deployed Nov 5, 2026 by lawrencecchen via namespace autofix #15020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant