Created
September 11, 2025 18:18
-
-
Save bgrins/fb0fc71f0434517251e2899803d7e980 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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