Skip to content

Commit 922d9ce

Browse files
authored
chore(tracing): fix some of the start/stop scenarios (#6337)
1 parent abb6145 commit 922d9ce

File tree

10 files changed

+133
-144
lines changed

10 files changed

+133
-144
lines changed

src/server/browserContext.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import * as types from './types';
2929
import path from 'path';
3030
import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation';
3131
import { Debugger } from './supplements/debugger';
32-
import { Tracer } from './trace/recorder/tracer';
32+
import { Tracing } from './trace/recorder/tracing';
3333
import { HarTracer } from './supplements/har/harTracer';
3434
import { RecorderSupplement } from './supplements/recorderSupplement';
3535
import * as consoleApiSource from '../generated/consoleApiSource';
@@ -57,7 +57,7 @@ export abstract class BrowserContext extends SdkObject {
5757
private _selectors?: Selectors;
5858
private _origins = new Set<string>();
5959
private _harTracer: HarTracer | undefined;
60-
readonly tracing: Tracer;
60+
readonly tracing: Tracing;
6161

6262
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
6363
super(browser, 'browser-context');
@@ -70,7 +70,7 @@ export abstract class BrowserContext extends SdkObject {
7070

7171
if (this._options.recordHar)
7272
this._harTracer = new HarTracer(this, this._options.recordHar);
73-
this.tracing = new Tracer(this);
73+
this.tracing = new Tracing(this);
7474
}
7575

7676
_setSelectors(selectors: Selectors) {
@@ -264,7 +264,7 @@ export abstract class BrowserContext extends SdkObject {
264264
this._closedStatus = 'closing';
265265

266266
await this._harTracer?.flush();
267-
await this.tracing.stop();
267+
await this.tracing.dispose();
268268

269269
// Cleanup.
270270
const promises: Promise<void>[] = [];

src/server/frames.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export class FrameManager {
7171
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
7272
readonly _signalBarriers = new Set<SignalBarrier>();
7373
private _webSockets = new Map<string, network.WebSocket>();
74+
readonly _responses: network.Response[] = [];
7475

7576
constructor(page: Page) {
7677
this._page = page;
@@ -198,6 +199,7 @@ export class FrameManager {
198199
frame._onClearLifecycle();
199200
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
200201
frame.emit(Frame.Events.Navigation, navigationEvent);
202+
this._responses.length = 0;
201203
if (!initial) {
202204
debugLogger.log('api', ` navigated to "${url}"`);
203205
this._page.frameNavigatedToNewDocument(frame);
@@ -264,8 +266,10 @@ export class FrameManager {
264266
}
265267

266268
requestReceivedResponse(response: network.Response) {
267-
if (!response.request()._isFavicon)
268-
this._page.emit(Page.Events.Response, response);
269+
if (response.request()._isFavicon)
270+
return;
271+
this._responses.push(response);
272+
this._page.emit(Page.Events.Response, response);
269273
}
270274

271275
requestFinished(request: network.Request) {

src/server/snapshot/inMemorySnapshotter.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
3838
}
3939

4040
async initialize(): Promise<string> {
41-
await this._snapshotter.initialize();
41+
await this._snapshotter.start();
4242
return await this._server.start();
4343
}
4444

@@ -62,10 +62,6 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
6262
});
6363
}
6464

65-
async setAutoSnapshotIntervalForTest(interval: number): Promise<void> {
66-
await this._snapshotter.setAutoSnapshotInterval(interval);
67-
}
68-
6965
onBlob(blob: SnapshotterBlob): void {
7066
this._blobs.set(blob.sha1, blob.buffer);
7167
}

src/server/snapshot/snapshotter.ts

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,46 @@ export class Snapshotter {
4040
private _context: BrowserContext;
4141
private _delegate: SnapshotterDelegate;
4242
private _eventListeners: RegisteredListener[] = [];
43-
private _interval = 0;
4443
private _snapshotStreamer: string;
4544
private _snapshotBinding: string;
45+
private _initialized = false;
46+
private _started = false;
47+
private _fetchedResponses = new Map<network.Response, string>();
4648

4749
constructor(context: BrowserContext, delegate: SnapshotterDelegate) {
4850
this._context = context;
4951
this._delegate = delegate;
50-
for (const page of context.pages())
51-
this._onPage(page);
52-
this._eventListeners = [
53-
helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
54-
];
5552
const guid = createGuid();
5653
this._snapshotStreamer = '__playwright_snapshot_streamer_' + guid;
5754
this._snapshotBinding = '__playwright_snapshot_binding_' + guid;
5855
}
5956

60-
async initialize() {
57+
async start() {
58+
this._started = true;
59+
if (!this._initialized) {
60+
this._initialized = true;
61+
await this._initialize();
62+
}
63+
this._runInAllFrames(`window["${this._snapshotStreamer}"].reset()`);
64+
65+
// Replay resources loaded in all pages.
66+
for (const page of this._context.pages()) {
67+
for (const response of page._frameManager._responses)
68+
this._saveResource(page, response).catch(e => debugLogger.log('error', e));
69+
}
70+
}
71+
72+
async stop() {
73+
this._started = false;
74+
}
75+
76+
async _initialize() {
77+
for (const page of this._context.pages())
78+
this._onPage(page);
79+
this._eventListeners = [
80+
helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
81+
];
82+
6183
await this._context.exposeBinding(this._snapshotBinding, false, (source, data: SnapshotData) => {
6284
const snapshot: FrameSnapshot = {
6385
snapshotName: data.snapshotName,
@@ -87,11 +109,15 @@ export class Snapshotter {
87109
});
88110
const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", "${this._snapshotBinding}")`;
89111
await this._context._doAddInitScript(initScript);
112+
this._runInAllFrames(initScript);
113+
}
114+
115+
private _runInAllFrames(expression: string) {
90116
const frames = [];
91117
for (const page of this._context.pages())
92118
frames.push(...page.frames());
93119
frames.map(frame => {
94-
frame._existingMainContext()?.rawEvaluate(initScript).catch(debugExceptionHandler);
120+
frame._existingMainContext()?.rawEvaluate(expression).catch(debugExceptionHandler);
95121
});
96122
}
97123

@@ -112,37 +138,20 @@ export class Snapshotter {
112138
page.frames().map(frame => snapshotFrame(frame));
113139
}
114140

115-
async setAutoSnapshotInterval(interval: number): Promise<void> {
116-
this._interval = interval;
117-
const frames = [];
118-
for (const page of this._context.pages())
119-
frames.push(...page.frames());
120-
await Promise.all(frames.map(frame => this._setIntervalInFrame(frame, interval)));
121-
}
122-
123141
private _onPage(page: Page) {
124-
const processNewFrame = (frame: Frame) => {
125-
this._annotateFrameHierarchy(frame);
126-
this._setIntervalInFrame(frame, this._interval);
127-
const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", "${this._snapshotBinding}")`;
128-
frame._existingMainContext()?.rawEvaluate(initScript).catch(debugExceptionHandler);
129-
};
142+
// Annotate frame hierarchy so that snapshots could include frame ids.
130143
for (const frame of page.frames())
131-
processNewFrame(frame);
132-
this._eventListeners.push(helper.addEventListener(page, Page.Events.FrameAttached, processNewFrame));
133-
134-
// Push streamer interval on navigation.
135-
this._eventListeners.push(helper.addEventListener(page, Page.Events.InternalFrameNavigatedToNewDocument, frame => {
136-
this._setIntervalInFrame(frame, this._interval);
137-
}));
144+
this._annotateFrameHierarchy(frame);
145+
this._eventListeners.push(helper.addEventListener(page, Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame)));
138146

139-
// Capture resources.
140147
this._eventListeners.push(helper.addEventListener(page, Page.Events.Response, (response: network.Response) => {
141148
this._saveResource(page, response).catch(e => debugLogger.log('error', e));
142149
}));
143150
}
144151

145152
private async _saveResource(page: Page, response: network.Response) {
153+
if (!this._started)
154+
return;
146155
const isRedirect = response.status() >= 300 && response.status() <= 399;
147156
if (isRedirect)
148157
return;
@@ -163,9 +172,25 @@ export class Snapshotter {
163172
const status = response.status();
164173
const requestBody = original.postDataBuffer();
165174
const requestSha1 = requestBody ? calculateSha1(requestBody) : '';
175+
if (requestBody)
176+
this._delegate.onBlob({ sha1: requestSha1, buffer: requestBody });
166177
const requestHeaders = original.headers();
167-
const body = await response.body().catch(e => debugLogger.log('error', e));
168-
const responseSha1 = body ? calculateSha1(body) : '';
178+
179+
// Only fetch response bodies once.
180+
let responseSha1 = this._fetchedResponses.get(response);
181+
{
182+
if (responseSha1 === undefined) {
183+
const body = await response.body().catch(e => debugLogger.log('error', e));
184+
// Bail out after each async hop.
185+
if (!this._started)
186+
return;
187+
responseSha1 = body ? calculateSha1(body) : '';
188+
if (body)
189+
this._delegate.onBlob({ sha1: responseSha1, buffer: body });
190+
this._fetchedResponses.set(response, responseSha1);
191+
}
192+
}
193+
169194
const resource: ResourceSnapshot = {
170195
pageId: page.guid,
171196
frameId: response.frame().guid,
@@ -181,17 +206,6 @@ export class Snapshotter {
181206
timestamp: monotonicTime()
182207
};
183208
this._delegate.onResourceSnapshot(resource);
184-
if (requestBody)
185-
this._delegate.onBlob({ sha1: requestSha1, buffer: requestBody });
186-
if (body)
187-
this._delegate.onBlob({ sha1: responseSha1, buffer: body });
188-
}
189-
190-
private async _setIntervalInFrame(frame: Frame, interval: number) {
191-
const context = frame._existingMainContext();
192-
await context?.evaluate(({ snapshotStreamer, interval }) => {
193-
(window as any)[snapshotStreamer].setSnapshotInterval(interval);
194-
}, { snapshotStreamer: this._snapshotStreamer, interval }).catch(debugExceptionHandler);
195209
}
196210

197211
private async _annotateFrameHierarchy(frame: Frame) {

src/server/snapshot/snapshotterInjected.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
5151
cssText?: string, // Text for stylesheets.
5252
cssRef?: number, // Previous snapshotNumber for overridden stylesheets.
5353
};
54+
55+
function resetCachedData(obj: any) {
56+
delete obj[kCachedData];
57+
}
58+
5459
function ensureCachedData(obj: any): CachedData {
5560
if (!obj[kCachedData])
5661
obj[kCachedData] = {};
@@ -69,14 +74,11 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
6974

7075
class Streamer {
7176
private _removeNoScript = true;
72-
private _timer: NodeJS.Timeout | undefined;
7377
private _lastSnapshotNumber = 0;
7478
private _staleStyleSheets = new Set<CSSStyleSheet>();
75-
private _allStyleSheetsWithUrlOverride = new Set<CSSStyleSheet>();
7679
private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
7780
private _fakeBase: HTMLBaseElement;
7881
private _observer: MutationObserver;
79-
private _interval = 0;
8082

8183
constructor() {
8284
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
@@ -125,8 +127,6 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
125127
if (this._readingStyleSheet)
126128
return;
127129
this._staleStyleSheets.add(sheet);
128-
if (sheet.href !== null)
129-
this._allStyleSheetsWithUrlOverride.add(sheet);
130130
}
131131

132132
private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet): string | undefined {
@@ -162,29 +162,29 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
162162
(iframeElement as any)[kSnapshotFrameId] = frameId;
163163
}
164164

165-
captureSnapshot(snapshotName?: string) {
166-
this._streamSnapshot(snapshotName);
167-
}
165+
reset() {
166+
this._staleStyleSheets.clear();
168167

169-
setSnapshotInterval(interval: number) {
170-
this._interval = interval;
171-
if (interval)
172-
this._streamSnapshot();
168+
const visitNode = (node: Node | ShadowRoot) => {
169+
resetCachedData(node);
170+
if (node.nodeType === Node.ELEMENT_NODE) {
171+
const element = node as Element;
172+
if (element.shadowRoot)
173+
visitNode(element.shadowRoot);
174+
}
175+
for (let child = node.firstChild; child; child = child.nextSibling)
176+
visitNode(child);
177+
};
178+
visitNode(document.documentElement);
173179
}
174180

175-
private _streamSnapshot(snapshotName?: string) {
176-
if (this._timer) {
177-
clearTimeout(this._timer);
178-
this._timer = undefined;
179-
}
181+
captureSnapshot(snapshotName: string) {
180182
try {
181183
const snapshot = this._captureSnapshot(snapshotName);
182184
if (snapshot)
183185
(window as any)[snapshotBinding](snapshot);
184186
} catch (e) {
185187
}
186-
if (this._interval)
187-
this._timer = setTimeout(() => this._streamSnapshot(), this._interval);
188188
}
189189

190190
private _sanitizeUrl(url: string): string {
@@ -298,11 +298,11 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
298298
const result: NodeSnapshot = [nodeName, attrs];
299299

300300
const visitChild = (child: Node) => {
301-
const snapshotted = visitNode(child);
302-
if (snapshotted) {
303-
result.push(snapshotted.n);
301+
const snapshot = visitNode(child);
302+
if (snapshot) {
303+
result.push(snapshot.n);
304304
expectValue(child);
305-
equals = equals && snapshotted.equals;
305+
equals = equals && snapshot.equals;
306306
}
307307
};
308308

@@ -432,10 +432,12 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
432432
};
433433

434434
let allOverridesAreRefs = true;
435-
for (const sheet of this._allStyleSheetsWithUrlOverride) {
435+
for (const sheet of this._staleStyleSheets) {
436+
if (sheet.href === null)
437+
continue;
436438
const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
437439
if (content === undefined) {
438-
// Unable to capture stylsheet contents.
440+
// Unable to capture stylesheet contents.
439441
continue;
440442
}
441443
if (typeof content !== 'number')

src/server/trace/recorder/traceSnapshotter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class TraceSnapshotter extends EventEmitter implements SnapshotterDelegat
4343
}
4444

4545
async start(): Promise<void> {
46-
await this._snapshotter.initialize();
46+
await this._snapshotter.start();
4747
}
4848

4949
async dispose() {

0 commit comments

Comments
 (0)