fix(clock): fix pauseAt to arrive at wall time (#31297)
This commit is contained in:
parent
8ea663aa64
commit
897f7449ef
|
|
@ -45,8 +45,8 @@ type Timer = {
|
||||||
func: TimerHandler;
|
func: TimerHandler;
|
||||||
args: any[];
|
args: any[];
|
||||||
delay: number;
|
delay: number;
|
||||||
callAt: number;
|
callAt: Ticks;
|
||||||
createdAt: number;
|
createdAt: Ticks;
|
||||||
id: number;
|
id: number;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
};
|
};
|
||||||
|
|
@ -58,15 +58,15 @@ interface Embedder {
|
||||||
setInterval(task: () => void, delay: number): () => void;
|
setInterval(task: () => void, delay: number): () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Ticks = number & { readonly __brand: 'Ticks' };
|
||||||
|
type EmbedderTicks = number & { readonly __brand: 'EmbedderTicks' };
|
||||||
|
type WallTime = number & { readonly __brand: 'WallTime' };
|
||||||
|
|
||||||
type Time = {
|
type Time = {
|
||||||
// ms since Epoch
|
time: WallTime;
|
||||||
time: number;
|
ticks: Ticks;
|
||||||
// Ticks since the session began (ala performance.now)
|
|
||||||
ticks: number;
|
|
||||||
// Whether fixed time was set.
|
|
||||||
isFixedTime: boolean;
|
isFixedTime: boolean;
|
||||||
// Origin time since Epoch when session started.
|
origin: WallTime;
|
||||||
origin: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type LogEntryType = 'fastForward' |'install' | 'pauseAt' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime';
|
type LogEntryType = 'fastForward' |'install' | 'pauseAt' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime';
|
||||||
|
|
@ -79,11 +79,11 @@ export class ClockController {
|
||||||
private _embedder: Embedder;
|
private _embedder: Embedder;
|
||||||
readonly disposables: (() => void)[] = [];
|
readonly disposables: (() => void)[] = [];
|
||||||
private _log: { type: LogEntryType, time: number, param?: number }[] = [];
|
private _log: { type: LogEntryType, time: number, param?: number }[] = [];
|
||||||
private _realTime: { startTicks: number, lastSyncTicks: number } | undefined;
|
private _realTime: { startTicks: EmbedderTicks, lastSyncTicks: EmbedderTicks } | undefined;
|
||||||
private _currentRealTimeTimer: { callAt: number, dispose: () => void } | undefined;
|
private _currentRealTimeTimer: { callAt: Ticks, dispose: () => void } | undefined;
|
||||||
|
|
||||||
constructor(embedder: Embedder) {
|
constructor(embedder: Embedder) {
|
||||||
this._now = { time: 0, isFixedTime: false, ticks: 0, origin: -1 };
|
this._now = { time: asWallTime(0), isFixedTime: false, ticks: 0 as Ticks, origin: asWallTime(-1) };
|
||||||
this._embedder = embedder;
|
this._embedder = embedder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,17 +99,17 @@ export class ClockController {
|
||||||
|
|
||||||
install(time: number) {
|
install(time: number) {
|
||||||
this._replayLogOnce();
|
this._replayLogOnce();
|
||||||
this._innerSetTime(time);
|
this._innerSetTime(asWallTime(time));
|
||||||
}
|
}
|
||||||
|
|
||||||
setSystemTime(time: number) {
|
setSystemTime(time: number) {
|
||||||
this._replayLogOnce();
|
this._replayLogOnce();
|
||||||
this._innerSetTime(time);
|
this._innerSetTime(asWallTime(time));
|
||||||
}
|
}
|
||||||
|
|
||||||
setFixedTime(time: number) {
|
setFixedTime(time: number) {
|
||||||
this._replayLogOnce();
|
this._replayLogOnce();
|
||||||
this._innerSetFixedTime(time);
|
this._innerSetFixedTime(asWallTime(time));
|
||||||
}
|
}
|
||||||
|
|
||||||
performanceNow(): DOMHighResTimeStamp {
|
performanceNow(): DOMHighResTimeStamp {
|
||||||
|
|
@ -117,22 +117,22 @@ export class ClockController {
|
||||||
return this._now.ticks;
|
return this._now.ticks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _innerSetTime(time: number) {
|
private _innerSetTime(time: WallTime) {
|
||||||
this._now.time = time;
|
this._now.time = time;
|
||||||
this._now.isFixedTime = false;
|
this._now.isFixedTime = false;
|
||||||
if (this._now.origin < 0)
|
if (this._now.origin < 0)
|
||||||
this._now.origin = this._now.time;
|
this._now.origin = this._now.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _innerSetFixedTime(time: number) {
|
private _innerSetFixedTime(time: WallTime) {
|
||||||
this._innerSetTime(time);
|
this._innerSetTime(time);
|
||||||
this._now.isFixedTime = true;
|
this._now.isFixedTime = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _advanceNow(toTicks: number) {
|
private _advanceNow(to: Ticks) {
|
||||||
if (!this._now.isFixedTime)
|
if (!this._now.isFixedTime)
|
||||||
this._now.time += toTicks - this._now.ticks;
|
this._now.time = asWallTime(this._now.time + to - this._now.ticks);
|
||||||
this._now.ticks = toTicks;
|
this._now.ticks = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
async log(type: LogEntryType, time: number, param?: number) {
|
async log(type: LogEntryType, time: number, param?: number) {
|
||||||
|
|
@ -143,30 +143,32 @@ export class ClockController {
|
||||||
this._replayLogOnce();
|
this._replayLogOnce();
|
||||||
if (ticks < 0)
|
if (ticks < 0)
|
||||||
throw new TypeError('Negative ticks are not supported');
|
throw new TypeError('Negative ticks are not supported');
|
||||||
await this._runTo(this._now.ticks + ticks);
|
await this._runTo(shiftTicks(this._now.ticks, ticks));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _runTo(tickTo: number) {
|
private async _runTo(to: Ticks) {
|
||||||
if (this._now.ticks > tickTo)
|
if (this._now.ticks > to)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let firstException: Error | undefined;
|
let firstException: Error | undefined;
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await this._callFirstTimer(tickTo);
|
const result = await this._callFirstTimer(to);
|
||||||
if (!result.timerFound)
|
if (!result.timerFound)
|
||||||
break;
|
break;
|
||||||
firstException = firstException || result.error;
|
firstException = firstException || result.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._advanceNow(tickTo);
|
this._advanceNow(to);
|
||||||
if (firstException)
|
if (firstException)
|
||||||
throw firstException;
|
throw firstException;
|
||||||
}
|
}
|
||||||
|
|
||||||
async pauseAt(time: number) {
|
async pauseAt(time: number): Promise<number> {
|
||||||
this._replayLogOnce();
|
this._replayLogOnce();
|
||||||
this._innerPause();
|
this._innerPause();
|
||||||
await this._innerFastForwardTo(time);
|
const toConsume = time - this._now.time;
|
||||||
|
await this._innerFastForwardTo(shiftTicks(this._now.ticks, toConsume));
|
||||||
|
return toConsume;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _innerPause() {
|
private _innerPause() {
|
||||||
|
|
@ -180,7 +182,7 @@ export class ClockController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _innerResume() {
|
private _innerResume() {
|
||||||
const now = this._embedder.performanceNow();
|
const now = this._embedder.performanceNow() as EmbedderTicks;
|
||||||
this._realTime = { startTicks: now, lastSyncTicks: now };
|
this._realTime = { startTicks: now, lastSyncTicks: now };
|
||||||
this._updateRealTimeTimer();
|
this._updateRealTimeTimer();
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +197,7 @@ export class ClockController {
|
||||||
const firstTimer = this._firstTimer();
|
const firstTimer = this._firstTimer();
|
||||||
|
|
||||||
// Either run the next timer or move time in 100ms chunks.
|
// Either run the next timer or move time in 100ms chunks.
|
||||||
const callAt = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100);
|
const callAt = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100) as Ticks;
|
||||||
if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt)
|
if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -207,30 +209,30 @@ export class ClockController {
|
||||||
this._currentRealTimeTimer = {
|
this._currentRealTimeTimer = {
|
||||||
callAt,
|
callAt,
|
||||||
dispose: this._embedder.setTimeout(() => {
|
dispose: this._embedder.setTimeout(() => {
|
||||||
const now = Math.ceil(this._embedder.performanceNow());
|
const now = Math.ceil(this._embedder.performanceNow()) as EmbedderTicks;
|
||||||
this._currentRealTimeTimer = undefined;
|
this._currentRealTimeTimer = undefined;
|
||||||
const sinceLastSync = now - this._realTime!.lastSyncTicks;
|
const sinceLastSync = now - this._realTime!.lastSyncTicks;
|
||||||
this._realTime!.lastSyncTicks = now;
|
this._realTime!.lastSyncTicks = now;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
this._runTo(this._now.ticks + sinceLastSync).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
|
this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
|
||||||
}, callAt - this._now.ticks),
|
}, callAt - this._now.ticks),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fastForward(ticks: number) {
|
async fastForward(ticks: number) {
|
||||||
this._replayLogOnce();
|
this._replayLogOnce();
|
||||||
await this._innerFastForwardTo(this._now.ticks + ticks | 0);
|
await this._innerFastForwardTo(shiftTicks(this._now.ticks, ticks | 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async _innerFastForwardTo(toTicks: number) {
|
private async _innerFastForwardTo(to: Ticks) {
|
||||||
if (toTicks < this._now.ticks)
|
if (to < this._now.ticks)
|
||||||
throw new Error('Cannot fast-forward to the past');
|
throw new Error('Cannot fast-forward to the past');
|
||||||
for (const timer of this._timers.values()) {
|
for (const timer of this._timers.values()) {
|
||||||
if (toTicks > timer.callAt)
|
if (to > timer.callAt)
|
||||||
timer.callAt = toTicks;
|
timer.callAt = to;
|
||||||
}
|
}
|
||||||
await this._runTo(toTicks);
|
await this._runTo(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
|
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
|
||||||
|
|
@ -249,7 +251,7 @@ export class ClockController {
|
||||||
func: options.func,
|
func: options.func,
|
||||||
args: options.args || [],
|
args: options.args || [],
|
||||||
delay,
|
delay,
|
||||||
callAt: this._now.ticks + (delay || (this._duringTick ? 1 : 0)),
|
callAt: shiftTicks(this._now.ticks, (delay || (this._duringTick ? 1 : 0))),
|
||||||
createdAt: this._now.ticks,
|
createdAt: this._now.ticks,
|
||||||
id: this._uniqueTimerId++,
|
id: this._uniqueTimerId++,
|
||||||
error: new Error(),
|
error: new Error(),
|
||||||
|
|
@ -283,7 +285,7 @@ export class ClockController {
|
||||||
this._advanceNow(timer.callAt);
|
this._advanceNow(timer.callAt);
|
||||||
|
|
||||||
if (timer.type === TimerType.Interval)
|
if (timer.type === TimerType.Interval)
|
||||||
this._timers.get(timer.id)!.callAt += timer.delay;
|
timer.callAt = shiftTicks(timer.callAt, timer.delay);
|
||||||
else
|
else
|
||||||
this._timers.delete(timer.id);
|
this._timers.delete(timer.id);
|
||||||
return timer;
|
return timer;
|
||||||
|
|
@ -375,29 +377,29 @@ export class ClockController {
|
||||||
|
|
||||||
for (const { type, time, param } of this._log) {
|
for (const { type, time, param } of this._log) {
|
||||||
if (!isPaused && lastLogTime !== -1)
|
if (!isPaused && lastLogTime !== -1)
|
||||||
this._advanceNow(this._now.ticks + time - lastLogTime);
|
this._advanceNow(shiftTicks(this._now.ticks, time - lastLogTime));
|
||||||
lastLogTime = time;
|
lastLogTime = time;
|
||||||
|
|
||||||
if (type === 'install') {
|
if (type === 'install') {
|
||||||
this._innerSetTime(param!);
|
this._innerSetTime(asWallTime(param!));
|
||||||
} else if (type === 'fastForward' || type === 'runFor') {
|
} else if (type === 'fastForward' || type === 'runFor') {
|
||||||
this._advanceNow(this._now.ticks + param!);
|
this._advanceNow(shiftTicks(this._now.ticks, param!));
|
||||||
} else if (type === 'pauseAt') {
|
} else if (type === 'pauseAt') {
|
||||||
isPaused = true;
|
isPaused = true;
|
||||||
this._innerPause();
|
this._innerPause();
|
||||||
this._innerSetTime(param!);
|
this._innerSetTime(asWallTime(param!));
|
||||||
} else if (type === 'resume') {
|
} else if (type === 'resume') {
|
||||||
this._innerResume();
|
this._innerResume();
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
} else if (type === 'setFixedTime') {
|
} else if (type === 'setFixedTime') {
|
||||||
this._innerSetFixedTime(param!);
|
this._innerSetFixedTime(asWallTime(param!));
|
||||||
} else if (type === 'setSystemTime') {
|
} else if (type === 'setSystemTime') {
|
||||||
this._innerSetTime(param!);
|
this._innerSetTime(asWallTime(param!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPaused && lastLogTime > 0)
|
if (!isPaused && lastLogTime > 0)
|
||||||
this._advanceNow(this._now.ticks + this._embedder.dateNow() - lastLogTime);
|
this._advanceNow(shiftTicks(this._now.ticks, this._embedder.dateNow() - lastLogTime));
|
||||||
|
|
||||||
this._log.length = 0;
|
this._log.length = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -710,3 +712,11 @@ export function inject(globalObject: WindowOrWorkerGlobalScope) {
|
||||||
builtin: platformOriginals(globalObject).bound,
|
builtin: platformOriginals(globalObject).bound,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asWallTime(n: number): WallTime {
|
||||||
|
return n as WallTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftTicks(ticks: Ticks, ms: number): Ticks {
|
||||||
|
return ticks + ms as Ticks;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1350,6 +1350,30 @@ it.describe('fastForward', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.describe('pauseAt', () => {
|
||||||
|
it('pause at target time', async ({ clock }) => {
|
||||||
|
clock.install(0);
|
||||||
|
await clock.pauseAt(1000);
|
||||||
|
expect(clock.Date.now()).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fire target timers', async ({ clock }) => {
|
||||||
|
clock.install(0);
|
||||||
|
const stub = createStub();
|
||||||
|
clock.setTimeout(stub, 1000);
|
||||||
|
clock.setTimeout(stub, 1001);
|
||||||
|
await clock.pauseAt(1000);
|
||||||
|
expect(stub.callCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns consumed clicks', async ({ clock }) => {
|
||||||
|
const now = Date.now();
|
||||||
|
clock.install(now);
|
||||||
|
const consumedTicks = await clock.pauseAt(now + 1000 * 60 * 60 * 24);
|
||||||
|
expect(consumedTicks).toBe(1000 * 60 * 60 * 24);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it.describe('performance.now()', () => {
|
it.describe('performance.now()', () => {
|
||||||
it('should start at 0', async ({ clock }) => {
|
it('should start at 0', async ({ clock }) => {
|
||||||
const result = clock.performance.now();
|
const result = clock.performance.now();
|
||||||
|
|
|
||||||
|
|
@ -313,13 +313,13 @@ it.describe('stubTimers', () => {
|
||||||
});
|
});
|
||||||
await page.clock.runFor(1000);
|
await page.clock.runFor(1000);
|
||||||
expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000);
|
expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000);
|
||||||
expect(await promise).toEqual({ prev: 2000, next: 3000 });
|
expect(await promise).toEqual({ prev: 1000, next: 2000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.describe('popup', () => {
|
it.describe('popup', () => {
|
||||||
it('should tick after popup', async ({ page }) => {
|
it('should tick after popup', async ({ page }) => {
|
||||||
await page.clock.install();
|
await page.clock.install({ time: 0 });
|
||||||
const now = new Date('2015-09-25');
|
const now = new Date('2015-09-25');
|
||||||
await page.clock.pauseAt(now);
|
await page.clock.pauseAt(now);
|
||||||
const [popup] = await Promise.all([
|
const [popup] = await Promise.all([
|
||||||
|
|
@ -334,7 +334,7 @@ it.describe('popup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tick before popup', async ({ page }) => {
|
it('should tick before popup', async ({ page }) => {
|
||||||
await page.clock.install();
|
await page.clock.install({ time: 0 });
|
||||||
const now = new Date('2015-09-25');
|
const now = new Date('2015-09-25');
|
||||||
await page.clock.pauseAt(now);
|
await page.clock.pauseAt(now);
|
||||||
await page.clock.runFor(1000);
|
await page.clock.runFor(1000);
|
||||||
|
|
@ -368,7 +368,7 @@ it.describe('popup', () => {
|
||||||
res.setHeader('Content-Type', 'text/html');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.end(`<script>window.time = Date.now()</script>`);
|
res.end(`<script>window.time = Date.now()</script>`);
|
||||||
});
|
});
|
||||||
await page.clock.install();
|
await page.clock.install({ time: 0 });
|
||||||
await page.clock.pauseAt(1000);
|
await page.clock.pauseAt(1000);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
// Wait for 2 second in real life to check that it is past in popup.
|
// Wait for 2 second in real life to check that it is past in popup.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue