Skip to content

fix(perf): commit transcript rows on motion frames - #33629

Merged
teamleaderleo merged 3 commits into
feat-cmux-nextfrom
lane/transcript-first-motion
Nov 3, 2026
Merged

teamleaderleo merged 3 commits into
feat-cmux-nextfrom
lane/transcript-first-motion

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Transcript motion currently writes scrollTop in a frame callback, while the browser's scroll event arrives after that frame paints. This keeps virtualization one frame behind a fast motion and can leave the first viewport partially blank. The change routes the existing motion notification through the transcript's normal scroll handling synchronously before paint, cancels stale queued work, and preserves reader follow intent. Zero-duration jumps remain unchanged and no overscan or native logic changes.

Leo: “i feel like the app is slow on scrolling thru stuff... claude desktop is 120hz or something and ours is wack + it has sometimes not even rendered stuff!?”

Performance

Same-worker source A/B: baseline 5dee1ab988ec69569df92ed636c5b971d43f152a, candidate 9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64, fixed tests and harness. Timing and painted-coverage passes are separate. Two rounds are listed in order.

Metric Before After Delta Harness or run link
First-motion painted blank frames 1 0 −1 red/green test, run 41623659964
First-motion blank time (ms) 87 0 −87 same run
First-motion max uncovered (px) 219.25 0 −219.25 same run
Fling painted blank frames, rounds 0/1 1 / 1 0 / 0 −1 / −1 same run, painted-coverage pass
Fling painted blank time (ms) 16 / 16 0 / 0 −16 / −16 same run
Fling max uncovered (px) 190.5 / 190.5 0 / 0 −190.5 / −190.5 same run
Fling interval p95 (ms) 98 / 94 81 / 174 −17 / +80 same run, 3000-row WebKit timing pass
Fling longest frame (ms) 123 / 101 112 / 192 −11 / +91 same run
Fling React p95 (ms) 75 / 60 60 / 110 −15 / +50 same run
Fling dropped frames/s, nominal 120 Hz 103.745 / 104.493 101.030 / 109.671 −2.715 / +5.178 same run
Long-scroll interval p95 (ms) 40 / 52 42 / 45 +2 / −7 same run
Wheel interval p95 (ms) 28 / 27 26 / 24 −2 / −3 same run
Long-scroll / wheel painted blank frames 0 / 0 0 / 0 0 / 0 same run, both rounds

The coverage improvement is consistent; timing is mixed and does not demonstrate a speedup or native 120 Hz. The completed ABBA follow-up below reproduces large outliers in both variants. Native presentation remains a separate fleet measurement.

Validation at this head: full web check passes; the baseline fails the named first-motion assertion and the candidate passes it, the real six-viewport wheel test and existing regressions. The source and fixed-harness hashes, restored source and all expected stages are verified in the run receipt. Earlier wrapper failures are superseded and provide no product evidence.

Nonblocking review follow-ups: #33672 covers listener rebinding if the read-only scroller is replaced; #33673 covers the existing motion cancellation latch. Neither is a demonstrated current-flow regression.

First-motion ABBA timing follow-up

Run 41627761171, candidate 9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64, baseline 5dee1ab988ec69569df92ed636c5b971d43f152a. Same worker and fixed 3000-row WebKit harness; order A/B/B/A. Each cell lists rounds 0/1. This pass measures timing only; the zero-blank coverage evidence remains the separate run 41623659964.

Pass Source Fling p95 (ms) Fling max (ms) React p95 (ms) row.measure p95 (ms) Long p95 (ms) Wheel p95 (ms)
1 baseline 124 / 116 157 / 124 80 / 78 20 / 22 59 / 50 28 / 28
2 candidate 121 / 133 131 / 148 83 / 96 18 / 30 49 / 49 28 / 26
3 candidate 161 / 99 165 / 130 107 / 72 27 / 20 48 / 48 26 / 27
4 baseline 112 / 184 117 / 189 81 / 123 22 / 42 48 / 67 26 / 24

Baseline fling p95 spans 112–184 ms; candidate 99–161 ms. Both variants show large variation, and baseline also produces an outlier larger than the prior candidate 174 ms. The prior sample does not isolate a candidate regression. These results establish no reliable speedup, and they remain far above the 8.3 ms target. No averaging of per-run p95s is presented as an aggregate p95.

