Skip to content

Commit e8fb7d7

Browse files
fix: oom from timers when app is backgrounded
1 parent 8cf164d commit e8fb7d7

2 files changed

Lines changed: 128 additions & 16 deletions

File tree

src/api/useDebounceIfVisible.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { getDefaultStore } from "jotai";
2+
import { useCallback, useMemo, useRef } from "react";
3+
import { useUnmount } from "react-use";
4+
import { useThrottledCallback, useDebouncedCallback } from "use-debounce";
5+
import { isDocumentVisibleAtom } from "../atoms";
6+
7+
const store = getDefaultStore();
8+
9+
/**
10+
* Only use the throttled callback while visible.
11+
* Otherwise, clear the timers and use requestAnimationFrame to call
12+
* the callback on the next frame (rAF is paused while backgrounded, so no timers
13+
* accumulate).
14+
*/
15+
export function useThrottledCallbackIfVisible<
16+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17+
T extends (...args: any) => ReturnType<T>,
18+
>(...args: Parameters<typeof useThrottledCallback<T>>) {
19+
const throttledFn = useThrottledCallback(...args);
20+
const rafRef = useRef<number | null>(null);
21+
22+
const argFunc = args[0];
23+
const funcRef = useRef(argFunc);
24+
funcRef.current = argFunc;
25+
26+
useUnmount(() => {
27+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
28+
});
29+
30+
const fn = useCallback(
31+
(...args: Parameters<T>) => {
32+
if (store.get(isDocumentVisibleAtom)) {
33+
throttledFn(...args);
34+
return;
35+
}
36+
37+
// hidden, don't accumulate timers from throttledFn
38+
throttledFn.cancel();
39+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
40+
rafRef.current = requestAnimationFrame(() => {
41+
return funcRef.current(...args);
42+
});
43+
},
44+
[throttledFn],
45+
);
46+
47+
const cancel = useCallback(() => {
48+
throttledFn.cancel();
49+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
50+
rafRef.current = null;
51+
}, [throttledFn]);
52+
53+
return useMemo(() => Object.assign(fn, { cancel }), [fn, cancel]);
54+
}
55+
56+
/**
57+
* Only use the debounced callback while visible.
58+
* Otherwise, clear the timers and use requestAnimationFrame to call
59+
* the callback on the next frame (rAF is paused while backgrounded, so no timers
60+
* accumulate).
61+
*/
62+
export function useDebouncedCallbackIfVisible<
63+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
64+
T extends (...args: any) => ReturnType<T>,
65+
>(...args: Parameters<typeof useDebouncedCallback<T>>) {
66+
const debouncedFn = useDebouncedCallback(...args);
67+
const rafRef = useRef<number | null>(null);
68+
69+
const argFunc = args[0];
70+
const funcRef = useRef(argFunc);
71+
funcRef.current = argFunc;
72+
73+
useUnmount(() => {
74+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
75+
});
76+
77+
const fn = useCallback(
78+
(...args: Parameters<T>) => {
79+
if (store.get(isDocumentVisibleAtom)) {
80+
debouncedFn(...args);
81+
return;
82+
}
83+
84+
// hidden, don't accumulate timers from debouncedFn
85+
debouncedFn.cancel();
86+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
87+
rafRef.current = requestAnimationFrame(() => {
88+
return funcRef.current(...args);
89+
});
90+
},
91+
[debouncedFn],
92+
);
93+
94+
const cancel = useCallback(() => {
95+
debouncedFn.cancel();
96+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
97+
rafRef.current = null;
98+
}, [debouncedFn]);
99+
100+
return useMemo(() => Object.assign(fn, { cancel }), [fn, cancel]);
101+
}

