Skip to content

Commit

Permalink
chore(evaluate): implement non-stalling evaluate (#6354)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Apr 29, 2021
1 parent 06a9268 commit 434f474
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 142 deletions.
81 changes: 37 additions & 44 deletions src/dispatchers/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export class DispatcherConnection {
}

const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
let callMetadata: CallMetadata = {
const callMetadata: CallMetadata = {
id: `call@${id}`,
...validMetadata,
objectId: sdkObject?.guid,
Expand All @@ -232,59 +232,52 @@ export class DispatcherConnection {
snapshots: []
};

try {
if (sdkObject) {
// Process logs for waitForNavigation/waitForLoadState
if (params?.info?.waitId) {
const info = params.info;
switch (info.phase) {
case 'before':
callMetadata.apiName = info.apiName;
this._waitOperations.set(info.waitId, callMetadata);
break;
case 'log':
const originalMetadata = this._waitOperations.get(info.waitId)!;
originalMetadata.log.push(info.message);
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
// Fall through.
case 'after':
return;
}
if (sdkObject && params?.info?.waitId) {
// Process logs for waitForNavigation/waitForLoadState
const info = params.info;
switch (info.phase) {
case 'before': {
callMetadata.apiName = info.apiName;
this._waitOperations.set(info.waitId, callMetadata);
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
return;
} case 'log': {
const originalMetadata = this._waitOperations.get(info.waitId)!;
originalMetadata.log.push(info.message);
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
return;
} case 'after': {
const originalMetadata = this._waitOperations.get(info.waitId)!;
originalMetadata.endTime = monotonicTime();
originalMetadata.error = info.error;
this._waitOperations.delete(info.waitId);
await sdkObject.instrumentation.onAfterCall(sdkObject, originalMetadata);
return;
}
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
}
const result = await (dispatcher as any)[method](validParams, callMetadata);
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
}


let result: any;
let error: any;
await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata);
try {
result = await (dispatcher as any)[method](validParams, callMetadata);
} catch (e) {
// Dispatching error
callMetadata.error = e.message;
if (callMetadata.log.length)
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
this.onmessage({ id, error: serializeError(e) });
error = serializeError(e);
} finally {
callMetadata.endTime = monotonicTime();
if (sdkObject) {
// Process logs for waitForNavigation/waitForLoadState
if (params?.info?.waitId) {
const info = params.info;
switch (info.phase) {
case 'before':
callMetadata.endTime = 0;
// Fall through.
case 'log':
return;
case 'after':
const originalMetadata = this._waitOperations.get(info.waitId)!;
originalMetadata.endTime = callMetadata.endTime;
originalMetadata.error = info.error;
this._waitOperations.delete(info.waitId);
callMetadata = originalMetadata;
break;
}
}
await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
}
await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata);
}

if (error)
this.onmessage({ id, error });
else
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
}

private _replaceDispatchersWithGuids(payload: any): any {
Expand Down
13 changes: 12 additions & 1 deletion src/server/chromium/crExecutionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,18 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
this._contextId = contextPayload.id;
}

async rawEvaluate(expression: string): Promise<string> {
async rawEvaluateJSON(expression: string): Promise<any> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
expression,
contextId: this._contextId,
returnByValue: true,
}).catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return remoteObject.value;
}

async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
expression,
contextId: this._contextId,
Expand Down
2 changes: 1 addition & 1 deletion src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
);
})();
`;
this._injectedScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId));
this._injectedScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', objectId));
}
return this._injectedScriptPromise;
}
Expand Down
12 changes: 11 additions & 1 deletion src/server/firefox/ffExecutionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
this._executionContextId = executionContextId;
}

async rawEvaluate(expression: string): Promise<string> {
async rawEvaluateJSON(expression: string): Promise<any> {
const payload = await this._session.send('Runtime.evaluate', {
expression,
returnByValue: true,
executionContextId: this._executionContextId,
}).catch(rewriteError);
checkException(payload.exceptionDetails);
return payload.result!.value;
}

async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
const payload = await this._session.send('Runtime.evaluate', {
expression,
returnByValue: false,
Expand Down
78 changes: 59 additions & 19 deletions src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ export class FrameManager {
return;
for (const barrier of this._signalBarriers)
barrier.addFrameNavigation(frame);
if (frame._pendingDocument && frame._pendingDocument.documentId === documentId) {
if (frame.pendingDocument() && frame.pendingDocument()!.documentId === documentId) {
// Do not override request with undefined.
return;
}
frame._pendingDocument = { documentId, request: undefined };
frame.setPendingDocument({ documentId, request: undefined });
}

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

let keepPending: DocumentInfo | undefined;
if (frame._pendingDocument) {
if (frame._pendingDocument.documentId === undefined) {
const pendingDocument = frame.pendingDocument();
if (pendingDocument) {
if (pendingDocument.documentId === undefined) {
// Pending with unknown documentId - assume it is the one being committed.
frame._pendingDocument.documentId = documentId;
pendingDocument.documentId = documentId;
}
if (frame._pendingDocument.documentId === documentId) {
if (pendingDocument.documentId === documentId) {
// Committing a pending document.
frame._currentDocument = frame._pendingDocument;
frame._currentDocument = pendingDocument;
} else {
// Sometimes, we already have a new pending when the old one commits.
// An example would be Chromium error page followed by a new navigation request,
// where the error page commit arrives after Network.requestWillBeSent for the
// new navigation.
// We commit, but keep the pending request since it's not done yet.
keepPending = frame._pendingDocument;
keepPending = pendingDocument;
frame._currentDocument = { documentId, request: undefined };
}
frame._pendingDocument = undefined;
frame.setPendingDocument(undefined);
} else {
// No pending - just commit a new document.
frame._currentDocument = { documentId, request: undefined };
Expand All @@ -205,7 +206,7 @@ export class FrameManager {
this._page.frameNavigatedToNewDocument(frame);
}
// Restore pending if any - see comments above about keepPending.
frame._pendingDocument = keepPending;
frame.setPendingDocument(keepPending);
}

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

frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) {
const frame = this._frames.get(frameId);
if (!frame || !frame._pendingDocument)
if (!frame || !frame.pendingDocument())
return;
if (documentId !== undefined && frame._pendingDocument.documentId !== documentId)
if (documentId !== undefined && frame.pendingDocument()!.documentId !== documentId)
return;
const navigationEvent: NavigationEvent = {
url: frame._url,
name: frame._name,
newDocument: frame._pendingDocument,
newDocument: frame.pendingDocument(),
error: new Error(errorText),
};
frame._pendingDocument = undefined;
frame.setPendingDocument(undefined);
frame.emit(Frame.Events.Navigation, navigationEvent);
}

Expand All @@ -255,7 +256,7 @@ export class FrameManager {
const frame = request.frame();
this._inflightRequestStarted(request);
if (request._documentId)
frame._pendingDocument = { documentId: request._documentId, request };
frame.setPendingDocument({ documentId: request._documentId, request });
if (request._isFavicon) {
const route = request._route();
if (route)
Expand All @@ -281,11 +282,11 @@ export class FrameManager {
requestFailed(request: network.Request, canceled: boolean) {
const frame = request.frame();
this._inflightRequestFinished(request);
if (frame._pendingDocument && frame._pendingDocument.request === request) {
if (frame.pendingDocument() && frame.pendingDocument()!.request === request) {
let errorText = request.failure()!.errorText;
if (canceled)
errorText += '; maybe frame was detached?';
this.frameAbortedNavigation(frame._id, errorText, frame._pendingDocument.documentId);
this.frameAbortedNavigation(frame._id, errorText, frame.pendingDocument()!.documentId);
}
if (!request._isFavicon)
this._page.emit(Page.Events.RequestFailed, request);
Expand Down Expand Up @@ -399,7 +400,7 @@ export class Frame extends SdkObject {
private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
_currentDocument: DocumentInfo;
_pendingDocument?: DocumentInfo;
private _pendingDocument: DocumentInfo | undefined;
readonly _page: Page;
private _parentFrame: Frame | null;
_url = '';
Expand All @@ -412,6 +413,7 @@ export class Frame extends SdkObject {
private _setContentCounter = 0;
readonly _detachedPromise: Promise<void>;
private _detachedCallback = () => {};
private _nonStallingEvaluations = new Set<(error: Error) => void>();

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

setPendingDocument(documentInfo: DocumentInfo | undefined) {
this._pendingDocument = documentInfo;
if (documentInfo)
this._invalidateNonStallingEvaluations();
}

pendingDocument(): DocumentInfo | undefined {
return this._pendingDocument;
}

private async _invalidateNonStallingEvaluations() {
if (!this._nonStallingEvaluations)
return;
const error = new Error('Navigation interrupted the evaluation');
for (const callback of this._nonStallingEvaluations)
callback(error);
}

async nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
if (this._pendingDocument)
throw new Error('Frame is currently attempting a navigation');
const context = this._existingMainContext();
if (!context)
throw new Error('Frame does not yet have a main execution context');

let callback = () => {};
const frameInvalidated = new Promise<void>((f, r) => callback = r);
this._nonStallingEvaluations.add(callback);
try {
return await Promise.race([
context.rawEvaluateJSON(expression),
frameInvalidated
]);
} finally {
this._nonStallingEvaluations.delete(callback);
}
}

private _recalculateLifecycle() {
const events = new Set<types.LifecycleEvent>(this._firedLifecycleEvents);
for (const child of this._childFrames) {
Expand Down Expand Up @@ -584,7 +624,7 @@ export class Frame extends SdkObject {
return this._context('main');
}

_existingMainContext(): dom.FrameExecutionContext | null {
private _existingMainContext(): dom.FrameExecutionContext | null {
return this._contextData.get('main')?.context || null;
}

Expand Down
10 changes: 5 additions & 5 deletions src/server/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export type FuncOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R |
export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>;

export interface ExecutionContextDelegate {
rawEvaluate(expression: string): Promise<ObjectId>;
rawEvaluateJSON(expression: string): Promise<any>;
rawEvaluateHandle(expression: string): Promise<ObjectId>;
rawCallFunctionNoReply(func: Function, ...args: any[]): void;
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>;
Expand Down Expand Up @@ -75,7 +76,7 @@ export class ExecutionContext extends SdkObject {
${utilityScriptSource.source}
return new pwExport();
})();`;
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new JSHandle(this, 'object', objectId));
this._utilityScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', objectId));
}
return this._utilityScriptPromise;
}
Expand All @@ -84,9 +85,8 @@ export class ExecutionContext extends SdkObject {
return this._delegate.createHandle(this, remoteObject);
}

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

async doSlowMo() {
Expand Down
2 changes: 1 addition & 1 deletion src/server/snapshot/inMemorySnapshotter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
if (this._frameSnapshots.has(snapshotName))
throw new Error('Duplicate snapshot name: ' + snapshotName);

this._snapshotter.captureSnapshot(page, snapshotName, element);
this._snapshotter.captureSnapshot(page, snapshotName, element).catch(() => {});
return new Promise<SnapshotRenderer>(fulfill => {
const listener = helper.addEventListener(this, 'snapshot', (renderer: SnapshotRenderer) => {
if (renderer.snapshotName === snapshotName) {
Expand Down
Loading

0 comments on commit 434f474

Please sign in to comment.