Receipt acceptance, fixed-source hashes, four log digests, and source restoration passed. Full web check/regression/coverage validation is reused from 41623659964 at the identical candidate tree. The native comparison is complete; the timing result is mixed and does not establish a uniform speedup.

Native pair: one run per arm on the same fleet worker with --app-cache --config=bench --vdisplay=120 --suite=budgets --only=acp --raw=acp. The native result is structural memory and first-content improvement, with mixed interaction timing; the own-mode next-chat value is a single sample and remains an unresolved follow-up rather than a claimed regression or speedup.

Native metric (mode) Before After Delta
First chat content (chromium) 1085.2 ms 871.0 ms -214.2 ms (-19.7%)
First chat content (own) 949.8 ms 789.5 ms -160.3 ms (-16.9%)
Next chat content median (chromium) 125.6 ms 76.6 ms -49.0 ms
Next chat content median (own, n=1) 35.8 ms 109.3 ms +73.5 ms; verify before any claim
Memory per extra chat (chromium) 453.1 MB 137.2 MB -315.9 MB (-70%)
Memory per extra chat (own) 490.7 MB 118.1 MB -372.6 MB (-76%)
Two-chat footprint (chromium / own) 1535 / 1611 MB 1217 / 1201 MB -318 / -410 MB
CEF renderer at two chats (chromium / own) 882 / 949 MB 558 / 562 MB -324 / -387 MB
Typing to paint p50 / p95 / max (chromium) 6.3 / 9.9 / 16.7 ms 5.2 / 10.5 / 18.8 ms mixed
Typing to paint p50 / max (own) 5.7 / 19.1 ms 5.2 / 14.9 ms better
Session composer (chromium / own) 53.9 / 36.5 ms 40.0 / 47.3 ms mixed

Native evidence: base step 1825c9e956d52749d471c17c at 5dee1ab988ec69569df92ed636c5b971d43f152a; candidate step 311f1ae7 at 9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64; both ran once on the same fleet worker with identical arguments.

Gallery failures are tracked under #33516, which already records the same isolated Git-action scene failing across unrelated PRs. Diagnostic run 41629680371 rebuilds the exact original merge and replay base: Git-action shift is 0.02984955 on both; composer worktree-search is 0.004427–0.004447 on both, within baseline. Source, loaded CSS/font hashes and sampled geometry match. Neither scene mounts the changed scroller. The original excess shifts do not reproduce from this patch; gallery is not the reason for this hold. This does not claim the shared gallery flake itself is fixed.

Independent review: Opus 5.5 medium SHIP at 5ecd071; formatting-only test delta is carried to this head verified independently; review receipt.

🤖 Generated with Codex

Agent session: agent session

teamleaderleo and others added 3 commits November 3, 2026 10:30
Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b2f8bf910242d7b9370a
Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b2f8bf910242d7b9370a
Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d60a77e16a11b66e2ecf
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 9e3e834: 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=pr33629 -f ref=lane/transcript-first-motion -f platform=macos -f backend_mode=local

Gallery before/after for 9e3e834: 16 states rendered, 0 images differ, 3212 more touched states not rendered. Images are in the run's artifact, not on this public PR; the independent review of this head reads them on its own, so the PR needs no hand-made screenshots of these states.

gh run download 41621699046 -R manaflow-ai/cmux -n gallery-advisory-33629-9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64 (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/standard Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/before Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/before Apple System Colors unchanged 0% own App.tsx
agent-pane.copy-controls/output Apple System Colors Light unchanged 0% own App.tsx
agent-pane.copy-controls/output Apple System Colors unchanged 0% own App.tsx

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

Shipped in:

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 3, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (claude opus-5.5 medium, carried after formatting-only verification): SHIP at 9e3e834

Original independent review: SHIP at 5ecd071; original verdict follows below.

Codex verified the complete intervening diff: only the empty line before the closing }); in webviews/test/transcript-fling.test.ts was removed. git diff --ignore-blank-lines 5ecd071757bf3dbb390e85cd9bbfddd151c11cc6..9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64 is empty. This is a carry of the existing Opus review, not a new Opus invocation; product code and test behavior are identical.

