Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions desktop/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Toaster } from "@/shared/ui/sonner";
import { TooltipProvider } from "@/shared/ui/tooltip";
import { recoverLocalStorageQuotaOnStartup } from "@/shared/lib/localStorageQuota";
import { startLocalStorageSweep } from "@/shared/lib/localStorageSweep";
import { startUserTimingSweep } from "@/shared/lib/userTimingSweep";
import { initializeConversationDensityPreference } from "@/shared/lib/conversationDensityPreference";
import { initializeFontSizePreference } from "@/shared/lib/fontSizePreference";

Expand Down Expand Up @@ -131,6 +132,7 @@ async function bootstrap() {
initializeConversationDensityPreference();
initializeFontSizePreference();
startLocalStorageSweep();
startUserTimingSweep({ enabled: import.meta.env.DEV });
await installE2eBridgeIfConfigured();
await migrateLegacyCommunityStorageBeforeRender();
renderApp();
Expand Down
62 changes: 62 additions & 0 deletions desktop/src/shared/lib/userTimingSweep.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
USER_TIMING_SWEEP_INTERVAL_MS,
startUserTimingSweep,
} from "./userTimingSweep.ts";

function makeTimers() {
const intervals = [];
return {
intervals,
setInterval: (callback, ms) => {
intervals.push({ callback, ms, cleared: false });
return intervals.length;
},
clearInterval: (id) => {
intervals[id - 1].cleared = true;
},
};
}

test("clears measures on every tick until stopped", () => {
const timers = makeTimers();
let cleared = 0;
const stop = startUserTimingSweep({
enabled: true,
performance: {
clearMeasures: () => {
cleared += 1;
},
},
setInterval: timers.setInterval,
clearInterval: timers.clearInterval,
});

assert.equal(timers.intervals.length, 1);
assert.equal(timers.intervals[0].ms, USER_TIMING_SWEEP_INTERVAL_MS);
timers.intervals[0].callback();
timers.intervals[0].callback();
assert.equal(cleared, 2);

stop();
assert.equal(timers.intervals[0].cleared, true);
});

test("is a no-op when disabled or when clearMeasures is unavailable", () => {
const timers = makeTimers();
startUserTimingSweep({
enabled: false,
performance: { clearMeasures: () => {} },
setInterval: timers.setInterval,
clearInterval: timers.clearInterval,
});
startUserTimingSweep({
enabled: true,
performance: {},
setInterval: timers.setInterval,
clearInterval: timers.clearInterval,
});
assert.equal(timers.intervals.length, 0);
});
43 changes: 43 additions & 0 deletions desktop/src/shared/lib/userTimingSweep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Periodic sweep of the User Timing measure buffer for development builds.
*
* React's development bundle records a `performance.measure()` entry, with a
* serialized `detail` object, for every component render (its "Components ⚛"
* DevTools track). WebKit keeps user-timing entries for the life of the page,
* outside the JavaScript heap, so an idle `tauri dev` session grows by tens of
* MB per minute until the web process is OOM-killed (about 186k entries after
* 17 minutes with one channel open). The production bundle never emits them.
*
* Nothing in Buzz reads a measure later: the sidebar boot measure in
* features/channels/hooks.ts reads its own entry synchronously. Marks are left
* alone; React does not accumulate those.
*/

export const USER_TIMING_SWEEP_INTERVAL_MS = 10_000;

type UserTimingSweepOptions = {
enabled: boolean;
intervalMs?: number;
performance?: Pick<Performance, "clearMeasures">;
setInterval?: (callback: () => void, ms: number) => number;
clearInterval?: (id: number) => void;
};

/** Starts the sweep when enabled; returns a function that stops it. */
export function startUserTimingSweep({
enabled,
intervalMs = USER_TIMING_SWEEP_INTERVAL_MS,
performance = globalThis.performance,
setInterval = globalThis.setInterval,
clearInterval = globalThis.clearInterval,
}: UserTimingSweepOptions): () => void {
if (!enabled || typeof performance?.clearMeasures !== "function") {
return () => {};
}
const timer = setInterval(() => {
performance.clearMeasures();
}, intervalMs);
return () => {
clearInterval(timer);
};
}
Loading