Skip to content

Commit 0380400

Browse files
authored
chore: refactor waiting for lifecycle events (#2851)
Instead of checking lifecycle events on every change, we notify precisely when lifecycle event in the subtree is satisfied. This allows FrameTask to be later switched to event-based approach, and will easily translate to the rpc client.
1 parent db3439d commit 0380400

File tree

1 file changed

+56
-45
lines changed

1 file changed

+56
-45
lines changed

src/frames.ts

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class FrameManager {
160160
frame._pendingDocument = undefined;
161161
for (const task of frame._frameTasks)
162162
task.onNewDocument(frame._currentDocument);
163-
this.clearFrameLifecycle(frame);
163+
frame._onClearLifecycle();
164164
if (!initial)
165165
this._page.emit(Events.Page.FrameNavigated, frame);
166166
}
@@ -200,25 +200,8 @@ export class FrameManager {
200200

201201
frameLifecycleEvent(frameId: string, event: types.LifecycleEvent) {
202202
const frame = this._frames.get(frameId);
203-
if (!frame)
204-
return;
205-
if (frame._firedLifecycleEvents.has(event))
206-
return;
207-
frame._firedLifecycleEvents.add(event);
208-
this._notifyLifecycle(frame, event);
209-
if (frame === this._mainFrame && event === 'load')
210-
this._page.emit(Events.Page.Load);
211-
if (frame === this._mainFrame && event === 'domcontentloaded')
212-
this._page.emit(Events.Page.DOMContentLoaded);
213-
}
214-
215-
clearFrameLifecycle(frame: Frame) {
216-
frame._firedLifecycleEvents.clear();
217-
// Keep the current navigation request if any.
218-
frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request === frame._currentDocument.request));
219-
frame._stopNetworkIdleTimer();
220-
if (frame._inflightRequests.size === 0)
221-
frame._startNetworkIdleTimer();
203+
if (frame)
204+
frame._onLifecycleEvent(event);
222205
}
223206

