Skip to content

Commit f7eb9ca

Browse files
committed
chore: extract large snapshots in the trace as resources
1 parent 23b41bb commit f7eb9ca

File tree

14 files changed

+95
-51
lines changed

14 files changed

+95
-51
lines changed

packages/playwright-core/src/server/trace/recorder/snapshotter.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,24 @@ export class Snapshotter {
131131
frameId: frame.guid,
132132
frameUrl: data.url,
133133
doctype: data.doctype,
134-
html: data.html,
135134
viewport: data.viewport,
136135
timestamp: monotonicTime(),
137136
wallTime: data.wallTime,
138137
collectionTime: data.collectionTime,
139-
resourceOverrides: [],
140138
isMainFrame: page.mainFrame() === frame
141139
};
140+
const htmlJson = JSON.stringify(data.html);
141+
if (htmlJson.length < 200) {
142+
snapshot.html = data.html;
143+
} else {
144+
const buffer = Buffer.from(htmlJson);
145+
const sha1 = calculateSha1(buffer) + '.json';
146+
snapshot.sha1 = sha1;
147+
this._delegate.onSnapshotterBlob({ sha1, buffer });
148+
}
142149
for (const { url, content, contentType } of data.resourceOverrides) {
150+
if (!snapshot.resourceOverrides)
151+
snapshot.resourceOverrides = [];
143152
if (typeof content === 'string') {
144153
const buffer = Buffer.from(content);
145154
const sha1 = calculateSha1(buffer) + '.' + (mime.getExtension(contentType) || 'dat');

packages/playwright-core/src/server/trace/recorder/tracing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import type { Progress } from '@protocol/progress';
5252
import type * as types from '../../types';
5353
import type { ScreencastListener } from '../../screencast';
5454

55-
const version: trace.VERSION = 8;
55+
const version: trace.VERSION = 9;
5656

5757
export type TracerOptions = {
5858
name?: string;

packages/playwright-core/src/tools/trace/traceCli.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ export async function traceSnapshot(traceFile: string, actionId: string, options
690690

691691
const snapshotKey = `${snapshotName}@${callId}`;
692692

693-
const rendered = renderer.render();
693+
const rendered = await renderer.render();
694694
const defaultName = `snapshot-${actionId}-${snapshotName}.html`;
695695

696696
if (options.serve) {
@@ -704,10 +704,11 @@ export async function traceSnapshot(traceFile: string, actionId: string, options
704704
const url = new URL('http://localhost' + request.url!);
705705
const searchParams = url.searchParams;
706706
searchParams.set('name', snapshotKey);
707-
const snapshotResponse = snapshotServer.serveSnapshot(pageId, searchParams, '/snapshot');
708-
response.statusCode = snapshotResponse.status;
709-
snapshotResponse.headers.forEach((value, key) => response.setHeader(key, value));
710-
snapshotResponse.text().then(text => response.end(text));
707+
snapshotServer.serveSnapshot(pageId, searchParams, '/snapshot').then(snapshotResponse => {
708+
response.statusCode = snapshotResponse.status;
709+
snapshotResponse.headers.forEach((value, key) => response.setHeader(key, value));
710+
snapshotResponse.text().then(text => response.end(text));
711+
});
711712
return true;
712713
});
713714

packages/playwright-core/src/utils/isomorphic/lruCache.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class LRUCache<K, V> {
2525
this._size = 0;
2626
}
2727

28-
getOrCompute(key: K, compute: () => { value: V, size: number }): V {
28+
async getOrCompute(key: K, compute: () => Promise<{ value: V, size: number }>): Promise<V> {
2929
if (this._map.has(key)) {
3030
const result = this._map.get(key)!;
3131
// reinserting makes this the least recently used entry
@@ -34,7 +34,7 @@ export class LRUCache<K, V> {
3434
return result.value;
3535
}
3636

37-
const result = compute();
37+
const result = await compute();
3838

3939
while (this._map.size && this._size + result.size > this._maxSize) {
4040
const [firstKey, firstValue] = this._map.entries().next().value!;

packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ export class SnapshotRenderer {
4646
private _snapshot: FrameSnapshot;
4747
private _callId: string;
4848
private _screencastFrames: PageEntry['screencastFrames'];
49+
private _readSha1: (sha1: string) => Promise<Blob | undefined>;
4950

50-
constructor(htmlCache: LRUCache<SnapshotRenderer, string>, resources: ResourceSnapshot[], snapshots: FrameSnapshot[], screencastFrames: PageEntry['screencastFrames'], index: number) {
51+
constructor(htmlCache: LRUCache<SnapshotRenderer, string>, readSha1: (sha1: string) => Promise<Blob | undefined>, resources: ResourceSnapshot[], snapshots: FrameSnapshot[], screencastFrames: PageEntry['screencastFrames'], index: number) {
5152
this._htmlCache = htmlCache;
53+
this._readSha1 = readSha1;
5254
this._resources = resources;
5355
this._snapshots = snapshots;
5456
this._index = index;
@@ -74,9 +76,9 @@ export class SnapshotRenderer {
7476
return closestFrame?.sha1;
7577
}
7678

77-
render(): RenderedFrameSnapshot {
79+
async render(): Promise<RenderedFrameSnapshot> {
7880
const result: string[] = [];
79-
const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined) => {
81+
const visit = async (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined) => {
8082
// Text node.
8183
if (typeof n === 'string') {
8284
// Best-effort Electron support: rewrite custom protocol in url() links in stylesheets.
@@ -92,7 +94,12 @@ export class SnapshotRenderer {
9294
// Node reference.
9395
const referenceIndex = snapshotIndex - n[0][0];
9496
if (referenceIndex >= 0 && referenceIndex <= snapshotIndex) {
95-
const nodes = snapshotNodes(this._snapshots[referenceIndex]);
97+
const refSnapshot = this._snapshots[referenceIndex];
98+
let nodes: NodeSnapshot[] = (refSnapshot as any)._nodes;
99+
if (!nodes) {
100+
nodes = snapshotNodes(await this._ensureHtml(refSnapshot));
101+
(refSnapshot as any)._nodes = nodes;
102+
}
96103
const nodeIndex = n[0][1];
97104
if (nodeIndex >= 0 && nodeIndex < nodes.length)
98105
return visit(nodes[nodeIndex], referenceIndex, parentTag, parentAttrs);
@@ -134,7 +141,7 @@ export class SnapshotRenderer {
134141
}
135142
result.push('>');
136143
for (const child of children)
137-
visit(child, snapshotIndex, nodeName, attrs);
144+
await visit(child, snapshotIndex, nodeName, attrs);
138145
if (!autoClosing.has(nodeName))
139146
result.push('</', nodeName, '>');
140147
return;
@@ -145,8 +152,8 @@ export class SnapshotRenderer {
145152
};
146153

147154
const snapshot = this._snapshot;
148-
const html = this._htmlCache.getOrCompute(this, () => {
149-
visit(snapshot.html, this._index, undefined, undefined);
155+
const html = await this._htmlCache.getOrCompute(this, async () => {
156+
await visit(await this._ensureHtml(snapshot), this._index, undefined, undefined);
150157
const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : '';
151158
const html = prefix + [
152159
// Hide the document in order to prevent flickering. We will unhide once script has processed shadow.
@@ -159,6 +166,19 @@ export class SnapshotRenderer {
159166
return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index };
160167
}
161168

169+
private async _ensureHtml(snapshot: FrameSnapshot): Promise<NodeSnapshot> {
170+
if (!snapshot.html) {
171+
try {
172+
const blob = await this._readSha1(snapshot.sha1!);
173+
const text = await blob!.text();
174+
snapshot.html = JSON.parse(text);
175+
} catch {
176+
snapshot.html = ['html'];
177+
}
178+
}
179+
return snapshot.html!;
180+
}
181+
162182
resourceByUrl(url: string, method: string): ResourceSnapshot | undefined {
163183
const snapshot = this._snapshot;
164184
let sameFrameResource: ResourceSnapshot | undefined;
@@ -193,12 +213,12 @@ export class SnapshotRenderer {
193213
let result = sameFrameResource ?? otherFrameResource;
194214
if (result && method.toUpperCase() === 'GET') {
195215
// Patch override if necessary.
196-
let override = snapshot.resourceOverrides.find(o => o.url === url);
216+
let override = snapshot.resourceOverrides?.find(o => o.url === url);
197217
if (override?.ref) {
198218
// "ref" means use the same content as "ref" snapshots ago.
199219
const index = this._index - override.ref;
200220
if (index >= 0 && index < this._snapshots.length)
201-
override = this._snapshots[index].resourceOverrides.find(o => o.url === url);
221+
override = this._snapshots[index].resourceOverrides?.find(o => o.url === url);
202222
}
203223
if (override?.sha1) {
204224
result = {
@@ -220,23 +240,20 @@ export class SnapshotRenderer {
220240

221241
const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
222242

223-
function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
224-
if (!(snapshot as any)._nodes) {
225-
const nodes: NodeSnapshot[] = [];
226-
const visit = (n: NodeSnapshot) => {
227-
if (typeof n === 'string') {
228-
nodes.push(n);
229-
} else if (isNodeNameAttributesChildNodesSnapshot(n)) {
230-
const [,, ...children] = n;
231-
for (const child of children)
232-
visit(child);
233-
nodes.push(n);
234-
}
235-
};
236-
visit(snapshot.html);
237-
(snapshot as any)._nodes = nodes;
238-
}
239-
return (snapshot as any)._nodes;
243+
function snapshotNodes(html: NodeSnapshot): NodeSnapshot[] {
244+
const nodes: NodeSnapshot[] = [];
245+
const visit = (n: NodeSnapshot) => {
246+
if (typeof n === 'string') {
247+
nodes.push(n);
248+
} else if (isNodeNameAttributesChildNodesSnapshot(n)) {
249+
const [,, ...children] = n;
250+
for (const child of children)
251+
visit(child);
252+
nodes.push(n);
253+
}
254+
};
255+
visit(html);
256+
return nodes;
240257
}
241258

242259
type ViewportSize = { width: number, height: number };

packages/playwright-core/src/utils/isomorphic/trace/snapshotServer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ export class SnapshotServer {
2929
this._resourceLoader = resourceLoader;
3030
}
3131

32-
serveSnapshot(pageOrFrameId: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
32+
async serveSnapshot(pageOrFrameId: string, searchParams: URLSearchParams, snapshotUrl: string): Promise<Response> {
3333
const snapshot = this._snapshot(pageOrFrameId, searchParams);
3434
if (!snapshot)
3535
return new Response(null, { status: 404 });
3636

37-
const renderedSnapshot = snapshot.render();
37+
const renderedSnapshot = await snapshot.render();
3838
this._snapshotIds.set(snapshotUrl, snapshot);
3939
return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
4040
}

packages/playwright-core/src/utils/isomorphic/trace/snapshotStorage.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@ export class SnapshotStorage {
2929
private _cache = new LRUCache<SnapshotRenderer, string>(100_000_000); // 100MB per each trace
3030
private _contextToResources = new Map<string, ResourceSnapshot[]>();
3131
private _resourceUrlsWithOverrides = new Set<string>();
32+
private _readSha1: (sha1: string) => Promise<Blob | undefined>;
33+
34+
constructor(readSha1: (sha1: string) => Promise<Blob | undefined>) {
35+
this._readSha1 = readSha1;
36+
}
3237

3338
addResource(contextId: string, resource: ResourceSnapshot): void {
3439
resource.request.url = rewriteURLForCustomProtocol(resource.request.url);
3540
this._ensureResourcesForContext(contextId).push(resource);
3641
}
3742

3843
addFrameSnapshot(contextId: string, snapshot: FrameSnapshot, screencastFrames: PageEntry['screencastFrames']) {
39-
for (const override of snapshot.resourceOverrides)
44+
for (const override of snapshot.resourceOverrides ?? [])
4045
override.url = rewriteURLForCustomProtocol(override.url);
4146
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
4247
if (!frameSnapshots) {
@@ -50,7 +55,7 @@ export class SnapshotStorage {
5055
}
5156
frameSnapshots.raw.push(snapshot);
5257
const resources = this._ensureResourcesForContext(contextId);
53-
const renderer = new SnapshotRenderer(this._cache, resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1);
58+
const renderer = new SnapshotRenderer(this._cache, this._readSha1, resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1);
5459
frameSnapshots.renderers.push(renderer);
5560
return renderer;
5661
}
@@ -72,7 +77,7 @@ export class SnapshotStorage {
7277
// while serving snapshots with different override values.
7378
for (const frameSnapshots of this._frameSnapshots.values()) {
7479
for (const snapshot of frameSnapshots.raw) {
75-
for (const override of snapshot.resourceOverrides)
80+
for (const override of snapshot.resourceOverrides ?? [])
7681
this._resourceUrlsWithOverrides.add(override.url);
7782
}
7883
}

packages/playwright-core/src/utils/isomorphic/trace/traceLoader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class TraceLoader {
5353
if (!ordinals.length)
5454
throw new Error('Cannot find .trace file');
5555

56-
this._snapshotStorage = new SnapshotStorage();
56+
this._snapshotStorage = new SnapshotStorage(sha1 => this._backend.readBlob('resources/' + sha1));
5757

5858
// 3 * ordinals progress increments below.
5959
const total = ordinals.length * 3;

packages/playwright-core/src/utils/isomorphic/trace/traceModernizer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export class TraceVersionError extends Error {
3333

3434
// 6 => 10/2023 ~1.40
3535
// 7 => 05/2024 ~1.45
36-
const latestVersion: trace.VERSION = 8;
36+
// 8 => 03/2026 ~1.58
37+
const latestVersion: trace.VERSION = 9;
3738

3839
export class TraceModernizer {
3940
private _contextEntry: ContextEntry;
@@ -438,4 +439,8 @@ export class TraceModernizer {
438439
}
439440
return result;
440441
}
442+
443+
_modernize_8_to_9(events: traceV8.TraceEvent[]): trace.TraceEvent[] {
444+
return events as trace.TraceEvent[];
445+
}
441446
}

packages/playwright/src/worker/testTracing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import type EventEmitter from 'events';
3131

3232
export type Attachment = TestInfo['attachments'][0];
3333
export const testTraceEntryName = 'test.trace';
34-
const version: trace.VERSION = 8;
34+
const version: trace.VERSION = 9;
3535
let traceOrdinal = 0;
3636

3737
type TraceFixtureValue = PlaywrightWorkerOptions['trace'] | undefined;

0 commit comments

Comments
 (0)