(session: CaptureSession)
| 1870 | } |
| 1871 | |
| 1872 | export async function closeCaptureSession(session: CaptureSession): Promise<void> { |
| 1873 | // Realized static-dedup telemetry: how much the cache actually helped this |
| 1874 | // render (vs the prediction logged at arm time). Both capture paths |
| 1875 | // (sequential orchestrator + parallel workers) close their session here, so |
| 1876 | // this is the one uniform emit point. Zero the count afterward so the |
| 1877 | // idempotent re-close (HDR cleanup) doesn't double-log. |
| 1878 | const reused = session.staticDedupCount ?? 0; |
| 1879 | if (session.staticFrames && reused > 0) { |
| 1880 | const captured = session.capturePerf.frames; // excludes reuses by design |
| 1881 | const total = captured + reused; |
| 1882 | const pct = total > 0 ? Math.round((reused / total) * 100) : 0; |
| 1883 | const avgTotalMs = captured > 0 ? Math.round(session.capturePerf.totalMs / captured) : 0; |
| 1884 | console.log( |
| 1885 | `[static-dedup] reused ${reused}/${total} frame(s) (${pct}%), ` + |
| 1886 | `est. ~${reused * avgTotalMs}ms saved (avg ${avgTotalMs}ms/frame)`, |
| 1887 | ); |
| 1888 | session.staticDedupCount = 0; |
| 1889 | } |
| 1890 | // INVARIANT: closeCaptureSession is idempotent. The renderOrchestrator HDR |
| 1891 | // cleanup path tracks a `domSessionClosed` flag and may still re-call this |
| 1892 | // in the outer finally if the inner cleanup raised before the flag flipped. |
| 1893 | // |
| 1894 | // Naive idempotency would be unsafe under pool semantics: releaseBrowser |
| 1895 | // decrements pooledBrowserRefCount, so calling it twice for the same |
| 1896 | // acquire could close a browser that another session still holds. We make |
| 1897 | // it safe by gating each release behind a per-session "released" flag — |
| 1898 | // the second call sees the flag already set and skips the release. |
| 1899 | // |
| 1900 | // We set the flag AFTER (not before) the await so that if a release throws |
| 1901 | // midway, the unreleased resource is retried by the outer defensive call. |
| 1902 | // Example: page release succeeds, browser release throws → pageReleased=true |
| 1903 | // but browserReleased=false → second call no-ops on page and retries browser. |
| 1904 | // This matches the orchestrator's intent for HDR cleanup. |
| 1905 | if (!session.pageReleased && session.page) { |
| 1906 | const pageClosed = await waitForCloseWithTimeout(session.page.close()); |
| 1907 | if (!pageClosed) { |
| 1908 | console.warn("[FrameCapture] Timed out closing page; forcing browser process shutdown"); |
| 1909 | forceReleaseBrowser(session.browser); |
| 1910 | session.browserReleased = true; |
| 1911 | } |
| 1912 | session.pageReleased = true; |
| 1913 | } |
| 1914 | if (!session.browserReleased && session.browser) { |
| 1915 | const browserClosed = await waitForCloseWithTimeout( |
| 1916 | releaseBrowser(session.browser, session.config), |
| 1917 | ); |
| 1918 | if (!browserClosed) { |
| 1919 | console.warn("[FrameCapture] Timed out closing browser; forcing browser process shutdown"); |
| 1920 | forceReleaseBrowser(session.browser); |
| 1921 | } |
| 1922 | session.browserReleased = true; |
| 1923 | } |
| 1924 | session.isInitialized = false; |
| 1925 | } |
| 1926 | |
| 1927 | export function prepareCaptureSessionForReuse( |
| 1928 | session: CaptureSession, |
no test coverage detected