chore(tracing): look up snapshot resources only in the same context (#34645)

This commit is contained in:
Yury Semikhatsky 2025-02-06 12:38:51 -08:00 committed by GitHub
parent 427d7a22ea
commit 3d3154de86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 45 additions and 25 deletions

View file

@ -103,7 +103,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
wallTime: 0, wallTime: 0,
monotonicTime: 0, monotonicTime: 0,
sdkLanguage: context.attribution.playwright.options.sdkLanguage, sdkLanguage: context.attribution.playwright.options.sdkLanguage,
testIdAttributeName testIdAttributeName,
contextId: context.guid,
}; };
if (context instanceof BrowserContext) { if (context instanceof BrowserContext) {
this._snapshotter = new Snapshotter(context, this); this._snapshotter = new Snapshotter(context, this);

View file

@ -72,7 +72,7 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
} }
onEntryFinished(entry: har.Entry) { onEntryFinished(entry: har.Entry) {
this._storage.addResource(entry); this._storage.addResource('', entry);
} }
onContentBlob(sha1: string, buffer: Buffer) { onContentBlob(sha1: string, buffer: Buffer) {
@ -85,7 +85,7 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
onFrameSnapshot(snapshot: FrameSnapshot): void { onFrameSnapshot(snapshot: FrameSnapshot): void {
++this._snapshotCount; ++this._snapshotCount;
const renderer = this._storage.addFrameSnapshot(snapshot, []); const renderer = this._storage.addFrameSnapshot('', snapshot, []);
this._snapshotReadyPromises.get(snapshot.snapshotName || '')?.resolve(renderer); this._snapshotReadyPromises.get(snapshot.snapshotName || '')?.resolve(renderer);
} }

View file

@ -120,14 +120,16 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer) if (!snapshotServer)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
return snapshotServer.serveSnapshotInfo(relativePath, url.searchParams); const pageOrFrameId = relativePath.substring('/snapshotInfo/'.length);
return snapshotServer.serveSnapshotInfo(pageOrFrameId, url.searchParams);
} }
if (relativePath.startsWith('/snapshot/')) { if (relativePath.startsWith('/snapshot/')) {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer) if (!snapshotServer)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
const response = snapshotServer.serveSnapshot(relativePath, url.searchParams, url.href); const pageOrFrameId = relativePath.substring('/snapshot/'.length);
const response = snapshotServer.serveSnapshot(pageOrFrameId, url.searchParams, url.href);
if (isDeployedAsHttps) if (isDeployedAsHttps)
response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests'); response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests');
return response; return response;
@ -137,7 +139,8 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer) if (!snapshotServer)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
return snapshotServer.serveClosestScreenshot(relativePath, url.searchParams); const pageOrFrameId = relativePath.substring('/closest-screenshot/'.length);
return snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams);
} }
if (relativePath.startsWith('/sha1/')) { if (relativePath.startsWith('/sha1/')) {

View file

@ -31,8 +31,8 @@ export class SnapshotServer {
this._resourceLoader = resourceLoader; this._resourceLoader = resourceLoader;
} }
serveSnapshot(pathname: string, searchParams: URLSearchParams, snapshotUrl: string): Response { serveSnapshot(pageOrFrameId: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams); const snapshot = this._snapshot(pageOrFrameId, searchParams);
if (!snapshot) if (!snapshot)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
@ -41,16 +41,16 @@ export class SnapshotServer {
return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
} }
async serveClosestScreenshot(pathname: string, searchParams: URLSearchParams): Promise<Response> { async serveClosestScreenshot(pageOrFrameId: string, searchParams: URLSearchParams): Promise<Response> {
const snapshot = this._snapshot(pathname.substring('/closest-screenshot'.length), searchParams); const snapshot = this._snapshot(pageOrFrameId, searchParams);
const sha1 = snapshot?.closestScreenshot(); const sha1 = snapshot?.closestScreenshot();
if (!sha1) if (!sha1)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
return new Response(await this._resourceLoader(sha1)); return new Response(await this._resourceLoader(sha1));
} }
serveSnapshotInfo(pathname: string, searchParams: URLSearchParams): Response { serveSnapshotInfo(pageOrFrameId: string, searchParams: URLSearchParams): Response {
const snapshot = this._snapshot(pathname.substring('/snapshotInfo'.length), searchParams); const snapshot = this._snapshot(pageOrFrameId, searchParams);
return this._respondWithJson(snapshot ? { return this._respondWithJson(snapshot ? {
viewport: snapshot.viewport(), viewport: snapshot.viewport(),
url: snapshot.snapshot().frameUrl, url: snapshot.snapshot().frameUrl,
@ -61,9 +61,9 @@ export class SnapshotServer {
}); });
} }
private _snapshot(pathname: string, params: URLSearchParams) { private _snapshot(pageOrFrameId: string, params: URLSearchParams) {
const name = params.get('name')!; const name = params.get('name')!;
return this._snapshotStorage.snapshotByName(pathname.slice(1), name); return this._snapshotStorage.snapshotByName(pageOrFrameId, name);
} }
private _respondWithJson(object: any): Response { private _respondWithJson(object: any): Response {

View file

@ -20,19 +20,19 @@ import type { PageEntry } from '../types/entries';
import { LRUCache } from './lruCache'; import { LRUCache } from './lruCache';
export class SnapshotStorage { export class SnapshotStorage {
private _resources: ResourceSnapshot[] = [];
private _frameSnapshots = new Map<string, { private _frameSnapshots = new Map<string, {
raw: FrameSnapshot[], raw: FrameSnapshot[],
renderers: SnapshotRenderer[] renderers: SnapshotRenderer[],
}>(); }>();
private _cache = new LRUCache<SnapshotRenderer, string>(100_000_000); // 100MB per each trace private _cache = new LRUCache<SnapshotRenderer, string>(100_000_000); // 100MB per each trace
private _contextToResources = new Map<string, ResourceSnapshot[]>();
addResource(resource: ResourceSnapshot): void { addResource(contextId: string, resource: ResourceSnapshot): void {
resource.request.url = rewriteURLForCustomProtocol(resource.request.url); resource.request.url = rewriteURLForCustomProtocol(resource.request.url);
this._resources.push(resource); this._ensureResourcesForContext(contextId).push(resource);
} }
addFrameSnapshot(snapshot: FrameSnapshot, screencastFrames: PageEntry['screencastFrames']) { addFrameSnapshot(contextId: string, snapshot: FrameSnapshot, screencastFrames: PageEntry['screencastFrames']) {
for (const override of snapshot.resourceOverrides) for (const override of snapshot.resourceOverrides)
override.url = rewriteURLForCustomProtocol(override.url); override.url = rewriteURLForCustomProtocol(override.url);
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId); let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
@ -46,7 +46,8 @@ export class SnapshotStorage {
this._frameSnapshots.set(snapshot.pageId, frameSnapshots); this._frameSnapshots.set(snapshot.pageId, frameSnapshots);
} }
frameSnapshots.raw.push(snapshot); frameSnapshots.raw.push(snapshot);
const renderer = new SnapshotRenderer(this._cache, this._resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1); const resources = this._ensureResourcesForContext(contextId);
const renderer = new SnapshotRenderer(this._cache, resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1);
frameSnapshots.renderers.push(renderer); frameSnapshots.renderers.push(renderer);
return renderer; return renderer;
} }
@ -62,6 +63,16 @@ export class SnapshotStorage {
finalize() { finalize() {
// Resources are not necessarily sorted in the trace file, so sort them now. // Resources are not necessarily sorted in the trace file, so sort them now.
this._resources.sort((a, b) => (a._monotonicTime || 0) - (b._monotonicTime || 0)); for (const resources of this._contextToResources.values())
resources.sort((a, b) => (a._monotonicTime || 0) - (b._monotonicTime || 0));
}
private _ensureResourcesForContext(contextId: string): ResourceSnapshot[] {
let resources = this._contextToResources.get(contextId);
if (!resources) {
resources = [];
this._contextToResources.set(contextId, resources);
}
return resources;
} }
} }

View file

@ -105,7 +105,7 @@ export class TraceModel {
this.contextEntries.push(contextEntry); this.contextEntries.push(contextEntry);
} }
this._snapshotStorage!.finalize(); this._snapshotStorage.finalize();
} }
async hasEntry(filename: string): Promise<boolean> { async hasEntry(filename: string): Promise<boolean> {
@ -153,5 +153,6 @@ function createEmptyContext(): ContextEntry {
errors: [], errors: [],
stdio: [], stdio: [],
hasSource: false, hasSource: false,
contextId: '',
}; };
} }

