chore: migrate waitForFunction to not use rerunnable task (#19730)

This commit is contained in:
Dmitry Gozman 2022-12-27 17:22:44 -08:00 committed by GitHub
parent 0b223b9036
commit 0fba4d5611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 54 additions and 112 deletions

View File

@ -45,7 +45,6 @@ import { asLocator } from './isomorphic/locatorGenerators';
type ContextData = { type ContextData = {
contextPromise: ManualPromise<dom.FrameExecutionContext | Error>; contextPromise: ManualPromise<dom.FrameExecutionContext | Error>;
context: dom.FrameExecutionContext | null; context: dom.FrameExecutionContext | null;
rerunnableTasks: Set<RerunnableTask<any>>;
}; };
type DocumentInfo = { type DocumentInfo = {
@ -509,8 +508,8 @@ export class Frame extends SdkObject {
this._detachedPromise = new Promise<void>(x => this._detachedCallback = x); this._detachedPromise = new Promise<void>(x => this._detachedCallback = x);
this._contextData.set('main', { contextPromise: new ManualPromise(), context: null, rerunnableTasks: new Set() }); this._contextData.set('main', { contextPromise: new ManualPromise(), context: null });
this._contextData.set('utility', { contextPromise: new ManualPromise(), context: null, rerunnableTasks: new Set() }); this._contextData.set('utility', { contextPromise: new ManualPromise(), context: null });
this._setContext('main', null); this._setContext('main', null);
this._setContext('utility', null); this._setContext('utility', null);
@ -1477,8 +1476,12 @@ export class Frame extends SdkObject {
if (typeof options.pollingInterval === 'number') if (typeof options.pollingInterval === 'number')
assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval); assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval);
expression = js.normalizeEvaluationExpression(expression, isFunction); expression = js.normalizeEvaluationExpression(expression, isFunction);
const task: dom.SchedulableTask<R> = injectedScript => injectedScript.evaluateHandle((injectedScript, { expression, isFunction, polling, arg }) => { return controller.run(async progress => {
const predicate = (arg: any): R => { return this.retryWithProgressAndTimeouts(progress, [100], async () => {
const context = await this._mainContext();
const injectedScript = await context.injectedScript();
const handle = await injectedScript.evaluateHandle((injected, { expression, isFunction, polling, arg }) => {
const predicate = (): R => {
let result = self.eval(expression); let result = self.eval(expression);
if (isFunction === true) { if (isFunction === true) {
result = result(arg); result = result(arg);
@ -1491,13 +1494,37 @@ export class Frame extends SdkObject {
} }
return result; return result;
}; };
let fulfill: (result: R) => void;
let reject: (error: Error) => void;
let aborted = false;
const result = new Promise<R>((f, r) => { fulfill = f; reject = r; });
const next = () => {
if (aborted)
return;
try {
const success = predicate();
if (success) {
fulfill(success);
return;
}
if (typeof polling !== 'number') if (typeof polling !== 'number')
return injectedScript.pollRaf(progress => predicate(arg) || progress.continuePolling); requestAnimationFrame(next);
return injectedScript.pollInterval(polling, progress => predicate(arg) || progress.continuePolling); else
setTimeout(next, polling);
} catch (e) {
reject(e);
}
};
next();
return { result, abort: () => aborted = true };
}, { expression, isFunction, polling: options.pollingInterval, arg }); }, { expression, isFunction, polling: options.pollingInterval, arg });
return controller.run( progress.cleanupWhenAborted(() => handle.evaluate(h => h.abort()).catch(() => {}));
progress => this._scheduleRerunnableHandleTask(progress, world, task), return handle.evaluateHandle(h => h.result);
this._page._timeoutSettings.timeout(options)); });
}, this._page._timeoutSettings.timeout(options));
} }
async waitForFunctionValueInUtility<R>(progress: Progress, pageFunction: js.Func1<any, R>) { async waitForFunctionValueInUtility<R>(progress: Progress, pageFunction: js.Func1<any, R>) {
@ -1540,8 +1567,6 @@ export class Frame extends SdkObject {
if (data.context) if (data.context)
data.context.contextDestroyed(error); data.context.contextDestroyed(error);
data.contextPromise.resolve(error); data.contextPromise.resolve(error);
for (const rerunnableTask of data.rerunnableTasks)
rerunnableTask.terminate(error);
} }
if (this._parentFrame) if (this._parentFrame)
this._parentFrame._childFrames.delete(this); this._parentFrame._childFrames.delete(this);
@ -1577,27 +1602,14 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {
const data = this._contextData.get(world)!;
const rerunnableTask = new RerunnableTask<T>(data, progress, task, false /* returnByValue */);
if (this._detached)
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
if (data.context)
rerunnableTask.rerun(data.context);
return rerunnableTask.handlePromise!;
}
private _setContext(world: types.World, context: dom.FrameExecutionContext | null) { private _setContext(world: types.World, context: dom.FrameExecutionContext | null) {
const data = this._contextData.get(world)!; const data = this._contextData.get(world)!;
data.context = context; data.context = context;
if (context) { if (context)
data.contextPromise.resolve(context); data.contextPromise.resolve(context);
for (const rerunnableTask of data.rerunnableTasks) else
rerunnableTask.rerun(context);
} else {
data.contextPromise = new ManualPromise(); data.contextPromise = new ManualPromise();
} }
}
_contextCreated(world: types.World, context: dom.FrameExecutionContext) { _contextCreated(world: types.World, context: dom.FrameExecutionContext) {
const data = this._contextData.get(world)!; const data = this._contextData.get(world)!;
@ -1706,66 +1718,6 @@ export class Frame extends SdkObject {
} }
} }
class RerunnableTask<T> {
readonly promise: ManualPromise<T> | undefined;
readonly handlePromise: ManualPromise<js.SmartHandle<T>> | undefined;
private _task: dom.SchedulableTask<T>;
private _progress: Progress;
private _returnByValue: boolean;
private _contextData: ContextData;
constructor(data: ContextData, progress: Progress, task: dom.SchedulableTask<T>, returnByValue: boolean) {
this._task = task;
this._progress = progress;
this._returnByValue = returnByValue;
if (returnByValue)
this.promise = new ManualPromise<T>();
else
this.handlePromise = new ManualPromise<js.SmartHandle<T>>();
this._contextData = data;
this._contextData.rerunnableTasks.add(this);
}
terminate(error: Error) {
this._reject(error);
}
private _resolve(value: T | js.SmartHandle<T>) {
if (this.promise)
this.promise.resolve(value as T);
if (this.handlePromise)
this.handlePromise.resolve(value as js.SmartHandle<T>);
}
private _reject(error: Error) {
if (this.promise)
this.promise.reject(error);
if (this.handlePromise)
this.handlePromise.reject(error);
}
async rerun(context: dom.FrameExecutionContext) {
try {
const injectedScript = await context.injectedScript();
const pollHandler = new dom.InjectedScriptPollHandler(this._progress, await this._task(injectedScript));
const result = this._returnByValue ? await pollHandler.finish() : await pollHandler.finishHandle();
this._contextData.rerunnableTasks.delete(this);
this._resolve(result);
} catch (e) {
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e)) {
this._contextData.rerunnableTasks.delete(this);
this._reject(e);
}
// Unlike other places, we don't check frame for being detached since the whole scope of this
// evaluation is within the frame's execution context. So we only let JavaScript errors and
// session termination errors go through.
// We will try again in the new execution context.
}
}
}
class SignalBarrier { class SignalBarrier {
private _progress: Progress | null; private _progress: Progress | null;
private _protectCount = 0; private _protectCount = 0;

View File

@ -387,17 +387,7 @@ export class InjectedScript {
return this.poll(predicate, next => requestAnimationFrame(next)); return this.poll(predicate, next => requestAnimationFrame(next));
} }
pollInterval<T>(pollInterval: number, predicate: Predicate<T>): InjectedScriptPoll<T> { private poll<T>(predicate: Predicate<T>, scheduleNext: (next: () => void) => void): InjectedScriptPoll<T> {
return this.poll(predicate, next => setTimeout(next, pollInterval));
}
pollLogScale<T>(predicate: Predicate<T>): InjectedScriptPoll<T> {
const pollIntervals = [100, 250, 500];
let attempts = 0;
return this.poll(predicate, next => setTimeout(next, pollIntervals[attempts++] || 1000));
}
poll<T>(predicate: Predicate<T>, scheduleNext: (next: () => void) => void): InjectedScriptPoll<T> {
return this._runAbortableTask(progress => { return this._runAbortableTask(progress => {
let fulfill: (result: T) => void; let fulfill: (result: T) => void;
let reject: (error: Error) => void; let reject: (error: Error) => void;