Skip to content

Commit 434f474

Browse files
authored
chore(evaluate): implement non-stalling evaluate (#6354)
1 parent 06a9268 commit 434f474

File tree

14 files changed

+252
-142
lines changed

14 files changed

+252
-142
lines changed

src/dispatchers/dispatcher.ts

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export class DispatcherConnection {
217217
}
218218

219219
const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
220-
let callMetadata: CallMetadata = {
220+
const callMetadata: CallMetadata = {
221221
id: `call@${id}`,
222222
...validMetadata,
223223
objectId: sdkObject?.guid,
@@ -232,59 +232,52 @@ export class DispatcherConnection {
232232
snapshots: []
233233
};
234234

235-
try {
236-
if (sdkObject) {
237-
// Process logs for waitForNavigation/waitForLoadState
238-
if (params?.info?.waitId) {
239-
const info = params.info;
240-
switch (info.phase) {
241-
case 'before':
242-
callMetadata.apiName = info.apiName;
243-
this._waitOperations.set(info.waitId, callMetadata);
244-
break;
245-
case 'log':
246-
const originalMetadata = this._waitOperations.get(info.waitId)!;
247-
originalMetadata.log.push(info.message);
248-
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
249-
// Fall through.
250-
case 'after':
251-
return;
252-
}
235+
if (sdkObject && params?.info?.waitId) {
236+
// Process logs for waitForNavigation/waitForLoadState
237+
const info = params.info;
238+
switch (info.phase) {
239+
case 'before': {
240+
callMetadata.apiName = info.apiName;
241+
this._waitOperations.set(info.waitId, callMetadata);
242+
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
243+
return;
244+
} case 'log': {
245+
const originalMetadata = this._waitOperations.get(info.waitId)!;
246+
originalMetadata.log.push(info.message);
247+
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
248+
return;
249+
} case 'after': {
250+
const originalMetadata = this._waitOperations.get(info.waitId)!;
251+
originalMetadata.endTime = monotonicTime();
252+
originalMetadata.error = info.error;
253+
this._waitOperations.delete(info.waitId);
254+
await sdkObject.instrumentation.onAfterCall(sdkObject, originalMetadata);
255+
return;
253256
}
254-
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
255257
}
256-
const result = await (dispatcher as any)[method](validParams, callMetadata);
257-
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
258+
}
259+
260+
261+
let result: any;
262+
let error: any;
263+
await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata);
264+
try {
265+
result = await (dispatcher as any)[method](validParams, callMetadata);
258266
} catch (e) {
259267
// Dispatching error
260268
callMetadata.error = e.message;
261269
if (callMetadata.log.length)
262270
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
263-
this.onmessage({ id, error: serializeError(e) });
271+
error = serializeError(e);
264272
} finally {
265273
callMetadata.endTime = monotonicTime();
266-
if (sdkObject) {
267-
// Process logs for waitForNavigation/waitForLoadState
268-
if (params?.info?.waitId) {
269-
const info = params.info;
270-
switch (info.phase) {
271-
case 'before':
272-
callMetadata.endTime = 0;
273-
// Fall through.
274-
case 'log':
275-
return;
276-
case 'after':
277-
const originalMetadata = this._waitOperations.get(info.waitId)!;
278-
originalMetadata.endTime = callMetadata.endTime;
279-
originalMetadata.error = info.error;
280-
this._waitOperations.delete(info.waitId);
281-
callMetadata = originalMetadata;
282-
break;
283-
}
284-
}
285-
await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
286-
}
274+
await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata);
287275
}
276+
277+
if (error)
278+
this.onmessage({ id, error });
279+
else
280+
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
288281
}
289282

290283
private _replaceDispatchersWithGuids(payload: any): any {

src/server/chromium/crExecutionContext.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,18 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
3131
this._contextId = contextPayload.id;
3232
}
3333

34-
async rawEvaluate(expression: string): Promise<string> {
34+
async rawEvaluateJSON(expression: string): Promise<any> {
35+
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
36+
expression,
37+
contextId: this._contextId,
38+
returnByValue: true,
39+
}).catch(rewriteError);
40+
if (exceptionDetails)
41+
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
42+
return remoteObject.value;
43+
}
44+
45+
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
3546
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
3647
expression,
3748
contextId: this._contextId,

src/server/dom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
9898
);
9999
})();
100100
`;
101-
this._injectedScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId));
101+
this._injectedScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', objectId));
102102
}
103103
return this._injectedScriptPromise;
104104
}

src/server/firefox/ffExecutionContext.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
3030
this._executionContextId = executionContextId;
3131
}
3232

33-
async rawEvaluate(expression: string): Promise<string> {
33+
async rawEvaluateJSON(expression: string): Promise<any> {
34+
const payload = await this._session.send('Runtime.evaluate', {
35+
expression,
36+
returnByValue: true,
37+
executionContextId: this._executionContextId,
38+
}).catch(rewriteError);
39+
checkException(payload.exceptionDetails);
40+
return payload.result!.value;
41+
}
42+
43+
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
3444
const payload = await this._session.send('Runtime.evaluate', {
3545
expression,
3646
returnByValue: false,

src/server/frames.ts

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ export class FrameManager {
158158
return;
159159
for (const barrier of this._signalBarriers)
160160
barrier.addFrameNavigation(frame);
161-
if (frame._pendingDocument && frame._pendingDocument.documentId === documentId) {
161+
if (frame.pendingDocument() && frame.pendingDocument()!.documentId === documentId) {
162162
// Do not override request with undefined.
163163
return;
164164
}
165-
frame._pendingDocument = { documentId, request: undefined };
165+
frame.setPendingDocument({ documentId, request: undefined });
166166
}
167167

168168
frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) {
@@ -173,24 +173,25 @@ export class FrameManager {
173173
frame._name = name;
174174

175175
let keepPending: DocumentInfo | undefined;
176-
if (frame._pendingDocument) {
177-
if (frame._pendingDocument.documentId === undefined) {
176+
const pendingDocument = frame.pendingDocument();
177+
if (pendingDocument) {
178+
if (pendingDocument.documentId === undefined) {
178179
// Pending with unknown documentId - assume it is the one being committed.
179-
frame._pendingDocument.documentId = documentId;
180+
pendingDocument.documentId = documentId;
180181
}
181-
if (frame._pendingDocument.documentId === documentId) {
182+
if (pendingDocument.documentId === documentId) {
182183
// Committing a pending document.
183-
frame._currentDocument = frame._pendingDocument;
184+
frame._currentDocument = pendingDocument;
184185
} else {
185186
// Sometimes, we already have a new pending when the old one commits.
186187
// An example would be Chromium error page followed by a new navigation request,
187188
// where the error page commit arrives after Network.requestWillBeSent for the
188189
// new navigation.
189190
// We commit, but keep the pending request since it's not done yet.
190-
keepPending = frame._pendingDocument;
191+
keepPending = pendingDocument;
191192
frame._currentDocument = { documentId, request: undefined };
192193
}
193-
frame._pendingDocument = undefined;
194+
frame.setPendingDocument(undefined);
194195
} else {
195196
// No pending - just commit a new document.
196197
frame._currentDocument = { documentId, request: undefined };
@@ -205,7 +206,7 @@ export class FrameManager {
205206
this._page.frameNavigatedToNewDocument(frame);
206207
}
207208
// Restore pending if any - see comments above about keepPending.
208-
frame._pendingDocument = keepPending;
209+
frame.setPendingDocument(keepPending);
209210
}
210211

211212
frameCommittedSameDocumentNavigation(frameId: string, url: string) {
@@ -220,17 +221,17 @@ export class FrameManager {
220221

221222
frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) {
222223
const frame = this._frames.get(frameId);
223-
if (!frame || !frame._pendingDocument)
224+
if (!frame || !frame.pendingDocument())
224225
return;
225-
if (documentId !== undefined && frame._pendingDocument.documentId !== documentId)
226+
if (documentId !== undefined && frame.pendingDocument()!.documentId !== documentId)
226227
return;
227228
const navigationEvent: NavigationEvent = {
228229
url: frame._url,
229230
name: frame._name,
230-
newDocument: frame._pendingDocument,
231+
newDocument: frame.pendingDocument(),
231232
error: new Error(errorText),
232233
};
233-
frame._pendingDocument = undefined;
234+
frame.setPendingDocument(undefined);
234235
frame.emit(Frame.Events.Navigation, navigationEvent);
235236
}
236237

@@ -255,7 +256,7 @@ export class FrameManager {
255256
const frame = request.frame();
256257
this._inflightRequestStarted(request);
257258
if (request._documentId)
258-
frame._pendingDocument = { documentId: request._documentId, request };
259+
frame.setPendingDocument({ documentId: request._documentId, request });
259260
if (request._isFavicon) {
260261
const route = request._route();
261262
if (route)
@@ -281,11 +282,11 @@ export class FrameManager {
281282
requestFailed(request: network.Request, canceled: boolean) {
282283
const frame = request.frame();
283284
this._inflightRequestFinished(request);
284-
if (frame._pendingDocument && frame._pendingDocument.request === request) {
285+
if (frame.pendingDocument() && frame.pendingDocument()!.request === request) {
285286
let errorText = request.failure()!.errorText;
286287
if (canceled)
287288
errorText += '; maybe frame was detached?';
288-
this.frameAbortedNavigation(frame._id, errorText, frame._pendingDocument.documentId);
289+
this.frameAbortedNavigation(frame._id, errorText, frame.pendingDocument()!.documentId);
289290
}
290291
if (!request._isFavicon)
291292
this._page.emit(Page.Events.RequestFailed, request);
@@ -399,7 +400,7 @@ export class Frame extends SdkObject {
399400
private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
400401
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
401402
_currentDocument: DocumentInfo;
402-
_pendingDocument?: DocumentInfo;
403+
private _pendingDocument: DocumentInfo | undefined;
403404
readonly _page: Page;
404405
private _parentFrame: Frame | null;
405406
_url = '';
@@ -412,6 +413,7 @@ export class Frame extends SdkObject {
412413
private _setContentCounter = 0;
413414
readonly _detachedPromise: Promise<void>;
414415
private _detachedCallback = () => {};
416+
private _nonStallingEvaluations = new Set<(error: Error) => void>();
415417

416418
constructor(page: Page, id: string, parentFrame: Frame | null) {
417419
super(page, 'frame');
@@ -451,6 +453,44 @@ export class Frame extends SdkObject {
451453
this._startNetworkIdleTimer();
452454
}
453455

456+
setPendingDocument(documentInfo: DocumentInfo | undefined) {
457+
this._pendingDocument = documentInfo;
458+
if (documentInfo)
459+
this._invalidateNonStallingEvaluations();
460+
}
461+
462+
pendingDocument(): DocumentInfo | undefined {
463+
return this._pendingDocument;
464+
}
465+
466+
private async _invalidateNonStallingEvaluations() {
467+
if (!this._nonStallingEvaluations)
468+
return;
469+
const error = new Error('Navigation interrupted the evaluation');
470+
for (const callback of this._nonStallingEvaluations)
471+
callback(error);
472+
}
473+
474+
async nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
475+
if (this._pendingDocument)
476+
throw new Error('Frame is currently attempting a navigation');
477+
const context = this._existingMainContext();
478+
if (!context)
479+
throw new Error('Frame does not yet have a main execution context');
480+
481+
let callback = () => {};
482+
const frameInvalidated = new Promise<void>((f, r) => callback = r);
483+
this._nonStallingEvaluations.add(callback);
484+
try {
485+
return await Promise.race([
486+
context.rawEvaluateJSON(expression),
487+
frameInvalidated
488+
]);
489+
} finally {
490+
this._nonStallingEvaluations.delete(callback);
491+
}
492+
}
493+
454494
private _recalculateLifecycle() {
455495
const events = new Set<types.LifecycleEvent>(this._firedLifecycleEvents);
456496
for (const child of this._childFrames) {
@@ -584,7 +624,7 @@ export class Frame extends SdkObject {
584624
return this._context('main');
585625
}
586626

587-
_existingMainContext(): dom.FrameExecutionContext | null {
627+
private _existingMainContext(): dom.FrameExecutionContext | null {
588628
return this._contextData.get('main')?.context || null;
589629
}
590630

src/server/javascript.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export type FuncOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R |
4343
export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>;
4444

4545
export interface ExecutionContextDelegate {
46-
rawEvaluate(expression: string): Promise<ObjectId>;
46+
rawEvaluateJSON(expression: string): Promise<any>;
47+
rawEvaluateHandle(expression: string): Promise<ObjectId>;
4748
rawCallFunctionNoReply(func: Function, ...args: any[]): void;
4849
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
4950
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>;
@@ -75,7 +76,7 @@ export class ExecutionContext extends SdkObject {
7576
${utilityScriptSource.source}
7677
return new pwExport();
7778
})();`;
78-
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new JSHandle(this, 'object', objectId));
79+
this._utilityScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', objectId));
7980
}
8081
return this._utilityScriptPromise;
8182
}
@@ -84,9 +85,8 @@ export class ExecutionContext extends SdkObject {
8485
return this._delegate.createHandle(this, remoteObject);
8586
}
8687

87-
async rawEvaluate(expression: string): Promise<void> {
88-
// Make sure to never return a value.
89-
await this._delegate.rawEvaluate(expression + '; 0');
88+
async rawEvaluateJSON(expression: string): Promise<any> {
89+
return await this._delegate.rawEvaluateJSON(expression);
9090
}
9191

9292
async doSlowMo() {

src/server/snapshot/inMemorySnapshotter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
5151
if (this._frameSnapshots.has(snapshotName))
5252
throw new Error('Duplicate snapshot name: ' + snapshotName);
5353

54-
this._snapshotter.captureSnapshot(page, snapshotName, element);
54+
this._snapshotter.captureSnapshot(page, snapshotName, element).catch(() => {});
5555
return new Promise<SnapshotRenderer>(fulfill => {
5656
const listener = helper.addEventListener(this, 'snapshot', (renderer: SnapshotRenderer) => {
5757
if (renderer.snapshotName === snapshotName) {

0 commit comments

Comments
 (0)