diff --git a/package-lock.json b/package-lock.json index 548715024d74b..28ac6268a687d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4802,6 +4802,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8377,6 +8383,7 @@ "packages/trace-viewer": { "version": "0.0.0", "dependencies": { + "idb-keyval": "^6.2.2", "yaml": "^2.6.0" } }, diff --git a/packages/trace-viewer/package.json b/packages/trace-viewer/package.json index 57a3dfd4094f3..699014a40fc7a 100644 --- a/packages/trace-viewer/package.json +++ b/packages/trace-viewer/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "dependencies": { + "idb-keyval": "^6.2.2", "yaml": "^2.6.0" } } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index cdce3261feac0..0e7ff887cc332 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import * as idbKeyval from 'idb-keyval'; + import { splitProgress } from './progress'; import { unwrapPopoutUrl } from './snapshotRenderer'; import { SnapshotServer } from './snapshotServer'; @@ -33,11 +35,14 @@ self.addEventListener('activate', function(event: any) { }); const scopePath = new URL(self.registration.scope).pathname; - const loadedTraces = new Map(); - const clientIdToTraceUrls = new Map, traceViewerServer: TraceViewerServer }>(); +function simulateServiceWorkerRestart() { + loadedTraces.clear(); + clientIdToTraceUrls.clear(); +} + async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise { await gc(); const clientId = client?.id ?? ''; @@ -49,6 +54,7 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client: clientIdToTraceUrls.set(clientId, data); } data.traceUrls.add(traceUrl); + await saveClientIdParams(); const traceModel = new TraceModel(); try { @@ -101,6 +107,10 @@ async function doFetch(event: FetchEvent): Promise { await gc(); return new Response(null, { status: 200 }); } + if (relativePath === '/restartServiceWorker') { + simulateServiceWorkerRestart(); + return new Response(null, { status: 200 }); + } const traceUrl = url.searchParams.get('trace'); @@ -122,6 +132,16 @@ async function doFetch(event: FetchEvent): Promise { } } + if (!clientIdToTraceUrls.has(event.clientId)) { + // Service worker was restarted upon subresource fetch. + // It was stopped because ping did not keep it alive since the tab itself was throttled. + const params = await loadClientIdParams(event.clientId); + if (params) { + for (const traceUrl of params.traceUrls) + await loadTrace(traceUrl, null, client, params.limit, () => {}); + } + } + if (relativePath.startsWith('/snapshotInfo/')) { const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; if (!snapshotServer) @@ -221,6 +241,36 @@ async function gc() { if (!usedTraces.has(traceUrl)) loadedTraces.delete(traceUrl); } + + await saveClientIdParams(); +} + +// Persist clientIdToTraceUrls to localStorage to avoid losing it when the service worker is restarted. +async function saveClientIdParams() { + const serialized: Record = {}; + for (const [clientId, data] of clientIdToTraceUrls) { + serialized[clientId] = { + limit: data.limit, + traceUrls: [...data.traceUrls] + }; + } + + const newValue = JSON.stringify(serialized); + const oldValue = await idbKeyval.get('clientIdToTraceUrls'); + if (newValue === oldValue) + return; + idbKeyval.set('clientIdToTraceUrls', newValue); +} + +async function loadClientIdParams(clientId: string): Promise<{ limit: number | undefined, traceUrls: string[] } | undefined> { + const serialized = await idbKeyval.get('clientIdToTraceUrls') as string | undefined; + if (!serialized) + return; + const deserialized = JSON.parse(serialized); + return deserialized[clientId]; } // @ts-ignore diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 086b347bbc461..064be8097f8af 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -2021,3 +2021,22 @@ test.describe(() => { await expect(frame.getByRole('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); }); }); + +test('should survive service worker restart', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(server.EMPTY_PAGE); + await page.setContent('Old world'); + await page.evaluate(() => document.body.textContent = 'New world'); + }); + const snapshot1 = await traceViewer.snapshotFrame('Evaluate'); + await expect(snapshot1.locator('body')).toHaveText('New world'); + + const status = await traceViewer.page.evaluate(async () => { + const response = await fetch('restartServiceWorker'); + return response.status; + }); + expect(status).toBe(200); + + const snapshot2 = await traceViewer.snapshotFrame('Set content'); + await expect(snapshot2.locator('body')).toHaveText('Old world'); +});