Repository navigation
cmux-next iPhone Feed: UIKit rows laid out off the main thread (hitches 50 → 1–3 ms/s) - #24683
Merged
Merged
Conversation
…out once per event and a change reconfigures that row only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Feed's list is a UICollectionView of UIKit cells (FeedRowCell) with a diffable data source keyed by event id. Each row's layout (FeedRowLayout: every frame and attributed string) is computed off the main thread and kept until its event or row state changes; a fixed-height column layout (FeedListLayout) places rows by prefix sums. Markdown bodies are native attributed text, code cards and tables (NativeMarkdown, the web renderer's metrics); only math still renders in a pooled web view, once scrolling settles. Live inserts and removals are snapshot edits, the row under the reader stays in place, and new rows above count into the N new pill. Labels draw asynchronously. Interactions kept: reply box, question cards, Allow/Deny with Face ID, the ... and long-press menus, rounded swipe actions, quote bubbles, Show more, copy and share, routing to the source Mac. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (correctness subagent at 26bc5b4): - the unread dot and swipe actions redraw on a read change (the plan was empty); - a width or text size change keeps the list's order and row state, lays every row out again and reconfigures them all, and counts nothing as new; - the question card's Back, Next and Answer sit at their card coordinates; - a saved question draft shows on the card (seeded before the layout), the card state and draft stay until the question stops pending (a refused answer keeps the picks), and Other takes the keyboard only when the person opens it; - stale accessibility identifiers and hints are reset on reuse; the header is measured again on a text size change; only never-shown events count as new; one reply observation per row, ended when the row leaves; duplicate ids are dropped; width changes apply after the layout pass; the minute timer stops. Performance: a new cell made mid-scroll cost ~20 ms (its first commit of every part); parts a row may not show are made when first needed. Batch results are checked against current keys in one pass (it was quadratic). The rig flushes the frame log before exiting (the last window was lost). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A store update re-keyed, copied and deep-compared all ~300 rows and refiltered four dictionaries (~10 ms of main thread per change, 2-5 a second with agents working). Rows whose event and state did not change keep their shown key; heights are kept per row; pruning runs only when the order changed. The real Feed rig links each rig Mac it is told to (Connect). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- a submitted question redraws its card at once (mutateState); - VoiceOver stops reading Unread after a read on the Mac; - a prefetched cell catches up on a read flag or the minute when it scrolls in; - a text size change re-measures the header off the layout pass, with the collection view's size category; - a web body's view is made only once the row settles; - reuse resigns the question card's Other field; set lookups in commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… measuring it Under real agent load every new ask changed the Needs you count, and the list measured its SwiftUI header again each time (a hosting controller sizing pass). The count now only redraws the header cell; its height is measured when something that can change it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SwiftUI FeedCell is gone on this branch; its new base behavior is carried into the UIKit rows: a terminal agent's reply (typed into its terminal, the item's sending line, "Sent to the terminal", one reply at a time, FeedItem.takesReply), "Open in Terminal" under a terminal agent's question, and links in bodies (a web link opens in Safari, a file path opens the event, as InlineMarkdownCache and the web renderer do). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…annot reach each other Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
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)
|
Collaborator
Author
Correctness reviews (subagent) and resolutionsReview 1, at
|
| Finding | Fix |
|---|---|
| The unread dot and swipe actions were not redrawn on a read-only change (empty plan returned early) | redraw computed before the early return |
| A width or Dynamic Type change dropped rows, lost row state and counted a false "N new" | relayoutAll keeps order and state, waits for all layouts, then reconfigures every row; nothing counts as new |
| Question card Back, Next and Answer were offset by the card's x | Frames used as given (card coordinates) |
| A saved question draft was not shown, but was acted on | Drafts are seeded before layout; state kept until the question stops pending; Other takes the keyboard only on .beginOtherEditing |
| Non-blocking: stale accessibility ids, header not re-measured on text size change, false new counts, Other refocusing, reply observation chains piling up, submit dropping picks | All fixed |
| Risks: duplicate ids, snapshots applied mid-layout, minute timer not invalidated | Ids deduplicated; width change deferred; timer invalidated in deinit |
Review 2, at 5144b51: nothing blocking; fixed in 6da6b1c
| Finding | Fix |
|---|---|
| A submitted question did not redraw at once | mutateState |
| VoiceOver kept reading "Unread" after a read on the Mac | accessibilityElementsHidden updated in redraw |
| A prefetched cell missed a read flag or minute change | Catch-up in willDisplay |
| Header measured during a trait-change layout pass | Deferred, using the collection view's size category |
| A web body's view was created mid-scroll | Created only once the row settles |
| Nits: Set lookups, reply height kept, Other resigned on reuse | Done |
Not changed: the tab bar badge rebuild cost (SwiftUI TabView, in RootView, also in the base) is left for a follow-up.
|
Found 2 test failures on Blacksmith runners: Failures
|
Collaborator
|
Bead |
… the frames below it Review (merge carry-over): links in short bodies open too; a tap on blank space near a link opens the event; the hit test lays out with unbounded height; Open in Terminal is an explicit action, not an option id that an agent could also send; file paths in code are not underlined (the web Feed never linked them). Under real use a commit still cost ~10 ms: it rebuilt the plan's dictionary, scanned every row for warm-up shapes and set all 300 heights. The plan reads the kept keys, warm-up looks at changed rows only, and the layout re-sums from the first changed row (FeedListLayoutTests, iOS simulator: 11 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext-ios-feed-uikit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t pass after an insert traps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n insert re-sums safely Review (at 3687fdf): setHeights' quick path trusted frames that a full reset had not rebuilt yet (prepare() still pending), and resum wrote past their end. It now runs only on current frames. A link tap below a capped body's visible part opens the event. FeedListLayoutTests (iOS simulator): red at 9e2ed53 (crash in FeedListLayout.resum), green here, 12 passed with FeedListPlanTests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
Review 3, the merge carry-over (at
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Summary
The Feed's rows are now UIKit: a
UICollectionViewwith a diffable data source keyed by event id, and cells made of plain UIKit views (no SwiftUI hosting in rows). Each row's layout (every frame and attributed string) is computed off the main thread and kept until its event or row state changes. Markdown bodies are native attributed text, code cards and tables using the web renderer's metrics; only math still uses the pooled web view, once scrolling settles. Replaces #24467, whose SwiftUI trimming showed no gain.Why: each SwiftUI row that scrolled in cost 14–20 ms of main-thread work (SwiftUI building and measuring the whole row tree), about one per row at fling speed. A store update also re-walked all ~300 rows.
What stays the same: the reply box (including terminal-agent replies typed into the terminal), question cards with drafts, Allow/Deny with Face ID, Answer/Open in Terminal, Decline, the
…and long-press menus, the rounded swipe actions with the drawer handoff, quote bubbles, Show more, Copy/Share, links (web opens Safari, a file path opens the event), pull to refresh, the "N new" pill with the reading position kept, routing to the source Mac, and the demo driver.Details, numbers and evidence:
cmux-next-ios/verify/feed-perf-20261023.md.Results (iPhone 16 Plus simulator, 320 events, 2400 pt/s, base vs fix in the same hour)
The < 5 ms/s target is met in every run. Without live traffic the latest runs have no frame over 33.3 ms. With live traffic a few single frames reach 34–51 ms per run, from a new cell's first commit or an insert landing in that frame.
Real use: two rig Macs, real Codex and Claude sessions on one, permission asks on the other, the phone linked to both. See the verify doc.
Changelog
Testing
swift test --filter "Feed|TerminalFeed|MultiMac|FrameInterval|Markdown"on mini-cmux7s: 83 tests in 21 suites passed.FeedListPlanTestscommitted red first (8ef597f55f73): one layout per event, state and width; a change reconfigures only its row; snapshot edits; only never-shown events count as new.verify/evidence/feedui/shots/(not committed; numbers in the doc).Observed / Not run
sampleprofiles as listed; side-by-side screenshots.durable: truesilently downgraded to session-only inside cmux #3395 documents the fix), any device run (simulator only, as asked).Review
Two correctness subagent reviews (at
26bc5b4and5144b51); findings and fixes are in a PR comment.No capture: the Feed is iOS; light and dark simulator stills of every item kind are listed above and described in the verify doc.
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Replaces the Feed's SwiftUI rows with UIKit
UICollectionViewcells laid out off the main thread, cutting main-thread hitches from 38–62 ms/s to 0.7–3.3 ms/s on a 320-event iPhone simulator feed (p99 frame time 16.67 ms, within the 33.3 ms budget). The prior SwiftUI trimming attempt showed no gain.Refactors
Written for commit 659e639. Summary will update on new commits.