fix(trace): survive sw restart (#37442)
This commit is contained in:
		
							parent
							
								
									a008e126c0
								
							
						
					
					
						commit
						25f89ac4d2
					
				|  | @ -4802,6 +4802,12 @@ | ||||||
|         "node": ">=0.10.0" |         "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": { |     "node_modules/ignore": { | ||||||
|       "version": "5.3.2", |       "version": "5.3.2", | ||||||
|       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", |       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", | ||||||
|  | @ -8377,6 +8383,7 @@ | ||||||
|     "packages/trace-viewer": { |     "packages/trace-viewer": { | ||||||
|       "version": "0.0.0", |       "version": "0.0.0", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|  |         "idb-keyval": "^6.2.2", | ||||||
|         "yaml": "^2.6.0" |         "yaml": "^2.6.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ | ||||||
|   "version": "0.0.0", |   "version": "0.0.0", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "idb-keyval": "^6.2.2", | ||||||
|     "yaml": "^2.6.0" |     "yaml": "^2.6.0" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,8 @@ | ||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | import * as idbKeyval from 'idb-keyval'; | ||||||
|  | 
 | ||||||
| import { splitProgress } from './progress'; | import { splitProgress } from './progress'; | ||||||
| import { unwrapPopoutUrl } from './snapshotRenderer'; | import { unwrapPopoutUrl } from './snapshotRenderer'; | ||||||
| import { SnapshotServer } from './snapshotServer'; | import { SnapshotServer } from './snapshotServer'; | ||||||
|  | @ -33,11 +35,14 @@ self.addEventListener('activate', function(event: any) { | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const scopePath = new URL(self.registration.scope).pathname; | const scopePath = new URL(self.registration.scope).pathname; | ||||||
| 
 |  | ||||||
| const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>(); | const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>(); | ||||||
| 
 |  | ||||||
| const clientIdToTraceUrls = new Map<string, { limit: number | undefined, traceUrls: Set<string>, traceViewerServer: TraceViewerServer }>(); | 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> { | async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise<TraceModel> { | ||||||
|   await gc(); |   await gc(); | ||||||
|   const clientId = client?.id ?? ''; |   const clientId = client?.id ?? ''; | ||||||
|  | @ -49,6 +54,7 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client: | ||||||
|     clientIdToTraceUrls.set(clientId, data); |     clientIdToTraceUrls.set(clientId, data); | ||||||
|   } |   } | ||||||
|   data.traceUrls.add(traceUrl); |   data.traceUrls.add(traceUrl); | ||||||
|  |   await saveClientIdParams(); | ||||||
| 
 | 
 | ||||||
|   const traceModel = new TraceModel(); |   const traceModel = new TraceModel(); | ||||||
|   try { |   try { | ||||||
|  | @ -101,6 +107,10 @@ async function doFetch(event: FetchEvent): Promise<Response> { | ||||||
|       await gc(); |       await gc(); | ||||||
|       return new Response(null, { status: 200 }); |       return new Response(null, { status: 200 }); | ||||||
|     } |     } | ||||||
|  |     if (relativePath === '/restartServiceWorker') { | ||||||
|  |       simulateServiceWorkerRestart(); | ||||||
|  |       return new Response(null, { status: 200 }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     const traceUrl = url.searchParams.get('trace'); |     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/')) { |     if (relativePath.startsWith('/snapshotInfo/')) { | ||||||
|       const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; |       const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; | ||||||
|       if (!snapshotServer) |       if (!snapshotServer) | ||||||
|  | @ -221,6 +241,36 @@ async function gc() { | ||||||
|     if (!usedTraces.has(traceUrl)) |     if (!usedTraces.has(traceUrl)) | ||||||
|       loadedTraces.delete(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
 | // @ts-ignore
 | ||||||
|  |  | ||||||
|  | @ -2021,3 +2021,22 @@ test.describe(() => { | ||||||
|     await expect(frame.getByRole('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); |     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'); | ||||||
|  | }); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue