Skip to content

Commit fe281c5

Browse files
fix(desktop): throttle hidden preview rendering (#7445)
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: maria <[email protected]>
1 parent bcfd485 commit fe281c5

4 files changed

Lines changed: 327 additions & 30 deletions

File tree

‎apps/desktop/src/preview/Manager.test.ts‎

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,6 +1797,216 @@ describe("PreviewManager", () => {
17971797
),
17981798
);
17991799

1800+
effectIt.effect("keeps window unthrottled until the final frame capture stops", () =>
1801+
withManager((manager) =>
1802+
Effect.gen(function* () {
1803+
const setBackgroundThrottling = vi.fn();
1804+
const capturePage = vi.fn(async () => ({
1805+
toJPEG: () => Buffer.from("recording-frame"),
1806+
getSize: () => ({ width: 1280, height: 720 }),
1807+
}));
1808+
const webContentsById = new Map([
1809+
[41, makeTestPreviewWebContents(capturePage, 41)],
1810+
[42, makeTestPreviewWebContents(capturePage, 42)],
1811+
]);
1812+
fromId.mockImplementation((id) =>
1813+
id === undefined ? null : (webContentsById.get(id) ?? null),
1814+
);
1815+
1816+
yield* manager.createTab("tab_capture_throttling_1");
1817+
yield* manager.createTab("tab_capture_throttling_2");
1818+
yield* manager.registerWebview("tab_capture_throttling_1", 41);
1819+
yield* manager.registerWebview("tab_capture_throttling_2", 42);
1820+
yield* manager.setMainWindow({
1821+
isDestroyed: () => false,
1822+
once: vi.fn(),
1823+
webContents: { setBackgroundThrottling },
1824+
} as never);
1825+
1826+
yield* manager.startRecording("tab_capture_throttling_1");
1827+
yield* manager.startRecording("tab_capture_throttling_2");
1828+
expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
1829+
1830+
yield* manager.stopRecording("tab_capture_throttling_1");
1831+
expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
1832+
1833+
yield* manager.stopRecording("tab_capture_throttling_2");
1834+
expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true]]);
1835+
}),
1836+
),
1837+
);
1838+
1839+
effectIt.effect("does not commit failed starts and retries throttle restoration", () =>
1840+
withManager((manager) =>
1841+
Effect.gen(function* () {
1842+
const setBackgroundThrottling = vi.fn<(enabled: boolean) => void>();
1843+
const capturePage = vi.fn(async () => ({
1844+
toJPEG: () => Buffer.from("recording-frame"),
1845+
getSize: () => ({ width: 1280, height: 720 }),
1846+
}));
1847+
fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
1848+
1849+
yield* manager.createTab("tab_capture_throttling_failure");
1850+
yield* manager.registerWebview("tab_capture_throttling_failure", 42);
1851+
yield* manager.setMainWindow({
1852+
isDestroyed: () => false,
1853+
once: vi.fn(),
1854+
webContents: { setBackgroundThrottling },
1855+
} as never);
1856+
1857+
setBackgroundThrottling.mockImplementationOnce(() => {
1858+
throw new Error("start throttling update failed");
1859+
});
1860+
const failedStart = yield* Effect.exit(
1861+
manager.startRecording("tab_capture_throttling_failure"),
1862+
);
1863+
expect(Exit.isFailure(failedStart)).toBe(true);
1864+
1865+
yield* manager.startRecording("tab_capture_throttling_failure");
1866+
expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false]]);
1867+
1868+
setBackgroundThrottling.mockImplementationOnce(() => {
1869+
throw new Error("stop throttling update failed");
1870+
});
1871+
yield* manager.stopRecording("tab_capture_throttling_failure");
1872+
expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false], [true], [true]]);
1873+
1874+
yield* manager.startRecording("tab_capture_throttling_failure");
1875+
yield* manager.stopRecording("tab_capture_throttling_failure");
1876+
expect(setBackgroundThrottling.mock.calls).toEqual([
1877+
[false],
1878+
[false],
1879+
[true],
1880+
[true],
1881+
[false],
1882+
[true],
1883+
]);
1884+
}),
1885+
),
1886+
);
1887+
1888+
effectIt.effect("does not publish a replacement window when capture reconciliation fails", () =>
1889+
withManager((manager) =>
1890+
Effect.gen(function* () {
1891+
const setBackgroundThrottling = vi.fn(() => {
1892+
throw new Error("replacement throttling update failed");
1893+
});
1894+
const capturePage = vi.fn(async () => ({
1895+
toJPEG: () => Buffer.from("recording-frame"),
1896+
getSize: () => ({ width: 1280, height: 720 }),
1897+
}));
1898+
fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
1899+
1900+
yield* manager.createTab("tab_capture_replacement_failure");
1901+
yield* manager.registerWebview("tab_capture_replacement_failure", 42);
1902+
yield* manager.startRecording("tab_capture_replacement_failure");
1903+
1904+
const failedReplacement = yield* Effect.exit(
1905+
manager.setMainWindow({
1906+
isDestroyed: () => false,
1907+
once: vi.fn(),
1908+
webContents: { setBackgroundThrottling },
1909+
} as never),
1910+
);
1911+
expect(Exit.isFailure(failedReplacement)).toBe(true);
1912+
1913+
yield* manager.stopRecording("tab_capture_replacement_failure");
1914+
expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
1915+
}),
1916+
),
1917+
);
1918+
1919+
effectIt.effect("ignores close events from replaced main windows", () =>
1920+
withManager((manager) =>
1921+
Effect.gen(function* () {
1922+
let closeFirstWindow: (() => void) | undefined;
1923+
const firstWindowThrottling = vi.fn();
1924+
const replacementWindowThrottling = vi.fn();
1925+
const capturePage = vi.fn(async () => ({
1926+
toJPEG: () => Buffer.from("recording-frame"),
1927+
getSize: () => ({ width: 1280, height: 720 }),
1928+
}));
1929+
fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
1930+
1931+
yield* manager.createTab("tab_replaced_window_close");
1932+
yield* manager.registerWebview("tab_replaced_window_close", 42);
1933+
yield* manager.setMainWindow({
1934+
isDestroyed: () => false,
1935+
once: vi.fn((event: string, listener: () => void) => {
1936+
if (event === "closed") closeFirstWindow = listener;
1937+
}),
1938+
webContents: { setBackgroundThrottling: firstWindowThrottling },
1939+
} as never);
1940+
yield* manager.setMainWindow({
1941+
isDestroyed: () => false,
1942+
once: vi.fn(),
1943+
webContents: { setBackgroundThrottling: replacementWindowThrottling },
1944+
} as never);
1945+
1946+
closeFirstWindow?.();
1947+
yield* manager.startRecording("tab_replaced_window_close");
1948+
expect(firstWindowThrottling).not.toHaveBeenCalled();
1949+
expect(replacementWindowThrottling.mock.calls).toEqual([[false]]);
1950+
yield* manager.stopRecording("tab_replaced_window_close");
1951+
expect(replacementWindowThrottling.mock.calls).toEqual([[false], [true]]);
1952+
}),
1953+
),
1954+
);
1955+
1956+
effectIt.effect("releases frame capture when the main window closes", () =>
1957+
withManager((manager) =>
1958+
Effect.gen(function* () {
1959+
let closeMainWindow: (() => void) | undefined;
1960+
const firstWindowThrottling = vi.fn();
1961+
const replacementWindowThrottling = vi.fn();
1962+
const capturePage = vi.fn(async () => ({
1963+
toJPEG: () => Buffer.from("recording-frame"),
1964+
getSize: () => ({ width: 1280, height: 720 }),
1965+
}));
1966+
const webContentsById = new Map([
1967+
[42, makeTestPreviewWebContents(capturePage, 42)],
1968+
[43, makeTestPreviewWebContents(capturePage, 43)],
1969+
]);
1970+
fromId.mockImplementation((id) =>
1971+
id === undefined ? null : (webContentsById.get(id) ?? null),
1972+
);
1973+
1974+
yield* manager.createTab("tab_window_close_recording");
1975+
yield* manager.createTab("tab_window_close_race");
1976+
yield* manager.registerWebview("tab_window_close_recording", 42);
1977+
yield* manager.registerWebview("tab_window_close_race", 43);
1978+
yield* manager.setMainWindow({
1979+
isDestroyed: () => false,
1980+
once: vi.fn((event: string, listener: () => void) => {
1981+
if (event === "closed") closeMainWindow = listener;
1982+
}),
1983+
webContents: { setBackgroundThrottling: firstWindowThrottling },
1984+
} as never);
1985+
yield* manager.startRecording("tab_window_close_recording");
1986+
expect(firstWindowThrottling.mock.calls).toEqual([[false]]);
1987+
1988+
closeMainWindow?.();
1989+
const racedStart = yield* Effect.exit(manager.startRecording("tab_window_close_race"));
1990+
expect(Exit.isFailure(racedStart)).toBe(true);
1991+
if (Exit.isFailure(racedStart)) {
1992+
expect(Option.getOrThrow(Cause.findErrorOption(racedStart.cause))).toMatchObject({
1993+
_tag: "PreviewMainWindowClosedError",
1994+
tabId: "tab_window_close_race",
1995+
});
1996+
}
1997+
yield* Effect.yieldNow;
1998+
yield* Effect.yieldNow;
1999+
2000+
yield* manager.setMainWindow({
2001+
isDestroyed: () => false,
2002+
once: vi.fn(),
2003+
webContents: { setBackgroundThrottling: replacementWindowThrottling },
2004+
} as never);
2005+
expect(replacementWindowThrottling).not.toHaveBeenCalled();
2006+
}),
2007+
),
2008+
);
2009+
18002010
effectIt.effect("captures hidden preview recordings independently for concurrent tabs", () =>
18012011
withManager((manager) =>
18022012
Effect.gen(function* () {
@@ -2106,13 +2316,16 @@ describe("PreviewManager", () => {
21062316
effectIt.effect("shares background frame capture between recording and picture-in-picture", () =>
21072317
withManager((manager) =>
21082318
Effect.gen(function* () {
2319+
const setBackgroundThrottling = vi.fn();
2320+
const mainWindowWebContents = { setBackgroundThrottling };
21092321
const jpeg = Buffer.from("shared-preview-frame");
21102322
const capturePage = vi.fn(async () => ({
21112323
toJPEG: () => jpeg,
21122324
getSize: () => ({ width: 1280, height: 720 }),
21132325
}));
21142326
fromId.mockReturnValue({
21152327
id: 42,
2328+
hostWebContents: mainWindowWebContents,
21162329
isDestroyed: () => false,
21172330
getType: () => "webview",
21182331
getURL: () => "https://example.com",
@@ -2165,6 +2378,12 @@ describe("PreviewManager", () => {
21652378
const states: PreviewManager.PreviewTabState[] = [];
21662379
const recordingFrames: DesktopPreviewRecordingFrame[] = [];
21672380

2381+
yield* manager.setMainWindow({
2382+
isDestroyed: () => false,
2383+
once: vi.fn(),
2384+
webContents: mainWindowWebContents,
2385+
} as never);
2386+
21682387
yield* manager.subscribeStateChanges((_tabId, state) =>
21692388
Effect.sync(() => {
21702389
states.push(state);
@@ -2179,6 +2398,7 @@ describe("PreviewManager", () => {
21792398
yield* manager.registerWebview("tab_pip", 42);
21802399
yield* manager.openPictureInPicture("tab_pip");
21812400

2401+
expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
21822402
expect(browserWindowConstructor).toHaveBeenCalledWith(
21832403
expect.objectContaining({
21842404
alwaysOnTop: true,
@@ -2224,6 +2444,7 @@ describe("PreviewManager", () => {
22242444
expect(recordingFrames).toHaveLength(1);
22252445

22262446
yield* manager.stopRecording("tab_pip");
2447+
expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
22272448
const framesBeforePictureInPictureOnlyTick = pictureInPictureSend.mock.calls.length;
22282449
yield* TestClock.adjust(100);
22292450
expect(capturePage).toHaveBeenCalledTimes(3);
@@ -2232,7 +2453,11 @@ describe("PreviewManager", () => {
22322453
);
22332454
expect(recordingFrames).toHaveLength(1);
22342455

2456+
setBackgroundThrottling.mockImplementationOnce(() => {
2457+
throw new Error("picture-in-picture throttling restore failed");
2458+
});
22352459
yield* manager.closePictureInPicture("tab_pip");
2460+
expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true], [true]]);
22362461
expect(pictureInPictureWindow.close).toHaveBeenCalledOnce();
22372462
expect(states.at(-1)?.pictureInPicture).toBe(false);
22382463
const capturesAfterClose = capturePage.mock.calls.length;

0 commit comments

Comments
 (0)