224207
requestStarted(request: network.Request) {
@@ -259,13 +242,6 @@ export class FrameManager {
259242
this._page.emit(Events.Page.RequestFailed, request);
260243
}
261244

262-
private _notifyLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) {
263-
for (let parent: Frame | null = frame; parent; parent = parent.parentFrame()) {
264-
for (const frameTask of parent._frameTasks)
265-
frameTask.onLifecycle(frame, lifecycleEvent);
266-
}
267-
}
268-
269245
removeChildFramesRecursively(frame: Frame) {
270246
for (const child of frame.childFrames())
271247
this._removeFramesRecursively(child);
@@ -313,7 +289,8 @@ export class FrameManager {
313289

314290
export class Frame {
315291
_id: string;
316-
readonly _firedLifecycleEvents: Set<types.LifecycleEvent>;
292+
private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
293+
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
317294
_currentDocument: DocumentInfo;
318295
_pendingDocument?: DocumentInfo;
319296
_frameTasks = new Set<FrameTask>();
@@ -332,7 +309,6 @@ export class Frame {
332309

333310
constructor(page: Page, id: string, parentFrame: Frame | null) {
334311
this._id = id;
335-
this._firedLifecycleEvents = new Set();
336312
this._page = page;
337313
this._parentFrame = parentFrame;
338314
this._currentDocument = { documentId: undefined, request: undefined };
@@ -353,6 +329,51 @@ export class Frame {
353329
return `${subject}.${method}`;
354330
}
355331

332+
_onLifecycleEvent(event: types.LifecycleEvent) {
333+
if (this._firedLifecycleEvents.has(event))
334+
return;
335+
this._firedLifecycleEvents.add(event);
336+
// Recalculate subtree lifecycle for the whole tree - it should not be that big.
337+
this._page.mainFrame()._recalculateLifecycle();
338+
}
339+
340+
_onClearLifecycle() {
341+
this._firedLifecycleEvents.clear();
342+
// Recalculate subtree lifecycle for the whole tree - it should not be that big.
343+
this._page.mainFrame()._recalculateLifecycle();
344+
// Keep the current navigation request if any.
345+
this._inflightRequests = new Set(Array.from(this._inflightRequests).filter(request => request === this._currentDocument.request));
346+
this._stopNetworkIdleTimer();
347+
if (this._inflightRequests.size === 0)
348+
this._startNetworkIdleTimer();
349+
}
350+
351+
private _recalculateLifecycle() {
352+
const events = new Set<types.LifecycleEvent>(this._firedLifecycleEvents);
353+
for (const child of this._childFrames) {
354+
child._recalculateLifecycle();
355+
// We require a particular lifecycle event to be fired in the whole
356+
// frame subtree, and then consider it done.
357+
for (const event of events) {
358+
if (!child._subtreeLifecycleEvents.has(event))
359+
events.delete(event);
360+
}
361+
}
362+
const mainFrame = this._page.mainFrame();
363+
for (const event of events) {
364+
// Checking whether we have already notified about this event.
365+
if (!this._subtreeLifecycleEvents.has(event)) {
366+
for (const frameTask of this._frameTasks)
367+
frameTask.onLifecycle(event);
368+
if (this === mainFrame && event === 'load')
369+
this._page.emit(Events.Page.Load);
370+
if (this === mainFrame && event === 'domcontentloaded')
371+
this._page.emit(Events.Page.DOMContentLoaded);
372+
}
373+
}
374+
this._subtreeLifecycleEvents = events;
375+
}
376+
356377
async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
357378
return runNavigationTask(this, options, this._apiName('goto'), async progress => {
358379
progress.logger.info(`navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`);
@@ -553,7 +574,7 @@ export class Frame {
553574
const lifecyclePromise = new Promise((resolve, reject) => {
554575
this._page._frameManager._consoleMessageTags.set(tag, () => {
555576
// Clear lifecycle right after document.open() - see 'tag' below.
556-
this._page._frameManager.clearFrameLifecycle(this);
577+
this._onClearLifecycle();
557578
this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject);
558579
});
559580
});
@@ -939,7 +960,7 @@ export class Frame {
939960
assert(!this._networkIdleTimer);
940961
if (this._firedLifecycleEvents.has('networkidle'))
941962
return;
942-
this._networkIdleTimer = setTimeout(() => { this._page._frameManager.frameLifecycleEvent(this._id, 'networkidle'); }, 500);
963+
this._networkIdleTimer = setTimeout(() => this._onLifecycleEvent('networkidle'), 500);
943964
}
944965

945966
_stopNetworkIdleTimer() {
@@ -1081,10 +1102,10 @@ class FrameTask {
10811102
}
10821103
}
10831104

1084-
onLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) {
1085-
if (this._progress && frame === this._frame && frame._url !== 'about:blank')
1105+
onLifecycle(lifecycleEvent: types.LifecycleEvent) {
1106+
if (this._progress && this._frame._url !== 'about:blank')
10861107
this._progress.logger.info(`"${lifecycleEvent}" event fired`);
1087-
if (this._onLifecycle && this._checkLifecycleRecursively(this._frame, this._onLifecycle.waitUntil))
1108+
if (this._onLifecycle && this._onLifecycle.waitUntil === lifecycleEvent)
10881109
this._onLifecycle.resolve();
10891110
}
10901111

@@ -1114,24 +1135,14 @@ class FrameTask {
11141135
waitUntil = 'networkidle';
11151136
if (!types.kLifecycleEvents.has(waitUntil))
11161137
throw new Error(`Unsupported waitUntil option ${String(waitUntil)}`);
1117-
if (this._checkLifecycleRecursively(this._frame, waitUntil))
1138+
if (this._frame._subtreeLifecycleEvents.has(waitUntil))
11181139
return Promise.resolve();
11191140
return new Promise(resolve => {
11201141
assert(!this._onLifecycle);
11211142
this._onLifecycle = { waitUntil, resolve };
11221143
});
11231144
}
11241145

1125-
private _checkLifecycleRecursively(frame: Frame, waitUntil: types.LifecycleEvent): boolean {
1126-
if (!frame._firedLifecycleEvents.has(waitUntil))
1127-
return false;
1128-
for (const child of frame.childFrames()) {
1129-
if (!this._checkLifecycleRecursively(child, waitUntil))
1130-
return false;
1131-
}
1132-
return true;
1133-
}
1134-
11351146
done() {
11361147
this._frame._frameTasks.delete(this);
11371148
}

0 commit comments

Comments
 (0)