mirror of
https://github.com/block/buzz.git
synced 2026-11-05 00:41:28 +00:00
fix(desktop): share one timer across same-interval useNow consumers (#5861)
## Summary Every `useNow(1000)` consumer owned its own `setInterval`. With dozens of "agent working" surfaces mounted (sidebar channel badges, tray menu, agent session panels, managed-agent rows), each ticked on its own unaligned 1 s timer — a render/composite pass per consumer per second. On a machine running ~23 agent sessions this pinned a sustained **~25% of a core** in `com.apple.WebKit.WebContent` while the app sat idle. This PR makes same-interval `useNow` consumers share one timer: all of them tick in a single `setInterval` callback, so React batches the state updates into one render pass. The last unsubscriber tears the timer down; the visibility gate (pause while hidden, snap fresh on return) is unchanged. Attribution receipts (live dev build, 23 acp sessions): the shimmer was the original suspect from `sample` stacks, but probing `animation: none` left CPU flat (~25%), while clamping `useNow` intervals dropped it immediately. Repeated A/B with this exact change: **~25% → ~3–9%** webview CPU under the same agent load (ambient variance from live agent activity; the delta reproduced across three alternations). ### Related issue None found — follow-up to the presence-firehose investigation (#5830 fixed the subscription side; this is the remaining local render cost). ### Testing - `pnpm test` — 4792/4792 pass, including a new test asserting N same-interval consumers create exactly one timer and the last unmount releases it - `pnpm typecheck`, `biome check` — clean - Live-local per TESTING.md: hot-patched into a running dev desktop with 23 active acp sessions; webview CPU dropped from ~25% sustained to ~3–9% (A/B/A alternation, `ps` sampling over 30 s windows) Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -197,6 +197,60 @@ describe("visibility-gated hooks", () => {
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
it("useNow consumers with the same interval share one timer", async () => {
|
||||
mock.timers.enable({ apis: ["Date", "setInterval"], now: 1_000 });
|
||||
const dom = new JSDOM(
|
||||
"<!doctype html><html><body><div id='root'></div></body></html>",
|
||||
);
|
||||
Object.defineProperty(dom.window.document, "visibilityState", {
|
||||
configurable: true,
|
||||
value: "visible",
|
||||
});
|
||||
Object.assign(globalThis, {
|
||||
document: dom.window.document,
|
||||
HTMLElement: dom.window.HTMLElement,
|
||||
IS_REACT_ACT_ENVIRONMENT: true,
|
||||
window: dom.window,
|
||||
});
|
||||
const setIntervalSpy = mock.method(globalThis, "setInterval");
|
||||
const observed = [];
|
||||
function Clock({ id }) {
|
||||
const now = useNow(1_000);
|
||||
React.useEffect(() => {
|
||||
observed.push([id, now]);
|
||||
}, [id, now]);
|
||||
return null;
|
||||
}
|
||||
const root = createRoot(document.getElementById("root"));
|
||||
|
||||
await act(async () =>
|
||||
root.render(
|
||||
React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(Clock, { id: "a" }),
|
||||
React.createElement(Clock, { id: "b" }),
|
||||
React.createElement(Clock, { id: "c" }),
|
||||
),
|
||||
),
|
||||
);
|
||||
assert.equal(setIntervalSpy.mock.callCount(), 1);
|
||||
|
||||
await act(async () => mock.timers.tick(1_000));
|
||||
assert.deepEqual(observed.filter(([, now]) => now === 2_000).length, 3);
|
||||
|
||||
// Last unmount releases the shared timer; a fresh mount recreates it.
|
||||
await act(async () => root.unmount());
|
||||
const secondRoot = createRoot(document.getElementById("root"));
|
||||
await act(async () =>
|
||||
secondRoot.render(React.createElement(Clock, { id: "d" })),
|
||||
);
|
||||
assert.equal(setIntervalSpy.mock.callCount(), 2);
|
||||
|
||||
await act(async () => secondRoot.unmount());
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
it("focused polling pauses on blur and resumes after activation yields", async () => {
|
||||
const dom = new JSDOM(
|
||||
"<!doctype html><html><body><div id='root'></div></body></html>",
|
||||
|
||||
@@ -2,10 +2,50 @@ import * as React from "react";
|
||||
|
||||
import { useDocumentVisible } from "@/shared/lib/useDocumentVisible";
|
||||
|
||||
type SharedTicker = {
|
||||
listeners: Set<() => void>;
|
||||
intervalId: ReturnType<typeof setInterval>;
|
||||
};
|
||||
|
||||
const tickers = new Map<number, SharedTicker>();
|
||||
|
||||
/**
|
||||
* One `setInterval` per distinct interval, shared by every subscriber. All
|
||||
* same-cadence consumers tick in a single timer callback, so React batches
|
||||
* their state updates into one render/composite pass instead of N unaligned
|
||||
* passes per interval. With dozens of "working" badges mounted, per-consumer
|
||||
* timers pinned a sustained ~25% of a core in the WebKit process.
|
||||
*/
|
||||
function subscribeToSharedTick(
|
||||
intervalMs: number,
|
||||
listener: () => void,
|
||||
): () => void {
|
||||
let ticker = tickers.get(intervalMs);
|
||||
if (!ticker) {
|
||||
const created: SharedTicker = {
|
||||
listeners: new Set(),
|
||||
intervalId: setInterval(() => {
|
||||
for (const tick of created.listeners) tick();
|
||||
}, intervalMs),
|
||||
};
|
||||
tickers.set(intervalMs, created);
|
||||
ticker = created;
|
||||
}
|
||||
ticker.listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
ticker.listeners.delete(listener);
|
||||
if (ticker.listeners.size === 0) {
|
||||
clearInterval(ticker.intervalId);
|
||||
tickers.delete(intervalMs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `Date.now()`, re-rendering the calling component every `intervalMs`.
|
||||
* Each consumer owns one `setInterval` cleaned up on unmount — mount the hook
|
||||
* only where a live clock is actually displayed so idle components never tick.
|
||||
* Consumers with the same interval share one timer — mount the hook only where
|
||||
* a live clock is actually displayed so idle components never tick.
|
||||
*/
|
||||
export function useNow(intervalMs: number): number {
|
||||
const [now, setNow] = React.useState(() => Date.now());
|
||||
@@ -15,8 +55,7 @@ export function useNow(intervalMs: number): number {
|
||||
if (!documentVisible) return;
|
||||
|
||||
setNow(Date.now());
|
||||
const id = setInterval(() => setNow(Date.now()), intervalMs);
|
||||
return () => clearInterval(id);
|
||||
return subscribeToSharedTick(intervalMs, () => setNow(Date.now()));
|
||||
}, [documentVisible, intervalMs]);
|
||||
|
||||
return now;
|
||||
|
||||
Reference in New Issue
Block a user