Visual verdict: unchanged by the formatting-only diff. The original nonblocking follow-ups remain; validation results are separate and are not claimed by this carry.

diff --git a/webviews/test/transcript-fling.test.ts b/webviews/test/transcript-fling.test.ts
index 491095a559f..77c4186fb8c 100644
--- a/webviews/test/transcript-fling.test.ts
+++ b/webviews/test/transcript-fling.test.ts
@@ -212,5 +212,4 @@ await requireBrowserLane("transcript-fling.test.ts", async () => {
       }
     });
   }
-
 });

Review (claude opus-5.5 medium): SHIP at 5ecd071

Visual verdict: no intended design change. The diff changes when scroll state is committed, not any UI.

What I checked (from the supplied source only; nothing was run):

  • Reentrancy with animateScroll and stopScroll. The motion step in runScroll writes the offset, calls announceScroll, then checks if (!active) return (scrollMotion.ts:169-171). So if anything inside the now-synchronous handleScroll → flush → flushSync path stops the ride, it is not rescheduled. The only stopScroll reachable from handleScroll is wheelGlide.current?.stop() in the older-room clamp (App.tsx:5079). It runs only while a wheel ride exists, and end → finished clears that ride cleanly. Anchor and settle writes made during the flushed placement effect are picked up by animateScroll's externalDelta rebase (scrollMotion.ts:70-71) and by animateScrollBy's += on the live offset.
  • Native event after a motion flush. The browser's own scroll event for that write arrives on the next frame. By then scrolledTo already holds the same top, so movedUp and readerReachedBottom are false and atLatest equals followingRef (App.tsx:5126-5136). The resulting enqueue matches immediate and is dropped (scrollPacer.ts:49). A change in either the offset or the follow intent clears immediate and commits; the unit test covers the same-offset intent change.
  • Stale immediate. Placement paths that set the scroll offset outside the pacer call scrollPacer.current?.cancel() first, which clears immediate: App.tsx:4366, 4379, 4419, 4534, 4579, 5052, 5225, 4224 and Find's findMoved. Rows are chosen from scrollRef.current, which queueScroll updates before the dedup check (App.tsx:3041), so a dropped duplicate loses at most a redundant render, not the offset.
  • Pending RAF. flush cancels any queued frame before it commits, and cancel clears both the queued frame and immediate.
  • Duration-zero calls (ScrollMap activate, reduced-motion footnotes) don't announce. That is correct: they write from a task, so their native scroll event is handled in the same rendering step as the write, before paint.
  • Session transitions. handleScroll returns early while !didOpenAtLatest. stopJump ends any ride on a sessionId change (App.tsx:3083), and the listener reads the latest handler through motionScroll.current.

Follow-ups (none blocking):

  1. settleScroll has no active guard. If end() runs inside announceScroll (scrollMotion.ts:223), the step still reaches frame = requestAnimationFrame(step) (:229). That would keep a "stopped" settle running with its listeners and its running entry already removed. No path reaches it today, but this PR makes announceScroll listeners do much more work synchronously. Mirror runScroll's active check.
  2. pacing.scrolled() now fires twice per motion frame, once from the motion listener and once from the deduplicated native event (App.tsx:5068). If ScrollPacing derives intervals from these calls, the adaptive render rate sees short and long intervals alternating. Either call it only on the native path or skip it when the pacer dedups.
  3. The listener binds to the scroller node present at mount (App.tsx:5157-5165, deps []). If the scroller is replaced, for example when readOnly toggles and ReadOnlyBoundary swaps its wrapper, the motion fast path silently falls back to the base behaviour. Other [] effects in this component already have the same limitation.
  4. debug.ts startFling numbers change meaning. isBlank now reads acpmuxPerf.mounted after the synchronous commit, which is what paints. Bench numbers before this head aren't comparable to numbers after it; say so in the Performance table.
  5. The wheel half of the new browser test asserts zero blanks on the browser's native scroll path, which this PR doesn't change. Playwright's pixel wheel isn't a notch (isWheelNotch returns false for an integer deltaY with a matching legacy delta), so wheelGlide is bypassed. If that case is red, it is this PR's own red: either fix the native path or make that case diagnostic-only. Even a green result shows DOM coverage, not compositor presentation (scrollCoverage.ts:4), so it doesn't establish zero native blanks.

