Skip to content

Instantly share code, notes, and snippets.

@bgrins
Created September 11, 2025 18:18
Show Gist options
  • Save bgrins/fb0fc71f0434517251e2899803d7e980 to your computer and use it in GitHub Desktop.
Save bgrins/fb0fc71f0434517251e2899803d7e980 to your computer and use it in GitHub Desktop.
class TestInvoker {
constructor(syncCallback, asyncCallback, reportCallback, params) {
this._syncCallback = syncCallback;
this._asyncCallback = asyncCallback;
this._reportCallback = reportCallback;
this._params = params;
}
}
class BaseRAFTestInvoker extends TestInvoker {
start() {
return new Promise((resolve) => {
if (this._params.waitBeforeSync)
setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync);
else
this._scheduleCallbacks(resolve);
});
}
}
class RAFTestInvoker extends BaseRAFTestInvoker {
_scheduleCallbacks(resolve) {
requestAnimationFrame(() => this._syncCallback());
requestAnimationFrame(() => {
setTimeout(() => {
this._asyncCallback();
setTimeout(async () => {
const result = await this._reportCallback();
resolve(result);
}, 0);
}, 0);
});
}
}
class AsyncRAFTestInvoker extends BaseRAFTestInvoker {
static mc = new MessageChannel();
_scheduleCallbacks(resolve) {
let gotTimer = false;
let gotMessage = false;
let gotPromise = false;
const tryTriggerAsyncCallback = () => {
if (!gotTimer || !gotMessage || !gotPromise)
return;
this._asyncCallback();
setTimeout(async () => {
await this._reportCallback();
resolve();
}, 0);
};
requestAnimationFrame(async () => {
await this._syncCallback();
gotPromise = true;
tryTriggerAsyncCallback();
});
requestAnimationFrame(() => {
setTimeout(async () => {
await Promise.resolve();
gotTimer = true;
tryTriggerAsyncCallback();
});
AsyncRAFTestInvoker.mc.port1.addEventListener(
"message",
async function () {
await Promise.resolve();
gotMessage = true;
tryTriggerAsyncCallback();
},
{ once: true }
);
AsyncRAFTestInvoker.mc.port1.start();
AsyncRAFTestInvoker.mc.port2.postMessage("speedometer");
});
}
}
export const TEST_INVOKER_LOOKUP = {
__proto__: null,
raf: RAFTestInvoker,
async: AsyncRAFTestInvoker,
};
export class TestRunner {
#frame;
#page;
#params;
#suite;
#test;
#callback;
#type;
constructor(frame, page, params, suite, test, callback, type) {
this.#suite = suite;
this.#test = test;
this.#params = params;
this.#callback = callback;
this.#page = page;
this.#frame = frame;
this.#type = type;
}
get page() {
return this.#page;
}
get test() {
return this.#test;
}
_runSyncStep(test, page) {
test.run(page);
}
async runTest() {
// Prepare all mark labels outside the measuring loop.
const suiteName = this.#suite.name;
const testName = this.#test.name;
const syncStartLabel = `${suiteName}.${testName}-start`;
const syncEndLabel = `${suiteName}.${testName}-sync-end`;
const asyncEndLabel = `${suiteName}.${testName}-async-end`;
let syncTime;
let asyncStartTime;
let asyncTime;
const runSync = async () => {
if (this.#params.warmupBeforeSync) {
performance.mark("warmup-start");
const startTime = performance.now();
// Infinite loop for the specified ms.
while (performance.now() - startTime < this.#params.warmupBeforeSync)
continue;
performance.mark("warmup-end");
}
performance.mark(syncStartLabel);
const syncStartTime = performance.now();
if (this.#type === "async")
await this._runSyncStep(this.test, this.page);
else
this._runSyncStep(this.test, this.page);
const mark = performance.mark(syncEndLabel);
const syncEndTime = mark.startTime;
syncTime = syncEndTime - syncStartTime;
asyncStartTime = syncEndTime;
};
const measureAsync = () => {
// Some browsers don't immediately update the layout for paint.
// Force the layout here to ensure we're measuring the layout time.
this.page.layout();
const asyncEndTime = performance.now();
performance.mark(asyncEndLabel);
asyncTime = asyncEndTime - asyncStartTime;
if (this.#params.warmupBeforeSync)
performance.measure("warmup", "warmup-start", "warmup-end");
performance.measure(`${suiteName}.${testName}-sync`, syncStartLabel, syncEndLabel);
performance.measure(`${suiteName}.${testName}-async`, syncEndLabel, asyncEndLabel);
};
const report = () => this.#callback(this.#test, syncTime, asyncTime);
const invokerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
const invokerClass = TEST_INVOKER_LOOKUP[invokerType];
const invoker = new invokerClass(runSync, measureAsync, report, this.#params);
return invoker.start();
}
}
export class AsyncTestRunner extends TestRunner {
constructor(frame, page, params, suite, test, callback, type) {
super(frame, page, params, suite, test, callback, type);
}
async _runSyncStep(test, page) {
await test.run(page);
}
}
export const TEST_RUNNER_LOOKUP = {
__proto__: null,
default: TestRunner,
async: AsyncTestRunner,
remote: TestRunner,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment