Repository navigation
cmux-tui: the terminal revision is read without the registry lock; a progress report reads only its own tabs - #28534
Conversation
80 fake terminal agents run the real hook helper (one durable journal append per turn event) and a CLI call in a loop, a churn client creates and closes terminals, and an app client sends what the cmux app sends (put-frontend-projection window saves, list-workspaces, session.snapshot, identify). It reports the app requests' latency, the requests over the app's 2 s timeout and both daemon locks by site, the shape of the 2026-10-29 report (#27698). Run: cmux-ci run --class exclusive --measurement --script scripts/measure/perf-10x-daemon.sh --ref SHA --arg=scenarios=agents Measurement only; no CI step, gate or budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olders by site server-stats include=["locks"] reports the workspace registry lock and, new, the mux state lock: wait and hold histograms, the current holder, the last stall, and the top twenty acquiring sites by total hold and by p99 hold, each with its hold and wait p99 and maximum. Sites come from #[track_caller] (lock_state_pinned now carries it too, so its callers are named instead of the wrapper). Per site, two power-of-two histograms (256 bytes each) are updated under the site table lock that the release already took, while the lock is still held: no allocation after a site's first acquisition and no new contention. The state lock's existing rank and holds bookkeeping is unchanged. The section is optional (absent unless named), so older SDK decoders keep working; schema, bindings, api-compat surface and commands.md updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pers in lock stats #[track_caller] on the Mux wrappers that take the workspace registry or the mux state lock (with_state, surface, resolve_resource_path, commit_state, commit_resource_mutation_plan, with_resource_projection, the resource effect helpers, tree_decorations and others), so server-stats locks names the request path that holds the lock instead of the wrapper's own line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…again (red) lock_state_pinned settles the late answers of indeterminate registry commits (cx-g1fa.3) while it holds the registry and state. The settle then ran publish_resource_event, whose pending-directory step locks state again: a lock-rank panic in debug builds and a self-deadlock of the whole daemon in release builds (std Mutex), on a path a loaded Mac reaches when a journal commit outlives its 30 s receipt wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urnal event The settle runs under the registry and state; publish_resource_event also publishes pending terminal directories, which locks state again (and then the registry). Publish the journal event only: subscribers still read the new head, and a directory still pending is published by the terminal's next output or the next committed graph change, as before the settle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lippy redundant_clone) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rash ratchet) The state guard kept its MutexGuard in an Option only to record the release before the unlock, and its Deref used two expect() calls: the crash ratchet counted them (cmux-tui-core expect 190 -> 192, #28147's red 'cmux-next crash ratchet'). Fields drop after Drop::drop, so the release is recorded in the body while the lock is still held, with no Option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ic path (panic ratchet) diagnostics.rs: the per-site octave histogram indexed its bucket array and added with +=, which the Rust panic ratchet counts (cmux-tui-core indexing_slicing 854 > baseline 853; arithmetic_side_effects rose too; review of #28147 at c88bc3a). The bucket is now taken with get_mut, and the counts, the running sum, the p99 bound and the acquisitions use saturating or checked arithmetic. The values are unchanged: bucket 0 holds 0, bucket i holds 2^(i-1)..2^i - 1, the last bucket everything above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cli 0) No shipped agent hook, plugin or status line calls the cmux CLI on each turn; the hooks send only session.journal.append. agent_cli=0 runs that load (lead decision 2026-10-30: the hook-only load is Austin's day). A diagnostic fleet bench, not a CI step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d a progress report reads only its own tabs (red) #27698 lock work, design step 1. On Austin's day (9 runs, pa-d harness 856cf72) the registry lock is held about 78 % of the time. Two of its largest holders on the hook-only load: - frontend_projection.rs:9, the terminal revision that identify and every list-workspaces reply read: 151,415 acquisitions, 7.0 % of the run; - terminal_progress.rs:18, a progress report that read the whole session topology to find one terminal's tabs: 7.1 % of the run, max 2 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k; a progress report reads only its own tabs #27698 lock work, design step 1 (1d): - The registry gets a read-only connection to its own file (RegistryConnection::read_committed). In WAL mode it reads the last committed state and never waits for a writer; its mutex is a leaf held only for the read. terminal_registry_revision, which identify and every list-workspaces reply call, reads there, with no registry lock. An in-memory registry (no file) and a failed read keep the locked read. - publish_terminal_progress reads this terminal's own tab rows (resource_tabs_of_content) instead of the whole session topology, under the registry lock it still needs for its commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urns the last committed value read_committed (the registry's read-only WAL connection) returns the state committed when its read starts: never a partial or uncommitted write, and it does not wait for the open transaction. After the commit it returns the new value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p the reviewed fleet script The fleet refuses an allowlisted script that differs from its reviewed version on main or feat-cmux-next (scripts/measure/perf-10x-daemon.sh, steps 142d441b and deb15036). perf-10x-daemon.sh is back to the reviewed blob 85fb5db; the hook-only load is a scenario of the Python bench (--arg=scenarios=agents-hooks), which that script passes through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-topology load reads The test asserted that a progress report reads no resource_screens, resource_panes or resource_workspaces row. The commit of any resource patch (even an empty one) looks up such rows, so the test failed on the fixed head too (step c9a332d2). The whole-topology load is marked by the per-screen side tables that only it reads (resource_screen_rows, resource_column_docks, resource_lone_docks, resource_parked_splits). The red step 1e49666e at 044b745 read all four; the fixed head reads none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Austin's-day A/B for B3 1d (harness owner pa-d), as the lead asked: before b472663 (scratch/pa-b3-1d-before: the 1d tree with fix 5cc5b7e reverted, never merge) against after fee4db7 (fix/pa-b3-holders). The terminal revision read no longer appears among the registry lock's top 20 holders (from about 35-47 s of hold per run to at most about 0.4 s), the registry lock held share falls by 0.7 points, and refused connections fall by 12 %. Lock wait p99 and app no-reply do not change.
The lock detail is missing for 3 of 10 runs: base round 2 and head rounds 1 and 2. The read after the load got
Note: the held share falls by much less than that site's hold (0.7 points, about 2.7 s of a 390 s window). The removed hold was mostly not on the path that keeps the lock busy, or other holders took its place. Leases: base r1 pos 1 slot-cmux24-5b938a5c9204; head r1 pos 2 slot-cmux24-a567cadae62e; head r2 pos 1 slot-cmux24-638500b88d78; base r2 pos 2 slot-cmux24-2fcbeb86041f; base r3 pos 1 slot-cmux24-917460d1729c; head r3 pos 2 slot-cmux24-a9e2171a4d5b; head r4 pos 1 slot-cmux24-cf70c191e467; base r4 pos 2 slot-cmux24-9f48a4e5277f; base r5 pos 1 slot-cmux24-6713c4d86e6d; head r5 pos 2 slot-cmux24-6ee5a59481c4; |
…olders Conflicts were feat's squash of #28147 (pa-b3) plus its later hook_queue section against this branch's copy of the pa-b3 commits. Rust: feat's side (hook_queue, try_lock outside tests). Generated bindings and spec: feat's files (this PR changes no protocol). bench-daemon-latency.py: this PR's --agent-cli and agents-hooks scenario on feat's agents scenario.
|
Review (claude-review-subagent-cw): SHIP at c076a3d I found no blocking defect. I did not compile or run tests. I read the code and the CI results only. Merge (c076a3d = 5b321e6 + feat 1209ff7)
Net change over feat
Non-blocking notes
Observed / Not run
|
|
Conflict wave: merged feat-cmux-next 1209ff7 into this branch (merge c076a3d). The conflicts were feat's squash of #28147 (pa-b3), plus its later Fleet evidence at c076a3d:
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base 1209ff7; does not block Shipped in:
|
|
Found 8 test failures on Blacksmith runners: Failures
|
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Summary
On Austin's day (9 runs of the pa-d harness,
/work/tmp/pa-d/ab-28147), the workspace registry lock is held about 78 % of the time. On the hook-only load that the shipped agent hooks make, two of its largest holders take the lock for work that does not need it:terminal_registry_revision(mux/frontend_projection.rs:9): one committed value thatidentifyand everylist-workspacesreply read. 151,415 acquisitions in 9 runs, 7.0 % of the run time, mean 1.7 ms, max 1.9 s, spent mostly waiting for the connection behind writer commits.publish_terminal_progress(mux/terminal_progress.rs:18): a terminal's progress report (OSC 9;4, program status) read the whole session topology (every screen, pane, tab and browser, with their side tables) to find that one terminal's tabs. 7.1 % of the run time, mean 5.4 ms, max 2.1 s.This PR:
RegistryConnection::read_committed). The database runs in WAL mode, so this connection reads the state committed when its read starts. It never returns a partial or uncommitted write, and an open write transaction never blocks it. Its own mutex is a leaf lock, held only for the read. An in-memory registry (no file) has no reader, and callers keep the locked read.terminal_registry_revisionon that connection, with no registry lock. If no reader exists, or the read fails, it falls back to the locked read.publish_terminal_progressread only this terminal's own tab rows (resource_tabs_of_content). It still holds the registry for its commit.agents-hooksscenario to the diagnostic bench (bench-daemon-latency.py --scenarios agents-hooks). It is the agents load with no CLI calls: no shipped hook, plugin or status line calls the cmux CLI on each turn. The bench is not a CI step.scripts/measure/perf-10x-daemon.shstays at its reviewed version.This is the next PR of the #27698 lock series. It is stacked on #28147 (head d8f9ee9), so review only the commits after it. 1e (an agent hook that changes no projection takes no registry lock) builds on this PR's reader connection. #27780 (Austin) touches the same lock.
Owner: PL-daemon (session cmux-b6).
Testing
Tests, in
cmux-tui-core/src/mux/tests/registry_reads.rs:the_terminal_revision_read_does_not_wait_for_the_registry_lockholds the registry lock and reads the revision from another thread, with a 5 s bound.a_progress_report_reads_only_its_own_terminals_tabsrecords each table the report reads, using a SQLite authorizer on the registry connection. It checks that the report reads none of the per-screen side tables that only a whole-topology load reads (resource_screen_rows,resource_column_docks,resource_lone_docks,resource_parked_splits), and that the report still publishes a revision.a_committed_read_during_an_open_write_transaction_sees_only_committed_dataholds a write transaction open that changes the revision, and reads on the reader connection from another thread. The read must not wait, must return the committed value and not the uncommitted one, and must return the new value after the commit. It guards the reader; it is not a red test.Red, fleet step
1e49666eaf503381f65e5869,cmux-tui-rust-check.sh test registry_readsat 044b745 (the tests, without the fix):the_terminal_revision_read_does_not_wait_for_the_registry_lockFAILED: "the terminal revision read waited for the held registry lock" (registry_reads.rs:40, after 5.07 s).a_progress_report_reads_only_its_own_terminals_tabsFAILED. At that commit the test still had an earlier assertion, which was too strict. Its failure printed every table the report read (log line at 20:10:20Z):a progress report read resource_screens: {"json_each", "meta", "resource_browsers", "resource_column_docks", "resource_lone_docks", "resource_mutations", "resource_panes", "resource_parked_splits", "resource_screen_rows", "resource_screens", "resource_tabs", "resource_terminals", "resource_workspaces", "sqlite_master", "stash_capture_workspaces", "state_pending_changes", "terminal_hosts"}All four side tables that the shipped assertion checks are in it. So the shipped assertion also fails without the fix.
Green:
cmux-tui-rust-check.sh allat the head 5b321e6, fleet step1323ed5be0f10f315cc33d30. It compiled, fmt and clippy pass, and 12970 of 12983 tests passed. All threeregistry_readstests passed. None of the 13 failures is this PR's:graphics_pointer::evicted_known_undelivered_motion_allows_same_cell_retry: 6 single runs passed, one attempt each, 3 at the head and 3 at the base (e364c319, 44e68d12, f3a4ab25, 58063d9e, 8be434c3, 0479c3ff).local_resize_bounds_the_drain_to_a_queued_snapshot.apps_store::the_customize_folder_installs_itself_and_reloads_when_it_changes.Observed / Not run
lockssection, so they have no p99 per holder.Performance
No perf numbers: perf claim: pending A/B (pa-d). This PR claims no measured speedup until the A/B above is posted here.
Changelog
Fixed: On a busy Mac, agents' terminal progress and cmux NEXT's identify requests no longer wait behind other saves.
Proof
No capture: daemon-only change, no UI.
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Reduces time waiting on the shared workspace registry lock by moving two of its largest read holders onto paths that don't take it, and fixes a self-deadlock on the settle of a late registry commit.
The terminal registry revision, read by
identifyand everylist-workspacesreply, now reads on a new read-only WAL connection to the registry's own file, with no registry lock; an in-memory registry or a failed read keeps the locked read. Terminal progress reports now read only their own terminal's tab rows instead of the whole session topology.Bug Fixes
Option/expect()StateGuardwith a directMutexGuard, and removed panic-ratchet hits in the new histogram's indexing and arithmetic.New Features
server-statsinclude=["locks"]reports the registry and state lock holders by site, with#[track_caller]naming the request path that holds the lock.agents-hooksscenario to the daemon latency bench (hook-only load, no CLI calls); diagnostic use only, not a CI step.Written for commit c076a3d. Summary will update on new commits.