Repository navigation
acpmux: resolve and peer notices read only the peer rows they need (daemon at a core with 8k peer rows) - #32487
Conversation
…ers list Red on base: every streamed record, session_changed and ask check of a peer walks every peer's rows, so with 50 peers of 400 rows a notice costs ~50x what it does alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0c6cd8c5091cf25d139
RemoteRows indexes each peer's rows by key and by the peer's own session id. The notice loop's streamed records, session_changed, purges, lagged notices, ask checks, cache saves, stale marks, warm rounds and peer counts read that index instead of walking every peer's rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0c6cd8c5091cf25d139
|
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)
Shipped in:
|
…e it could name Red on base: each _acpmux/resolve copies and sorts every peer row (a miss four times), so five resolves among 8,000 rows cost about eight full lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0c6cd8c5091cf25d139
Every lifecycle command (acpmux send, last, wait) names its session through _acpmux/resolve, and each copied and sorted all of a Mac's 8,044 peer rows: Hub::locate was ~90% of the samples of a daemon at a core. locate now copies only the rows may_name takes (a superset of what its matchers can match), reads moved chats' targets and close-match names on their own, and a miss reads the closed rows once instead of twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0c6cd8c5091cf25d139
may_name read up to eight summary fields per row, so five resolves among 8,000 peer rows still cost about one full list. It now reads the row's peer and receipt and two summary fields, and the miss path's name search makes no lowercase copy of each name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0c6cd8c5091cf25d139
|
Found 2 test failures on Blacksmith runners: Failures
|
|
Review (codex gpt-6.1-sol high fast): SHIP at 5a0559e Two rounds: SHIP at 022c697 (the per-peer RemoteRows index: consistent across every write path, Deref read-only, swapped lookups keep peer/mux semantics, sound locking, reasonable cost-test threshold). Then SHIP on the delta 022c697..5a0559e: |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0c6cd8c5091cf25d139
…het) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0c6cd8c5091cf25d139
|
Review (codex gpt-6.1-sol high fast): SHIP at ca665e6 The delta since the SHIP at 5a0559e only fixes CI lints: dead code (an unused list builder removed, |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Summary
A linked Mac's acpmux daemon sat at 77-105% of a core with 1.8 GB RSS for 16 h, and
acpmux lsthere took 6.8 s or timed out. It has 43 peers listing 8,044 rows, plus 450 sessions of its own. Three 10 ssamples of it, taken a minute apart, look the same:Hub::locateholds ~4,000 of the 4,441 samples per thread-window, about 0.9 of a core. Under it sitremote_sessions()(a copy of every peer row), the sort of the full list, andserde_jsonindexing and malloc/memmove churn.locateanswers_acpmux/resolve, and every lifecycle CLI command (acpmux send,last,wait, …) resolves its session name there (cli/resolve.rscontrol_id). Each resolve built the full list: every peer row copied and all 8.5k sorted. A query the live list missed built it again, and a miss built it up to four times. On a host whose agents and reconcilers run those commands all day, that is the core.locatecopies only the peer rows its query could name.Names::may_nameis a superset of whatby_bead,by_link,by_key_exactandby_keycan match: id, prefix, name, short name, bead tag, link session id, and every row of aHOST/…key's host. It is judged on the row's peer and receipt plus two summary fields, so a row costs a few compares, and only the rows it takes are copied and sorted. The full order is a total sort, so restricting it keeps the same relative order. Moved chats' targets are read from the same kind of list (live or all) the matches came from. Close-match names come from a name-only pass, andsession_name::candidatesis order-independent. A miss reads the closed rows once instead of twice.session/update,_acpmux/event) found its row with a walk over every peer's rows on the one serial notice loop. So did each ask check,session_changed, purge,_acpmux/lagged, cache save, stale mark, warm round andpeers()count.RemoteRowsnow indexes each peer's rows by key and by the peer's own session id, kept on every write as its(mux.parent, mux.handle)index already is.Checked and ruled out, from the samples and the code:
flockare by design:agent_host/wait.rsdeath watches, one blocking thread per live agent host (deduped by path), waiting on the host's lock. They use no CPU.peer_link::Backoff), and a peer down for a day parks for an hour (acpmux: preserve SSH failure reasons and slow retries for long-down peers #27002). Dials don't show in the samples.watch_delta.rs), and rows arrive onesession_changedat a time (fix 2).lslatency is the shared list builder's pace (list_guard.rs, ~1/32 of a core): it answers from cached rows and dials no peer. The resolves above held the rows lock and the CPU it waits on.Performance
Hub::locateshare of daemon samplessample PID 10, ×3, build 735d16bHOST/prefix, short name, miss)hub::peer_notice_cost_tests::a_resolve_…, debug build, same CI job (base = fix reverted)hub::peer_notice_cost_tests::a_peer_notice_…, debug build, same CI jobpson the linked Macsampleheaderpeer_link::BackoffA Mac's daemon updates only when its app relaunches, so the fleet "after" figures come from the first relaunch after this lands.
Testing
Red first:
e28a2a9(notice cost) and26c8c7b(resolve cost) each add a failing test before their fix. Each CI run ran the tests at the head, then reverted the fix in the same job and ran them again:cargo test -p acpmux --lib -- hub::peer_notice_cost_testscargo test -p acpmux --lib -- hub::locate hub::own_host hub::peers hub::remote_rows hub::peer_notice_cost_tests hub::peer_snapshot hub::peer_cache hub::permissions hub::purge_tombstones hub::peer_grace hub::key_route cli::resolve session_name: 150 passed.cargo test -p acpmux --test pairing -- resolve: passed.hub::remote_rows::tests(the peer index follows insert, replace, take, retain,mark_peer_staleandtake_peer, leaving no empty entry), anda_resolve_still_finds_what_it_named_among_peers(exact, bare across peers,HOST/exact before prefix, session id, miss).Observed / Not run
Observed: three
samples of the affected daemon and its--json status(taken for this PR); the CI runs above.Not run: the full acpmux suite and clippy (the PR's CI runs them). The after-relaunch CPU, RSS and
lslatency on the linked Mac are pending until it relaunches on a build with this.Changelog
Fixed: the acpmux daemon no longer spends a core resolving session names when it is linked to peers that list thousands of sessions.
Risk: normal. It adds a read-side filter and an index on a map whose writes already go through one owner type. It changes no wire format, policy or persisted data.
Agent session: agent session
🤖 Generated with Claude Code