fix(trace): survive sw restart (#37442)

This commit is contained in:
Pavel Feldman 2025-09-16 17:41:55 -07:00 committed by GitHub
parent a008e126c0
commit 25f89ac4d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 79 additions and 2 deletions

7
package-lock.json generated
View File

@ -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"
}
},

View File

@ -4,6 +4,7 @@
"version": "0.0.0",
"type": "module",
"dependencies": {
"idb-keyval": "^6.2.2",
"yaml": "^2.6.0"
}
}

View File

@ -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<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>();
const clientIdToTraceUrls = new Map<string, { limit: number | undefined, traceUrls: Set<string>, 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<TraceModel> {
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<Response> {
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<Response> {
}
}
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<string, {
limit: number | undefined,
traceUrls: string[]
}> = {};
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

View File

@ -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');
});