src/api/useSetAtomWsData.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import type {
1111
import { isEmaObjectKey } from "./worker/types";
1212
import { DateTime } from "luxon";
1313
import { useInterval } from "react-use";
14-
import { useThrottledCallback, useDebouncedCallback } from "use-debounce";
14+
import {
15+
useThrottledCallbackIfVisible,
16+
useDebouncedCallbackIfVisible,
17+
} from "./useDebounceIfVisible";
1518
import type z from "zod";
1619
import {
1720
skipRateAtom,
@@ -241,31 +244,37 @@ function useUpdateAtoms() {
241244
useInterval(updateSlotDurationDbMs, 1_000);
242245

243246
const setEstimatedSlotDuration = useSetAtom(estimatedSlotDurationAtom);
244-
const setDbEstimatedSlotDuration = useThrottledCallback(
247+
const setDbEstimatedSlotDuration = useThrottledCallbackIfVisible(
245248
(value?: EstimatedSlotDuration) => setEstimatedSlotDuration(value),
246249
slotDurationDbMs,
247250
);
248251

249252
const setEstimatedTps = useSetAtom(estimatedTpsAtom);
250-
const setDbEstimatedTps = useThrottledCallback((value?: EstimatedTps) => {
251-
setEstimatedTps(value);
252-
}, estimatedTpsDebounceMs);
253+
const setDbEstimatedTps = useThrottledCallbackIfVisible(
254+
(value?: EstimatedTps) => {
255+
setEstimatedTps(value);
256+
},
257+
estimatedTpsDebounceMs,
258+
);
253259

254260
const setLiveNetworkMetrics = useSetAtom(liveNetworkMetricsAtom);
255-
const setDbLiveNetworkMetrics = useThrottledCallback(
261+
const setDbLiveNetworkMetrics = useThrottledCallbackIfVisible(
256262
(value?: LiveNetworkMetrics) => {
257263
setLiveNetworkMetrics(value);
258264
},
259265
liveNetworkMetricsDebounceMs,
260266
);
261267

262268
const setLiveTileMetrics = useSetAtom(liveTileMetricsAtom);
263-
const setDbLiveTileMetrics = useThrottledCallback((value?: TileMetrics) => {
264-
setLiveTileMetrics(value);
265-
}, liveTileMetricsDebounceMs);
269+
const setDbLiveTileMetrics = useThrottledCallbackIfVisible(
270+
(value?: TileMetrics) => {
271+
setLiveTileMetrics(value);
272+
},
273+
liveTileMetricsDebounceMs,
274+
);
266275

267276
const setLivePrimaryMetrics = useSetAtom(liveTilePrimaryMetricAtom);
268-
const setDbLivePrimaryMetrics = useThrottledCallback(
277+
const setDbLivePrimaryMetrics = useThrottledCallbackIfVisible(
269278
(value?: LiveTilePrimaryMetric) => {
270279
setLivePrimaryMetrics(value);
271280
},
@@ -274,7 +283,7 @@ function useUpdateAtoms() {
274283

275284
const setRateLiveTxnWaterfall = useSetAtom(rateLiveWaterfallAtom);
276285
const setLiveTxnWaterfall = useSetAtom(liveTxnWaterfallAtom);
277-
const setDbLiveTxnWaterfall = useThrottledCallback(
286+
const setDbLiveTxnWaterfall = useThrottledCallbackIfVisible(
278287
(value?: LiveTxnWaterfall) => {
279288
setLiveTxnWaterfall(value);
280289
setRateLiveTxnWaterfall(value?.waterfall);
@@ -283,7 +292,7 @@ function useUpdateAtoms() {
283292
);
284293

285294
const setTileTimer = useSetAtom(tileTimerAtom);
286-
const setDbTileTimer = useThrottledCallback((value?: number[]) => {
295+
const setDbTileTimer = useThrottledCallbackIfVisible((value?: number[]) => {
287296
setTileTimer(value);
288297
}, tileTimerDebounceMs);
289298

@@ -307,15 +316,15 @@ function useUpdateAtoms() {
307316
const setSlotStatus = useSetAtom(setSlotStatusAtom);
308317

309318
const setGossipNetworkStats = useSetAtom(gossipNetworkStatsAtom);
310-
const setDbGossipNetworkStats = useThrottledCallback(
319+
const setDbGossipNetworkStats = useThrottledCallbackIfVisible(
311320
(value?: GossipNetworkStats) => {
312321
setGossipNetworkStats(value);
313322
},
314323
gossipNetworkDebounceMs,
315324
);
316325

317326
const setGossipPeersSize = useSetAtom(gossipPeersSizeAtom);
318-
const setDbGossipPeersSize = useThrottledCallback(
327+
const setDbGossipPeersSize = useThrottledCallbackIfVisible(
319328
(value?: GossipPeersSize) => {
320329
setGossipPeersSize(value);
321330
},
@@ -427,7 +436,7 @@ function useUpdateAtoms() {
427436
const peersBuffer = useRef(new Map<string, Peer>());
428437
const removePeersBuffer = useRef(new Map<string, PeerRemove>());
429438

430-
const dbFlushBuffer = useDebouncedCallback(
439+
const dbFlushBuffer = useDebouncedCallbackIfVisible(
431440
() => {
432441
updatePeers([...peersBuffer.current.values()]);
433442
removePeers([...removePeersBuffer.current.values()]);
@@ -459,6 +468,7 @@ function useUpdateAtoms() {
459468
}
460469
}
461470

471+
// only triggers when document is visible
462472
dbFlushBuffer();
463473
},
464474
[dbFlushBuffer],
@@ -472,7 +482,7 @@ function useUpdateAtoms() {
472482
toRemove: new Set<string>(),
473483
});
474484

475-
const dbFlushSupermajorityPeersBuffers = useDebouncedCallback(
485+
const dbFlushSupermajorityPeersBuffers = useDebouncedCallbackIfVisible(
476486
() => {
477487
updateSupermajorityOnlinePeers(
478488
[...supermajorityPeersBuffers.current.toAdd],
@@ -499,6 +509,7 @@ function useUpdateAtoms() {
499509
}
500510
}
501511

512+
// only triggers when document is visible
502513
dbFlushSupermajorityPeersBuffers();
503514
},
504515
[dbFlushSupermajorityPeersBuffers],

0 commit comments

Comments
 (0)