cmux-next iPhone Feed: UIKit rows laid out off the main thread (hitches 50 → 1–3 ms/s)

Merged
AZ
14d ago
+5129-1004

Summary

The Feed's rows are now UIKit: a UICollectionView with 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)

RunBase hitch ms/s · p99 · main CPU ms/sFix hitch ms/s · p99 · main CPU ms/s
no live traffic (4 runs)38–56 · 35–37 ms · ~1830.7–1.7 · 16.67 ms · ~58
4 live changes/s (4 runs)49–62 · 35–38 ms · ~1901.1–3.3 · 16.67 ms · ~80

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

  • Changed: the Feed scrolls smoothly with many events: rows are native views laid out ahead of time.

Testing

  • swift test --filter "Feed|TerminalFeed|MultiMac|FrameInterval|Markdown" on mini-cmux7s: 83 tests in 21 suites passed.
  • New FeedListPlanTests committed 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.
  • Demo simulator builds on the fleet; screenshots of base vs new for every item kind, light and dark, under verify/evidence/feedui/shots/ (not committed; numbers in the doc).

Observed / Not run

  • Observed: frame-log A/B runs and sample profiles as listed; side-by-side screenshots.
  • Not run: Instruments traces (xctrace does not record over SSH on the minis; hq #3395 documents the fix), any device run (simulator only, as asked).

Review

Two correctness subagent reviews (at 26bc5b4 and 5144b51); 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


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Replaces the Feed's SwiftUI rows with UIKit UICollectionView cells 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

  • Each row's frames and attributed strings are computed off the main thread and cached until its event, row state, or text size changes; a store update re-keys only the rows whose event changed, and a reconfigure re-sums only the frames below the first changed row.
  • Markdown bodies, code cards, and tables render as native attributed text using the web renderer's metrics; only math uses the pooled web view, after scrolling settles.
  • Fixes a crash when a row height changed right after an insert, before the layout pass ran: the quick re-sum path now only runs on current frames.
  • All Feed interactions are preserved: reply box (including terminal-agent replies), question cards with drafts, Allow/Deny with Face ID, Open in Terminal, swipe actions, quote bubbles, Show more, copy/share, links (web opens Safari, a file path opens the event), and the "N new" pill with the reading position kept.