perf(desktop): instant channel switching — non-blocking first paint, persisted snapshots#1452
Merged
Conversation
Cold channel opens on reply-heavy channels awaited up to 3 extra serial 200-event relay pages (pageOlderMessagesUntilRowFloor) before the skeleton dropped — up to 4 serialized round trips before first paint. Commit the first 60-event window immediately and run the top-up in the background, same pattern as the aux backfill (it self-merges into the cache key). Guard against the now-possible overlap between the background top-up and a scroll-up fetch with a per-channel in-flight pass map: concurrent callers share the running pass instead of issuing duplicate REQs for the same `until` window. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…shots Channels revisited after gcTime expiry or an app restart went fully cold: skeleton up for a relay round trip even though the timeline was known. Persist the newest 80-event slice per channel to localStorage (same stale-then-revalidate pattern as the sidebar's channelSnapshot, keyed per relay + channel, LRU-capped at 20 channels per relay) and hand it to placeholderData so a cold revisit paints immediately while the history fetch revalidates behind it. selectTimelineLoadingState gains one narrow pre-settle rule: placeholder rows (cache or snapshot) drop the skeleton; live-sub-seeded partial data still holds it, so the intro-flash and older-fetch-spinner traps stay covered. gcTime raised 5min -> 60min so in-hour switches stay warm in-memory. Snapshots are GC'd on workspace removal, skip pending optimistic events, and skip identical rewrites. Cold snapshot loads widen the `#e` aux backfill to the snapshot-merged timeline (mergeHistoryOverSnapshot), not just the fresh fetch window. A snapshot row deleted or edited while the app was closed never reappears in a history fetch (the relay soft-deletes), so its tombstone/edit is only reachable by `#e` over that row's id; backfilling only the fresh window would resurrect the ghost and the post-settle snapshot rewrite would persist it forever. Warm refetches keep the narrow fresh-window backfill. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
The scroll-history cold-load test asserted the OLD contract: skeleton held while the paced row-floor top-up ran. The top-up is now backgrounded, so the test asserts the new one instead — first rows paint well inside the paced top-up window (proving the queryFn no longer awaits it) and the skeleton is gone once they do. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
da714b9 to
66ce8a6
Compare
wpfleger96
added a commit
that referenced
this pull request
Jul 2, 2026
readiness.rs grew by 1 line (cargo fmt reformatted two long closures inline after rebase). tauri.ts grew by 26 lines from PRs that landed on main (#1452, #1416, #1449 and others) between our prior base and this rebase tip (e42dae3). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
added a commit
that referenced
this pull request
Jul 2, 2026
…into HEAD * origin/paul/nip-am-agent-turn-metrics: fix(profile): consolidate agent profile runtime metadata (#1451) fix(desktop): simplify workspace rail badges (#1462) perf(desktop): instant channel switching — non-blocking first paint, persisted snapshots (#1452) perf(relay): bounded-concurrency multi-filter query execution (S2) (#1457) fix(desktop): classify timeline prepends so history loads don't bump unread (#1416) fix(desktop): quiet gate for workspace switches instead of boot splash (#1449) fix(read-path): reach complete threads, dense-second timelines, and all people in the GUI (#1418) E1+E3: reduce relay ingest/fan-out DB round trips; ack p99 −7–16%, fd p99 −6–28%, p999 tails −29–53% vs PR #1453 tip (#1454) perf(relay): defer post-commit dispatch and avoid verify clone (#1453) fix(relay): include git hook tools in runtime image (#1326) feat(chart): per-pod emptyDir git scratch when persistence disabled (multi-replica HA) (#1450) fix(relay): remove media bearer-token auth (#1444) fix(desktop): stop search shortcut from hijacking the sidebar (#1447) Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
added a commit
that referenced
this pull request
Jul 2, 2026
readiness.rs grew by 1 line (cargo fmt reformatted two long closures inline after rebase). tauri.ts grew by 26 lines from PRs that landed on main (#1452, #1416, #1449 and others) between our prior base and this rebase tip (e42dae3). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
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.
What
Client-side channel-switching performance bundle — the goal is Slack/Discord-tier switching: never show a skeleton for a channel you've seen before, and make first-ever opens paint in one round trip.
Three commits, reviewable independently:
1. Don't block first paint on the row-floor top-up
The cold-load queryFn awaited up to 3 extra serial 200-event relay pages (
pageOlderMessagesUntilRowFloor) whenever the first 60-event window rendered fewer than 30 top-level rows — which reply-heavy channels almost always trip. That made a cold open up to 4 serialized relay round trips before the skeleton dropped. Now the first window commits immediately and the top-up runs in the background (same self-merging pattern as the aux backfill), guarded by a per-channel in-flight map so a scroll-up fetch shares the running pass instead of duplicating REQs.2. Persisted per-channel message snapshots + longer gcTime
Channels revisited after
gcTimeexpiry or an app restart went fully cold. Now the newest 80-event slice per channel is persisted to localStorage (keyed per relay + channel, LRU-capped at 20 channels/relay, GC'd on workspace removal, pending optimistic events excluded) and fed toplaceholderData— a cold revisit paints instantly and revalidates behind the paint.selectTimelineLoadingStategains one narrow pre-settle rule: placeholder rows drop the skeleton; live-sub-seeded partial data still holds it, so the intro-flash and older-fetch-spinner traps stay covered.gcTimeraised 5→60 min.Ghost-resurrection guard: a snapshot row deleted or edited while the app was closed never reappears in a history fetch (the relay soft-deletes), so its tombstone/edit is only reachable by
#eover that row's id. Cold snapshot loads therefore widen the aux backfill to the snapshot-merged timeline (mergeHistoryOverSnapshot), not just the fresh fetch window — otherwise the deleted message would repaint from the snapshot and the post-settle snapshot rewrite would persist it forever. Warm refetches keep the narrow fresh-window backfill.3. e2e contract update
The scroll-history cold-load test asserted the old contract (skeleton held while the paced top-up ran); it now asserts the new one (rows paint well inside the paced top-up window, skeleton gone once they do).
Dropped from this PR
An earlier revision included sidebar hover prefetch. Cut after review: with persisted snapshots, every previously-visited channel already paints instantly, so hover prefetch only helps the first-ever visit — and it paid for that with an uncancellable fetch+top-up pipeline per hovered row and unbounded cross-channel fan-out. If first-visit latency matters later, it should return as its own PR with a hover-intent delay.
Result
Testing
pnpm typecheck,pnpm check,pnpm test— 1491 unit tests (16 new: messageSnapshot round-trip/eviction/bounds, loading-state placeholder rules, merged-timeline aux-backfill window).scroll-history.spec.ts) run locally — 12 passed; the 2 failures (preserves user scroll…,does not teleport upward…) reproduce identically at the unmodified merge-base (c88799a), so they're pre-existing, not from this branch.Follow-ups (separate PRs, discussed in #faster-channel-switching-and-loading)
#ebackfill queries (relay migration).get_channelsround-trip diet.