Repository navigation
docs(threads): set scrolling requirements and targets for long threads - #2459
Merged
Merged
Conversation
This was referenced Nov 4, 2026
jameskraus
force-pushed
the
jameskraus/thread-scroll-targets
branch
2 times, most recently
from
November 4, 2026 21:08
b27cd53 to
ebfed0a
Compare
jameskraus
marked this pull request as ready for review
November 4, 2026 21:08
jameskraus
force-pushed
the
jameskraus/thread-scroll-targets
branch
from
November 5, 2026 00:47
ebfed0a to
6ade2d8
Compare
wesbillman
reviewed
Nov 5, 2026
wesbillman
left a comment
Collaborator
There was a problem hiding this comment.
🤖 Carl, an automated reviewer, commenting via Wes’s GitHub account.
No blocking findings. The requirements are coherent and match the browser and native measurements in #2460, apart from the native cadence issue flagged there. The targets and ceilings are a product decision and still need the human review the PR asks for.
CI: the failing Browser journeys (webkit, 14/14) job is a builderlab login 502 and is unrelated to this docs change.
wesbillman
approved these changes
Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: James Kraus <jamespkraus@gmail.com>
jameskraus
force-pushed
the
jameskraus/thread-scroll-targets
branch
from
November 5, 2026 18:34
6ade2d8 to
a0b40f9
Compare
jameskraus
added a commit
that referenced
this pull request
Nov 5, 2026
…S app (#2460) ## Summary Thread scrolling has targets (#2459) and no way to measure them. This adds a long-thread workload and two ways to run it: - **Browser cases** in the existing suite: `bin/pnpm perf --scenario thread-scroll [--browser webkit]`. They run in CI (Chromium, report-only) and locally in macOS WebKit. - **A native macOS harness**, `scripts/perf/thread-native/run.mjs`. It drives the real desktop app on the same mock thread, records the pane at 60 fps, and reports frame intervals and blank frames for each build it compares. ## The workload `tests/browser/perf/thread-corpus.mjs` fills the fixture's strict-window thread with 460 generic weekly-update replies from 40 authors. The replies mix Markdown lists, bold, inline code, mentions, links, quotes, plain prose and 19 collapsed branches. Their heights follow a real long thread's (median about 280px). Every page loads deterministically: upward wheel input requests a page, the fixture holds it until the sample sees the request, and the sample waits for the rows. There are no fixed delays. ## Browser cases | Case | Measures | | --- | --- | | `thread-scroll-open` | Click Reply on the root to visible thread rows (`firstRowsMs`) | | `thread-scroll-fling` | One 6000px upward gesture over all 441 top-level replies | | `thread-scroll-reverse` | Up 6000px, down 3000px, up 3500px | - The runner can now measure any surface (`selector`/`ids`). In WebKit it skips DevTools counters, CPU throttling and long-animation-frame data. - `cadence.mjs` adds `rafGapsOver33`. - In CI, the reporter writes a medians table to the job summary. - **Why browser cases:** rendering and scroll cost for 460 mounted replies needs a real engine. Lower layers cannot mount the thread at that scale. - **What each engine shows:** - macOS WebKit reproduces the compositing stalls in frame intervals: fling rAF p95 is 63 ms on `main`. - Chromium at 4× CPU shows main-thread cost. Its frame rate cannot see compositing stalls. - Neither engine shows momentum or painted pixels. `domGapFrames` stays 0 while the app shows blank regions. Local timings for `--scenario thread-scroll --repeat 1` on an M-series MacBook Pro: Chromium 23.8s for the three cases, WebKit 37.6s. ## Native harness - `host.spec.mjs` runs the browser fixture with the corpus and builds one frontend per compared revision. A revision other than the working tree builds in a temporary detached checkout with its own dependencies, so its styles, Tailwind classes and configuration all come from that revision. The host serves the selected build through a loopback proxy that injects `agent.js`; its state-changing endpoints accept only POST, and a malformed request gets a 400. - The desktop app opens that proxy and signs as an ephemeral fixture account through the HTTP broker. It uses its own app identifier and never touches installed app storage. - For each run, the harness: - shows and activates the window - sends synthetic trackpad flings with momentum phases (`drive.swift`) - records the pane with ffmpeg at a constant 60 fps - scores blank frames from pixels - Frame metrics count every animation-frame interval from the first wheel event to the frame after Jump to latest lands, including stalls that end on an unchanged scroll offset. The window logic is shared with the browser cases' `cadence.mjs`. - Builds alternate ABBA. A run whose gestures did not arrive (under 10,000px scrolled) is repeated. Baseline on `main` with this suite, four runs: | p95 frame interval | Frames over two 60 Hz frames | Dropped frames | Blank frames | Longest blank | | --- | --- | --- | --- | --- | | 85 ms [84–87] | 76 [67–78] | 194 | 54 [52–56] | 300 ms | Every row is over the ceiling, and the next two PRs move them. `docs/performance.md` gains a **Current measurements** table that each change updates. ## Explainer video https://github.com/user-attachments/assets/76ef0ec2-d569-498e-8015-d2ff4df421af Captions are burned in; narration is ASR-checked against the script. Footage uses the mock thread only. The narration quotes the first measurement session (2026-11-04); the tables above come from the re-run on the rebased stack (2026-11-05), whose medians are within a few milliseconds of it. ## Stack 1. #2459: requirements and targets. 2. **This PR**: the suite. 3. #2461: closed message action bars skip the glass. 4. #2462: isolate thread rows inside the scroller. ## Known gaps - The native harness is a local stopgap. It needs a logged-in Mac, Screen Recording and Accessibility permissions, and an idle machine, so it does not run in CI. - The app under test is a development Rust build serving a production frontend. - Synthetic momentum approximates a trackpad. - Hosted CI cost of the three Chromium cases: the "Perf suite smoke check" step takes 3m 59s here against 3m 40s on #2459, which has no new cases (+19s inside the 15-minute job). ## Readiness Agent reviews ran, and their recommended blockers are fixed. Waiting on human review. Signed-off-by: James Kraus <jamespkraus@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jameskraus
added a commit
that referenced
this pull request
Nov 5, 2026
## Summary
Scrolling a long thread in the macOS app stutters and shows blank
regions. Most of the cost is compositing, not JavaScript.
Every row keeps its closed message action bar laid out. The bar sits at
opacity 0, so it stays focusable at its slot, but it still carries the
`popover-surface` glass: `backdrop-filter: blur(8px)` from
`elevated-material`. WebKit composites every backdrop filter, even an
invisible one, so a 460-reply thread holds about 460 composited backdrop
layers. Every scroll update recomputes and re-sends them.
Inside the hover/fine-pointer block, closed bars now skip the blur:
```css
.messageActions:not(:popover-open) {
backdrop-filter: none;
}
```
- Every visible state of the hover toolbar is `:popover-open`, so the
open bar keeps the complete material, including its entrance animation.
- Non-hover layouts, where the bar is inline and visible, are untouched.
- Base and fix are pixel-identical at rest and with a hovered bar open.
- `DESIGN.md` records the exception beside the hover toolbar entry.
## Evidence
Native macOS app, 460-reply mock thread, four ABBA runs per build in one
session (`scripts/perf/thread-native/run.mjs`):
| | Before | After |
| --- | --- | --- |
| p95 frame interval | 85 ms [84–87] | 50 ms [46–52] |
| Frames over two 60 Hz frames | 76 [67–78] | 44 [39–49] |
| Dropped frames | 194 | 94 |
| Blank frames | 54 [52–56] | 9 [9–10] |
| Longest blank | 300 ms | 133 ms |
https://github.com/user-attachments/assets/cfd6302d-e4b5-4a2a-884b-2d01b1fea420
Browser cases (`bin/pnpm perf --scenario thread-scroll`, five rotated
rounds):
- **macOS WebKit:** fling rAF p95 63 → 47 ms, frames over 33 ms 48 → 14.
Reverse 63 → 49 ms, 105 → 27.
- **Chromium at 4× CPU:** task time flat (1347 → 1328 ms); script time
+10% within that.
## Tests
`message-actions-floating.spec.mjs` now asserts that the open toolbar
keeps its backdrop blur and the closed bar has none. Without the rule,
both assertions fail in Chromium and WebKit; with it, the spec passes in
both (36 tests).
## Explainer video
https://github.com/user-attachments/assets/bd53378e-c3c4-4efe-a013-937c75ed6618
Captions are burned in; narration is ASR-checked against the script.
Footage uses the mock thread only. The narration quotes the first
measurement session (2026-11-04); the tables above come from the re-run
on the rebased stack (2026-11-05), whose medians are within a few
milliseconds of it.
## Stack
1. #2459: requirements and targets.
2. #2460: the performance suite.
3. **This PR**.
4. #2462: isolate thread rows inside the scroller.
## Readiness
Agent review ran. Needs a human try in the macOS app: hover replies in a
long thread and check the toolbar's glass, then scroll quickly through
the thread.
Signed-off-by: James Kraus <jamespkraus@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jameskraus
added a commit
that referenced
this pull request
Nov 5, 2026
## Summary
The thread history scroller (`.threadHistory`) is not a stacking
context, so the paint-order parent of every row layer sits outside the
scroller. In WebKit, each async scroll update then re-walks those layers
from the panel around the scroller and re-sends their positions. The UI
process's scrolling thread also hit-tests all of their layers on every
wheel event: 1.9–2.4s per fling on the full thread.
The rows wrapper now isolates them:
```css
.threadHistory > [data-thread-rows] {
isolation: isolate;
}
```
- The jump-to-latest control and the composer stay outside the new
stacking context, so the typing indicator still sits under the jump
pill. Isolating the whole section would reverse that.
- Popovers, menus and previews are in the top layer or portaled, and
every negative-z wash in a row already has an isolated ancestor.
- Geometry does not change.
## Evidence
Native macOS app, 460-reply mock thread, four ABBA runs per build in one
session:
| | Before (closed bars without glass) | After |
| --- | --- | --- |
| p95 frame interval | 50 ms [46–52] | 30 ms [29–31] |
| Frames over two 60 Hz frames | 44 [39–49] | 10 [7–13] |
| Dropped frames | 94 | 44 |
| Blank frames | 9 [9–10] | 6 [1–7] |
| Longest blank | 133 ms | 67 ms |
https://github.com/user-attachments/assets/2817e561-a515-45b2-bb39-060e3e786809
Across the stack, compared with `main`: p95 85 → 30 ms, long frames 76 →
10, dropped frames 194 → 44, blank frames 54 → 6.
- An Instruments profile of the same fling shows WebContent compositing
at 2.3s → 1.2s, the UI scrolling thread at 1.9s → 0.13s, and the UI main
thread at 1.4s → 0.2s.
- **macOS WebKit browser cases:** fling rAF p95 47 → 32 ms, frames over
33 ms 14 → 0. Reverse 49 → 29 ms, 27 → 2.
- **Chromium at 4× CPU:** task time flat.
The p95 now meets the 33 ms ceiling. Long frames and blank frames remain
above their ceilings, and the 16.7 ms target needs scroll cost that does
not grow with loaded replies (virtualization).
## Not chosen
- **`isolation` on the scroller itself:** the composer's typing
indicator would cover the jump-to-latest pill.
- **`content-visibility: auto` on replies:** half-second reveal frames
and blank panes during fast flings.
- **`contain: content` on replies:** clips the keyboard-selection and
arrival washes, reply spines in narrow panes, and focus rings.
## Explainer video
https://github.com/user-attachments/assets/3d7d0c89-cb42-4fa4-8ab7-0b1562b8aef3
Captions are burned in; narration is ASR-checked against the script.
Footage uses the mock thread only. The narration quotes the first
measurement session (2026-11-04); the tables above come from the re-run
on the rebased stack (2026-11-05), whose medians are within a few
milliseconds of it.
## Stack
1. #2459: requirements and targets.
2. #2460: the performance suite.
3. #2461: closed message action bars skip the glass.
4. **This PR**.
## Readiness
Agent review ran. Needs a human try in the macOS app: scroll a long
thread, use Jump to latest, and type in the reply box while the jump
control shows.
Signed-off-by: James Kraus <jamespkraus@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wpfleger96
added a commit
that referenced
this pull request
Nov 5, 2026
…-service * origin/main: fix(sidebar): highlight collapsed sections with unread channels (#2539) perf(threads): isolate thread rows inside the scroller (#2462) perf(messages): skip the glass on closed message action bars (#2461) perf(threads): measure long-thread scrolling in browsers and the macOS app (#2460) docs(threads): set scrolling requirements and targets for long threads (#2459) fix(composer): seed required recipients in saved briefs (#2667) fix: preserve agent activity ownership and remove Solo composer gap (#2613) feat(agents): open agent card links in Create agent (#2662) Align Bestie messaging with the shared conversation workspace (#2614) feat(messages): add a compact message density (#2547) fix(dialog): limit footer click-through to the gradient gutter (#2545) fix(channels): put the huddle launcher after the other header tools (#2660) feat(drafts-sent): bundle Drafts & sent for everyone (#2644) feat(identity): one Builderlab sign-in, the native NIP-FI session (#2576) fix(agents): let people dismiss connection setup cards (#2631) AIDA-2425: Preserve links beside pasted images (#2520) feat(activity): show reactions to your messages in Activity (#2638) # Conflicts: # docs/identity.md # src-tauri/src/federated_identity/agent/mod.rs # src-tauri/src/federated_identity/mod.rs # src-tauri/src/federated_identity/tests.rs # src/bundled/codex/runtime.ts
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.
Summary
Long threads scroll poorly in the macOS app: the pane stutters, and fast scrolls and reversals show blank regions where loaded replies belong. This adds Scrolling long threads to
docs/channels.mdunder "Viewing threads". The section states what scrolling a long thread must do, and gives advisory targets to measure it against.Requirements, for every reply the panel has loaded:
Targets, on a fixed 460-reply mock thread (the strict window's full history) in the macOS app at 60 Hz, per run of two upward trackpad flings with a reversal and Jump to latest:
The targets are advisory: the performance suite (next PR) reports them and never fails CI on them.
Stack
Readiness
Documentation only. Needs human review of the requirements and targets.