perf(scroll): cut pin-bottom forced layout/repaint cost on WebKitGTK - #860
Merged
Conversation
The bottom-pin re-assert loop was the dominant main-thread cost in busy rooms on Linux (RenderCostProbe layoutPaint 189-359ms with react as low as 2ms): every new message, typing toggle or reaction restarted a 60-frame loop of per-frame forced layouts, each write followed by a full-scroller repaint (overflowY toggle). - Convergence early-exit: the loop now stops after 8 consecutive stable frames instead of always burning its full budget. - Gated repaint: the overflow-toggle repaint only runs when the pin actually moved scrollTop (the WebKit stale-paint bug is specific to programmatic scrolls); fluux:pin-repaint=always|off overrides for on-device A/B. - Typing/reactions deferral: a toggle while a pin loop is active no longer restarts it - the running loop picks up the height change on its next frame. - [PinLoopProbe] instrumentation: one rate-limited fluux.log line per costly run (trigger, frames, writes, flush/scroll/repaint ms) so the next on-device report attributes the cost directly; all reassert call sites now carry a trigger label.
mremond
added a commit
that referenced
this pull request
Jul 15, 2026
…KitGTK half-freeze) (#1012) ## Summary Fixes the residual WebKitGTK "half-freeze" that testers still hit on 0.17.x — even though #860 (pin-loop convergence) shipped in 0.17.1. **Root cause.** The pin-to-bottom `forceRepaint()` (an `overflowY` toggle → forced reflow) is a *full scroller re-layout + repaint* on WebKitGTK, ~50–150ms each (near-free on Chromium/WKWebView-macOS). #860 made each pin run *converge*, but never coalesced the **rate** of runs: the `new-message` trigger is ungated, so every arriving message supersedes the loop and fires a fresh synchronous repaint. A **burst** of new content — live group chatter, a reaction/media storm, or a reconnect flushing queued messages — therefore fires one forced WebKitGTK repaint per arrival, saturating the main thread for up to ~1.5s. This matches the field reports exactly: the freeze only ever reproduces while **new content arrives** (messages, reactions, pictures), never on scrolling or room-switching, and correlates with flaky/Tor connections (a reconnect delivers a burst). **Why it stayed unconfirmed.** `[PinLoopProbe]` is per-run with a 50ms threshold + 5s cooldown, so a burst of ten 60ms repaints logs *one* line and hides the other ~540ms — structurally blind to bursts. ## Fix Generalizes the existing MAM-catchup repaint suppression to **live bursts** (new pure module `pinRepaintBurst.ts`): while content-arrival pins keep firing within `PIN_BURST_WINDOW_MS` (200ms), the intermediate `forceRepaint`s are suppressed — the scroll position is still written via `scrollToIndex`, so the layout stays correct; only the paint is deferred — and the pin loop's convergence forces exactly **one** trailing repaint. A burst of N repaints collapses to ~1–2. The first arrival of a burst still paints immediately, so single sends stay snappy. Burst state is a hook-level ref so the debt survives loop supersede and is flushed by whichever run finally converges; reset on user-scroll takeover and conversation switch. Also adds a burst-aware `[PinBurstProbe] burst settled: … suppressedRepaints=N` log line so an on-device log confirms the mechanism was real and the coalescing engaged. ## Verification - TDD red/green: `MessageList.pinBottomRepaint.test.tsx` → a burst of 8 messages produces 8 forced repaints without the fix, <8 with it; plus 8 unit tests for the tracker. - All 50 `test:scroll` WebKit + Chromium scroll invariants pass (send-stick, at-bottom-stick, reaction-stick, media-drift all intact). - 653 conversation unit tests, full typecheck (SDK + app), and lint all green. WebKitGTK repaint cost is not reproducible on macOS/Chromium, so on-device confirmation from the reporter (via the new `[PinBurstProbe]` line) is the final check.
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.
Fixes the remaining Linux/WebKitGTK freezes in busy rooms: the bottom-pin loop forced a full layout every frame for 60 frames and a full-scroller repaint (overflow toggle) on every trigger — new message, typing toggle, reaction — multiplying WebKitGTK's expensive layout/paint (RenderCostProbe layoutPaint 189–359ms).
fluux:pin-repaint=always|offto A/B on-device).[PinLoopProbe]log line attributes each costly pin run (trigger, frames, flush/scroll/repaint ms) in fluux.log.