chore(clock): split wall and monotonic time (#31198)

This commit is contained in:
Pavel Feldman 2024-06-09 14:50:50 -07:00 committed by GitHub
parent 43d6d012d4
commit e280d0bd35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 109 deletions

View File

@ -44,7 +44,7 @@ enum TimerType {
type Timer = {
type: TimerType;
func: TimerHandler;
args: () => any[];
args: any[];
delay: number;
callAt: number;
createdAt: number;
@ -58,10 +58,9 @@ interface Embedder {
}
export class ClockController {
readonly start: number;
private _now: number;
readonly timeOrigin: number;
private _now: { time: number, ticks: number, timeFrozen: boolean };
private _loopLimit: number;
private _adjustedSystemTime = 0;
private _duringTick = false;
private _timers = new Map<number, Timer>();
private _uniqueTimerId = idCounterStart;
@ -70,8 +69,8 @@ export class ClockController {
constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) {
const start = Math.floor(getEpoch(startDate));
this.start = start;
this._now = start;
this.timeOrigin = start;
this._now = { time: start, ticks: 0, timeFrozen: false };
this._embedder = embedder;
this._loopLimit = loopLimit;
}
@ -82,11 +81,22 @@ export class ClockController {
}
now(): number {
return this._now;
return this._now.time;
}
setTime(now: Date | number, options: { freeze?: boolean } = {}) {
this._now.time = getEpoch(now);
this._now.timeFrozen = !!options.freeze;
}
performanceNow(): DOMHighResTimeStamp {
return this._now - this._adjustedSystemTime - this.start;
return this._now.ticks;
}
private _advanceNow(toTicks: number) {
if (!this._now.timeFrozen)
this._now.time += toTicks - this._now.ticks;
this._now.ticks = toTicks;
}
private async _doTick(msFloat: number): Promise<number> {
@ -94,119 +104,72 @@ export class ClockController {
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;
const tickTo = this._now.ticks + ms;
let tickFrom = this._now.ticks;
let previous = this._now.ticks;
let firstException: Error | undefined;
this._duringTick = true;
// 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;
}
// compensate for any setSystemTime() call during timer callback
if (oldNow !== this._now) {
tickFrom += this._now - oldNow;
tickTo += this._now - oldNow;
previous += this._now - oldNow;
}
const error = await this._callTimer(timer).catch(e => e);
firstException = firstException || error;
timer = this._firstTimerInRange(previous, tickTo);
previous = tickFrom;
}
this._duringTick = false;
this._now = tickTo;
this._advanceNow(tickTo);
if (firstException)
throw firstException;
return this._now;
return this._now.ticks;
}
async recordTick(tickValue: string | number) {
const msFloat = parseTime(tickValue);
this._now += msFloat;
this._advanceNow(this._now.ticks + msFloat);
}
async tick(tickValue: string | number): Promise<number> {
return await this._doTick(parseTime(tickValue));
}
async next() {
async next(): Promise<number> {
const timer = this._firstTimer();
if (!timer)
return this._now;
let err: Error | undefined;
this._duringTick = true;
this._now = timer.callAt;
try {
this._callTimer(timer);
await new Promise<void>(f => this._embedder.postTask(f));
} catch (e) {
err = e;
}
this._duringTick = false;
if (err)
throw err;
return this._now;
return this._now.ticks;
await this._callTimer(timer);
return this._now.ticks;
}
async runToFrame() {
async runToFrame(): Promise<number> {
return this.tick(this.getTimeToNextFrame());
}
async runAll() {
async runAll(): Promise<number> {
for (let i = 0; i < this._loopLimit; i++) {
const numTimers = this._timers.size;
if (numTimers === 0)
return this._now;
return this._now.ticks;
await this.next();
}
const excessJob = this._firstTimer();
if (!excessJob)
return;
return this._now.ticks;
throw this._getInfiniteLoopError(excessJob);
}
async runToLast() {
async runToLast(): Promise<number> {
const timer = this._lastTimer();
if (!timer)
return this._now;
return await this.tick(timer.callAt - this._now);
return this._now.ticks;
return await this.tick(timer.callAt - this._now.ticks);
}
reset() {
this._timers.clear();
this._now = this.start;
}
setSystemTime(systemTime: Date | number) {
// determine time difference
const newNow = getEpoch(systemTime);
const difference = newNow - this._now;
this._adjustedSystemTime = this._adjustedSystemTime + difference;
// update 'system clock'
this._now = newNow;
// update timers and intervals to keep them stable
for (const timer of this._timers.values()) {
timer.createdAt += difference;
timer.callAt += difference;
}
this._now = { time: this.timeOrigin, ticks: 0, timeFrozen: false };
}
async jump(tickValue: string | number): Promise<number> {
@ -214,13 +177,13 @@ export class ClockController {
const ms = Math.floor(msFloat);
for (const timer of this._timers.values()) {
if (this._now + ms > timer.callAt)
timer.callAt = this._now + ms;
if (this._now.ticks + ms > timer.callAt)
timer.callAt = this._now.ticks + ms;
}
return await this.tick(ms);
}
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: () => any[] }): number {
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
if (options.func === undefined)
throw new Error('Callback must be provided to timer calls');
@ -233,10 +196,10 @@ export class ClockController {
const timer: Timer = {
type: options.type,
func: options.func,
args: options.args || (() => []),
args: options.args || [],
delay,
callAt: this._now + (delay || (this._duringTick ? 1 : 0)),
createdAt: this._now,
callAt: this._now.ticks + (delay || (this._duringTick ? 1 : 0)),
createdAt: this._now.ticks,
id: this._uniqueTimerId++,
error: new Error(),
};
@ -278,12 +241,32 @@ export class ClockController {
return lastTimer;
}
private _callTimer(timer: Timer) {
private async _callTimer(timer: Timer) {
this._advanceNow(timer.callAt);
if (timer.type === TimerType.Interval)
this._timers.get(timer.id)!.callAt += timer.delay;
else
this._timers.delete(timer.id);
callFunction(timer.func, timer.args());
this._duringTick = true;
try {
if (typeof timer.func !== 'function') {
(() => { eval(timer.func); })();
return;
}
let args = timer.args;
if (timer.type === TimerType.AnimationFrame)
args = [this._now.ticks];
else if (timer.type === TimerType.IdleCallback)
args = [{ didTimeout: false, timeRemaining: () => 0 }];
timer.func.apply(null, args);
await new Promise<void>(f => this._embedder.postTask(f));
} finally {
this._duringTick = false;
}
}
private _getInfiniteLoopError(job: Timer) {
@ -336,7 +319,7 @@ export class ClockController {
}
getTimeToNextFrame() {
return 16 - ((this._now - this.start) % 16);
return 16 - this._now.ticks % 16;
}
clearTimer(timerId: number, type: TimerType) {
@ -375,7 +358,7 @@ export class ClockController {
advanceAutomatically(advanceTimeDelta: number = 20): () => void {
return this._embedder.postTaskPeriodically(
() => this.tick(advanceTimeDelta!),
() => this._doTick(advanceTimeDelta!),
advanceTimeDelta,
);
}
@ -556,13 +539,6 @@ function compareTimers(a: Timer, b: Timer) {
// As timer ids are unique, no fallback `0` is necessary
}
function callFunction(func: TimerHandler, args: any[]) {
if (typeof func === 'function')
func.apply(null, args);
else
(() => { eval(func); })();
}
const maxTimeout = Math.pow(2, 31) - 1; // see https://heycam.github.io/webidl/#abstract-opdef-converttoint
const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs
@ -605,7 +581,7 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod
return clock.addTimer({
type: TimerType.Timeout,
func,
args: () => args,
args,
delay
});
},
@ -618,7 +594,7 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod
return clock.addTimer({
type: TimerType.Interval,
func,
args: () => args,
args,
delay,
});
},
@ -631,7 +607,6 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod
type: TimerType.AnimationFrame,
func: callback,
delay: clock.getTimeToNextFrame(),
args: () => [clock.performanceNow()],
});
},
cancelAnimationFrame: (timerId: number): void => {
@ -646,7 +621,6 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod
return clock.addTimer({
type: TimerType.IdleCallback,
func: callback,
args: () => [],
delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod,
});
},
@ -670,7 +644,7 @@ function getClearHandler(type: TimerType) {
function fakePerformance(clock: ClockController, performance: Performance): Performance {
const result: any = {
now: () => clock.performanceNow(),
timeOrigin: clock.start,
timeOrigin: clock.timeOrigin,
};
// eslint-disable-next-line no-proto
for (const key of Object.keys((performance as any).__proto__)) {

View File

@ -156,7 +156,7 @@ it.describe('setTimeout', () => {
const stub = createStub();
clock.setTimeout(stub, 5000);
await clock.tick(1000);
clock.setSystemTime(new clock.Date().getTime() + 1000);
clock.setTime(new clock.Date().getTime() + 1000);
await clock.tick(3990);
expect(stub.callCount).toBe(0);
await clock.tick(20);
@ -167,7 +167,7 @@ it.describe('setTimeout', () => {
const stub = createStub();
clock.setTimeout(stub, 5000);
await clock.tick(1000);
clock.setSystemTime(new clock.Date().getTime() - 1000);
clock.setTime(new clock.Date().getTime() - 1000);
await clock.tick(3990);
expect(stub.callCount).toBe(0);
await clock.tick(20);
@ -502,7 +502,7 @@ it.describe('tick', () => {
it('is not influenced by forward system clock changes', async ({ clock }) => {
const callback = () => {
clock.setSystemTime(new clock.Date().getTime() + 1000);
clock.setTime(new clock.Date().getTime() + 1000);
};
const stub = createStub();
clock.setTimeout(callback, 1000);
@ -515,7 +515,7 @@ it.describe('tick', () => {
it('is not influenced by forward system clock changes 2', async ({ clock }) => {
const callback = () => {
clock.setSystemTime(new clock.Date().getTime() - 1000);
clock.setTime(new clock.Date().getTime() - 1000);
};
const stub = createStub();
clock.setTimeout(callback, 1000);
@ -528,7 +528,7 @@ it.describe('tick', () => {
it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => {
const callback = () => {
clock.setSystemTime(new clock.Date().getTime() + 1000);
clock.setTime(new clock.Date().getTime() + 1000);
throw new Error();
};
const stub = createStub();
@ -544,7 +544,7 @@ it.describe('tick', () => {
it('is not influenced by forward system clock changes when an error is thrown 2', async ({ clock }) => {
const callback = () => {
clock.setSystemTime(new clock.Date().getTime() - 1000);
clock.setTime(new clock.Date().getTime() - 1000);
throw new Error();
};
const stub = createStub();
@ -653,7 +653,7 @@ it.describe('tick', () => {
it('is not influenced by forward system clock changes in promises', async ({ clock }) => {
const callback = () => {
void Promise.resolve().then(() => {
clock.setSystemTime(new clock.Date().getTime() + 1000);
clock.setTime(new clock.Date().getTime() + 1000);
});
};
const stub = createStub();
@ -1363,7 +1363,7 @@ it.describe('setInterval', () => {
clock.setInterval(stub, 10);
await clock.tick(11);
expect(stub.callCount).toBe(1);
clock.setSystemTime(new clock.Date().getTime() + 1000);
clock.setTime(new clock.Date().getTime() + 1000);
await clock.tick(8);
expect(stub.callCount).toBe(1);
await clock.tick(3);
@ -1374,7 +1374,7 @@ it.describe('setInterval', () => {
const stub = createStub();
clock.setInterval(stub, 10);
await clock.tick(5);
clock.setSystemTime(new clock.Date().getTime() - 1000);
clock.setTime(new clock.Date().getTime() - 1000);
await clock.tick(6);
expect(stub.callCount).toBe(1);
await clock.tick(10);
@ -1465,7 +1465,7 @@ it.describe('date', () => {
it('listens to system clock changes', async ({ clock }) => {
const date1 = new clock.Date();
clock.setSystemTime(date1.getTime() + 1000);
clock.setTime(date1.getTime() + 1000);
const date2 = new clock.Date();
expect(date2.getTime() - date1.getTime()).toBe(1000);
});
@ -2056,6 +2056,9 @@ it.describe('requestIdleCallback', () => {
clock.requestIdleCallback(stub);
await clock.tick(1000);
expect(stub.called).toBeTruthy();
const idleCallbackArg = stub.firstCall.args[0];
expect(idleCallbackArg.didTimeout).toBeFalsy();
expect(idleCallbackArg.timeRemaining()).toBe(0);
});
it('runs no later than timeout option even if there are any timers', async ({ clock }) => {

View File

@ -587,8 +587,8 @@ it.describe('popup', () => {
it('should tick before popup', async ({ page, browserName }) => {
const now = new Date('2015-09-25');
await page.clock.installFakeTimers(now);
const newNow = await page.clock.runFor(1000);
expect(newNow).toBe(now.getTime() + 1000);
const ticks = await page.clock.runFor(1000);
expect(ticks).toBe(1000);
const [popup] = await Promise.all([
page.waitForEvent('popup'),