Skip to content

Commit

Permalink
chore: simplify doTick (#31196)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Jun 7, 2024
1 parent 826343b commit dd3a412
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 1,180 deletions.
13 changes: 5 additions & 8 deletions packages/playwright-core/src/server/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,19 @@ export class Clock {

async runToNextTimer(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwClock.clock.next()`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.nextAsync()`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.next()`);
return this._now;
}

async runAllTimers(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runAll()`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAllAsync()`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAll()`);
return this._now;
}

async runToLastTimer(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runToLast()`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLastAsync()`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLast()`);
return this._now;
}

Expand Down Expand Up @@ -85,8 +82,8 @@ export class Clock {

async runFor(time: number | string): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tickAsync(${JSON.stringify(time)})`);
await this._browserContext.addInitScript(`globalThis.__pwClock.clock.recordTick(${JSON.stringify(time)})`);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`);
return this._now;
}

Expand Down
205 changes: 41 additions & 164 deletions packages/playwright-core/src/server/injected/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,149 +89,59 @@ export class ClockController {
return this._now - this._adjustedSystemTime - this.start;
}

private _doTick(tickValue: number | string, isAsync: boolean, resolve?: (time: number) => void, reject?: (error: Error) => void): number | undefined {
const msFloat =
typeof tickValue === 'number'
? tickValue
: parseTime(tickValue);
const ms = Math.floor(msFloat);
let tickTo = this._now + ms;

private async _doTick(msFloat: number): Promise<number> {
if (msFloat < 0)
throw new TypeError('Negative ticks are not supported');

const ms = Math.floor(msFloat);
let tickTo = this._now + ms;
let tickFrom = this._now;
let previous = this._now;
// ESLint fails to detect this correctly
/* eslint-disable prefer-const */
let timer;
let firstException: Error;
let oldNow: number;
let nextPromiseTick: (() => void) | null;
let compensationCheck: () => void;
let postTimerCall: () => void;

let firstException: Error | undefined;
this._duringTick = true;

// perform microtasks
oldNow = this._now;
if (oldNow !== this._now) {
// compensate for any setSystemTime() call during microtask callback
tickFrom += this._now - oldNow;
tickTo += this._now - oldNow;
}

const doTickInner = (): number | undefined => {
// perform each timer in the requested range
timer = this._firstTimerInRange(tickFrom, tickTo);
while (timer && tickFrom <= tickTo) {
if (this._timers.has(timer.id)) {
tickFrom = timer.callAt;
this._now = timer.callAt;
oldNow = this._now;
try {
this._callTimer(timer);
} catch (e) {
firstException = firstException || e;
}

if (isAsync) {
// finish up after native setImmediate callback to allow
// all native es6 promises to process their callbacks after
// each timer fires.
this._embedder.postTask(nextPromiseTick!);
return;
}
compensationCheck();
}
postTimerCall();
}

// perform process.nextTick()s again
oldNow = this._now;
if (oldNow !== this._now) {
// compensate for any setSystemTime() call during process.nextTick() callback
tickFrom += this._now - oldNow;
tickTo += this._now - oldNow;
}
this._duringTick = false;

// corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo]
timer = this._firstTimerInRange(tickFrom, tickTo);
if (timer) {
try {
this.tick(tickTo - this._now); // do it all again - for the remainder of the requested range
} catch (e) {
firstException = firstException || e;
}
} else {
// no timers remaining in the requested range: move the clock all the way to the end
this._now = tickTo;
// perform each timer in the requested range
let timer = this._firstTimerInRange(tickFrom, tickTo);
while (timer && tickFrom <= tickTo) {
tickFrom = timer.callAt;
this._now = timer.callAt;
const oldNow = this._now;
try {
this._callTimer(timer);
await new Promise<void>(f => this._embedder.postTask(f));
} catch (e) {
firstException = firstException || e;
}
if (firstException)
throw firstException;

if (isAsync)
resolve!(this._now);
else
return this._now;
};

nextPromiseTick =
isAsync ?
() => {
try {
compensationCheck();
postTimerCall();
doTickInner();
} catch (e) {
reject!(e);
}
} : null;

compensationCheck = () => {
// compensate for any setSystemTime() call during timer callback
if (oldNow !== this._now) {
tickFrom += this._now - oldNow;
tickTo += this._now - oldNow;
previous += this._now - oldNow;
}
};

postTimerCall = () => {
timer = this._firstTimerInRange(previous, tickTo);
previous = tickFrom;
};
}

return doTickInner();
}
this._duringTick = false;
this._now = tickTo;
if (firstException)
throw firstException;

tick(tickValue: string | number): number {
return this._doTick(tickValue, false)!;
return this._now;
}

async tickAsync(tickValue: string | number): Promise<number> {
await new Promise<void>(f => this._embedder.postTask(f));
return new Promise((resolve, reject) => this._doTick(tickValue, true, resolve, reject));
async recordTick(tickValue: string | number) {
const msFloat = parseTime(tickValue);
this._now += msFloat;
}

next() {
const timer = this._firstTimer();
if (!timer)
return this._now;

this._duringTick = true;
try {
this._now = timer.callAt;
this._callTimer(timer);
return this._now;
} finally {
this._duringTick = false;
}
async tick(tickValue: string | number): Promise<number> {
return await this._doTick(parseTime(tickValue));
}

async nextAsync() {
await new Promise<void>(f => this._embedder.postTask(f));
async next() {
const timer = this._firstTimer();
if (!timer)
return this._now;
Expand All @@ -241,74 +151,41 @@ export class ClockController {
this._now = timer.callAt;
try {
this._callTimer(timer);
await new Promise<void>(f => this._embedder.postTask(f));
} catch (e) {
err = e;
}
this._duringTick = false;

await new Promise<void>(f => this._embedder.postTask(f));
if (err)
throw err;
return this._now;
}

runAll() {
for (let i = 0; i < this._loopLimit; i++) {
const numTimers = this._timers.size;
if (numTimers === 0)
return this._now;
this.next();
}

const excessJob = this._firstTimer();
if (!excessJob)
return;
throw this._getInfiniteLoopError(excessJob);
}

runToFrame() {
async runToFrame() {
return this.tick(this.getTimeToNextFrame());
}

async runAllAsync() {
async runAll() {
for (let i = 0; i < this._loopLimit; i++) {
await new Promise<void>(f => this._embedder.postTask(f));
const numTimers = this._timers.size;
if (numTimers === 0)
return this._now;

this.next();
await this.next();
}
await new Promise<void>(f => this._embedder.postTask(f));

const excessJob = this._firstTimer();
if (!excessJob)
return;
throw this._getInfiniteLoopError(excessJob);
}

runToLast() {
async runToLast() {
const timer = this._lastTimer();
if (!timer)
return this._now;
return this.tick(timer.callAt - this._now);
}

runToLastAsync() {
return new Promise<number>((resolve, reject) => {
this._embedder.postTask(() => {
try {
const timer = this._lastTimer();
if (!timer) {
resolve(this._now);
return;
}
this.tickAsync(timer.callAt - this._now).then(resolve);
} catch (e) {
reject(e);
}
});
});
return await this.tick(timer.callAt - this._now);
}

reset() {
Expand All @@ -332,18 +209,15 @@ export class ClockController {
}
}

jump(tickValue: string | number): number {
const msFloat =
typeof tickValue === 'number'
? tickValue
: parseTime(tickValue);
async jump(tickValue: string | number): Promise<number> {
const msFloat = parseTime(tickValue);
const ms = Math.floor(msFloat);

for (const timer of this._timers.values()) {
if (this._now + ms > timer.callAt)
timer.callAt = this._now + ms;
}
return this.tick(ms);
return await this.tick(ms);
}

addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: () => any[] }): number {
Expand Down Expand Up @@ -524,9 +398,12 @@ function inRange(from: number, to: number, timer: Timer): boolean {
* number of milliseconds. This is used to support human-readable strings passed
* to clock.tick()
*/
function parseTime(str: string): number {
if (!str)
function parseTime(value: number | string): number {
if (typeof value === 'number')
return value;
if (!value)
return 0;
const str = value;

const strings = str.split(':');
const l = strings.length;
Expand Down
Loading

0 comments on commit dd3a412

Please sign in to comment.