mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fixes #3677. ## Problem The renderer never quiesces: recurring timers, query polling, and re-render tickers run at full rate whether the window is visible, hidden, or minimized. Measured on a live installed app: **27.5% mean renderer CPU visible vs 28.7% hidden** (60×1s `ps` samples of the WebContent process; `sample(1)` dominated by `WebCore::timerFired`/ThreadTimers, microtask checkpoints, JSON parsing, style matching). Matches all three reproductions in #3677 (macOS prerelease, Linux/WebKitGTK A/B/A minimize test, stable macOS). Per-timer instrumentation (dev build, wrapped `setInterval`/`setTimeout`/rAF) attributed the recurring work: `useNow` 60 fires/min, 40 active TanStack refetch intervals, agent-turn pruning 12/min, auto-restart ticks, huddle/reminder polls — none visibility-gated. ## Fix (two-tier gating, standard mechanisms only) Two separate signals in `desktop/src/shared/lib/useDocumentVisible.ts`, because they mean different things and (see residuals) are delivered differently on macOS: - **`useDocumentVisible`** — true Page Visibility only (`document.visibilityState`). Gates local UI work that must keep running on a visible-but-unfocused window: `useNow` relative clocks, agent-turn pruning, huddle bar state/model-status polling, auto-restart tick. Hidden ⇒ paused; `useNow` snaps to fresh `Date.now()` on return. - **`useAppFocused`** — visible AND `document.hasFocus()`. Gates network refetch polling only (`useFocusedRefetchInterval`, ~15 query families: forum/home/agents/channels/templates/emoji/user-status/projects/workflows/persona-catalog/pulse/presence-list). TanStack's `focusManager` is wired to this signal (idempotent, single install) with `refetchOnWindowFocus: true`, so stale queries refresh promptly on return. Deliberate side effect, documented in code: query retries pause on blur; mutations and the presence heartbeat (`retry: 0`) are unaffected. - **Never gated:** reminder due-notification poll (fires while hidden/unfocused — extracted to `reminderNotificationPoll.ts` with regression test), huddle pipeline hot-start (`check_pipeline_hotstart` survives backgrounding for the duration of a huddle), relay stall watchdog, presence heartbeat. Live WebSocket delivery untouched throughout. - Huddle model-status indicator now clears only on huddle phase end, not on visibility/focus changes. ## Validation - Instrumented dev build, populated channel, fires/min: **visible+focused** unchanged (`useNow 60 / prune 12 / watchdog 6 / query 4 / auto-restart 4 / low-rate huddle/reminder/presence`); **visible+blurred**: query polls 0, UI clocks continue (`useNow 60 / prune 12`), reminders 2, presence live; **truly hidden**: only watchdog 6, reminders 2, presence ~2 — everything else 0. Return restored visible+focused, selection preserved, queries refreshed. - Hide-vs-blur decomposition (instrumented probe instance, AppleScript-driven): on macOS WKWebView, Cmd-H / minimize / full occlusion did **not** reliably produce `visibilityState === "hidden"` — they reliably produced focus loss. The CPU-dominant quiescence path on macOS is therefore the focus gate; the visibility gate is exercised fully on platforms that report hidden (e.g. WebKitGTK minimize per the Linux repro). - Gate-regression tests: signal separation, `useNow` hidden-pause + fresh-snap on return, focus-gated interval pause/resume-with-refresh, reminder delivery while hidden+unfocused (5 new, plus primitive wiring tests). - Push gate: desktop check, typecheck, full desktop suite **4549/4549** at `1237548d1`. ## Known residuals - **macOS hidden-signal limitation:** because WKWebView rarely reports `hidden` on app-hide/minimize, hidden-only consumers (`useNow`, prune, huddle UI polls) may keep ticking on macOS when the app is hidden. These are cheap local timers; the expensive network polling still quiesces via focus loss, which is what the measured 28% CPU was attributed to. If the residual local-timer cost proves measurable, the follow-up is bridging Tauri window hidden/minimized events into the visibility signal. - End-to-end CPU confirmation on a packaged build is the post-merge follow-up (against the 28% idle baseline). - Visible-state costs (skeleton animation pileups on stuck loading views, per-poll JSON payload churn) are intentionally out of scope — separate follow-up issue. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz>
39 lines
1004 B
TypeScript
39 lines
1004 B
TypeScript
import { focusManager, QueryClient } from "@tanstack/react-query";
|
|
|
|
import {
|
|
isAppFocused,
|
|
subscribeAppFocus,
|
|
} from "@/shared/lib/useDocumentVisible";
|
|
|
|
let focusManagerConfigured = false;
|
|
|
|
function configureQueryFocusManager() {
|
|
if (focusManagerConfigured) return;
|
|
focusManagerConfigured = true;
|
|
|
|
// Treat app blur as unfocused so query retries pause and stale queries with
|
|
// refetchOnWindowFocus refresh on return. Mutations are unaffected by this
|
|
// focus gate; presence heartbeats also explicitly use retry: 0.
|
|
focusManager.setEventListener((setFocused) => {
|
|
setFocused(isAppFocused());
|
|
return subscribeAppFocus(setFocused);
|
|
});
|
|
}
|
|
|
|
export function createBuzzQueryClient() {
|
|
configureQueryFocusManager();
|
|
return new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: 1,
|
|
refetchOnWindowFocus: false,
|
|
networkMode: "always",
|
|
gcTime: 5 * 60 * 1_000,
|
|
},
|
|
mutations: {
|
|
networkMode: "always",
|
|
},
|
|
},
|
|
});
|
|
}
|