Before landing: run scroll-pacer.test.ts and both new transcript-fling.test.ts cases, plus the existing fling, jump and cached-row suites, with cmux-ci test at this head. Record the red at the base and the green at this head. None of that evidence exists yet.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

A/B run41621761759 stopped before any product test: the fleet snapshot is shallow and the driver tried to resolve the candidate's third ancestor. The product diff is unchanged. The wrapper now explicitly supplies the two source commits, validates their exact file contents, and does not require unrelated Git history. One corrected run is dispatched at the same head9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64; no performance conclusion follows from the failed setup.

Review reconciliation: ScrollPacing.scrolled() starts one RAF sampler and resets a settle timer; it does not record an interval per notification. Thus the review's conditional concern about alternating intervals does not apply. Historical model_blank counts still change sampling order with this patch; before/after blank claims use the unchanged painted-DOM coverage oracle. Native-wheel coverage remains a required behavioral check for this candidate.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Nonblocking follow-ups from the independent review are filed as #33672 (motion listener rebinding after a scroller replacement) and #33673 (synchronous cancellation during settlement). Both include causal evidence, originating commits and focused reproducer steps, with area and severity labels. Neither is a demonstrated current product regression. The strict native-wheel painted-coverage regression remains part of this PR’s validation, not a deferred finding.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

First-motion ABBA timing follow-up

Run 41627761171, candidate 9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64, baseline 5dee1ab988ec69569df92ed636c5b971d43f152a. Same worker and fixed 3000-row WebKit harness; order A/B/B/A. Each cell lists rounds 0/1. This pass measures timing only; the zero-blank coverage evidence remains the separate run 41623659964.

Pass Source Fling p95 (ms) Fling max (ms) React p95 (ms) row.measure p95 (ms) Long p95 (ms) Wheel p95 (ms)
1 baseline 124 / 116 157 / 124 80 / 78 20 / 22 59 / 50 28 / 28
2 candidate 121 / 133 131 / 148 83 / 96 18 / 30 49 / 49 28 / 26
3 candidate 161 / 99 165 / 130 107 / 72 27 / 20 48 / 48 26 / 27
4 baseline 112 / 184 117 / 189 81 / 123 22 / 42 48 / 67 26 / 24

Baseline fling p95 spans 112–184 ms; candidate 99–161 ms. Both variants show large variation, and baseline also produces an outlier larger than the prior candidate 174 ms. The prior sample does not isolate a candidate regression. These results establish no reliable speedup, and they remain far above the 8.3 ms target. No averaging of per-run p95s is presented as an aggregate p95.

Receipt acceptance, fixed-source hashes, four log digests, and source restoration passed. Full web check/regression/coverage validation is reused from 41623659964 at the identical candidate tree. Gallery attribution remains unresolved and the PR stays held.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Remaining guard red is inherited at head 9e3e8347a19dfe28cfe1cf6c93e337c9736f7d64: job125868833997 fails exactly test_ci_warm_sources.py::ThisTree.test_every_compile_job_names_its_warm_sources, with stable-next.yml:swift-suites is exempt but its steps wire ['deriveddata-reuse']; list them under 'warm' instead.

Base 5dee1ab988ec69569df92ed636c5b971d43f152a job125855765106 fails that same test with that exact error. Both report one completed assertion failure; no crash or cut-short run is excused. The test, warm-source declaration and workflow are unchanged by this PR. The Guard status failure aggregates it. All eight webview per-test verdicts are green; gallery attribution is recorded in #33516.

The PR stays held only for the pending same-worker native timing comparison. Existing review and product head are unchanged.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (claude opus-5.5 medium): SHIP at 9e3e834

Visual verdict: unchanged by this diff. This carries the independent Opus review at 5ecd071; the intervening change is formatting-only and the product tree is unchanged.
Follow-ups: #33672 and #33673 remain nonblocking.

@teamleaderleo
teamleaderleo merged commit dc4e369 into feat-cmux-next Nov 3, 2026
252 of 260 checks passed
@teamleaderleo
teamleaderleo deleted the lane/transcript-first-motion branch November 3, 2026 18:56

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 9e3e8347 Deployed Nov 3, 2026 by teamleaderleo via cmux-tui tree preflight #105532
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