resources/suite-runner.mjs (176 lines of code) (raw):

import { TestRunner } from "./shared/test-runner.mjs"; import { WarmupSuite } from "./benchmark-runner.mjs"; export class SuiteRunner { #frame; #page; #params; #suite; #client; #suiteResults; constructor(frame, page, params, suite, client, measuredValues) { // FIXME: Create SuiteRunner-local measuredValues. this.#suiteResults = measuredValues.tests[suite.name]; if (!this.#suiteResults) { this.#suiteResults = { tests: {}, total: 0 }; measuredValues.tests[suite.name] = this.#suiteResults; } this.#frame = frame; this.#page = page; this.#client = client; this.#suite = suite; this.#params = params; } get frame() { return this.#frame; } get page() { return this.#page; } get params() { return this.#params; } get suite() { return this.#suite; } get client() { return this.#client; } get suiteResults() { return this.#suiteResults; } async run() { await this._prepareSuite(); await this._runSuite(); } async _prepareSuite() { const suiteName = this.#suite.name; const suitePrepareStartLabel = `suite-${suiteName}-prepare-start`; const suitePrepareEndLabel = `suite-${suiteName}-prepare-end`; performance.mark(suitePrepareStartLabel); await this._loadFrame(); await this.#suite.prepare(this.#page); performance.mark(suitePrepareEndLabel); performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel); } async _runSuite() { const suiteName = this.#suite.name; const suiteStartLabel = `suite-${suiteName}-start`; const suiteEndLabel = `suite-${suiteName}-end`; performance.mark(suiteStartLabel); for (const test of this.#suite.tests) { if (this.#client?.willRunTest) await this.#client.willRunTest(this.#suite, test); const testRunner = new TestRunner(this.#frame, this.#page, this.#params, this.#suite, test, this._recordTestResults); await testRunner.runTest(); } performance.mark(suiteEndLabel); performance.measure(`suite-${suiteName}`, suiteStartLabel, suiteEndLabel); this._validateSuiteTotal(); await this._updateClient(); } _validateSuiteTotal() { // When the test is fast and the precision is low (for example with Firefox' // privacy.resistFingerprinting preference), it's possible that the measured // total duration for an entire is 0. const suiteTotal = this.#suiteResults.total; if (suiteTotal === 0) throw new Error(`Got invalid 0-time total for suite ${this.#suite.name}: ${suiteTotal}`); } async _loadFrame() { return new Promise((resolve, reject) => { const frame = this.#frame; frame.onload = () => resolve(); frame.onerror = () => reject(); frame.src = `${this.#suite.url}?${this.#params.toSearchParams()}`; }); } _recordTestResults = async (test, syncTime, asyncTime) => { // Skip reporting updates for the warmup suite. if (this.#suite === WarmupSuite) return; const total = syncTime + asyncTime; this.#suiteResults.tests[test.name] = { tests: { Sync: syncTime, Async: asyncTime }, total: total }; this.#suiteResults.total += total; }; async _updateClient(suite = this.#suite) { if (this.#client?.didFinishSuite) await this.#client.didFinishSuite(suite); } } export class RemoteSuiteRunner extends SuiteRunner { #appId; get appId() { return this.#appId; } set appId(id) { this.#appId = id; } async run() { this.postMessageCallbacks = new Map(); const handler = this._handlePostMessage.bind(this); window.addEventListener("message", handler); // FIXME: use this._suite in all SuiteRunner methods directly. try { await this._prepareSuite(); await this._runSuite(); } finally { window.removeEventListener("message", handler); } } async _prepareSuite() { const suiteName = this.suite.name; const suitePrepareStartLabel = `suite-${suiteName}-prepare-start`; const suitePrepareEndLabel = `suite-${suiteName}-prepare-end`; performance.mark(suitePrepareStartLabel); // Wait for the app-ready message from the workload. const appReadyPromise = this._subscribeOnce("app-ready"); await this._loadFrame(this.suite); const response = await appReadyPromise; await this.suite.prepare(this.page); // Capture appId to pass along with messages. this.appId = response?.appId; performance.mark(suitePrepareEndLabel); performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel); } async _runSuite() { // Ask workload to run its own tests. this.frame.contentWindow.postMessage({ id: this.appId, key: "benchmark-connector", type: "benchmark-suite", name: this.suite.config?.name || "default" }, "*"); // Capture metrics from the completed tests. const response = await this._subscribeOnce("suite-complete"); this.suiteResults.tests = { ...this.suiteResults.tests, ...response.result.tests, }; this.suiteResults.total += response.result.total; this._validateSuiteTotal(); await this._updateClient(); } _handlePostMessage(event) { const callback = this.postMessageCallbacks.get(event.data.type); if (callback) callback(event); } _startSubscription(type, callback) { if (this.postMessageCallbacks.has(type)) throw new Error("Callback exists already"); this.postMessageCallbacks.set(type, callback); } _stopSubscription(type) { if (!this.postMessageCallbacks.has(type)) throw new Error("Callback does not exist"); this.postMessageCallbacks.delete(type); } _subscribeOnce(type) { return new Promise((resolve) => { this._startSubscription(type, (e) => { this._stopSubscription(type); resolve(e.data); }); }); } } export const SUITE_RUNNER_LOOKUP = { __proto__: null, default: SuiteRunner, remote: RemoteSuiteRunner, };