authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-08-29 20:31:33+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-29 10:31:33-07:00
log72c44a847c0a76f33b0d21f47548125b7032ec35
tree9e4c2d2a8c7a4b767129a303ac61aa7c9bf52c32
parent660cddd3bc9801e089afcabba11c62f41aeac5c3
signature Signed by PGP key B5690EEEBB952194

perf(desktop): skip duplicate browser updates (#8018)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

4 files changed, 333 insertions(+), 11 deletions(-)

apps/desktop/src/preview/Manager.test.ts+226-6
......@@ -332,8 +332,18 @@ const settle = function* (until: () => boolean) {
332332
333333const makeTestPictureInPictureWindow = (loadURL: () => Promise<void> = async () => undefined) => {
334334 const listeners = new Map<string, () => void>();
335 const webContentsListeners = new Map<string, () => void>();
335336 const send = vi.fn();
336337 let destroyed = false;
338 const webContents = {
339 on: vi.fn((event: string, listener: () => void) => {
340 webContentsListeners.set(event, listener);
341 }),
342 off: vi.fn((event: string) => {
343 webContentsListeners.delete(event);
344 }),
345 send,
346 };
337347 const pictureInPictureWindow = {
338348 isDestroyed: vi.fn(() => destroyed),
339349 once: vi.fn((event: string, listener: () => void) => {
......@@ -353,11 +363,12 @@ const makeTestPictureInPictureWindow = (loadURL: () => Promise<void> = async ()
353363 destroyed = true;
354364 listeners.get("closed")?.();
355365 }),
356 webContents: {
357 send,
366 get webContents() {
367 if (destroyed) throw new Error("Picture-in-picture window is closed.");
368 return webContents;
358369 },
359370 };
360 return { pictureInPictureWindow, send };
371 return { pictureInPictureWindow, send, webContentsListeners };
361372};
362373
363374describe("PreviewManager", () => {
......@@ -2413,6 +2424,8 @@ describe("PreviewManager", () => {
24132424 pictureInPictureListeners.get("closed")?.();
24142425 }),
24152426 webContents: {
2427 on: vi.fn(),
2428 off: vi.fn(),
24162429 send: pictureInPictureSend,
24172430 },
24182431 };
......@@ -2478,6 +2491,7 @@ describe("PreviewManager", () => {
24782491 );
24792492 expect(states.at(-1)?.pictureInPicture).toBe(true);
24802493 expect(capturePage).toHaveBeenCalledOnce();
2494 const pictureInPictureFramesBeforeRecording = pictureInPictureSend.mock.calls.length;
24812495
24822496 yield* manager.startRecording("tab_pip");
24832497 expect(capturePage).toHaveBeenCalledOnce();
......@@ -2485,6 +2499,7 @@ describe("PreviewManager", () => {
24852499
24862500 yield* TestClock.adjust(100);
24872501 expect(capturePage).toHaveBeenCalledTimes(2);
2502 expect(pictureInPictureSend).toHaveBeenCalledTimes(pictureInPictureFramesBeforeRecording);
24882503 expect(recordingFrames).toHaveLength(1);
24892504
24902505 yield* manager.stopRecording("tab_pip");
......@@ -2492,9 +2507,7 @@ describe("PreviewManager", () => {
24922507 const framesBeforePictureInPictureOnlyTick = pictureInPictureSend.mock.calls.length;
24932508 yield* TestClock.adjust(100);
24942509 expect(capturePage).toHaveBeenCalledTimes(3);
2495 expect(pictureInPictureSend.mock.calls.length).toBeGreaterThan(
2496 framesBeforePictureInPictureOnlyTick,
2497 );
2510 expect(pictureInPictureSend.mock.calls.length).toBe(framesBeforePictureInPictureOnlyTick);
24982511 expect(recordingFrames).toHaveLength(1);
24992512
25002513 setBackgroundThrottling.mockImplementationOnce(() => {
......@@ -2511,6 +2524,213 @@ describe("PreviewManager", () => {
25112524 ),
25122525 );
25132526
2527 effectIt.effect("starts picture-in-picture without an extra recording capture", () =>
2528 withManager((manager) =>
2529 Effect.gen(function* () {
2530 const jpeg = Buffer.from("shared-preview-frame");
2531 const capturePage = vi.fn(async () => ({
2532 toJPEG: () => jpeg,
2533 getSize: () => ({ width: 1280, height: 720 }),
2534 }));
2535 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
2536 const { pictureInPictureWindow, send } = makeTestPictureInPictureWindow();
2537 browserWindowConstructor.mockImplementation(function () {
2538 return pictureInPictureWindow;
2539 });
2540 const recordingFrames: DesktopPreviewRecordingFrame[] = [];
2541
2542 yield* manager.subscribeRecordingFrames((frame) =>
2543 Effect.sync(() => {
2544 recordingFrames.push(frame);
2545 }),
2546 );
2547 yield* manager.createTab("tab_recording_then_pip");
2548 yield* manager.registerWebview("tab_recording_then_pip", 42);
2549 yield* manager.startRecording("tab_recording_then_pip");
2550
2551 expect(recordingFrames).toHaveLength(1);
2552 yield* manager.openPictureInPicture("tab_recording_then_pip");
2553 expect(capturePage).toHaveBeenCalledOnce();
2554 expect(send).not.toHaveBeenCalled();
2555
2556 yield* TestClock.adjust(100);
2557
2558 expect(capturePage).toHaveBeenCalledTimes(2);
2559 expect(recordingFrames).toHaveLength(2);
2560 expect(send).toHaveBeenCalledOnce();
2561 yield* manager.closePictureInPicture("tab_recording_then_pip");
2562 yield* manager.stopRecording("tab_recording_then_pip");
2563 }),
2564 ),
2565 );
2566
2567 effectIt.effect("stops frame capture when the native picture-in-picture window closes", () =>
2568 withManager((manager) =>
2569 Effect.gen(function* () {
2570 const capturePage = vi.fn(async () => ({
2571 toJPEG: () => Buffer.from("native-close-preview-frame"),
2572 getSize: () => ({ width: 1280, height: 720 }),
2573 }));
2574 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
2575 const { pictureInPictureWindow } = makeTestPictureInPictureWindow();
2576 browserWindowConstructor.mockImplementation(function () {
2577 return pictureInPictureWindow;
2578 });
2579 const states: PreviewManager.PreviewTabState[] = [];
2580 yield* manager.subscribeStateChanges((_tabId, state) =>
2581 Effect.sync(() => {
2582 states.push(state);
2583 }),
2584 );
2585
2586 yield* manager.createTab("tab_native_pip_close");
2587 yield* manager.registerWebview("tab_native_pip_close", 42);
2588 yield* manager.openPictureInPicture("tab_native_pip_close");
2589
2590 pictureInPictureWindow.close();
2591 yield* settle(() => states.at(-1)?.pictureInPicture === false);
2592
2593 expect(states.at(-1)?.pictureInPicture).toBe(false);
2594 const capturesAfterClose = capturePage.mock.calls.length;
2595 yield* TestClock.adjust(200);
2596 expect(capturePage).toHaveBeenCalledTimes(capturesAfterClose);
2597 }),
2598 ),
2599 );
2600
2601 effectIt.effect("retries an unchanged picture-in-picture frame after delivery fails", () =>
2602 withManager((manager) =>
2603 Effect.gen(function* () {
2604 const jpeg = Buffer.from("retry-preview-frame");
2605 const capturePage = vi.fn(async () => ({
2606 toJPEG: () => jpeg,
2607 getSize: () => ({ width: 1280, height: 720 }),
2608 }));
2609 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
2610 const { pictureInPictureWindow, send } = makeTestPictureInPictureWindow();
2611 send.mockImplementationOnce(() => {
2612 throw new Error("picture-in-picture delivery failed");
2613 });
2614 browserWindowConstructor.mockImplementation(function () {
2615 return pictureInPictureWindow;
2616 });
2617
2618 yield* manager.createTab("tab_pip_delivery_retry");
2619 yield* manager.registerWebview("tab_pip_delivery_retry", 42);
2620 yield* manager.openPictureInPicture("tab_pip_delivery_retry");
2621 expect(send).toHaveBeenCalledOnce();
2622
2623 yield* TestClock.adjust(100);
2624
2625 expect(capturePage).toHaveBeenCalledTimes(2);
2626 expect(send).toHaveBeenCalledTimes(2);
2627 yield* manager.closePictureInPicture("tab_pip_delivery_retry");
2628 }),
2629 ),
2630 );
2631
2632 effectIt.effect("replays an unchanged picture-in-picture frame after its renderer reloads", () =>
2633 withManager((manager) =>
2634 Effect.gen(function* () {
2635 const jpeg = Buffer.from("reloaded-preview-frame");
2636 const capturePage = vi.fn(async () => ({
2637 toJPEG: () => jpeg,
2638 getSize: () => ({ width: 1280, height: 720 }),
2639 }));
2640 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
2641 const { pictureInPictureWindow, send, webContentsListeners } =
2642 makeTestPictureInPictureWindow();
2643 browserWindowConstructor.mockImplementation(function () {
2644 return pictureInPictureWindow;
2645 });
2646
2647 yield* manager.createTab("tab_pip_reload");
2648 yield* manager.registerWebview("tab_pip_reload", 42);
2649 yield* manager.openPictureInPicture("tab_pip_reload");
2650 expect(send).toHaveBeenCalledOnce();
2651
2652 webContentsListeners.get("did-finish-load")?.();
2653 yield* TestClock.adjust(100);
2654
2655 expect(send).toHaveBeenCalledTimes(2);
2656 yield* manager.closePictureInPicture("tab_pip_reload");
2657 }),
2658 ),
2659 );
2660
2661 effectIt.effect("keeps recording cadence when pixels remain unchanged", () =>
2662 withManager((manager) =>
2663 Effect.gen(function* () {
2664 const unchanged = Buffer.from("unchanged-preview-frame");
2665 const capturePage = vi.fn(async () => ({
2666 toJPEG: () => unchanged,
2667 getSize: () => ({ width: 1280, height: 720 }),
2668 }));
2669 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
2670 const frames: DesktopPreviewRecordingFrame[] = [];
2671
2672 yield* manager.subscribeRecordingFrames((frame) =>
2673 Effect.sync(() => {
2674 frames.push(frame);
2675 }),
2676 );
2677 yield* manager.createTab("tab_unchanged_frame");
2678 yield* manager.registerWebview("tab_unchanged_frame", 42);
2679 yield* manager.startRecording("tab_unchanged_frame");
2680
2681 expect(frames.map((frame) => frame.data)).toEqual([unchanged.toString("base64")]);
2682
2683 yield* TestClock.adjust(100);
2684
2685 expect(capturePage).toHaveBeenCalledTimes(2);
2686 expect(frames.map((frame) => frame.data)).toEqual([
2687 unchanged.toString("base64"),
2688 unchanged.toString("base64"),
2689 ]);
2690
2691 yield* TestClock.adjust(100);
2692
2693 expect(capturePage).toHaveBeenCalledTimes(3);
2694 expect(frames.map((frame) => frame.data)).toEqual([
2695 unchanged.toString("base64"),
2696 unchanged.toString("base64"),
2697 unchanged.toString("base64"),
2698 ]);
2699 yield* manager.stopRecording("tab_unchanged_frame");
2700 }),
2701 ),
2702 );
2703
2704 effectIt.effect("retries recording delivery when pixels remain unchanged", () =>
2705 withManager((manager) =>
2706 Effect.gen(function* () {
2707 const jpeg = Buffer.from("retry-recording-frame");
2708 const capturePage = vi.fn(async () => ({
2709 toJPEG: () => jpeg,
2710 getSize: () => ({ width: 1280, height: 720 }),
2711 }));
2712 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
2713 let deliveries = 0;
2714
2715 yield* manager.subscribeRecordingFrames(() =>
2716 Effect.sync(() => {
2717 deliveries += 1;
2718 if (deliveries === 1) throw new Error("recording delivery failed");
2719 }),
2720 );
2721 yield* manager.createTab("tab_recording_delivery_retry");
2722 yield* manager.registerWebview("tab_recording_delivery_retry", 42);
2723 yield* manager.startRecording("tab_recording_delivery_retry");
2724 expect(deliveries).toBe(1);
2725
2726 yield* TestClock.adjust(100);
2727
2728 expect(deliveries).toBe(2);
2729 yield* manager.stopRecording("tab_recording_delivery_retry");
2730 }),
2731 ),
2732 );
2733
25142734 effectIt.effect("retries a cold hidden-tab capture without dropping recording", () =>
25152735 withManager((manager) =>
25162736 Effect.gen(function* () {
apps/desktop/src/preview/Manager.ts+47-5
......@@ -381,6 +381,7 @@ type FrameCaptureConsumer = "picture-in-picture" | "recording";
381381interface FrameCaptureSession {
382382 readonly scope: Scope.Closeable;
383383 readonly consumers: ReadonlySet<FrameCaptureConsumer>;
384 readonly lastPictureInPictureFrame: Buffer | null;
384385}
385386
386387interface PictureInPictureSession {
......@@ -671,7 +672,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
671672 return [
672673 undefined,
673674 replaceMap(sessions, (copy) => {
674 copy.set(tabId, { ...current, consumers });
675 copy.set(tabId, {
676 ...current,
677 consumers,
678 lastPictureInPictureFrame:
679 consumer === "picture-in-picture" ? null : current.lastPictureInPictureFrame,
680 });
675681 }),
676682 ] as const;
677683 }
......@@ -2615,18 +2621,25 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
26152621 tabId,
26162622 webContentsId: wc.id,
26172623 },
2618 () => image.toJPEG(RECORDING_JPEG_QUALITY).toString("base64"),
2624 () => image.toJPEG(RECORDING_JPEG_QUALITY),
26192625 );
2626 const frameSession = (yield* SynchronizedRef.get(frameCaptureSessionsRef)).get(tabId);
2627 if (frameSession?.scope !== captureSession.scope) return;
2628 const recording = frameSession.consumers.has("recording");
2629 const pictureInPicture =
2630 frameSession.consumers.has("picture-in-picture") &&
2631 frameSession.lastPictureInPictureFrame?.equals(encoded) !== true;
2632 if (!recording && !pictureInPicture) return;
26202633 const receivedAt = yield* currentIso;
26212634 const frame: DesktopPreviewRecordingFrame = {
26222635 tabId,
2623 data: encoded,
2636 data: encoded.toString("base64"),
26242637 width: size.width,
26252638 height: size.height,
26262639 receivedAt,
26272640 };
26282641 const deliveries: Array<Effect.Effect<void>> = [];
2629 if (currentCaptureSession.consumers.has("recording")) {
2642 if (recording) {
26302643 const listeners = yield* Ref.get(recordingFrameListenersRef);
26312644 deliveries.push(
26322645 Effect.forEach(
......@@ -2636,7 +2649,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
26362649 ),
26372650 );
26382651 }
2639 if (currentCaptureSession.consumers.has("picture-in-picture")) {
2652 if (pictureInPicture) {
26402653 const pictureInPictureWindow = (yield* SynchronizedRef.get(pictureInPictureSessionsRef)).get(
26412654 tabId,
26422655 )?.window;
......@@ -2686,6 +2699,15 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
26862699 );
26872700 },
26882701 );
2702 yield* SynchronizedRef.update(frameCaptureSessionsRef, (sessions) => {
2703 if (sessions.get(tabId) !== frameSession) return sessions;
2704 return replaceMap(sessions, (copy) => {
2705 copy.set(tabId, {
2706 ...frameSession,
2707 lastPictureInPictureFrame: encoded,
2708 });
2709 });
2710 });
26892711 }).pipe(
26902712 Effect.catch((error) =>
26912713 Effect.logWarning("Picture-in-picture frame delivery failed.", {
......@@ -2753,6 +2775,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
27532775 copy.set(tabId, {
27542776 scope,
27552777 consumers: new Set([consumer]),
2778 lastPictureInPictureFrame: null,
27562779 });
27572780 }),
27582781 ] as const;
......@@ -2907,6 +2930,18 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
29072930 ),
29082931 );
29092932 };
2933 const onDidFinishLoad = () => {
2934 runFork(
2935 SynchronizedRef.update(frameCaptureSessionsRef, (sessions) => {
2936 const current = sessions.get(tabId);
2937 if (!current?.consumers.has("picture-in-picture")) return sessions;
2938 return replaceMap(sessions, (copy) => {
2939 copy.set(tabId, { ...current, lastPictureInPictureFrame: null });
2940 });
2941 }),
2942 );
2943 };
2944 const pipWebContents = pictureInPictureWindow.webContents;
29102945 yield* attempt(
29112946 {
29122947 operation: "pictureInPicture.configure",
......@@ -2927,6 +2962,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
29272962 skipTransformProcessType: true,
29282963 });
29292964 }
2965 pipWebContents.on("did-finish-load", onDidFinishLoad);
29302966 },
29312967 ).pipe(
29322968 Effect.onError(() =>
......@@ -2941,6 +2977,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
29412977 ),
29422978 ),
29432979 );
2980 yield* Scope.addFinalizer(
2981 initializationScope,
2982 Effect.sync(() => {
2983 pipWebContents.off("did-finish-load", onDidFinishLoad);
2984 }).pipe(Effect.ignore),
2985 );
29442986 yield* SynchronizedRef.update(pictureInPictureSessionsRef, (sessions) =>
29452987 replaceMap(sessions, (copy) => {
29462988 copy.set(tabId, session);
apps/web/src/previewStateStore.test.ts+33
......@@ -20,6 +20,7 @@ import {
2020 rememberPreviewUrl,
2121 removePreviewThread,
2222 resetPreviewStateForTests,
23 subscribeThreadPreviewState,
2324 setActivePreviewTab,
2425 updatePreviewServerSnapshot,
2526} from "./previewStateStore";
......@@ -331,6 +332,38 @@ describe("previewStateStore (single-tab)", () => {
331332 expect(state.snapshot?.canGoBack).toBe(false);
332333 });
333334
335 it("does not publish duplicate desktop browser state", () => {
336 const snapshot = makeSnapshot();
337 applyPreviewServerSnapshot(ref, snapshot);
338 const overlay = {
339 hasWebContents: true,
340 canGoBack: true,
341 canGoForward: false,
342 loading: false,
343 zoomFactor: 1,
344 pictureInPicture: false,
345 colorScheme: "system" as const,
346 audioMuted: false,
347 audible: false,
348 controller: "none" as const,
349 favicon: {
350 dataUrl: "data:image/png;base64,AA==",
351 pageUrl: "https://example.com",
352 capturedAt: 1,
353 },
354 };
355 let updateCount = 0;
356 const unsubscribe = subscribeThreadPreviewState(ref, () => {
357 updateCount += 1;
358 });
359
360 applyPreviewDesktopState(ref, snapshot.tabId, overlay);
361 applyPreviewDesktopState(ref, snapshot.tabId, { ...overlay, favicon: { ...overlay.favicon } });
362 unsubscribe();
363
364 expect(updateCount).toBe(1);
365 });
366
334367 it("retains multiple tabs and switches active desktop state", () => {
335368 const first = makeSnapshot();
336369 const second = { ...makeSnapshot(), tabId: "tab_2", updatedAt: "2026-01-02T00:00:00.000Z" };
apps/web/src/previewStateStore.ts+27
......@@ -368,12 +368,39 @@ export function reconcilePreviewServerSessions(
368368 });
369369}
370370
371function isPreviewStateEqual(
372 previous: DesktopPreviewOverlay | null,
373 next: DesktopPreviewOverlay | null,
374) {
375 return (
376 previous === next ||
377 (previous !== null &&
378 next !== null &&
379 previous.hasWebContents === next.hasWebContents &&
380 previous.canGoBack === next.canGoBack &&
381 previous.canGoForward === next.canGoForward &&
382 previous.loading === next.loading &&
383 previous.zoomFactor === next.zoomFactor &&
384 previous.pictureInPicture === next.pictureInPicture &&
385 previous.colorScheme === next.colorScheme &&
386 previous.audioMuted === next.audioMuted &&
387 previous.audible === next.audible &&
388 previous.controller === next.controller &&
389 previous.favicon?.dataUrl === next.favicon?.dataUrl &&
390 previous.favicon?.pageUrl === next.favicon?.pageUrl &&
391 previous.favicon?.capturedAt === next.favicon?.capturedAt)
392 );
393}
394
371395export function applyPreviewDesktopState(
372396 ref: ScopedThreadRef,
373397 tabId: string,
374398 overlay: DesktopPreviewOverlay | null,
375399): void {
376400 updateThreadPreviewState(ref, (current) => {
401 if (isPreviewStateEqual(current.desktopByTabId[tabId] ?? null, overlay)) {
402 return current;
403 }
377404 const desktopByTabId = { ...current.desktopByTabId };
378405 if (overlay) desktopByTabId[tabId] = overlay;
379406 else delete desktopByTabId[tabId];