View file

@ -92,6 +92,7 @@ export class TraceModernizer {
contextEntry.sdkLanguage = event.sdkLanguage; contextEntry.sdkLanguage = event.sdkLanguage;
contextEntry.options = event.options; contextEntry.options = event.options;
contextEntry.testIdAttributeName = event.testIdAttributeName; contextEntry.testIdAttributeName = event.testIdAttributeName;
contextEntry.contextId = event.contextId ?? '';
break; break;
} }
case 'screencast-frame': { case 'screencast-frame': {
@ -156,11 +157,11 @@ export class TraceModernizer {
break; break;
} }
case 'resource-snapshot': case 'resource-snapshot':
this._snapshotStorage.addResource(event.snapshot); this._snapshotStorage.addResource(this._contextEntry.contextId, event.snapshot);
contextEntry.resources.push(event.snapshot); contextEntry.resources.push(event.snapshot);
break; break;
case 'frame-snapshot': case 'frame-snapshot':
this._snapshotStorage.addFrameSnapshot(event.snapshot, this._pageEntry(event.snapshot.pageId).screencastFrames); this._snapshotStorage.addFrameSnapshot(this._contextEntry.contextId, event.snapshot, this._pageEntry(event.snapshot.pageId).screencastFrames);
break; break;
} }
// Make sure there is a page entry for each page, even without screencast frames, // Make sure there is a page entry for each page, even without screencast frames,
@ -388,12 +389,13 @@ export class TraceModernizer {
wallTime: 0, wallTime: 0,
monotonicTime: 0, monotonicTime: 0,
sdkLanguage: 'javascript', sdkLanguage: 'javascript',
contextId: '',
}; };
result.push(event); result.push(event);
} }
for (const event of events) { for (const event of events) {
if (event.type === 'context-options') { if (event.type === 'context-options') {
result.push({ ...event, monotonicTime: 0, origin: 'library' }); result.push({ ...event, monotonicTime: 0, origin: 'library', contextId: '' });
continue; continue;
} }
// Take wall and monotonic time from the first event. // Take wall and monotonic time from the first event.

View file

@ -40,6 +40,7 @@ export type ContextEntry = {
stdio: trace.StdioTraceEvent[]; stdio: trace.StdioTraceEvent[];
errors: trace.ErrorTraceEvent[]; errors: trace.ErrorTraceEvent[];
hasSource: boolean; hasSource: boolean;
contextId: string;
}; };
export type PageEntry = { export type PageEntry = {

View file

@ -44,6 +44,7 @@ export type ContextCreatedTraceEvent = {
options: BrowserContextEventOptions, options: BrowserContextEventOptions,
sdkLanguage?: Language, sdkLanguage?: Language,
testIdAttributeName?: string, testIdAttributeName?: string,
contextId?: string,
}; };
export type ScreencastFrameTraceEvent = { export type ScreencastFrameTraceEvent = {