From 45e35c9c149b97716515b6c6a03f0b9a9800433f Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Wed, 10 Jun 2026 16:45:05 +0200 Subject: [PATCH 01/69] f --- .../javascript-wc-indexeddb/scripts/build.js | 4 +- .../todomvc-localstorage/scripts/build.js | 3 +- index.html | 7 + resources/benchmark-configurator.mjs | 2 + resources/default-tests.mjs | 1 + resources/main.css | 60 +++++++- resources/main.mjs | 131 +++++++++++++++++ .../service-worker-startup/service-worker.js | 50 +++++++ resources/shared/params.mjs | 3 + resources/shared/sw-messages.mjs | 6 + resources/suite-runner.mjs | 3 +- .../angular-complex/scripts/build.js | 2 + .../backbone-complex/scripts/build.js | 2 + .../backbone/scripts/build.js | 3 +- .../jquery-complex/scripts/build.js | 4 +- .../jquery/scripts/build.js | 3 +- .../lit-complex/scripts/build.js | 2 + .../preact-complex/scripts/build.js | 2 + .../react-complex/scripts/build.js | 2 + .../react-redux-complex/scripts/build.js | 2 + .../svelte-complex/scripts/build.js | 2 + .../vue-complex/scripts/build.js | 2 + .../javascript-es5-complex/scripts/build.js | 2 + .../javascript-es5/dist/resources.txt | 10 ++ .../javascript-es5/scripts/build.js | 3 +- .../scripts/build.js | 2 + .../scripts/build.js | 2 + .../scripts/build.js | 4 +- sw.mjs | 135 ++++++++++++++++++ 29 files changed, 438 insertions(+), 16 deletions(-) create mode 100644 resources/service-worker-startup/service-worker.js create mode 100644 resources/shared/sw-messages.mjs create mode 100644 resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt create mode 100644 sw.mjs diff --git a/experimental/javascript-wc-indexeddb/scripts/build.js b/experimental/javascript-wc-indexeddb/scripts/build.js index 8540a774e..b9415f1b9 100644 --- a/experimental/javascript-wc-indexeddb/scripts/build.js +++ b/experimental/javascript-wc-indexeddb/scripts/build.js @@ -1,5 +1,7 @@ const fs = require("fs").promises; const { dirname } = require("path"); +const path = require("path"); +const { generateResourcesFile } = require("../../../resources/shared/generate-resources"); /** * createDirectory @@ -164,4 +166,4 @@ const build = async () => { console.log("Done with building!"); }; -build(); +build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/experimental/todomvc-localstorage/scripts/build.js b/experimental/todomvc-localstorage/scripts/build.js index 046de0e7e..550a45021 100644 --- a/experimental/todomvc-localstorage/scripts/build.js +++ b/experimental/todomvc-localstorage/scripts/build.js @@ -1,5 +1,6 @@ const fs = require("fs").promises; const path = require("path"); +const { generateResourcesFile } = require("../../../resources/shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -53,4 +54,4 @@ const build = async () => { console.log("done!!"); }; -build(); +build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/index.html b/index.html index 31ba85ee4..972e8051d 100644 --- a/index.html +++ b/index.html @@ -33,6 +33,13 @@ Test Instructions +
+ +
+
+
+
+
diff --git a/resources/benchmark-configurator.mjs b/resources/benchmark-configurator.mjs index ee250a78d..b6c53d136 100644 --- a/resources/benchmark-configurator.mjs +++ b/resources/benchmark-configurator.mjs @@ -56,6 +56,8 @@ export class BenchmarkConfigurator { this.#suites.forEach((suite) => { if (!suite.tags) suite.tags = []; + if (!("measurePrepare" in suite)) + suite.measurePrepare = false; if (suite.url.startsWith("experimental/")) suite.tags.unshift("all", "experimental"); else diff --git a/resources/default-tests.mjs b/resources/default-tests.mjs index ecfbc336a..51cb0d2d6 100644 --- a/resources/default-tests.mjs +++ b/resources/default-tests.mjs @@ -7,6 +7,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES5", url: "resources/todomvc/vanilla-examples/javascript-es5/dist/index.html", + resources: "resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); diff --git a/resources/main.css b/resources/main.css index 279f2a413..40929ff96 100644 --- a/resources/main.css +++ b/resources/main.css @@ -374,7 +374,16 @@ button.show-about { display: none; } -#progress { +#preload-progress { + display: none; +} + +:root[data-benchmark-state="PRELOADING"] #preload-progress, +:root[data-benchmark-state="PRELOADING"] #preload-info { + display: block; +} + +#progress, #preload-progress { position: absolute; bottom: -6px; left: 60px; @@ -384,7 +393,7 @@ button.show-about { border-right: 6px solid var(--background); } -#progress-completed { +#progress-completed, #preload-progress-completed { position: absolute; top: 0; left: 0; @@ -395,11 +404,11 @@ button.show-about { background-color: var(--inactive-color); } -#progress-completed::-webkit-progress-value { +#progress-completed::-webkit-progress-value, #preload-progress-completed::-webkit-progress-value { background-color: var(--foreground); } -#progress-completed::-moz-progress-bar { +#progress-completed::-moz-progress-bar, #preload-progress-completed::-moz-progress-bar { background-color: var(--foreground); } @@ -410,7 +419,8 @@ button.show-about { background-color: var(--background); } -#info { +#info, #preload-info { + display: none; position: absolute; bottom: -25px; left: 60px; @@ -420,11 +430,11 @@ button.show-about { text-align: center; font-size: 12px; } -#info-label { +#info-label, #preload-info-label { position: absolute; left: 6px; } -#info-progress { +#info-progress, #preload-info-progress { position: absolute; right: 6px; text-align: right; @@ -900,3 +910,39 @@ section#about .note { color: #fff; stroke: #fff; } + +#preload-progress, #preload-info { + display: none; +} + +[data-benchmark-state="PRELOADING"] { + cursor: wait; +} + +[data-benchmark-state="PRELOADING"] #preload-progress, +[data-benchmark-state="PRELOADING"] #preload-info { + display: block; +} + +[data-benchmark-state="PRELOADING"] .start-tests-button { + color: var(--foreground); + background-image: + linear-gradient( + -45deg, + var(--foreground) 15%, + transparent 15%, + transparent 50%, + var(--foreground) 50%, + var(--foreground) 65%, + transparent 65%, + transparent 100% + ); + background-size: 20px 20px; + animation: barber-pole 1s linear infinite; + pointer-events: none; +} + +@keyframes barber-pole { + 0% { background-position: 0 0, 0 0; } + 100% { background-position: 20px 0, 0 0; } +} diff --git a/resources/main.mjs b/resources/main.mjs index fb287dcb3..02799c984 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -1,9 +1,79 @@ import { BenchmarkRunner } from "./benchmark-runner.mjs"; import * as Statistics from "./statistics.mjs"; +import { SW_MESSAGES } from "./shared/sw-messages.mjs"; import { renderMetricView } from "./metric-ui.mjs"; import { defaultParams, params } from "./shared/params.mjs"; import { createDeveloperModeContainer } from "./developer-mode.mjs"; +export class PreloadServiceWorker { + constructor() { + this.registration = null; + this.sw = null; + } + + async setup() { + const existingRegistrations = await navigator.serviceWorker.getRegistrations(); + for (const existing of existingRegistrations) { + await existing.unregister(); + } + + this.registration = await navigator.serviceWorker.register("/sw.mjs", { type: "module" }); + await this.registration.update(); + await navigator.serviceWorker.ready; + + this.sw = navigator.serviceWorker.controller || this.registration.active; + return true; + } + + async precacheSuites(suites, resourceLoadDelay, onProgress) { + if (suites.length === 0) return; + + const suitesData = suites.filter(s => s.resources).map(s => ({ + name: s.name, + url: new URL(s.url, window.location.href).href, + resources: new URL(s.resources, window.location.href).href + })); + + if (suitesData.length === 0) return; + + const startTime = performance.now(); + return new Promise((resolve) => { + const channel = new MessageChannel(); + channel.port1.onmessage = (event) => { + if (event.data?.type === SW_MESSAGES.PRECACHE_DONE) { + const timeTakenMs = performance.now() - startTime; + const { totalSize, count } = event.data; + const sizeMB = (totalSize / (1024 * 1024)).toFixed(2); + const timeSec = (timeTakenMs / 1000).toFixed(2); + console.log(`Preloaded ${count} files (${sizeMB} MB) in ${timeSec}s`); + resolve(); + } else if (event.data?.type === SW_MESSAGES.PRECACHE_PROGRESS) { + onProgress(event.data); + } + }; + this.sw.postMessage({ + type: SW_MESSAGES.PRECACHE_SUITES, + suites: suitesData, + delay: resourceLoadDelay + }, [channel.port2]); + }); + } + + setState(state) { + this.sw.postMessage({ type: SW_MESSAGES.SET_STATE, state }); + } +} + + +const BENCHMARK_STATE = Object.freeze({ + IDLE: "IDLE", + PRELOADING: "PRELOADING", + READY: "READY", + RUNNING: "RUNNING", + DONE: "DONE", + ERROR: "ERROR", +}); + // FIXME(camillobruni): Add base class class MainBenchmarkClient { developerMode = false; @@ -16,6 +86,9 @@ class MainBenchmarkClient { _hasResults = false; _developerModeContainer = null; _metrics = Object.create(null); + _developerModeContainer = null; + _progressCompleted = null; + preloadServiceWorker = new PreloadServiceWorker(); _steppingPromise = null; _steppingResolver = null; _benchmarkConfiguratorPromise = null; @@ -71,6 +144,8 @@ class MainBenchmarkClient { if (this._isRunning) return false; + this._setBenchmarkState(BENCHMARK_STATE.RUNNING); + const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); @@ -146,6 +221,7 @@ class MainBenchmarkClient { this._isRunning = false; this._hasResults = true; this._metrics = metrics; + this._setBenchmarkState(BENCHMARK_STATE.DONE); const scoreResults = this._computeResults(this._measuredValuesList, "score"); if (scoreResults.isValid) @@ -166,6 +242,7 @@ class MainBenchmarkClient { this._isRunning = false; this._hasResults = true; this._metrics = Object.create(null); + this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._populateInvalidScore(); this.showResultsSummary(); throw error; @@ -343,6 +420,7 @@ class MainBenchmarkClient { document.getElementById("copy-csv").onclick = this.copyCSVResults.bind(this); document.querySelectorAll(".start-tests-button").forEach((button) => { button.onclick = this._startBenchmarkHandler.bind(this); + button.disabled = true; }); } @@ -357,10 +435,63 @@ class MainBenchmarkClient { document.body.append(this._developerModeContainer); } + await this._setupServiceWorker(benchmarkConfigurator); + if (params.startAutomatically) this.start(); } + async _setupServiceWorker(benchmarkConfigurator) { + await this.preloadServiceWorker.setup(); + + this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); + const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); + + try { + await this.preloadServiceWorker.precacheSuites(enabledSuites, params.resourceLoadDelay, (progressData) => { + const { loaded, total, url, suiteName } = progressData; + document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); + document.getElementById("preload-progress-completed").max = total; + document.getElementById("preload-progress-completed").value = loaded; + const filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; + const labelText = suiteName ? `${suiteName}: ${filename}` : filename; + document.getElementById("preload-info-label").textContent = labelText; + document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; + }); + this._enableStartButtons(); + } catch (error) { + console.error("Service Worker setup failed:", error); + this._setBenchmarkState(BENCHMARK_STATE.ERROR); + this._enableStartButtons(); + } + } + + _enableStartButtons() { + this._setBenchmarkState(BENCHMARK_STATE.READY); + document.querySelectorAll(".start-tests-button").forEach((button) => { + button.disabled = false; + }); + } + + _setBenchmarkState(state) { + document.body.setAttribute("data-benchmark-state", state); + if (this.preloadServiceWorker) { + this.preloadServiceWorker.setState(state); + } + + const startButton = document.querySelector(".start-tests-button"); + if (state === BENCHMARK_STATE.PRELOADING) { + document.getElementById("preload-progress-completed").value = 0; + document.getElementById("preload-info-label").textContent = "Connecting to Service Worker..."; + document.getElementById("preload-info-progress").textContent = ""; + document.body.style.setProperty("--preload-progress", "0%"); + if (startButton) startButton.textContent = "Preloading..."; + } else if (state === BENCHMARK_STATE.READY || state === BENCHMARK_STATE.IDLE || state === BENCHMARK_STATE.DONE || state === BENCHMARK_STATE.ERROR) { + document.body.style.removeProperty("--preload-progress"); + if (startButton) startButton.textContent = "Start Test"; + } + } + _hashChangeHandler() { this._showSection(window.location.hash); } diff --git a/resources/service-worker-startup/service-worker.js b/resources/service-worker-startup/service-worker.js new file mode 100644 index 000000000..d976611f8 --- /dev/null +++ b/resources/service-worker-startup/service-worker.js @@ -0,0 +1,50 @@ +const CACHE_NAME = "sw-startup-workload-v1"; +let ASSETS = ["./app/index.html", "./app/main.mjs", "./app/style.css", "./app/work_schedule.json", "https://esm.sh/react@18", "https://esm.sh/react-dom@18/client"]; + +async function cacheAssets() { + const cache = await caches.open(CACHE_NAME); + for (const url of ASSETS) { + try { + const response = await fetch(url); + if (response.ok) { + await cache.put(url, response); + console.log("SW caching:", url); + } + } catch (e) { + console.error(`Failed to cache ${url}:`, e); + } + } +} + +async function installAssets(event) { + await cacheAssets(); + self.skipWaiting(); +} + +self.addEventListener("install", (event) => { + event.waitUntil(installAssets(event)); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", (event) => { + event.respondWith( + caches.match(event.request).then((response) => { + return response || fetch(event.request); + }) + ); +}); + +async function prefetchAssets(event) { + await cacheAssets(); + event?.ports[0].postMessage({ + type: "PREFETCH_COMPLETE", + }); +} + +self.addEventListener("message", (event) => { + if (event.data.type === "PREFETCH") + event.waitUntil(prefetchAssets(event)); +}); diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index 520679d24..bc8ba51bb 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -35,6 +35,8 @@ export class Params { measurePrepare = false; // External config url to override internal tests. config = ""; + // Resource load delay in ms for the service worker pre-caching. + resourceLoadDelay = 0; constructor(searchParams = undefined) { if (searchParams) @@ -68,6 +70,7 @@ export class Params { this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES); this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare"); this.config = this._parseConfig(searchParams); + this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0); const unused = Array.from(searchParams.keys()); if (unused.length > 0) diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs new file mode 100644 index 000000000..251d23d28 --- /dev/null +++ b/resources/shared/sw-messages.mjs @@ -0,0 +1,6 @@ +export const SW_MESSAGES = Object.freeze({ + SET_STATE: "SET_STATE", + PRECACHE_SUITES: "PRECACHE_SUITES", + PRECACHE_PROGRESS: "PRECACHE_PROGRESS", + PRECACHE_DONE: "PRECACHE_DONE" +}); diff --git a/resources/suite-runner.mjs b/resources/suite-runner.mjs index 5cfc0b50a..1872a7e87 100644 --- a/resources/suite-runner.mjs +++ b/resources/suite-runner.mjs @@ -96,7 +96,7 @@ export class SuiteRunner { const { suiteTotal, suitePrepare } = this.#suiteResults.total; if (suiteTotal === 0) throw new Error(`Got invalid 0-time total for suite ${this.#suite.name}: ${suiteTotal}`); - if (this.#params.measurePrepare && suitePrepare === 0) + if ((this.#params.measurePrepare || this.#suite.measurePrepare) && suitePrepare === 0) throw new Error(`Got invalid 0-time prepare time for suite ${this.#suite.name}: ${suitePrepare}`); } @@ -172,7 +172,6 @@ export class RemoteSuiteRunner extends SuiteRunner { this.appId = response?.appId; performance.mark(suitePrepareEndLabel); - const entry = performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel); this.#prepareTime = entry.duration; } diff --git a/resources/todomvc/architecture-examples/angular-complex/scripts/build.js b/resources/todomvc/architecture-examples/angular-complex/scripts/build.js index 3a13c300a..fe1b2f80c 100644 --- a/resources/todomvc/architecture-examples/angular-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/angular-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -17,3 +18,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js index 802c9f845..56979dbe6 100644 --- a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -17,3 +18,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/backbone/scripts/build.js b/resources/todomvc/architecture-examples/backbone/scripts/build.js index ff06b6773..48a4d6b3f 100644 --- a/resources/todomvc/architecture-examples/backbone/scripts/build.js +++ b/resources/todomvc/architecture-examples/backbone/scripts/build.js @@ -1,5 +1,6 @@ const fs = require("fs").promises; const path = require("path"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -58,4 +59,4 @@ const build = async () => { console.log("done!!"); }; -build(); +build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js index 6f69450f0..686794700 100644 --- a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -19,4 +20,5 @@ const options = { cssFilesToAddLinksFor: ["big-dom-with-stacking-context-scrollable.css"], }; -buildComplex(options); \ No newline at end of file +buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); \ No newline at end of file diff --git a/resources/todomvc/architecture-examples/jquery/scripts/build.js b/resources/todomvc/architecture-examples/jquery/scripts/build.js index 06924222d..f03383346 100644 --- a/resources/todomvc/architecture-examples/jquery/scripts/build.js +++ b/resources/todomvc/architecture-examples/jquery/scripts/build.js @@ -1,5 +1,6 @@ const fs = require("fs").promises; const path = require("path"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -51,4 +52,4 @@ const build = async () => { console.log("done!!"); }; -build(); +build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/lit-complex/scripts/build.js b/resources/todomvc/architecture-examples/lit-complex/scripts/build.js index a8b2448ca..8553c547e 100644 --- a/resources/todomvc/architecture-examples/lit-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/lit-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -24,3 +25,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/preact-complex/scripts/build.js b/resources/todomvc/architecture-examples/preact-complex/scripts/build.js index d9309af11..5987ecd7e 100644 --- a/resources/todomvc/architecture-examples/preact-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/preact-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -15,3 +16,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/react-complex/scripts/build.js b/resources/todomvc/architecture-examples/react-complex/scripts/build.js index 814d42210..4d7f98b63 100644 --- a/resources/todomvc/architecture-examples/react-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/react-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -14,3 +15,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js index b51b34ec0..159788d5c 100644 --- a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -15,3 +16,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js index 42fcbdefc..261292720 100644 --- a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -15,3 +16,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/vue-complex/scripts/build.js b/resources/todomvc/architecture-examples/vue-complex/scripts/build.js index cb46077d8..093d4cbd8 100644 --- a/resources/todomvc/architecture-examples/vue-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/vue-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -17,3 +18,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js index 2590e7875..bc3d2c06a 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -17,3 +18,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt b/resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt new file mode 100644 index 000000000..3a9a2b41d --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt @@ -0,0 +1,10 @@ +app.js +base.css +controller.js +helpers.js +index.css +index.html +model.js +store.js +template.js +view.js diff --git a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js index 046de0e7e..7707ef610 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js @@ -1,5 +1,6 @@ const fs = require("fs").promises; const path = require("path"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -53,4 +54,4 @@ const build = async () => { console.log("done!!"); }; -build(); +build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js index 9e9481a91..8a2113d37 100644 --- a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -14,3 +15,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js index 338594b9c..576911094 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js @@ -3,6 +3,7 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -23,3 +24,4 @@ const options = { }; buildComplex(options); +generateResourcesFile(path.join(__dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js index be4499d96..b087510e3 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js @@ -1,5 +1,7 @@ const fs = require("fs").promises; const { dirname } = require("path"); +const path = require("path"); +const { generateResourcesFile } = require("../../../../shared/generate-resources"); /** * createDirectory @@ -153,4 +155,4 @@ const build = async () => { console.log("Done with building!"); }; -build(); +build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/sw.mjs b/sw.mjs new file mode 100644 index 000000000..7959db98e --- /dev/null +++ b/sw.mjs @@ -0,0 +1,135 @@ +const CACHE_NAME = "speedometer-cache-v4.0"; + +const BENCHMARK_STATE = { + IDLE: "IDLE", + RUNNING: "RUNNING", +}; + +import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; + +let currentState = BENCHMARK_STATE.IDLE; +let cachedUrls = new Set(); +let cachedSuitesPrefixes = new Set(); + +function replyToClient(event, msg) { + event.ports[0].postMessage(msg); +} + +function delayAsync(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function handlePrecache(event, { suites = [], delay = 0 }) { + await caches.delete(CACHE_NAME); + const cache = await caches.open(CACHE_NAME); + + let loaded = 0; + let totalSize = 0; + const urlsToCache = []; + + cachedSuitesPrefixes.clear(); + for (const suite of suites) { + if (!suite.resources) continue; + const prefix = new URL(".", suite.url).href; + cachedSuitesPrefixes.add(prefix); + urlsToCache.push(...await parseSuiteResources(suite)); + } + + const total = urlsToCache.length; + const promises = urlsToCache.map(async (item, index) => { + const size = await fetchAndCache(cache, item.url, delay * index); + totalSize += size; + loaded++; + replyToClient(event, { type: SW_MESSAGES.PRECACHE_PROGRESS, loaded, total, url: item.url, suiteName: item.suiteName }); + }); + + await Promise.all(promises); + + cachedUrls = new Set(urlsToCache.map(item => item.url)); + replyToClient(event, { type: SW_MESSAGES.PRECACHE_DONE, totalSize, count: urlsToCache.length }); +} + +async function parseSuiteResources(suite) { + try { + const response = await fetch(suite.resources); + if (!response.ok) return []; + const text = await response.text(); + const lines = text.split("\n").map(l => l.trim()).filter(l => l.length > 0); + return lines.map(resourceUrl => ({ + url: new URL(resourceUrl, suite.url).href, + suiteName: suite.name + })); + } catch (e) { + console.warn("Failed to fetch resources.txt for", suite.name); + return []; + } +} + +async function fetchAndCache(cache, url, delayMs) { + if (delayMs) await delayAsync(delayMs); + const request = new Request(url, { cache: "no-cache" }); + try { + await cache.add(request); + const cachedResponse = await cache.match(request); + if (!cachedResponse) return 0; + const blob = await cachedResponse.blob(); + return blob.size; + } catch (e) { + console.warn("Cache failed for", url, e); + return 0; + } +} + +self.addEventListener("install", function () { + self.skipWaiting(); +}); + +self.addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", function (event) { + const { data } = event; + if (!data) return; + + if (data.type === SW_MESSAGES.SET_STATE) { + currentState = data.state; + } else if (data.type === SW_MESSAGES.PRECACHE_SUITES) { + event.waitUntil(handlePrecache(event, data)); + } +}); + +self.addEventListener("fetch", function (event) { + const isCached = cachedUrls.has(event.request.url); + + if (isCached) { + event.respondWith(handleFetch(event.request)); + return; + } + + // We only enforce strict blocking when the benchmark is actively RUNNING + // to allow runner resources to be loaded. + if (currentState !== BENCHMARK_STATE.RUNNING) return; + + let isCachedSuite = false; + for (const prefix of cachedSuitesPrefixes) { + if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { + isCachedSuite = true; + break; + } + } + + if (isCachedSuite) { + console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); + event.respondWith(Promise.resolve(Response.error())); + return; + } + // Bypass Service Worker for everything else +}); + +async function handleFetch(request) { + const cache = await caches.open(CACHE_NAME); + const cachedResponse = await cache.match(request); + if (cachedResponse) return cachedResponse; + return fetch(request); +} From 29a283c0fb7b8dd5a9e20ffadd02d7f86a40f31f Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Wed, 10 Jun 2026 16:51:57 +0200 Subject: [PATCH 02/69] add --- .../dist/resources.txt | 39 +++++++++++++++ .../javascript-wc-indexeddb/scripts/build.js | 3 +- .../todomvc-localstorage/scripts/build.js | 3 +- .../service-worker-startup/service-worker.js | 50 ------------------- resources/shared/generate-resources.mjs | 27 ++++++++++ .../angular-complex/scripts/build.js | 3 +- .../backbone-complex/scripts/build.js | 3 +- .../backbone/scripts/build.js | 3 +- .../jquery-complex/scripts/build.js | 3 +- .../jquery/scripts/build.js | 3 +- .../lit-complex/scripts/build.js | 3 +- .../preact-complex/scripts/build.js | 3 +- .../react-complex/scripts/build.js | 3 +- .../react-redux-complex/scripts/build.js | 3 +- .../svelte-complex/scripts/build.js | 3 +- .../vue-complex/scripts/build.js | 3 +- .../javascript-es5-complex/scripts/build.js | 3 +- .../javascript-es5/scripts/build.js | 3 +- .../scripts/build.js | 3 +- .../scripts/build.js | 3 +- .../scripts/build.js | 3 +- 21 files changed, 84 insertions(+), 86 deletions(-) create mode 100644 experimental/javascript-wc-indexeddb/dist/resources.txt delete mode 100644 resources/service-worker-startup/service-worker.js create mode 100644 resources/shared/generate-resources.mjs diff --git a/experimental/javascript-wc-indexeddb/dist/resources.txt b/experimental/javascript-wc-indexeddb/dist/resources.txt new file mode 100644 index 000000000..7dc424065 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/resources.txt @@ -0,0 +1,39 @@ +index.html +libs/dexie.mjs +src/components/todo-app/todo-app.component.js +src/components/todo-app/todo-app.template.js +src/components/todo-bottombar/todo-bottombar.component.js +src/components/todo-bottombar/todo-bottombar.template.js +src/components/todo-item/todo-item.component.js +src/components/todo-item/todo-item.template.js +src/components/todo-list/todo-list.component.js +src/components/todo-list/todo-list.template.js +src/components/todo-topbar/todo-topbar.component.js +src/components/todo-topbar/todo-topbar.template.js +src/hooks/useDoubleClick.js +src/hooks/useKeyListener.js +src/hooks/useRouter.js +src/index.mjs +src/speedometer-utils/benchmark.mjs +src/speedometer-utils/helpers.mjs +src/speedometer-utils/params.mjs +src/speedometer-utils/test-invoker.mjs +src/speedometer-utils/test-runner.mjs +src/speedometer-utils/todomvc-utils.mjs +src/speedometer-utils/translations.mjs +src/storage/base-storage-manager.js +src/storage/dexieDB-manager.js +src/storage/indexedDB-manager.js +src/storage/storage-factory.js +src/utils/nanoid.js +src/workload-test.mjs +styles/app.constructable.js +styles/bottombar.constructable.js +styles/footer.css +styles/global.constructable.js +styles/global.css +styles/header.css +styles/main.constructable.js +styles/todo-item.constructable.js +styles/todo-list.constructable.js +styles/topbar.constructable.js diff --git a/experimental/javascript-wc-indexeddb/scripts/build.js b/experimental/javascript-wc-indexeddb/scripts/build.js index b9415f1b9..723a88731 100644 --- a/experimental/javascript-wc-indexeddb/scripts/build.js +++ b/experimental/javascript-wc-indexeddb/scripts/build.js @@ -1,7 +1,6 @@ const fs = require("fs").promises; const { dirname } = require("path"); const path = require("path"); -const { generateResourcesFile } = require("../../../resources/shared/generate-resources"); /** * createDirectory @@ -166,4 +165,4 @@ const build = async () => { console.log("Done with building!"); }; -build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); +build().then(() => import("../../../resources/shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); diff --git a/experimental/todomvc-localstorage/scripts/build.js b/experimental/todomvc-localstorage/scripts/build.js index 550a45021..452400417 100644 --- a/experimental/todomvc-localstorage/scripts/build.js +++ b/experimental/todomvc-localstorage/scripts/build.js @@ -1,6 +1,5 @@ const fs = require("fs").promises; const path = require("path"); -const { generateResourcesFile } = require("../../../resources/shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -54,4 +53,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); +build().then(() => import("../../../resources/shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); diff --git a/resources/service-worker-startup/service-worker.js b/resources/service-worker-startup/service-worker.js deleted file mode 100644 index d976611f8..000000000 --- a/resources/service-worker-startup/service-worker.js +++ /dev/null @@ -1,50 +0,0 @@ -const CACHE_NAME = "sw-startup-workload-v1"; -let ASSETS = ["./app/index.html", "./app/main.mjs", "./app/style.css", "./app/work_schedule.json", "https://esm.sh/react@18", "https://esm.sh/react-dom@18/client"]; - -async function cacheAssets() { - const cache = await caches.open(CACHE_NAME); - for (const url of ASSETS) { - try { - const response = await fetch(url); - if (response.ok) { - await cache.put(url, response); - console.log("SW caching:", url); - } - } catch (e) { - console.error(`Failed to cache ${url}:`, e); - } - } -} - -async function installAssets(event) { - await cacheAssets(); - self.skipWaiting(); -} - -self.addEventListener("install", (event) => { - event.waitUntil(installAssets(event)); -}); - -self.addEventListener("activate", (event) => { - event.waitUntil(self.clients.claim()); -}); - -self.addEventListener("fetch", (event) => { - event.respondWith( - caches.match(event.request).then((response) => { - return response || fetch(event.request); - }) - ); -}); - -async function prefetchAssets(event) { - await cacheAssets(); - event?.ports[0].postMessage({ - type: "PREFETCH_COMPLETE", - }); -} - -self.addEventListener("message", (event) => { - if (event.data.type === "PREFETCH") - event.waitUntil(prefetchAssets(event)); -}); diff --git a/resources/shared/generate-resources.mjs b/resources/shared/generate-resources.mjs new file mode 100644 index 000000000..d4ce09e76 --- /dev/null +++ b/resources/shared/generate-resources.mjs @@ -0,0 +1,27 @@ +import fs from 'fs'; +import path from 'path'; + +function walkDir(dir, fileList = []) { + const files = fs.readdirSync(dir); + for (const file of files) { + const filePath = path.join(dir, file); + if (fs.statSync(filePath).isDirectory()) { + walkDir(filePath, fileList); + } else { + fileList.push(filePath); + } + } + return fileList; +} + +export function generateResourcesFile(distPath) { + if (!fs.existsSync(distPath)) { + console.warn(`Directory ${distPath} does not exist, skipping resources.txt generation.`); + return; + } + const absoluteDist = path.resolve(distPath); + const files = walkDir(absoluteDist); + const relativePaths = files.map(f => path.relative(absoluteDist, f)).filter(f => f !== 'resources.txt'); + fs.writeFileSync(path.join(absoluteDist, 'resources.txt'), relativePaths.join('\n') + '\n', 'utf8'); + console.log(`Generated resources.txt at ${distPath}`); +} diff --git a/resources/todomvc/architecture-examples/angular-complex/scripts/build.js b/resources/todomvc/architecture-examples/angular-complex/scripts/build.js index fe1b2f80c..42c51cd1d 100644 --- a/resources/todomvc/architecture-examples/angular-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/angular-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -18,4 +17,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js index 56979dbe6..abd31b40d 100644 --- a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -18,4 +17,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/architecture-examples/backbone/scripts/build.js b/resources/todomvc/architecture-examples/backbone/scripts/build.js index 48a4d6b3f..7a9e9e0bd 100644 --- a/resources/todomvc/architecture-examples/backbone/scripts/build.js +++ b/resources/todomvc/architecture-examples/backbone/scripts/build.js @@ -1,6 +1,5 @@ const fs = require("fs").promises; const path = require("path"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -59,4 +58,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); +build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); diff --git a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js index 686794700..da240303a 100644 --- a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -21,4 +20,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); \ No newline at end of file +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); \ No newline at end of file diff --git a/resources/todomvc/architecture-examples/jquery/scripts/build.js b/resources/todomvc/architecture-examples/jquery/scripts/build.js index f03383346..8ff05bbaa 100644 --- a/resources/todomvc/architecture-examples/jquery/scripts/build.js +++ b/resources/todomvc/architecture-examples/jquery/scripts/build.js @@ -1,6 +1,5 @@ const fs = require("fs").promises; const path = require("path"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -52,4 +51,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); +build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); diff --git a/resources/todomvc/architecture-examples/lit-complex/scripts/build.js b/resources/todomvc/architecture-examples/lit-complex/scripts/build.js index 8553c547e..50a585b66 100644 --- a/resources/todomvc/architecture-examples/lit-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/lit-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -25,4 +24,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/architecture-examples/preact-complex/scripts/build.js b/resources/todomvc/architecture-examples/preact-complex/scripts/build.js index 5987ecd7e..0739fe789 100644 --- a/resources/todomvc/architecture-examples/preact-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/preact-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -16,4 +15,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/architecture-examples/react-complex/scripts/build.js b/resources/todomvc/architecture-examples/react-complex/scripts/build.js index 4d7f98b63..53032dcfc 100644 --- a/resources/todomvc/architecture-examples/react-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/react-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -15,4 +14,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js index 159788d5c..e384b8f14 100644 --- a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -16,4 +15,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js index 261292720..db20258c1 100644 --- a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -16,4 +15,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/architecture-examples/vue-complex/scripts/build.js b/resources/todomvc/architecture-examples/vue-complex/scripts/build.js index 093d4cbd8..dc7e48d71 100644 --- a/resources/todomvc/architecture-examples/vue-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/vue-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -18,4 +17,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js index bc3d2c06a..93f6ddb6d 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -18,4 +17,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js index 7707ef610..fbecbd875 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js @@ -1,6 +1,5 @@ const fs = require("fs").promises; const path = require("path"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const rootDirectory = "./"; const sourceDirectory = "./src"; @@ -54,4 +53,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); +build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); diff --git a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js index 8a2113d37..531d6d507 100644 --- a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -15,4 +14,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js index 576911094..7c7a99e4e 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js @@ -3,7 +3,6 @@ */ const path = require("path"); const { buildComplex } = require("big-dom-generator/utils/buildComplex"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); const options = { callerDirectory: path.resolve(__dirname), @@ -24,4 +23,4 @@ const options = { }; buildComplex(options); -generateResourcesFile(path.join(__dirname, "../dist")); +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js index b087510e3..86a42ad8c 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js @@ -1,7 +1,6 @@ const fs = require("fs").promises; const { dirname } = require("path"); const path = require("path"); -const { generateResourcesFile } = require("../../../../shared/generate-resources"); /** * createDirectory @@ -155,4 +154,4 @@ const build = async () => { console.log("Done with building!"); }; -build().then(() => generateResourcesFile(path.join(__dirname, "../dist"))); +build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); From d0ba583daea4711a37761698ad2fb012be337b54 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Wed, 10 Jun 2026 17:12:03 +0200 Subject: [PATCH 03/69] add-resources --- .../javascript-wc-indexeddb/scripts/build.js | 2 +- experimental/tests.mjs | 38 ++ .../todomvc-localstorage/dist/base.css | 141 ------- .../todomvc-localstorage/dist/index.css | 393 ------------------ .../todomvc-localstorage/dist/index.html | 18 +- .../todomvc-localstorage/scripts/build.js | 2 +- resources/default-tests.mjs | 33 +- resources/main.css | 44 +- resources/main.mjs | 49 ++- resources/shared/generate-resources.mjs | 13 +- resources/shared/sw-messages.mjs | 2 +- .../angular-complex/scripts/build.js | 2 +- .../backbone-complex/scripts/build.js | 2 +- .../backbone/scripts/build.js | 2 +- .../jquery-complex/scripts/build.js | 2 +- .../jquery/scripts/build.js | 2 +- .../lit-complex/scripts/build.js | 2 +- .../preact-complex/scripts/build.js | 2 +- .../react-complex/scripts/build.js | 2 +- .../react-redux-complex/scripts/build.js | 2 +- .../svelte-complex/scripts/build.js | 2 +- .../vue-complex/scripts/build.js | 2 +- .../javascript-es5-complex/scripts/build.js | 2 +- .../javascript-es5/scripts/build.js | 2 +- .../scripts/build.js | 2 +- .../scripts/build.js | 2 +- .../scripts/build.js | 2 +- sw.mjs | 42 +- tests/unittests/suites.mjs | 27 ++ 29 files changed, 205 insertions(+), 631 deletions(-) delete mode 100644 experimental/todomvc-localstorage/dist/base.css delete mode 100644 experimental/todomvc-localstorage/dist/index.css diff --git a/experimental/javascript-wc-indexeddb/scripts/build.js b/experimental/javascript-wc-indexeddb/scripts/build.js index 723a88731..3ae0f1739 100644 --- a/experimental/javascript-wc-indexeddb/scripts/build.js +++ b/experimental/javascript-wc-indexeddb/scripts/build.js @@ -165,4 +165,4 @@ const build = async () => { console.log("Done with building!"); }; -build().then(() => import("../../../resources/shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); +build().then(() => import("../../../resources/shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/experimental/tests.mjs b/experimental/tests.mjs index 136fb7cd5..1e54c9c99 100644 --- a/experimental/tests.mjs +++ b/experimental/tests.mjs @@ -4,9 +4,39 @@ import { numberOfItemsToAdd } from "../resources/shared/todomvc-utils.mjs"; import { freezeSuites } from "../resources/suites-helper.mjs"; export const ExperimentalSuites = freezeSuites([ + { + name: "TodoMVC-JavaScript-ES5-Cached", + url: "resources/todomvc/vanilla-examples/javascript-es5/dist/index.html", + resources: "resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", + tags: ["todomvc", "experimental", "cached"], + async prepare(page) { + (await page.waitForElement(".new-todo")).focus(); + }, + tests: [ + new BenchmarkTestStep(`Adding${numberOfItemsToAdd}Items`, (page) => { + const newTodo = page.querySelector(".new-todo"); + for (let i = 0; i < numberOfItemsToAdd; i++) { + newTodo.setValue(getTodoText("ja", i)); + newTodo.dispatchEvent("change"); + newTodo.enter("keypress"); + } + }), + new BenchmarkTestStep("CompletingAllItems", (page) => { + const checkboxes = page.querySelectorAll(".toggle"); + for (let i = 0; i < numberOfItemsToAdd; i++) + checkboxes[i].click(); + }), + new BenchmarkTestStep("DeletingAllItems", (page) => { + const deleteButtons = page.querySelectorAll(".destroy"); + for (let i = numberOfItemsToAdd - 1; i >= 0; i--) + deleteButtons[i].click(); + }), + ], + }, { name: "TodoMVC-LocalStorage", url: "experimental/todomvc-localstorage/dist/index.html", + // resources: "experimental/todomvc-localstorage/dist/resources.txt", tags: ["todomvc", "experimental"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -36,6 +66,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-Emoji", url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html", + // resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", tags: ["todomvc", "experimental"], async prepare(page) { await page.waitForElement("todo-app"); @@ -68,6 +99,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-WebComponents-PostMessage", url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html", + // resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", tags: ["experimental", "todomvc", "webcomponents"], async prepare() {}, type: "remote", @@ -78,6 +110,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-Jaspr-Dart2JS-O4", url: "experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/index.html", + // resources: "experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt", tags: ["todomvc", "experimental"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -106,6 +139,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-Jaspr-Dart2Wasm-O2", url: "experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/index.html", + // resources: "experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt", tags: ["todomvc", "experimental"], disabled: true, async prepare(page) { @@ -135,6 +169,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "NewsSite-PostMessage", url: "resources/newssite/news-next/dist/index.html", + // resources: "resources/newssite/news-next/dist/resources.txt", tags: ["experimental", "newssite", "language"], async prepare() {}, type: "remote", @@ -145,6 +180,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-WebComponents-IndexedDB", url: "experimental/javascript-wc-indexeddb/dist/index.html?useAsyncSteps=true&storageType=vanilla", + // resources: "experimental/javascript-wc-indexeddb/dist/resources.txt", tags: ["todomvc", "webcomponents", "experimental"], async prepare() {}, type: "remote", @@ -155,6 +191,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-WebComponents-DexieJS", url: "experimental/javascript-wc-indexeddb/dist/index.html?useAsyncSteps=true&storageType=dexie", + // resources: "experimental/javascript-wc-indexeddb/dist/resources.txt", tags: ["todomvc", "webcomponents", "experimental"], async prepare() {}, type: "remote", @@ -165,6 +202,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "Responsive-Design", url: "experimental/responsive-design/dist/index.html", + // resources: "experimental/responsive-design/dist/resources.txt", tags: ["responsive-design", "webcomponents", "experimental"], type: "async", async prepare(page) { diff --git a/experimental/todomvc-localstorage/dist/base.css b/experimental/todomvc-localstorage/dist/base.css deleted file mode 100644 index da65968a7..000000000 --- a/experimental/todomvc-localstorage/dist/base.css +++ /dev/null @@ -1,141 +0,0 @@ -hr { - margin: 20px 0; - border: 0; - border-top: 1px dashed #c5c5c5; - border-bottom: 1px dashed #f7f7f7; -} - -.learn a { - font-weight: normal; - text-decoration: none; - color: #b83f45; -} - -.learn a:hover { - text-decoration: underline; - color: #787e7e; -} - -.learn h3, -.learn h4, -.learn h5 { - margin: 10px 0; - font-weight: 500; - line-height: 1.2; - color: #000; -} - -.learn h3 { - font-size: 24px; -} - -.learn h4 { - font-size: 18px; -} - -.learn h5 { - margin-bottom: 0; - font-size: 14px; -} - -.learn ul { - padding: 0; - margin: 0 0 30px 25px; -} - -.learn li { - line-height: 20px; -} - -.learn p { - font-size: 15px; - font-weight: 300; - line-height: 1.3; - margin-top: 0; - margin-bottom: 0; -} - -#issue-count { - display: none; -} - -.quote { - border: none; - margin: 20px 0 60px 0; -} - -.quote p { - font-style: italic; -} - -.quote p:before { - content: '“'; - font-size: 50px; - opacity: .15; - position: absolute; - top: -20px; - left: 3px; -} - -.quote p:after { - content: '”'; - font-size: 50px; - opacity: .15; - position: absolute; - bottom: -42px; - right: 3px; -} - -.quote footer { - position: absolute; - bottom: -40px; - right: 0; -} - -.quote footer img { - border-radius: 3px; -} - -.quote footer a { - margin-left: 5px; - vertical-align: middle; -} - -.speech-bubble { - position: relative; - padding: 10px; - background: rgba(0, 0, 0, .04); - border-radius: 5px; -} - -.speech-bubble:after { - content: ''; - position: absolute; - top: 100%; - right: 30px; - border: 13px solid transparent; - border-top-color: rgba(0, 0, 0, .04); -} - -.learn-bar > .learn { - position: absolute; - width: 272px; - top: 8px; - left: -300px; - padding: 10px; - border-radius: 5px; - background-color: rgba(255, 255, 255, .6); - transition-property: left; - transition-duration: 500ms; -} - -@media (min-width: 899px) { - .learn-bar { - width: auto; - padding-left: 300px; - } - - .learn-bar > .learn { - left: 8px; - } -} diff --git a/experimental/todomvc-localstorage/dist/index.css b/experimental/todomvc-localstorage/dist/index.css deleted file mode 100644 index fcc3da583..000000000 --- a/experimental/todomvc-localstorage/dist/index.css +++ /dev/null @@ -1,393 +0,0 @@ -@charset "utf-8"; - -html, -body { - margin: 0; - padding: 0; -} - -button { - margin: 0; - padding: 0; - border: 0; - background: none; - font-size: 100%; - vertical-align: baseline; - font-family: inherit; - font-weight: inherit; - color: inherit; - -webkit-appearance: none; - appearance: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; - line-height: 1.4em; - background: #f5f5f5; - color: #111111; - min-width: 230px; - max-width: 550px; - margin: 0 auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-weight: 300; -} - -.hidden { - display: none; -} - -.todoapp { - background: #fff; - margin: 130px 0 40px 0; - position: relative; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), - 0 25px 50px 0 rgba(0, 0, 0, 0.1); -} - -.todoapp input::-webkit-input-placeholder { - font-style: italic; - font-weight: 400; - color: rgba(0, 0, 0, 0.4); -} - -.todoapp input::-moz-placeholder { - font-style: italic; - font-weight: 400; - color: rgba(0, 0, 0, 0.4); -} - -.todoapp input::input-placeholder { - font-style: italic; - font-weight: 400; - color: rgba(0, 0, 0, 0.4); -} - -.todoapp h1 { - position: absolute; - top: -140px; - width: 100%; - font-size: 80px; - font-weight: 200; - text-align: center; - color: #b83f45; - -webkit-text-rendering: optimizeLegibility; - -moz-text-rendering: optimizeLegibility; - text-rendering: optimizeLegibility; -} - -.new-todo, -.edit { - position: relative; - margin: 0; - width: 100%; - font-size: 24px; - font-family: inherit; - font-weight: inherit; - line-height: 1.4em; - color: inherit; - padding: 6px; - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); - box-sizing: border-box; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.new-todo { - padding: 16px 16px 16px 60px; - height: 65px; - border: none; - background: rgba(0, 0, 0, 0.003); - box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); -} - -.main { - position: relative; - z-index: 2; - border-top: 1px solid #e6e6e6; -} - -.toggle-all { - width: 1px; - height: 1px; - border: none; /* Mobile Safari */ - opacity: 0; - position: absolute; - right: 100%; - bottom: 100%; -} - -.toggle-all + label { - display: flex; - align-items: center; - justify-content: center; - width: 45px; - height: 65px; - font-size: 0; - position: absolute; - top: -65px; - left: -0; -} - -.toggle-all + label:before { - content: '❯'; - display: inline-block; - font-size: 22px; - color: #949494; - padding: 10px 27px 10px 27px; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} - -.toggle-all:checked + label:before { - color: #484848; -} - -.todo-list { - margin: 0; - padding: 0; - list-style: none; -} - -.todo-list li { - position: relative; - font-size: 24px; - border-bottom: 1px solid #ededed; -} - -.todo-list li:last-child { - border-bottom: none; -} - -.todo-list li.editing { - border-bottom: none; - padding: 0; -} - -.todo-list li.editing .edit { - display: block; - width: calc(100% - 43px); - padding: 12px 16px; - margin: 0 0 0 43px; -} - -.todo-list li.editing .view { - display: none; -} - -.todo-list li .toggle { - text-align: center; - width: 40px; - /* auto, since non-WebKit browsers doesn't support input styling */ - height: auto; - position: absolute; - top: 0; - bottom: 0; - margin: auto 0; - border: none; /* Mobile Safari */ - -webkit-appearance: none; - appearance: none; -} - -.todo-list li .toggle { - opacity: 0; -} - -.todo-list li .toggle + label { - /* - Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 - IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ - */ - background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); - background-repeat: no-repeat; - background-position: center left; -} - -.todo-list li .toggle:checked + label { - background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); -} - -.todo-list li label { - word-break: break-all; - padding: 15px 15px 15px 60px; - display: block; - line-height: 1.2; - transition: color 0.4s; - font-weight: 400; - color: #484848; -} - -.todo-list li.completed label { - color: #949494; - text-decoration: line-through; -} - -.todo-list li .destroy { - display: none; - position: absolute; - top: 0; - right: 10px; - bottom: 0; - width: 40px; - height: 40px; - margin: auto 0; - font-size: 30px; - color: #949494; - transition: color 0.2s ease-out; -} - -.todo-list li .destroy:hover, -.todo-list li .destroy:focus { - color: #C18585; -} - -.todo-list li .destroy:after { - content: '×'; - display: block; - height: 100%; - line-height: 1.1; -} - -.todo-list li:hover .destroy { - display: block; -} - -.todo-list li .edit { - display: none; -} - -.todo-list li.editing:last-child { - margin-bottom: -1px; -} - -.footer { - padding: 10px 15px; - height: 20px; - text-align: center; - font-size: 15px; - border-top: 1px solid #e6e6e6; -} - -.footer:before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 50px; - overflow: hidden; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), - 0 8px 0 -3px #f6f6f6, - 0 9px 1px -3px rgba(0, 0, 0, 0.2), - 0 16px 0 -6px #f6f6f6, - 0 17px 2px -6px rgba(0, 0, 0, 0.2); -} - -.todo-count { - float: left; - text-align: left; -} - -.todo-count strong { - font-weight: 300; -} - -.filters { - margin: 0; - padding: 0; - list-style: none; - position: absolute; - right: 0; - left: 0; -} - -.filters li { - display: inline; -} - -.filters li a { - color: inherit; - margin: 3px; - padding: 3px 7px; - text-decoration: none; - border: 1px solid transparent; - border-radius: 3px; -} - -.filters li a:hover { - border-color: #DB7676; -} - -.filters li a.selected { - border-color: #CE4646; -} - -.clear-completed, -html .clear-completed:active { - float: right; - position: relative; - line-height: 19px; - text-decoration: none; - cursor: pointer; -} - -.clear-completed:hover { - text-decoration: underline; -} - -.info { - margin: 65px auto 0; - color: #4d4d4d; - font-size: 11px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-align: center; -} - -.info p { - line-height: 1; -} - -.info a { - color: inherit; - text-decoration: none; - font-weight: 400; -} - -.info a:hover { - text-decoration: underline; -} - -/* - Hack to remove background from Mobile Safari. - Can't use it globally since it destroys checkboxes in Firefox -*/ -@media screen and (-webkit-min-device-pixel-ratio:0) { - .toggle-all, - .todo-list li .toggle { - background: none; - } - - .todo-list li .toggle { - height: 40px; - } -} - -@media (max-width: 430px) { - .footer { - height: 50px; - } - - .filters { - bottom: 10px; - } -} - -:focus, -.toggle:focus + label, -.toggle-all:focus + label { - box-shadow: 0 0 2px 2px #CF7D7D; - outline: 0; -} diff --git a/experimental/todomvc-localstorage/dist/index.html b/experimental/todomvc-localstorage/dist/index.html index ab573ce1b..2b016620f 100644 --- a/experimental/todomvc-localstorage/dist/index.html +++ b/experimental/todomvc-localstorage/dist/index.html @@ -6,8 +6,8 @@ TodoMVC: JavaScript Es5 - - + +
@@ -44,12 +44,12 @@

todos

Press 'enter' to add the todo.

Double-click to edit a todo

- - - - - - - + + + + + + + diff --git a/experimental/todomvc-localstorage/scripts/build.js b/experimental/todomvc-localstorage/scripts/build.js index 452400417..9997908a4 100644 --- a/experimental/todomvc-localstorage/scripts/build.js +++ b/experimental/todomvc-localstorage/scripts/build.js @@ -53,4 +53,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => import("../../../resources/shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); +build().then(() => import("../../../resources/shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/resources/default-tests.mjs b/resources/default-tests.mjs index 51cb0d2d6..b33553e8d 100644 --- a/resources/default-tests.mjs +++ b/resources/default-tests.mjs @@ -7,7 +7,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES5", url: "resources/todomvc/vanilla-examples/javascript-es5/dist/index.html", - resources: "resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", + // resources: "resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -36,6 +36,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES5-Complex-DOM", url: "resources/todomvc/vanilla-examples/javascript-es5-complex/dist/index.html", + // resources: "resources/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -64,6 +65,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES6-Webpack", url: "resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/index.html", + // resources: "resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -93,6 +95,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES6-Webpack-Complex-DOM", url: "resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/index.html", + // resources: "resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -122,6 +125,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-WebComponents", url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html", + // resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", tags: ["default", "todomvc", "webcomponents"], async prepare(page) { await page.waitForElement("todo-app"); @@ -154,6 +158,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-WebComponents-Complex-DOM", url: "resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/index.html", + // resources: "resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt", tags: ["todomvc", "webcomponents", "complex"], async prepare(page) { await page.waitForElement("todo-app"); @@ -186,6 +191,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React", url: "resources/todomvc/architecture-examples/react/dist/index.html#/home", + // resources: "resources/todomvc/architecture-examples/react/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -215,6 +221,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React-Complex-DOM", url: "resources/todomvc/architecture-examples/react-complex/dist/index.html#/home", + // resources: "resources/todomvc/architecture-examples/react-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -244,6 +251,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React-Redux", url: "resources/todomvc/architecture-examples/react-redux/dist/index.html", + // resources: "resources/todomvc/architecture-examples/react-redux/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -272,6 +280,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React-Redux-Complex-DOM", url: "resources/todomvc/architecture-examples/react-redux-complex/dist/index.html", + // resources: "resources/todomvc/architecture-examples/react-redux-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -300,6 +309,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Backbone", url: "resources/todomvc/architecture-examples/backbone/dist/index.html", + // resources: "resources/todomvc/architecture-examples/backbone/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -330,6 +340,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Backbone-Complex-DOM", url: "resources/todomvc/architecture-examples/backbone-complex/dist/index.html", + // resources: "resources/todomvc/architecture-examples/backbone-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -360,6 +371,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Angular", url: "resources/todomvc/architecture-examples/angular/dist/index.html", + // resources: "resources/todomvc/architecture-examples/angular/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -389,6 +401,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Angular-Complex-DOM", url: "resources/todomvc/architecture-examples/angular-complex/dist/index.html", + // resources: "resources/todomvc/architecture-examples/angular-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -418,6 +431,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Vue", url: "resources/todomvc/architecture-examples/vue/dist/index.html", + // resources: "resources/todomvc/architecture-examples/vue/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -447,6 +461,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Vue-Complex-DOM", url: "resources/todomvc/architecture-examples/vue-complex/dist/index.html", + // resources: "resources/todomvc/architecture-examples/vue-complex/dist/resources.txt", tags: ["todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -476,6 +491,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-jQuery", url: "resources/todomvc/architecture-examples/jquery/dist/index.html", + // resources: "resources/todomvc/architecture-examples/jquery/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -503,6 +519,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-jQuery-Complex-DOM", url: "resources/todomvc/architecture-examples/jquery-complex/dist/index.html", + // resources: "resources/todomvc/architecture-examples/jquery-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -530,6 +547,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Preact", url: "resources/todomvc/architecture-examples/preact/dist/index.html#/home", + // resources: "resources/todomvc/architecture-examples/preact/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -558,6 +576,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Preact-Complex-DOM", url: "resources/todomvc/architecture-examples/preact-complex/dist/index.html#/home", + // resources: "resources/todomvc/architecture-examples/preact-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -586,6 +605,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Svelte", url: "resources/todomvc/architecture-examples/svelte/dist/index.html", + // resources: "resources/todomvc/architecture-examples/svelte/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -614,6 +634,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Svelte-Complex-DOM", url: "resources/todomvc/architecture-examples/svelte-complex/dist/index.html", + // resources: "resources/todomvc/architecture-examples/svelte-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -642,6 +663,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Lit", url: "resources/todomvc/architecture-examples/lit/dist/index.html", + // resources: "resources/todomvc/architecture-examples/lit/dist/resources.txt", tags: ["todomvc", "webcomponents"], async prepare(page) { await page.waitForElement("todo-app"); @@ -673,6 +695,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Lit-Complex-DOM", url: "resources/todomvc/architecture-examples/lit-complex/dist/index.html", + // resources: "resources/todomvc/architecture-examples/lit-complex/dist/resources.txt", tags: ["default", "todomvc", "webcomponents", "complex", "complex-default"], async prepare(page) { await page.waitForElement("todo-app"); @@ -704,6 +727,7 @@ export const DefaultSuites = freezeSuites([ { name: "NewsSite-Next", url: "resources/newssite/news-next/dist/index.html", + // resources: "resources/newssite/news-next/dist/resources.txt", tags: ["default", "newssite", "language"], async prepare(page) { await page.waitForElement("#navbar-dropdown-toggle"); @@ -744,6 +768,7 @@ export const DefaultSuites = freezeSuites([ { name: "NewsSite-Nuxt", url: "resources/newssite/news-nuxt/dist/index.html", + // resources: "resources/newssite/news-nuxt/dist/resources.txt", tags: ["default", "newssite"], async prepare(page) { await page.waitForElement("#navbar-dropdown-toggle"); @@ -784,6 +809,7 @@ export const DefaultSuites = freezeSuites([ { name: "Editor-CodeMirror", url: "resources/editors/dist/codemirror.html", + // resources: "resources/editors/dist/resources.txt", tags: ["default", "editor"], async prepare(page) {}, tests: [ @@ -802,6 +828,7 @@ export const DefaultSuites = freezeSuites([ { name: "Editor-TipTap", url: "resources/editors/dist/tiptap.html", + // resources: "resources/editors/dist/resources.txt", tags: ["default", "editor"], async prepare(page) {}, tests: [ @@ -820,6 +847,7 @@ export const DefaultSuites = freezeSuites([ { name: "Charts-observable-plot", url: "resources/charts/dist/observable-plot.html", + // resources: "resources/charts/dist/resources.txt", tags: ["default", "chart"], async prepare(page) {}, tests: [ @@ -846,6 +874,7 @@ export const DefaultSuites = freezeSuites([ { name: "Charts-chartjs", url: "resources/charts/dist/chartjs.html", + // resources: "resources/charts/dist/resources.txt", tags: ["default", "chart"], async prepare(page) {}, tests: [ @@ -865,6 +894,7 @@ export const DefaultSuites = freezeSuites([ { name: "React-Stockcharts-SVG", url: "resources/react-stockcharts/build/index.html?type=svg", + // resources: "resources/react-stockcharts/build/resources.txt", tags: ["default", "chart", "svg"], async prepare(page) { await page.waitForElement("#render"); @@ -904,6 +934,7 @@ export const DefaultSuites = freezeSuites([ { name: "Perf-Dashboard", url: "resources/perf.webkit.org/public/v3/#/charts/?since=1678991819934&paneList=((55-1974-null-null-(5-2.5-500)))", + // resources: "resources/perf.webkit.org/public/v3/resources.txt", tags: ["default", "chart", "webcomponents"], async prepare(page) { await page.waitForElement("#app-is-ready"); diff --git a/resources/main.css b/resources/main.css index 40929ff96..a7e569a64 100644 --- a/resources/main.css +++ b/resources/main.css @@ -383,7 +383,8 @@ button.show-about { display: block; } -#progress, #preload-progress { +#progress, +#preload-progress { position: absolute; bottom: -6px; left: 60px; @@ -393,7 +394,8 @@ button.show-about { border-right: 6px solid var(--background); } -#progress-completed, #preload-progress-completed { +#progress-completed, +#preload-progress-completed { position: absolute; top: 0; left: 0; @@ -404,11 +406,13 @@ button.show-about { background-color: var(--inactive-color); } -#progress-completed::-webkit-progress-value, #preload-progress-completed::-webkit-progress-value { +#progress-completed::-webkit-progress-value, +#preload-progress-completed::-webkit-progress-value { background-color: var(--foreground); } -#progress-completed::-moz-progress-bar, #preload-progress-completed::-moz-progress-bar { +#progress-completed::-moz-progress-bar, +#preload-progress-completed::-moz-progress-bar { background-color: var(--foreground); } @@ -419,7 +423,8 @@ button.show-about { background-color: var(--background); } -#info, #preload-info { +#info, +#preload-info { display: none; position: absolute; bottom: -25px; @@ -430,11 +435,13 @@ button.show-about { text-align: center; font-size: 12px; } -#info-label, #preload-info-label { +#info-label, +#preload-info-label { position: absolute; left: 6px; } -#info-progress, #preload-info-progress { +#info-progress, +#preload-info-progress { position: absolute; right: 6px; text-align: right; @@ -911,7 +918,8 @@ section#about .note { stroke: #fff; } -#preload-progress, #preload-info { +#preload-progress, +#preload-info { display: none; } @@ -926,23 +934,17 @@ section#about .note { [data-benchmark-state="PRELOADING"] .start-tests-button { color: var(--foreground); - background-image: - linear-gradient( - -45deg, - var(--foreground) 15%, - transparent 15%, - transparent 50%, - var(--foreground) 50%, - var(--foreground) 65%, - transparent 65%, - transparent 100% - ); + background-image: linear-gradient(-45deg, var(--foreground) 15%, transparent 15%, transparent 50%, var(--foreground) 50%, var(--foreground) 65%, transparent 65%, transparent 100%); background-size: 20px 20px; animation: barber-pole 1s linear infinite; pointer-events: none; } @keyframes barber-pole { - 0% { background-position: 0 0, 0 0; } - 100% { background-position: 20px 0, 0 0; } + 0% { + background-position: 0 0, 0 0; + } + 100% { + background-position: 20px 0, 0 0; + } } diff --git a/resources/main.mjs b/resources/main.mjs index 02799c984..6b7c35e31 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -13,9 +13,8 @@ export class PreloadServiceWorker { async setup() { const existingRegistrations = await navigator.serviceWorker.getRegistrations(); - for (const existing of existingRegistrations) { + for (const existing of existingRegistrations) await existing.unregister(); - } this.registration = await navigator.serviceWorker.register("/sw.mjs", { type: "module" }); await this.registration.update(); @@ -26,18 +25,22 @@ export class PreloadServiceWorker { } async precacheSuites(suites, resourceLoadDelay, onProgress) { - if (suites.length === 0) return; + if (suites.length === 0) + return; - const suitesData = suites.filter(s => s.resources).map(s => ({ - name: s.name, - url: new URL(s.url, window.location.href).href, - resources: new URL(s.resources, window.location.href).href - })); + const suitesData = suites + .filter((s) => s.resources) + .map((s) => ({ + name: s.name, + url: new URL(s.url, window.location.href).href, + resources: new URL(s.resources, window.location.href).href, + })); - if (suitesData.length === 0) return; + if (suitesData.length === 0) + return; const startTime = performance.now(); - return new Promise((resolve) => { + await new Promise((resolve) => { const channel = new MessageChannel(); channel.port1.onmessage = (event) => { if (event.data?.type === SW_MESSAGES.PRECACHE_DONE) { @@ -51,11 +54,14 @@ export class PreloadServiceWorker { onProgress(event.data); } }; - this.sw.postMessage({ - type: SW_MESSAGES.PRECACHE_SUITES, - suites: suitesData, - delay: resourceLoadDelay - }, [channel.port2]); + this.sw.postMessage( + { + type: SW_MESSAGES.PRECACHE_SUITES, + suites: suitesData, + delay: resourceLoadDelay, + }, + [channel.port2] + ); }); } @@ -64,7 +70,6 @@ export class PreloadServiceWorker { } } - const BENCHMARK_STATE = Object.freeze({ IDLE: "IDLE", PRELOADING: "PRELOADING", @@ -84,10 +89,7 @@ class MainBenchmarkClient { _progressCompleted = null; _isRunning = false; _hasResults = false; - _developerModeContainer = null; _metrics = Object.create(null); - _developerModeContainer = null; - _progressCompleted = null; preloadServiceWorker = new PreloadServiceWorker(); _steppingPromise = null; _steppingResolver = null; @@ -475,9 +477,8 @@ class MainBenchmarkClient { _setBenchmarkState(state) { document.body.setAttribute("data-benchmark-state", state); - if (this.preloadServiceWorker) { + if (this.preloadServiceWorker) this.preloadServiceWorker.setState(state); - } const startButton = document.querySelector(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { @@ -485,10 +486,12 @@ class MainBenchmarkClient { document.getElementById("preload-info-label").textContent = "Connecting to Service Worker..."; document.getElementById("preload-info-progress").textContent = ""; document.body.style.setProperty("--preload-progress", "0%"); - if (startButton) startButton.textContent = "Preloading..."; + if (startButton) + startButton.textContent = "Preloading..."; } else if (state === BENCHMARK_STATE.READY || state === BENCHMARK_STATE.IDLE || state === BENCHMARK_STATE.DONE || state === BENCHMARK_STATE.ERROR) { document.body.style.removeProperty("--preload-progress"); - if (startButton) startButton.textContent = "Start Test"; + if (startButton) + startButton.textContent = "Start Test"; } } diff --git a/resources/shared/generate-resources.mjs b/resources/shared/generate-resources.mjs index d4ce09e76..8fb54f656 100644 --- a/resources/shared/generate-resources.mjs +++ b/resources/shared/generate-resources.mjs @@ -1,15 +1,14 @@ -import fs from 'fs'; -import path from 'path'; +import fs from "fs"; +import path from "path"; function walkDir(dir, fileList = []) { const files = fs.readdirSync(dir); for (const file of files) { const filePath = path.join(dir, file); - if (fs.statSync(filePath).isDirectory()) { + if (fs.statSync(filePath).isDirectory()) walkDir(filePath, fileList); - } else { + else fileList.push(filePath); - } } return fileList; } @@ -21,7 +20,7 @@ export function generateResourcesFile(distPath) { } const absoluteDist = path.resolve(distPath); const files = walkDir(absoluteDist); - const relativePaths = files.map(f => path.relative(absoluteDist, f)).filter(f => f !== 'resources.txt'); - fs.writeFileSync(path.join(absoluteDist, 'resources.txt'), relativePaths.join('\n') + '\n', 'utf8'); + const relativePaths = files.map((f) => path.relative(absoluteDist, f)).filter((f) => f !== "resources.txt"); + fs.writeFileSync(path.join(absoluteDist, "resources.txt"), `${relativePaths.join("\n")}\n`, "utf8"); console.log(`Generated resources.txt at ${distPath}`); } diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs index 251d23d28..076a84733 100644 --- a/resources/shared/sw-messages.mjs +++ b/resources/shared/sw-messages.mjs @@ -2,5 +2,5 @@ export const SW_MESSAGES = Object.freeze({ SET_STATE: "SET_STATE", PRECACHE_SUITES: "PRECACHE_SUITES", PRECACHE_PROGRESS: "PRECACHE_PROGRESS", - PRECACHE_DONE: "PRECACHE_DONE" + PRECACHE_DONE: "PRECACHE_DONE", }); diff --git a/resources/todomvc/architecture-examples/angular-complex/scripts/build.js b/resources/todomvc/architecture-examples/angular-complex/scripts/build.js index 42c51cd1d..cd2122f8d 100644 --- a/resources/todomvc/architecture-examples/angular-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/angular-complex/scripts/build.js @@ -17,4 +17,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js index abd31b40d..11bb3f4cc 100644 --- a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js @@ -17,4 +17,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/backbone/scripts/build.js b/resources/todomvc/architecture-examples/backbone/scripts/build.js index 7a9e9e0bd..6e6de847e 100644 --- a/resources/todomvc/architecture-examples/backbone/scripts/build.js +++ b/resources/todomvc/architecture-examples/backbone/scripts/build.js @@ -58,4 +58,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); +build().then(() => import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js index da240303a..e8b8ecb18 100644 --- a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js @@ -20,4 +20,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); \ No newline at end of file +import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/jquery/scripts/build.js b/resources/todomvc/architecture-examples/jquery/scripts/build.js index 8ff05bbaa..25a15a106 100644 --- a/resources/todomvc/architecture-examples/jquery/scripts/build.js +++ b/resources/todomvc/architecture-examples/jquery/scripts/build.js @@ -51,4 +51,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); +build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/resources/todomvc/architecture-examples/lit-complex/scripts/build.js b/resources/todomvc/architecture-examples/lit-complex/scripts/build.js index 50a585b66..32c17118d 100644 --- a/resources/todomvc/architecture-examples/lit-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/lit-complex/scripts/build.js @@ -24,4 +24,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/preact-complex/scripts/build.js b/resources/todomvc/architecture-examples/preact-complex/scripts/build.js index 0739fe789..62beccabc 100644 --- a/resources/todomvc/architecture-examples/preact-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/preact-complex/scripts/build.js @@ -15,4 +15,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/react-complex/scripts/build.js b/resources/todomvc/architecture-examples/react-complex/scripts/build.js index 53032dcfc..7b309f619 100644 --- a/resources/todomvc/architecture-examples/react-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/react-complex/scripts/build.js @@ -14,4 +14,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js index e384b8f14..064cde823 100644 --- a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.js @@ -15,4 +15,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js index db20258c1..675be7bcc 100644 --- a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js @@ -15,4 +15,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/architecture-examples/vue-complex/scripts/build.js b/resources/todomvc/architecture-examples/vue-complex/scripts/build.js index dc7e48d71..b72c4f84c 100644 --- a/resources/todomvc/architecture-examples/vue-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/vue-complex/scripts/build.js @@ -17,4 +17,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js index 93f6ddb6d..973da1277 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.js @@ -17,4 +17,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js index fbecbd875..3afe9ffa0 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.js @@ -53,4 +53,4 @@ const build = async () => { console.log("done!!"); }; -build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); +build().then(() => import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js index 531d6d507..c2b6e3983 100644 --- a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.js @@ -14,4 +14,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js index 7c7a99e4e..e80d21879 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.js @@ -23,4 +23,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));); +import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js index 86a42ad8c..7d9f8e924 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js +++ b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.js @@ -154,4 +154,4 @@ const build = async () => { console.log("Done with building!"); }; -build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"));)); +build().then(() => import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/sw.mjs b/sw.mjs index 7959db98e..dd906063c 100644 --- a/sw.mjs +++ b/sw.mjs @@ -16,7 +16,7 @@ function replyToClient(event, msg) { } function delayAsync(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } async function handlePrecache(event, { suites = [], delay = 0 }) { @@ -29,7 +29,8 @@ async function handlePrecache(event, { suites = [], delay = 0 }) { cachedSuitesPrefixes.clear(); for (const suite of suites) { - if (!suite.resources) continue; + if (!suite.resources) + continue; const prefix = new URL(".", suite.url).href; cachedSuitesPrefixes.add(prefix); urlsToCache.push(...await parseSuiteResources(suite)); @@ -45,19 +46,22 @@ async function handlePrecache(event, { suites = [], delay = 0 }) { await Promise.all(promises); - cachedUrls = new Set(urlsToCache.map(item => item.url)); + cachedUrls = new Set(urlsToCache.map((item) => item.url)); replyToClient(event, { type: SW_MESSAGES.PRECACHE_DONE, totalSize, count: urlsToCache.length }); } async function parseSuiteResources(suite) { try { const response = await fetch(suite.resources); - if (!response.ok) return []; - const text = await response.text(); - const lines = text.split("\n").map(l => l.trim()).filter(l => l.length > 0); - return lines.map(resourceUrl => ({ + if (!response.ok) + return []; + let text = await response.text(); + if (text.endsWith("\n")) + text = text.slice(0, -1); + const lines = text.split("\n").map((l) => l.trim()); + return lines.map((resourceUrl) => ({ url: new URL(resourceUrl, suite.url).href, - suiteName: suite.name + suiteName: suite.name, })); } catch (e) { console.warn("Failed to fetch resources.txt for", suite.name); @@ -66,12 +70,14 @@ async function parseSuiteResources(suite) { } async function fetchAndCache(cache, url, delayMs) { - if (delayMs) await delayAsync(delayMs); + if (delayMs) + await delayAsync(delayMs); const request = new Request(url, { cache: "no-cache" }); try { await cache.add(request); const cachedResponse = await cache.match(request); - if (!cachedResponse) return 0; + if (!cachedResponse) + return 0; const blob = await cachedResponse.blob(); return blob.size; } catch (e) { @@ -90,13 +96,13 @@ self.addEventListener("activate", function (event) { self.addEventListener("message", function (event) { const { data } = event; - if (!data) return; + if (!data) + return; - if (data.type === SW_MESSAGES.SET_STATE) { + if (data.type === SW_MESSAGES.SET_STATE) currentState = data.state; - } else if (data.type === SW_MESSAGES.PRECACHE_SUITES) { + else if (data.type === SW_MESSAGES.PRECACHE_SUITES) event.waitUntil(handlePrecache(event, data)); - } }); self.addEventListener("fetch", function (event) { @@ -107,9 +113,10 @@ self.addEventListener("fetch", function (event) { return; } - // We only enforce strict blocking when the benchmark is actively RUNNING + // We only enforce strict blocking when the benchmark is actively RUNNING // to allow runner resources to be loaded. - if (currentState !== BENCHMARK_STATE.RUNNING) return; + if (currentState !== BENCHMARK_STATE.RUNNING) + return; let isCachedSuite = false; for (const prefix of cachedSuitesPrefixes) { @@ -130,6 +137,7 @@ self.addEventListener("fetch", function (event) { async function handleFetch(request) { const cache = await caches.open(CACHE_NAME); const cachedResponse = await cache.match(request); - if (cachedResponse) return cachedResponse; + if (cachedResponse) + return cachedResponse; return fetch(request); } diff --git a/tests/unittests/suites.mjs b/tests/unittests/suites.mjs index 243b3a98d..86b520ad8 100644 --- a/tests/unittests/suites.mjs +++ b/tests/unittests/suites.mjs @@ -64,6 +64,33 @@ for (const [name, suites] of Object.entries(Suites)) { expect(suite.url.length).to.be.greaterThan(0); }); }); + it("should have resources.txt listing only valid files", async () => { + const baseUrl = `${window.location.origin}/`; + for (const suite of suites) { + if (!suite.resources) + continue; + const resourcesUrl = new URL(suite.resources, baseUrl).href; + const res = await fetch(resourcesUrl); + expect(res.ok).to.be(true); + let text = await res.text(); + expect(text.endsWith("\n")).to.be(true); + text = text.slice(0, -1); + + const files = text.split("\n"); + for (const file of files) + expect(file.trim().length).to.be.greaterThan(0); + + await Promise.all( + files.map(async (file) => { + const fileUrl = new URL(file, resourcesUrl).href; + const fileRes = await fetch(fileUrl, { method: "HEAD" }); + if (!fileRes.ok) + throw new Error(`Failed to load ${fileUrl} (listed in ${resourcesUrl})`); + expect(fileRes.ok).to.be(true); + }) + ); + } + }); }); } From f4b8259c0de90e575d488658d4ad94e5e87d6e96 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Wed, 10 Jun 2026 17:36:07 +0200 Subject: [PATCH 04/69] more-sw-fixes --- .../dist/src/speedometer-utils/params.mjs | 7 +++ .../responsive-design/package-lock.json | 4 ++ .../todomvc-localstorage/dist/index.html | 18 +++--- .../todomvc-localstorage/package-lock.json | 4 +- resources/main.mjs | 55 ++++++++++++------- resources/shared/params.mjs | 4 ++ resources/suite-runner.mjs | 6 +- sw.mjs | 37 ++++++++----- tests/unittests/params.mjs | 18 ++++++ tests/unittests/suites.mjs | 7 +-- 10 files changed, 109 insertions(+), 51 deletions(-) diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs index 520679d24..d15b68d76 100644 --- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs @@ -35,6 +35,8 @@ export class Params { measurePrepare = false; // External config url to override internal tests. config = ""; + // Resource load delay in ms for the service worker pre-caching. + resourceLoadDelay = 0; constructor(searchParams = undefined) { if (searchParams) @@ -68,6 +70,7 @@ export class Params { this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES); this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare"); this.config = this._parseConfig(searchParams); + this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0); const unused = Array.from(searchParams.keys()); if (unused.length > 0) @@ -203,6 +206,10 @@ export class Params { toSearchParams() { return this.toSearchParamsObject().toString(); } + + isDefault() { + return this === defaultParams; + } } function isValidJsonUrl(url) { diff --git a/experimental/responsive-design/package-lock.json b/experimental/responsive-design/package-lock.json index b78e3b8fb..b50e599b3 100644 --- a/experimental/responsive-design/package-lock.json +++ b/experimental/responsive-design/package-lock.json @@ -1015,6 +1015,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1219,6 +1220,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -3327,6 +3329,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -4132,6 +4135,7 @@ "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, diff --git a/experimental/todomvc-localstorage/dist/index.html b/experimental/todomvc-localstorage/dist/index.html index 2b016620f..ab573ce1b 100644 --- a/experimental/todomvc-localstorage/dist/index.html +++ b/experimental/todomvc-localstorage/dist/index.html @@ -6,8 +6,8 @@ TodoMVC: JavaScript Es5 - - + +
@@ -44,12 +44,12 @@

todos

Press 'enter' to add the todo.

Double-click to edit a todo

- - - - - - - + + + + + + + diff --git a/experimental/todomvc-localstorage/package-lock.json b/experimental/todomvc-localstorage/package-lock.json index 140b1e004..22d21efe6 100644 --- a/experimental/todomvc-localstorage/package-lock.json +++ b/experimental/todomvc-localstorage/package-lock.json @@ -1,11 +1,11 @@ { - "name": "todomvc-javascript-es5", + "name": "todomvc-localstorage", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "todomvc-javascript-es5", + "name": "todomvc-localstorage", "version": "1.0.0", "dependencies": { "todomvc-app-css": "^2.4.2", diff --git a/resources/main.mjs b/resources/main.mjs index 6b7c35e31..456c40c55 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -24,7 +24,7 @@ export class PreloadServiceWorker { return true; } - async precacheSuites(suites, resourceLoadDelay, onProgress) { + async precacheSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { if (suites.length === 0) return; @@ -59,6 +59,7 @@ export class PreloadServiceWorker { type: SW_MESSAGES.PRECACHE_SUITES, suites: suitesData, delay: resourceLoadDelay, + clearCache: clearCache, }, [channel.port2] ); @@ -146,10 +147,13 @@ class MainBenchmarkClient { if (this._isRunning) return false; - this._setBenchmarkState(BENCHMARK_STATE.RUNNING); - const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; + if (!params.isDefault()) + await this._cacheResources(benchmarkConfigurator); + + this._setBenchmarkState(BENCHMARK_STATE.RUNNING); + const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const totalSuitesCount = enabledSuites.length; @@ -445,29 +449,42 @@ class MainBenchmarkClient { async _setupServiceWorker(benchmarkConfigurator) { await this.preloadServiceWorker.setup(); + await this._cacheResources(benchmarkConfigurator); + } - this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); + async _cacheResources(benchmarkConfigurator) { const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); + const clearCache = !params.isDefault(); + this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); try { - await this.preloadServiceWorker.precacheSuites(enabledSuites, params.resourceLoadDelay, (progressData) => { - const { loaded, total, url, suiteName } = progressData; - document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); - document.getElementById("preload-progress-completed").max = total; - document.getElementById("preload-progress-completed").value = loaded; - const filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; - const labelText = suiteName ? `${suiteName}: ${filename}` : filename; - document.getElementById("preload-info-label").textContent = labelText; - document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; - }); + await this.preloadServiceWorker.precacheSuites( + enabledSuites, + params.resourceLoadDelay, + clearCache, + this._updateCacheProgress.bind(this) + ); + this._didInitialPrecache = true; this._enableStartButtons(); } catch (error) { - console.error("Service Worker setup failed:", error); + console.error("Service Worker precache failed:", error); this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._enableStartButtons(); } } + _updateCacheProgress(progressData) { + const { loaded, total, url, suiteName } = progressData; + document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); + const progress = document.getElementById("preload-progress-completed") + progress.max = total; + progress.value = loaded; + const filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; + const labelText = suiteName ? `${suiteName}: ${filename}` : filename; + document.getElementById("preload-info-label").textContent = labelText; + document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; + } + _enableStartButtons() { this._setBenchmarkState(BENCHMARK_STATE.READY); document.querySelectorAll(".start-tests-button").forEach((button) => { @@ -483,15 +500,13 @@ class MainBenchmarkClient { const startButton = document.querySelector(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { document.getElementById("preload-progress-completed").value = 0; - document.getElementById("preload-info-label").textContent = "Connecting to Service Worker..."; + document.getElementById("preload-info-label").textContent = ""; document.getElementById("preload-info-progress").textContent = ""; document.body.style.setProperty("--preload-progress", "0%"); - if (startButton) - startButton.textContent = "Preloading..."; + startButton.textContent = "Preloading..."; } else if (state === BENCHMARK_STATE.READY || state === BENCHMARK_STATE.IDLE || state === BENCHMARK_STATE.DONE || state === BENCHMARK_STATE.ERROR) { document.body.style.removeProperty("--preload-progress"); - if (startButton) - startButton.textContent = "Start Test"; + startButton.textContent = "Start Test"; } } diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index bc8ba51bb..d15b68d76 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -206,6 +206,10 @@ export class Params { toSearchParams() { return this.toSearchParamsObject().toString(); } + + isDefault() { + return this === defaultParams; + } } function isValidJsonUrl(url) { diff --git a/resources/suite-runner.mjs b/resources/suite-runner.mjs index 1872a7e87..5f2e4cfeb 100644 --- a/resources/suite-runner.mjs +++ b/resources/suite-runner.mjs @@ -105,8 +105,10 @@ export class SuiteRunner { const frame = this.#frame; frame.onload = () => resolve(); frame.onerror = () => reject(); - const splitUrl = this.#suite.url.split("?"); - frame.src = `${splitUrl[0]}?${splitUrl[1] ?? ""}&${this.#params.toSearchParams()}`; + const urlObj = new URL(this.#suite.url, window.location.href); + for (const [key, value] of this.#params.toSearchParamsObject()) + urlObj.searchParams.append(key, value); + frame.src = urlObj.href; }); } diff --git a/sw.mjs b/sw.mjs index dd906063c..59bbf50ef 100644 --- a/sw.mjs +++ b/sw.mjs @@ -19,15 +19,16 @@ function delayAsync(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function handlePrecache(event, { suites = [], delay = 0 }) { - await caches.delete(CACHE_NAME); +async function handlePrecache(event, { suites = [], delay = 0, clearCache = true }) { + if (clearCache) { + await caches.delete(CACHE_NAME); + cachedSuitesPrefixes.clear(); + } const cache = await caches.open(CACHE_NAME); let loaded = 0; let totalSize = 0; const urlsToCache = []; - - cachedSuitesPrefixes.clear(); for (const suite of suites) { if (!suite.resources) continue; @@ -46,7 +47,9 @@ async function handlePrecache(event, { suites = [], delay = 0 }) { await Promise.all(promises); - cachedUrls = new Set(urlsToCache.map((item) => item.url)); + for (const item of urlsToCache) { + cachedUrls.add(item.url); + } replyToClient(event, { type: SW_MESSAGES.PRECACHE_DONE, totalSize, count: urlsToCache.length }); } @@ -55,12 +58,9 @@ async function parseSuiteResources(suite) { const response = await fetch(suite.resources); if (!response.ok) return []; - let text = await response.text(); - if (text.endsWith("\n")) - text = text.slice(0, -1); - const lines = text.split("\n").map((l) => l.trim()); - return lines.map((resourceUrl) => ({ - url: new URL(resourceUrl, suite.url).href, + const text = await response.text(); + return text.trim().split("\n").map((resourceUrl) => ({ + url: new URL(resourceUrl.trim(), suite.url).href, suiteName: suite.name, })); } catch (e) { @@ -70,9 +70,16 @@ async function parseSuiteResources(suite) { } async function fetchAndCache(cache, url, delayMs) { + const request = new Request(url, { cache: "no-cache" }); + const existing = await cache.match(request); + if (existing) { + const blob = await existing.blob(); + return blob.size; + } + if (delayMs) await delayAsync(delayMs); - const request = new Request(url, { cache: "no-cache" }); + try { await cache.add(request); const cachedResponse = await cache.match(request); @@ -106,7 +113,9 @@ self.addEventListener("message", function (event) { }); self.addEventListener("fetch", function (event) { - const isCached = cachedUrls.has(event.request.url); + const urlObj = new URL(event.request.url); + const cleanUrl = urlObj.origin + urlObj.pathname; + const isCached = cachedUrls.has(cleanUrl) || cachedUrls.has(event.request.url); if (isCached) { event.respondWith(handleFetch(event.request)); @@ -136,7 +145,7 @@ self.addEventListener("fetch", function (event) { async function handleFetch(request) { const cache = await caches.open(CACHE_NAME); - const cachedResponse = await cache.match(request); + const cachedResponse = await cache.match(request, { ignoreSearch: true }); if (cachedResponse) return cachedResponse; return fetch(request); diff --git a/tests/unittests/params.mjs b/tests/unittests/params.mjs index e3efe45dc..21ffab292 100644 --- a/tests/unittests/params.mjs +++ b/tests/unittests/params.mjs @@ -76,6 +76,24 @@ describe("Params", () => { }); }); + describe("isDefault", () => { + it("should return true for defaultParams", () => { + expect(defaultParams.isDefault()).to.be(true); + }); + it("should return false for newly instantiated empty Params", () => { + const params = new Params(); + expect(params.isDefault()).to.be(false); + }); + it("should return false for custom params", () => { + const params = new Params( + new URLSearchParams({ + iterationCount: "100", + }) + ); + expect(params.isDefault()).to.be(false); + }); + }); + describe("parse input params", () => { it("should parse custom viewport", () => { const params = new Params( diff --git a/tests/unittests/suites.mjs b/tests/unittests/suites.mjs index 86b520ad8..37e9fbf9d 100644 --- a/tests/unittests/suites.mjs +++ b/tests/unittests/suites.mjs @@ -72,11 +72,10 @@ for (const [name, suites] of Object.entries(Suites)) { const resourcesUrl = new URL(suite.resources, baseUrl).href; const res = await fetch(resourcesUrl); expect(res.ok).to.be(true); - let text = await res.text(); - expect(text.endsWith("\n")).to.be(true); - text = text.slice(0, -1); + const text = await res.text(); + expect(text.trim().length).to.be.greaterThan(0, `resources.txt for ${suite.name} is empty`); - const files = text.split("\n"); + const files = text.trim().split("\n"); for (const file of files) expect(file.trim().length).to.be.greaterThan(0); From 6a6fe032532f63692714990795c9c0aadb493125 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Wed, 10 Jun 2026 17:39:54 +0200 Subject: [PATCH 05/69] pre-format --- resources/main.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/main.css b/resources/main.css index a7e569a64..721cd422e 100644 --- a/resources/main.css +++ b/resources/main.css @@ -932,6 +932,10 @@ section#about .note { display: block; } +[data-benchmark-state="RUNNING"] #info { + display: block; +} + [data-benchmark-state="PRELOADING"] .start-tests-button { color: var(--foreground); background-image: linear-gradient(-45deg, var(--foreground) 15%, transparent 15%, transparent 50%, var(--foreground) 50%, var(--foreground) 65%, transparent 65%, transparent 100%); From a7057a4a1f196907dd19205c73dc2d7a1accdd49 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Wed, 10 Jun 2026 17:43:00 +0200 Subject: [PATCH 06/69] formatting --- resources/main.mjs | 9 ++------- sw.mjs | 15 +++++++++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 456c40c55..e7b0e498d 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -458,12 +458,7 @@ class MainBenchmarkClient { this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); try { - await this.preloadServiceWorker.precacheSuites( - enabledSuites, - params.resourceLoadDelay, - clearCache, - this._updateCacheProgress.bind(this) - ); + await this.preloadServiceWorker.precacheSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); this._didInitialPrecache = true; this._enableStartButtons(); } catch (error) { @@ -476,7 +471,7 @@ class MainBenchmarkClient { _updateCacheProgress(progressData) { const { loaded, total, url, suiteName } = progressData; document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); - const progress = document.getElementById("preload-progress-completed") + const progress = document.getElementById("preload-progress-completed"); progress.max = total; progress.value = loaded; const filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; diff --git a/sw.mjs b/sw.mjs index 59bbf50ef..470efbf73 100644 --- a/sw.mjs +++ b/sw.mjs @@ -47,9 +47,9 @@ async function handlePrecache(event, { suites = [], delay = 0, clearCache = true await Promise.all(promises); - for (const item of urlsToCache) { + for (const item of urlsToCache) cachedUrls.add(item.url); - } + replyToClient(event, { type: SW_MESSAGES.PRECACHE_DONE, totalSize, count: urlsToCache.length }); } @@ -59,10 +59,13 @@ async function parseSuiteResources(suite) { if (!response.ok) return []; const text = await response.text(); - return text.trim().split("\n").map((resourceUrl) => ({ - url: new URL(resourceUrl.trim(), suite.url).href, - suiteName: suite.name, - })); + return text + .trim() + .split("\n") + .map((resourceUrl) => ({ + url: new URL(resourceUrl.trim(), suite.url).href, + suiteName: suite.name, + })); } catch (e) { console.warn("Failed to fetch resources.txt for", suite.name); return []; From 1842cf65cf77436e60a5f5b374781f09980d5bd6 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 11 Jun 2026 11:40:03 +0200 Subject: [PATCH 07/69] formatting --- resources/developer-mode.mjs | 9 ++++- resources/main.css | 71 +++++++++++++++++++++++++++++++----- resources/main.mjs | 21 ++++++++--- resources/shared/params.mjs | 3 ++ 4 files changed, 87 insertions(+), 17 deletions(-) diff --git a/resources/developer-mode.mjs b/resources/developer-mode.mjs index 0d250eb57..2f165e3bb 100644 --- a/resources/developer-mode.mjs +++ b/resources/developer-mode.mjs @@ -26,6 +26,7 @@ export function createDeveloperModeContainer() { settings.append(createUIForSyncStepDelay()); settings.append(createUIForAsyncSteps()); settings.append(createUIForLayoutMode()); + settings.append(createUIForPreload()); content.append(document.createElement("hr")); content.append(settings); @@ -50,6 +51,12 @@ function createUIForWarmupSuite() { }); } +function createUIForPreload() { + return createCheckboxUI("Enable service worker for resource preloading", params.preload, (isChecked) => { + params.preload = isChecked; + }); +} + function createUIForMeasurePrepare() { return createCheckboxUI("Measure Prepare", params.measurePrepare, (isChecked) => { params.measurePrepare = isChecked; @@ -72,7 +79,7 @@ function createCheckboxUI(labelValue, initialValue, paramsUpdateCallback) { }; const label = document.createElement("label"); - label.append(checkbox, " ", span(labelValue)); + label.append(checkbox, span(labelValue)); return label; } diff --git a/resources/main.css b/resources/main.css index 721cd422e..a54a07d64 100644 --- a/resources/main.css +++ b/resources/main.css @@ -10,7 +10,6 @@ --text-width: 650px; --metrics-line-height: 25px; --scrollbar-width: 10px; - --developer-mode-settings-height: 280px; } body { @@ -226,6 +225,16 @@ button, position: fixed; left: 10px; top: 10px; + max-height: calc(100vh - 20px); + display: grid; + grid-template-rows: minmax(0, 1fr); + box-sizing: border-box; +} + +.developer-mode details { + display: grid; + grid-template-rows: min-content minmax(0, 1fr); + min-height: 0; } .developer-mode summary { @@ -236,14 +245,20 @@ button, } .developer-mode-content { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; padding-bottom: 1rem; + box-sizing: border-box; } .developer-mode .suites { - max-height: calc(100vh - var(--developer-mode-settings-height)); + flex: 0 1 auto; + min-height: 0; margin-right: calc(0px - var(--scrollbar-width)); padding-right: var(--scrollbar-width); - overflow: auto; + overflow-y: auto; } .developer-mode-content ol { @@ -253,6 +268,7 @@ button, } .developer-mode-content .button-bar { display: flex; + flex: none; margin-top: 5px; gap: 3px; } @@ -265,20 +281,54 @@ button, content: " ✘"; } -.developer-mode-content .settings label { +.developer-mode-content .settings { + flex: none; + display: grid; + /* Col 1: checkbox, Col 2: text, Col 3: input, Col 4: value */ + grid-template-columns: min-content 1fr 1fr 5em; + align-items: center; + column-gap: 4px; + row-gap: 4px; +} +.developer-mode-content .settings hr { + grid-column: 1 / -1; width: 100%; + margin: 8px 0; +} + +.developer-mode-content .settings label { + display: contents; +} +.developer-mode-content .settings label > span:first-child { + /* Range and Select labels start here */ + grid-column: 2; +} +.developer-mode-content .settings label:not(:has(input[type="checkbox"]))::before { + content: "•"; + grid-column: 1; + margin-right: 0.2em; /* match checkbox margin so it visually centers */ display: flex; + align-items: center; + justify-content: center; + color: var(--foreground); } -.developer-mode-content .settings label * { - flex: 1; +.developer-mode-content input[type="checkbox"] { + margin: 0; + margin-right: 0.2em; } .developer-mode-content .settings input[type="checkbox"] { - flex: 0; - margin-left: 0px; + grid-column: 1; +} +.developer-mode-content .settings input[type="checkbox"] + span { + /* Checkbox text spans the rest of the row */ + grid-column: 2 / -1; +} +.developer-mode-content .settings select { + /* Select spans input and value columns */ + grid-column: 3 / -1; } .developer-mode-content .settings .range-label-data { - flex: 0; - min-width: 5em; + grid-column: 4; text-align: right; } @@ -313,6 +363,7 @@ button, background: rgba(255, 255, 255, 0.1); } .developer-mode-content hr { + flex: none; width: initial; margin: 10px 0; } diff --git a/resources/main.mjs b/resources/main.mjs index e7b0e498d..23d5ce489 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -16,6 +16,12 @@ export class PreloadServiceWorker { for (const existing of existingRegistrations) await existing.unregister(); + if (!params.preload) { + this.registration = null; + this.sw = null; + return false; + } + this.registration = await navigator.serviceWorker.register("/sw.mjs", { type: "module" }); await this.registration.update(); await navigator.serviceWorker.ready; @@ -25,7 +31,7 @@ export class PreloadServiceWorker { } async precacheSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { - if (suites.length === 0) + if (!this.sw || suites.length === 0) return; const suitesData = suites @@ -67,7 +73,8 @@ export class PreloadServiceWorker { } setState(state) { - this.sw.postMessage({ type: SW_MESSAGES.SET_STATE, state }); + if (this.sw) + this.sw.postMessage({ type: SW_MESSAGES.SET_STATE, state }); } } @@ -107,14 +114,14 @@ class MainBenchmarkClient { }); } - start() { + async start() { if (this._isStepping()) this._clearStepping(); - else if (this._startBenchmark()) + else if (await this._startBenchmark()) this._showSection("#running"); } - step() { + async step() { const currentSteppingResolver = this._steppingResolver; this._steppingPromise = new Promise((resolve) => { this._steppingResolver = resolve; @@ -122,7 +129,7 @@ class MainBenchmarkClient { if (this._isStepping()) currentSteppingResolver(); if (!this._isRunning) { - this._startBenchmark(); + await this._startBenchmark(); this._showSection("#running"); } } @@ -149,6 +156,8 @@ class MainBenchmarkClient { const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; + await this.preloadServiceWorker.setup(); + if (!params.isDefault()) await this._cacheResources(benchmarkConfigurator); diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index d15b68d76..92b846d91 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -37,6 +37,8 @@ export class Params { config = ""; // Resource load delay in ms for the service worker pre-caching. resourceLoadDelay = 0; + // Enable service worker for resource preloading. + preload = false; constructor(searchParams = undefined) { if (searchParams) @@ -71,6 +73,7 @@ export class Params { this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare"); this.config = this._parseConfig(searchParams); this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0); + this.preload = this._parseBooleanParam(searchParams, "preload"); const unused = Array.from(searchParams.keys()); if (unused.length > 0) From e35d860ce9d2f4ff0f47fdc4815cf244e0bf1012 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 11 Jun 2026 15:05:23 +0200 Subject: [PATCH 08/69] f --- resources/developer-mode.mjs | 2 +- resources/main.css | 71 +++++------------------------------- 2 files changed, 11 insertions(+), 62 deletions(-) diff --git a/resources/developer-mode.mjs b/resources/developer-mode.mjs index 2f165e3bb..0483cf44c 100644 --- a/resources/developer-mode.mjs +++ b/resources/developer-mode.mjs @@ -79,7 +79,7 @@ function createCheckboxUI(labelValue, initialValue, paramsUpdateCallback) { }; const label = document.createElement("label"); - label.append(checkbox, span(labelValue)); + label.append(checkbox, " ", span(labelValue)); return label; } diff --git a/resources/main.css b/resources/main.css index a54a07d64..721cd422e 100644 --- a/resources/main.css +++ b/resources/main.css @@ -10,6 +10,7 @@ --text-width: 650px; --metrics-line-height: 25px; --scrollbar-width: 10px; + --developer-mode-settings-height: 280px; } body { @@ -225,16 +226,6 @@ button, position: fixed; left: 10px; top: 10px; - max-height: calc(100vh - 20px); - display: grid; - grid-template-rows: minmax(0, 1fr); - box-sizing: border-box; -} - -.developer-mode details { - display: grid; - grid-template-rows: min-content minmax(0, 1fr); - min-height: 0; } .developer-mode summary { @@ -245,20 +236,14 @@ button, } .developer-mode-content { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; padding-bottom: 1rem; - box-sizing: border-box; } .developer-mode .suites { - flex: 0 1 auto; - min-height: 0; + max-height: calc(100vh - var(--developer-mode-settings-height)); margin-right: calc(0px - var(--scrollbar-width)); padding-right: var(--scrollbar-width); - overflow-y: auto; + overflow: auto; } .developer-mode-content ol { @@ -268,7 +253,6 @@ button, } .developer-mode-content .button-bar { display: flex; - flex: none; margin-top: 5px; gap: 3px; } @@ -281,54 +265,20 @@ button, content: " ✘"; } -.developer-mode-content .settings { - flex: none; - display: grid; - /* Col 1: checkbox, Col 2: text, Col 3: input, Col 4: value */ - grid-template-columns: min-content 1fr 1fr 5em; - align-items: center; - column-gap: 4px; - row-gap: 4px; -} -.developer-mode-content .settings hr { - grid-column: 1 / -1; - width: 100%; - margin: 8px 0; -} - .developer-mode-content .settings label { - display: contents; -} -.developer-mode-content .settings label > span:first-child { - /* Range and Select labels start here */ - grid-column: 2; -} -.developer-mode-content .settings label:not(:has(input[type="checkbox"]))::before { - content: "•"; - grid-column: 1; - margin-right: 0.2em; /* match checkbox margin so it visually centers */ + width: 100%; display: flex; - align-items: center; - justify-content: center; - color: var(--foreground); } -.developer-mode-content input[type="checkbox"] { - margin: 0; - margin-right: 0.2em; +.developer-mode-content .settings label * { + flex: 1; } .developer-mode-content .settings input[type="checkbox"] { - grid-column: 1; -} -.developer-mode-content .settings input[type="checkbox"] + span { - /* Checkbox text spans the rest of the row */ - grid-column: 2 / -1; -} -.developer-mode-content .settings select { - /* Select spans input and value columns */ - grid-column: 3 / -1; + flex: 0; + margin-left: 0px; } .developer-mode-content .settings .range-label-data { - grid-column: 4; + flex: 0; + min-width: 5em; text-align: right; } @@ -363,7 +313,6 @@ button, background: rgba(255, 255, 255, 0.1); } .developer-mode-content hr { - flex: none; width: initial; margin: 10px 0; } From e77b75f9b549fdaa2b4e983ddf0acdcc1580aca6 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 11 Jun 2026 15:22:51 +0200 Subject: [PATCH 09/69] formatting --- index.html | 2 +- resources/developer-mode.mjs | 2 +- resources/main.css | 9 ++++++++- resources/main.mjs | 4 ++-- resources/shared/params.mjs | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 972e8051d..6a26fc563 100644 --- a/index.html +++ b/index.html @@ -26,7 +26,7 @@
- +
About Speedometer diff --git a/resources/developer-mode.mjs b/resources/developer-mode.mjs index 0483cf44c..2d8260619 100644 --- a/resources/developer-mode.mjs +++ b/resources/developer-mode.mjs @@ -52,7 +52,7 @@ function createUIForWarmupSuite() { } function createUIForPreload() { - return createCheckboxUI("Enable service worker for resource preloading", params.preload, (isChecked) => { + return createCheckboxUI("Use service worker for resource preloading", params.preload, (isChecked) => { params.preload = isChecked; }); } diff --git a/resources/main.css b/resources/main.css index 721cd422e..00f76ea88 100644 --- a/resources/main.css +++ b/resources/main.css @@ -938,12 +938,19 @@ section#about .note { [data-benchmark-state="PRELOADING"] .start-tests-button { color: var(--foreground); - background-image: linear-gradient(-45deg, var(--foreground) 15%, transparent 15%, transparent 50%, var(--foreground) 50%, var(--foreground) 65%, transparent 65%, transparent 100%); + background-image: linear-gradient(-45deg, var(--foreground) 3px, transparent 3px, transparent 50%, var(--foreground) 50%, var(--foreground) calc(50% + 3px), transparent calc(50% + 3px), transparent 100%); background-size: 20px 20px; animation: barber-pole 1s linear infinite; pointer-events: none; } +[data-benchmark-state="PRELOADING"] .start-tests-button > div { + background-color: var(--background); + border-radius: 10px; + padding: 0 0.3em; + display: inline-block; +} + @keyframes barber-pole { 0% { background-position: 0 0, 0 0; diff --git a/resources/main.mjs b/resources/main.mjs index 23d5ce489..ecb9e576e 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -507,10 +507,10 @@ class MainBenchmarkClient { document.getElementById("preload-info-label").textContent = ""; document.getElementById("preload-info-progress").textContent = ""; document.body.style.setProperty("--preload-progress", "0%"); - startButton.textContent = "Preloading..."; + startButton.innerHTML = "
Preloading
"; } else if (state === BENCHMARK_STATE.READY || state === BENCHMARK_STATE.IDLE || state === BENCHMARK_STATE.DONE || state === BENCHMARK_STATE.ERROR) { document.body.style.removeProperty("--preload-progress"); - startButton.textContent = "Start Test"; + startButton.innerHTML = "
Start Test
"; } } diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index 92b846d91..b54bf1277 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -37,7 +37,7 @@ export class Params { config = ""; // Resource load delay in ms for the service worker pre-caching. resourceLoadDelay = 0; - // Enable service worker for resource preloading. + // Use service worker for resource preloading. preload = false; constructor(searchParams = undefined) { From 8b81a140e0e85f15b8240eae57e0f03623df8fed Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 15 Jun 2026 13:48:47 +0200 Subject: [PATCH 10/69] wip --- .../dist/resources.txt | 2 +- .../dist/src/speedometer-utils/params.mjs | 3 + .../src/speedometer-utils/test-invoker.mjs | 18 +-- .../src/speedometer-utils/test-runner.mjs | 116 ------------------ .../javascript-wc-indexeddb/scripts/build.mjs | 7 +- .../todomvc-localstorage/scripts/build.mjs | 6 +- .../angular-complex/scripts/build.mjs | 3 +- .../backbone-complex/scripts/build.mjs | 3 +- .../backbone/scripts/build.mjs | 6 +- .../jquery-complex/scripts/build.mjs | 1 + .../jquery/scripts/build.mjs | 3 + .../lit-complex/scripts/build.mjs | 3 +- .../preact-complex/scripts/build.mjs | 3 +- .../react-complex/scripts/build.mjs | 3 +- .../react-redux-complex/scripts/build.mjs | 3 +- .../svelte-complex/scripts/build.mjs | 3 +- .../vue-complex/scripts/build.mjs | 3 +- .../javascript-es5-complex/scripts/build.mjs | 3 +- .../javascript-es5/scripts/build.mjs | 6 +- .../scripts/build.mjs | 3 +- .../scripts/build.mjs | 3 +- .../scripts/build.mjs | 7 +- 22 files changed, 66 insertions(+), 142 deletions(-) delete mode 100644 experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs diff --git a/experimental/javascript-wc-indexeddb/dist/resources.txt b/experimental/javascript-wc-indexeddb/dist/resources.txt index 7dc424065..900a4aee1 100644 --- a/experimental/javascript-wc-indexeddb/dist/resources.txt +++ b/experimental/javascript-wc-indexeddb/dist/resources.txt @@ -17,8 +17,8 @@ src/index.mjs src/speedometer-utils/benchmark.mjs src/speedometer-utils/helpers.mjs src/speedometer-utils/params.mjs +src/speedometer-utils/step-runner.mjs src/speedometer-utils/test-invoker.mjs -src/speedometer-utils/test-runner.mjs src/speedometer-utils/todomvc-utils.mjs src/speedometer-utils/translations.mjs src/storage/base-storage-manager.js diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs index d15b68d76..b54bf1277 100644 --- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs @@ -37,6 +37,8 @@ export class Params { config = ""; // Resource load delay in ms for the service worker pre-caching. resourceLoadDelay = 0; + // Use service worker for resource preloading. + preload = false; constructor(searchParams = undefined) { if (searchParams) @@ -71,6 +73,7 @@ export class Params { this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare"); this.config = this._parseConfig(searchParams); this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0); + this.preload = this._parseBooleanParam(searchParams, "preload"); const unused = Array.from(searchParams.keys()); if (unused.length > 0) diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs index cb63c3de3..df7d96a21 100644 --- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs @@ -1,4 +1,4 @@ -class TestInvoker { +class StepScheduler { constructor(syncCallback, asyncCallback, reportCallback, params) { this._syncCallback = syncCallback; this._asyncCallback = asyncCallback; @@ -16,7 +16,7 @@ class TestInvoker { } } -class RAFTestInvoker extends TestInvoker { +class RAFStepScheduler extends StepScheduler { _scheduleCallbacks(resolve) { requestAnimationFrame(() => this._syncCallback()); requestAnimationFrame(() => { @@ -31,7 +31,7 @@ class RAFTestInvoker extends TestInvoker { } } -class AsyncRAFTestInvoker extends TestInvoker { +class AsyncRAFStepScheduler extends StepScheduler { static mc = new MessageChannel(); _scheduleCallbacks(resolve) { let gotTimer = false; @@ -62,7 +62,7 @@ class AsyncRAFTestInvoker extends TestInvoker { tryTriggerAsyncCallback(); }); - AsyncRAFTestInvoker.mc.port1.addEventListener( + AsyncRAFStepScheduler.mc.port1.addEventListener( "message", async function () { await Promise.resolve(); @@ -71,14 +71,14 @@ class AsyncRAFTestInvoker extends TestInvoker { }, { once: true } ); - AsyncRAFTestInvoker.mc.port1.start(); - AsyncRAFTestInvoker.mc.port2.postMessage("speedometer"); + AsyncRAFStepScheduler.mc.port1.start(); + AsyncRAFStepScheduler.mc.port2.postMessage("speedometer"); }); } } -export const TEST_INVOKER_LOOKUP = { +export const STEP_SCHEDULER_LOOKUP = { __proto__: null, - raf: RAFTestInvoker, - async: AsyncRAFTestInvoker, + raf: RAFStepScheduler, + async: AsyncRAFStepScheduler, }; diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs deleted file mode 100644 index 2d816e853..000000000 --- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs +++ /dev/null @@ -1,116 +0,0 @@ -import { TEST_INVOKER_LOOKUP } from "./test-invoker.mjs"; -import { forceLayout } from "./helpers.mjs"; - -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. - // - // Note: This matches the behavior of Page.layout() in benchmark-runner.mjs. - // Since shared code cannot depend on Page, we duplicate the logic here, - // falling back to document.body for remote workloads that don't have a frame. - const body = this.#frame?.contentDocument?.body ?? document.body; - const value = forceLayout(body, this.#params.layoutMode); - body._leakedLayoutValue = value; // Prevent dead code elimination. - - 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, -}; diff --git a/experimental/javascript-wc-indexeddb/scripts/build.mjs b/experimental/javascript-wc-indexeddb/scripts/build.mjs index c0ec96a78..cd26f8a33 100644 --- a/experimental/javascript-wc-indexeddb/scripts/build.mjs +++ b/experimental/javascript-wc-indexeddb/scripts/build.mjs @@ -1,6 +1,10 @@ +import path from "path"; +import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs"; import fs from "fs/promises"; import { dirname } from "path"; +const __dirname = import.meta.dirname; + /** * createDirectory * @@ -164,4 +168,5 @@ const build = async () => { console.log("Done with building!"); }; -build().then(() => import("../../../resources/shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); +await build(); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/experimental/todomvc-localstorage/scripts/build.mjs b/experimental/todomvc-localstorage/scripts/build.mjs index b59737cc1..ad6a77de9 100644 --- a/experimental/todomvc-localstorage/scripts/build.mjs +++ b/experimental/todomvc-localstorage/scripts/build.mjs @@ -1,6 +1,9 @@ +import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs"; import fs from "fs/promises"; import path from "path"; +const __dirname = import.meta.dirname; + const rootDirectory = "./"; const sourceDirectory = "./src"; const targetDirectory = "./dist"; @@ -53,4 +56,5 @@ const build = async () => { console.log("done!!"); }; -build().then(() => import("../../../resources/shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); +await build(); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/angular-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/angular-complex/scripts/build.mjs index 904de49da..4cdc47f81 100644 --- a/resources/todomvc/architecture-examples/angular-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/angular-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC Angular Complex DOM. */ @@ -18,4 +19,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs index 601c1ad47..2cc83bc87 100644 --- a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC Backbone Complex DOM. */ @@ -18,4 +19,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/backbone/scripts/build.mjs b/resources/todomvc/architecture-examples/backbone/scripts/build.mjs index 2a1289f5d..9d43978b1 100644 --- a/resources/todomvc/architecture-examples/backbone/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/backbone/scripts/build.mjs @@ -1,6 +1,9 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; import fs from "fs/promises"; import path from "path"; +const __dirname = import.meta.dirname; + const rootDirectory = "./"; const sourceDirectory = "./src"; const targetDirectory = "./dist"; @@ -58,4 +61,5 @@ const build = async () => { console.log("done!!"); }; -build().then(() => import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); +await build(); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs index 50059e7b3..ddaccd1d9 100644 --- a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC jQuery Complex DOM. */ diff --git a/resources/todomvc/architecture-examples/jquery/scripts/build.mjs b/resources/todomvc/architecture-examples/jquery/scripts/build.mjs index fc839e690..eef43065d 100644 --- a/resources/todomvc/architecture-examples/jquery/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/jquery/scripts/build.mjs @@ -1,6 +1,9 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; import fs from "fs/promises"; import path from "path"; +const __dirname = import.meta.dirname; + const rootDirectory = "./"; const sourceDirectory = "./src"; const targetDirectory = "./dist"; diff --git a/resources/todomvc/architecture-examples/lit-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/lit-complex/scripts/build.mjs index 6a16a4f25..0d7f34f09 100644 --- a/resources/todomvc/architecture-examples/lit-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/lit-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC Lit Complex DOM. */ @@ -25,4 +26,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs index b978b1689..3ac4ef8c2 100644 --- a/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Build the TodoMVC Preact Complex DOM example. */ @@ -16,4 +17,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs index 9ec31f6f2..c6101cf1a 100644 --- a/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Build the TodoMVC: React Complex DOM example. */ @@ -15,4 +16,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs index f6babcb74..5920511ec 100644 --- a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC React-Redux Complex DOM. */ @@ -16,4 +17,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs index 989b69490..128aa2666 100644 --- a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC Svelte Complex DOM. */ @@ -16,4 +17,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs index e0c2c7034..b20c8b700 100644 --- a/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC Vue Complex DOM. */ @@ -18,4 +19,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs index 165526763..2a3bbfdad 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs +++ b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC JavaScript Es5 Complex DOM. */ @@ -18,4 +19,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs index 05b6d5a49..13dba887d 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs +++ b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs @@ -1,6 +1,9 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; import fs from "fs/promises"; import path from "path"; +const __dirname = import.meta.dirname; + const rootDirectory = "./"; const sourceDirectory = "./src"; const targetDirectory = "./dist"; @@ -53,4 +56,5 @@ const build = async () => { console.log("done!!"); }; -build().then(() => import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); +await build(); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs index 21bdaccd2..4e5ec8dbc 100644 --- a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs +++ b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC JavaScript Es6 Webpack Complex DOM. */ @@ -15,4 +16,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs index c82898f37..f5da135d8 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs +++ b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs @@ -1,3 +1,4 @@ +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; /** * Builds the TodoMVC JavaScript Web Components Complex DOM. */ @@ -24,4 +25,4 @@ const options = { }; buildComplex(options); -import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist"))); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); diff --git a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs index 1400d3ec9..b768a19d0 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs +++ b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs @@ -1,6 +1,10 @@ +import path from "path"; +import { generateResourcesFile } from "../../../../shared/generate-resources.mjs"; import fs from "fs/promises"; import { dirname } from "path"; +const __dirname = import.meta.dirname; + /** * createDirectory * @@ -153,4 +157,5 @@ const build = async () => { console.log("Done with building!"); }; -build().then(() => import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); +await build(); +await generateResourcesFile(path.join(import.meta.dirname, "../dist")); From 476d9fda161039e2d393f4496fa7278b39c3ff53 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 15 Jun 2026 13:54:40 +0200 Subject: [PATCH 11/69] cleanup --- .../dist/resources.txt | 2 +- .../dist/src/speedometer-utils/benchmark.mjs | 8 +- .../src/speedometer-utils/step-runner.mjs | 116 ++++++++++++++++++ .../{test-invoker.mjs => step-scheduler.mjs} | 0 .../javascript-wc-indexeddb/scripts/build.mjs | 2 +- .../src/speedometer-utils/benchmark.mjs | 8 +- 6 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs rename experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/{test-invoker.mjs => step-scheduler.mjs} (100%) diff --git a/experimental/javascript-wc-indexeddb/dist/resources.txt b/experimental/javascript-wc-indexeddb/dist/resources.txt index 900a4aee1..601810cf1 100644 --- a/experimental/javascript-wc-indexeddb/dist/resources.txt +++ b/experimental/javascript-wc-indexeddb/dist/resources.txt @@ -18,7 +18,7 @@ src/speedometer-utils/benchmark.mjs src/speedometer-utils/helpers.mjs src/speedometer-utils/params.mjs src/speedometer-utils/step-runner.mjs -src/speedometer-utils/test-invoker.mjs +src/speedometer-utils/step-scheduler.mjs src/speedometer-utils/todomvc-utils.mjs src/speedometer-utils/translations.mjs src/storage/base-storage-manager.js diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs index 264145159..2ef5c7ff4 100644 --- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs @@ -1,5 +1,5 @@ /* eslint-disable no-case-declarations */ -import { TestRunner, AsyncTestRunner } from "./test-runner.mjs"; +import { StepRunner, AsyncStepRunner } from "./step-runner.mjs"; import { Params } from "./params.mjs"; /** @@ -15,10 +15,10 @@ export class BenchmarkStep { } async runAndRecord(params, suite, test, callback) { - const TestRunnerClass = params.useAsyncSteps ? AsyncTestRunner : TestRunner; + const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner; const type = params.useAsyncSteps ? "async" : "sync"; - const testRunner = new TestRunnerClass(null, null, params, suite, test, callback, type); - const result = await testRunner.runTest(); + const stepRunner = new StepRunnerClass(null, null, params, suite, test, callback, type); + const result = await stepRunner.runStep(); return result; } } diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs new file mode 100644 index 000000000..46187a6ba --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs @@ -0,0 +1,116 @@ +import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs"; +import { forceLayout } from "./helpers.mjs"; + +export class StepRunner { + #frame; + #page; + #params; + #suite; + #step; + #callback; + #type; + + constructor(frame, page, params, suite, step, callback, type) { + this.#suite = suite; + this.#step = step; + this.#params = params; + this.#callback = callback; + this.#page = page; + this.#frame = frame; + this.#type = type; + } + + get page() { + return this.#page; + } + + get step() { + return this.#step; + } + + _runSyncStep(step, page) { + step.run(page); + } + + async runStep() { + // Prepare all mark labels outside the measuring loop. + const suiteName = this.#suite.name; + const stepName = this.#step.name; + const syncStartLabel = `${suiteName}.${stepName}-start`; + const syncEndLabel = `${suiteName}.${stepName}-sync-end`; + const asyncEndLabel = `${suiteName}.${stepName}-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.step, this.page); + else + this._runSyncStep(this.step, 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. + // + // Note: This matches the behavior of Page.layout() in benchmark-runner.mjs. + // Since shared code cannot depend on Page, we duplicate the logic here, + // falling back to document.body for remote workloads that don't have a frame. + const body = this.#frame?.contentDocument?.body ?? document.body; + const value = forceLayout(body, this.#params.layoutMode); + body._leakedLayoutValue = value; // Prevent dead code elimination. + + const asyncEndTime = performance.now(); + performance.mark(asyncEndLabel); + + asyncTime = asyncEndTime - asyncStartTime; + + if (this.#params.warmupBeforeSync) + performance.measure("warmup", "warmup-start", "warmup-end"); + performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel); + performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel); + }; + + const report = () => this.#callback(this.#step, syncTime, asyncTime); + const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod; + const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType]; + const scheduler = new schedulerClass(runSync, measureAsync, report, this.#params); + + return scheduler.start(); + } +} + +export class AsyncStepRunner extends StepRunner { + constructor(frame, page, params, suite, step, callback, type) { + super(frame, page, params, suite, step, callback, type); + } + + async _runSyncStep(step, page) { + await step.run(page); + } +} + +export const STEP_RUNNER_LOOKUP = { + __proto__: null, + default: StepRunner, + async: AsyncStepRunner, + remote: StepRunner, +}; diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs similarity index 100% rename from experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs rename to experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs diff --git a/experimental/javascript-wc-indexeddb/scripts/build.mjs b/experimental/javascript-wc-indexeddb/scripts/build.mjs index cd26f8a33..54b00c881 100644 --- a/experimental/javascript-wc-indexeddb/scripts/build.mjs +++ b/experimental/javascript-wc-indexeddb/scripts/build.mjs @@ -101,7 +101,7 @@ const filesToMove = [ { src: "node_modules/todomvc-css/dist/todo-list.constructable.js", dest: "./dist/styles/todo-list.constructable.js" }, { src: "node_modules/todomvc-css/dist/todo-item.constructable.js", dest: "./dist/styles/todo-item.constructable.js" }, { src: "node_modules/dexie/dist/modern/dexie.mjs", dest: "./dist/libs/dexie.mjs" }, - { src: "node_modules/speedometer-utils/step-scheduler.mjs", dest: "./dist/src/speedometer-utils/test-invoker.mjs" }, + { src: "node_modules/speedometer-utils/step-scheduler.mjs", dest: "./dist/src/speedometer-utils/step-scheduler.mjs" }, { src: "node_modules/speedometer-utils/step-runner.mjs", dest: "./dist/src/speedometer-utils/step-runner.mjs" }, { src: "node_modules/speedometer-utils/params.mjs", dest: "./dist/src/speedometer-utils/params.mjs" }, { src: "src/speedometer-utils/benchmark.mjs", dest: "./dist/src/speedometer-utils/benchmark.mjs" }, diff --git a/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs b/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs index a16f0831f..87639c816 100644 --- a/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs +++ b/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs @@ -1,5 +1,5 @@ /* eslint-disable no-case-declarations */ -import { TestRunner, AsyncTestRunner } from "./test-runner.mjs"; +import { StepRunner, AsyncStepRunner } from "./step-runner.mjs"; import { Params } from "/node_modules/speedometer-utils/params.mjs"; /** @@ -15,10 +15,10 @@ export class BenchmarkStep { } async runAndRecord(params, suite, test, callback) { - const TestRunnerClass = params.useAsyncSteps ? AsyncTestRunner : TestRunner; + const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner; const type = params.useAsyncSteps ? "async" : "sync"; - const testRunner = new TestRunnerClass(null, null, params, suite, test, callback, type); - const result = await testRunner.runTest(); + const stepRunner = new StepRunnerClass(null, null, params, suite, test, callback, type); + const result = await stepRunner.runStep(); return result; } } From 4ed47537f9b0de15718654ac25e8e5fae2144f42 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 15 Jun 2026 17:00:52 +0200 Subject: [PATCH 12/69] fx --- .../responsive-design/dist/resources.txt | 39 ++ experimental/tests.mjs | 16 +- .../dist/out-dart2js-O4/resources.txt | 5 + .../dist/out-dart2wasm-O2/resources.txt | 7 + .../todomvc-localstorage/dist/resources.txt | 8 + resources/charts/dist/resources.txt | 12 + resources/default-tests.mjs | 55 ++- resources/editors/dist/resources.txt | 12 + resources/main.mjs | 62 ++- .../newssite/news-next/dist/resources.txt | 359 ++++++++++++++++++ .../newssite/news-nuxt/dist/resources.txt | 354 +++++++++++++++++ .../react-stockcharts/build/resources.txt | 9 + resources/shared/sw-messages.mjs | 6 +- .../angular-complex/dist/resources.txt | 13 + .../angular/dist/resources.txt | 11 + .../backbone-complex/dist/resources.txt | 16 + .../backbone/dist/resources.txt | 14 + .../jquery-complex/dist/resources.txt | 10 + .../jquery/dist/resources.txt | 8 + .../lit-complex/dist/resources.txt | 8 + .../lit/dist/resources.txt | 2 + .../preact-complex/dist/resources.txt | 6 + .../preact/dist/resources.txt | 4 + .../react-complex/dist/resources.txt | 8 + .../react-redux-complex/dist/resources.txt | 8 + .../react-redux/dist/resources.txt | 6 + .../react/dist/resources.txt | 6 + .../svelte-complex/dist/resources.txt | 6 + .../svelte/dist/resources.txt | 4 + .../vue-complex/dist/resources.txt | 8 + .../vue/dist/resources.txt | 6 + .../big-dom-generator/dist/resources.txt | 4 + .../todomvc/todomvc-css/dist/resources.txt | 31 ++ .../javascript-es5-complex/dist/resources.txt | 12 + .../dist/resources.txt | 7 + .../javascript-es6-webpack/dist/resources.txt | 5 + .../dist/resources.txt | 31 ++ .../dist/resources.txt | 34 ++ sw.mjs | 236 ++++++++---- 39 files changed, 1312 insertions(+), 136 deletions(-) create mode 100644 experimental/responsive-design/dist/resources.txt create mode 100644 experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt create mode 100644 experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt create mode 100644 experimental/todomvc-localstorage/dist/resources.txt create mode 100644 resources/charts/dist/resources.txt create mode 100644 resources/editors/dist/resources.txt create mode 100644 resources/newssite/news-next/dist/resources.txt create mode 100644 resources/newssite/news-nuxt/dist/resources.txt create mode 100644 resources/react-stockcharts/build/resources.txt create mode 100644 resources/todomvc/architecture-examples/angular-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/angular/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/backbone-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/backbone/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/jquery-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/jquery/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/lit-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/lit/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/preact-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/preact/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/react-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/react-redux-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/react-redux/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/react/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/svelte-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/svelte/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/vue-complex/dist/resources.txt create mode 100644 resources/todomvc/architecture-examples/vue/dist/resources.txt create mode 100644 resources/todomvc/big-dom-generator/dist/resources.txt create mode 100644 resources/todomvc/todomvc-css/dist/resources.txt create mode 100644 resources/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt create mode 100644 resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt create mode 100644 resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt create mode 100644 resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt create mode 100644 resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt diff --git a/experimental/responsive-design/dist/resources.txt b/experimental/responsive-design/dist/resources.txt new file mode 100644 index 000000000..67c1ae38c --- /dev/null +++ b/experimental/responsive-design/dist/resources.txt @@ -0,0 +1,39 @@ +app.js +index.html +public/images/5-healthy-smoothie.webp +public/images/beef-stroganoff.webp +public/images/beef-tacos.webp +public/images/best-comfort-food.webp +public/images/bread-at-home.webp +public/images/caesar-salad.webp +public/images/chicken-alfredo.webp +public/images/chocolate-cake.webp +public/images/chocolate-chip-cookies.webp +public/images/fruit-salad.webp +public/images/garlic-bread.webp +public/images/gluten-free-baking.webp +public/images/greek-salad.webp +public/images/homemade-pizza.webp +public/images/icons-outline.webp +public/images/lemon-drizzle-cake.webp +public/images/low-carb-desserts.webp +public/images/margherita-pizza.webp +public/images/mastering-art-of-french-cooking.webp +public/images/meal-prepping.webp +public/images/mediterranean-cuisine.webp +public/images/pancakes.webp +public/images/plant-based-protein.webp +public/images/quinoa-stuffed-peppers.webp +public/images/ramen-noodles.webp +public/images/seasonal-salads.webp +public/images/shrimp-paella.webp +public/images/spaghetti-carbonara.webp +public/images/superfoods-you-should-include.webp +public/images/sushi-platter.webp +public/images/thai-green-curry.webp +public/images/tomato-soup.webp +public/images/vegan-burger.webp +public/images/vegan-desserts.webp +public/images/vegetable-stir-fry.webp +public/images/vegetarian-stir-fry.webp +public/images/week-night-dinners.webp diff --git a/experimental/tests.mjs b/experimental/tests.mjs index 1e54c9c99..d41d0a061 100644 --- a/experimental/tests.mjs +++ b/experimental/tests.mjs @@ -36,7 +36,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-LocalStorage", url: "experimental/todomvc-localstorage/dist/index.html", - // resources: "experimental/todomvc-localstorage/dist/resources.txt", + resources: "experimental/todomvc-localstorage/dist/resources.txt", tags: ["todomvc", "experimental"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -66,7 +66,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-Emoji", url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", tags: ["todomvc", "experimental"], async prepare(page) { await page.waitForElement("todo-app"); @@ -99,7 +99,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-WebComponents-PostMessage", url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", tags: ["experimental", "todomvc", "webcomponents"], async prepare() {}, type: "remote", @@ -110,7 +110,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-Jaspr-Dart2JS-O4", url: "experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/index.html", - // resources: "experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt", + resources: "experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt", tags: ["todomvc", "experimental"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -139,7 +139,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-Jaspr-Dart2Wasm-O2", url: "experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/index.html", - // resources: "experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt", + resources: "experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt", tags: ["todomvc", "experimental"], disabled: true, async prepare(page) { @@ -169,7 +169,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "NewsSite-PostMessage", url: "resources/newssite/news-next/dist/index.html", - // resources: "resources/newssite/news-next/dist/resources.txt", + resources: "resources/newssite/news-next/dist/resources.txt", tags: ["experimental", "newssite", "language"], async prepare() {}, type: "remote", @@ -180,7 +180,6 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-WebComponents-IndexedDB", url: "experimental/javascript-wc-indexeddb/dist/index.html?useAsyncSteps=true&storageType=vanilla", - // resources: "experimental/javascript-wc-indexeddb/dist/resources.txt", tags: ["todomvc", "webcomponents", "experimental"], async prepare() {}, type: "remote", @@ -191,7 +190,6 @@ export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-WebComponents-DexieJS", url: "experimental/javascript-wc-indexeddb/dist/index.html?useAsyncSteps=true&storageType=dexie", - // resources: "experimental/javascript-wc-indexeddb/dist/resources.txt", tags: ["todomvc", "webcomponents", "experimental"], async prepare() {}, type: "remote", @@ -202,7 +200,7 @@ export const ExperimentalSuites = freezeSuites([ { name: "Responsive-Design", url: "experimental/responsive-design/dist/index.html", - // resources: "experimental/responsive-design/dist/resources.txt", + resources: "experimental/responsive-design/dist/resources.txt", tags: ["responsive-design", "webcomponents", "experimental"], type: "async", async prepare(page) { diff --git a/experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt b/experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt new file mode 100644 index 000000000..85de5f3ba --- /dev/null +++ b/experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt @@ -0,0 +1,5 @@ +base.css +favicon.ico +index.css +index.html +main.dart.js diff --git a/experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt b/experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt new file mode 100644 index 000000000..f66e98e5e --- /dev/null +++ b/experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt @@ -0,0 +1,7 @@ +base.css +favicon.ico +index.css +index.html +main.dart.js +main.mjs +main.wasm diff --git a/experimental/todomvc-localstorage/dist/resources.txt b/experimental/todomvc-localstorage/dist/resources.txt new file mode 100644 index 000000000..abac8a4db --- /dev/null +++ b/experimental/todomvc-localstorage/dist/resources.txt @@ -0,0 +1,8 @@ +app.js +controller.js +helpers.js +index.html +model.js +store.js +template.js +view.js diff --git a/resources/charts/dist/resources.txt b/resources/charts/dist/resources.txt new file mode 100644 index 000000000..0d970f580 --- /dev/null +++ b/resources/charts/dist/resources.txt @@ -0,0 +1,12 @@ +assets/chartjs-7fd89fd7.js +assets/chartjs-7fd89fd7.js.map +assets/developer-4ed993c7.js.map +assets/flights-airports-9a9e6422.js +assets/flights-airports-9a9e6422.js.map +assets/index-4ed993c7.js.map +assets/plot-562fbdb6.js +assets/plot-562fbdb6.js.map +chartjs.html +developer.html +index.html +observable-plot.html diff --git a/resources/default-tests.mjs b/resources/default-tests.mjs index b33553e8d..69368b2a8 100644 --- a/resources/default-tests.mjs +++ b/resources/default-tests.mjs @@ -7,7 +7,8 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES5", url: "resources/todomvc/vanilla-examples/javascript-es5/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", + preload: true, tags: ["default", "todomvc"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -36,7 +37,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES5-Complex-DOM", url: "resources/todomvc/vanilla-examples/javascript-es5-complex/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); @@ -65,7 +66,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES6-Webpack", url: "resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -95,7 +96,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES6-Webpack-Complex-DOM", url: "resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -125,7 +126,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-WebComponents", url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt", tags: ["default", "todomvc", "webcomponents"], async prepare(page) { await page.waitForElement("todo-app"); @@ -158,7 +159,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-WebComponents-Complex-DOM", url: "resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/index.html", - // resources: "resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt", + resources: "resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt", tags: ["todomvc", "webcomponents", "complex"], async prepare(page) { await page.waitForElement("todo-app"); @@ -191,7 +192,6 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React", url: "resources/todomvc/architecture-examples/react/dist/index.html#/home", - // resources: "resources/todomvc/architecture-examples/react/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -221,7 +221,6 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React-Complex-DOM", url: "resources/todomvc/architecture-examples/react-complex/dist/index.html#/home", - // resources: "resources/todomvc/architecture-examples/react-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -251,7 +250,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React-Redux", url: "resources/todomvc/architecture-examples/react-redux/dist/index.html", - // resources: "resources/todomvc/architecture-examples/react-redux/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/react-redux/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -280,7 +279,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-React-Redux-Complex-DOM", url: "resources/todomvc/architecture-examples/react-redux-complex/dist/index.html", - // resources: "resources/todomvc/architecture-examples/react-redux-complex/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/react-redux-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -309,7 +308,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Backbone", url: "resources/todomvc/architecture-examples/backbone/dist/index.html", - // resources: "resources/todomvc/architecture-examples/backbone/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/backbone/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -340,7 +339,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Backbone-Complex-DOM", url: "resources/todomvc/architecture-examples/backbone-complex/dist/index.html", - // resources: "resources/todomvc/architecture-examples/backbone-complex/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/backbone-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -371,7 +370,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Angular", url: "resources/todomvc/architecture-examples/angular/dist/index.html", - // resources: "resources/todomvc/architecture-examples/angular/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/angular/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -401,7 +400,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Angular-Complex-DOM", url: "resources/todomvc/architecture-examples/angular-complex/dist/index.html", - // resources: "resources/todomvc/architecture-examples/angular-complex/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/angular-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -431,7 +430,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Vue", url: "resources/todomvc/architecture-examples/vue/dist/index.html", - // resources: "resources/todomvc/architecture-examples/vue/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/vue/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -461,7 +460,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Vue-Complex-DOM", url: "resources/todomvc/architecture-examples/vue-complex/dist/index.html", - // resources: "resources/todomvc/architecture-examples/vue-complex/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/vue-complex/dist/resources.txt", tags: ["todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -491,7 +490,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-jQuery", url: "resources/todomvc/architecture-examples/jquery/dist/index.html", - // resources: "resources/todomvc/architecture-examples/jquery/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/jquery/dist/resources.txt", tags: ["default", "todomvc"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -519,7 +518,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-jQuery-Complex-DOM", url: "resources/todomvc/architecture-examples/jquery-complex/dist/index.html", - // resources: "resources/todomvc/architecture-examples/jquery-complex/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/jquery-complex/dist/resources.txt", tags: ["todomvc", "complex"], async prepare(page) { await page.waitForElement("#appIsReady"); @@ -547,7 +546,6 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Preact", url: "resources/todomvc/architecture-examples/preact/dist/index.html#/home", - // resources: "resources/todomvc/architecture-examples/preact/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -576,7 +574,6 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Preact-Complex-DOM", url: "resources/todomvc/architecture-examples/preact-complex/dist/index.html#/home", - // resources: "resources/todomvc/architecture-examples/preact-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -605,7 +602,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Svelte", url: "resources/todomvc/architecture-examples/svelte/dist/index.html", - // resources: "resources/todomvc/architecture-examples/svelte/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/svelte/dist/resources.txt", tags: ["todomvc"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -634,7 +631,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Svelte-Complex-DOM", url: "resources/todomvc/architecture-examples/svelte-complex/dist/index.html", - // resources: "resources/todomvc/architecture-examples/svelte-complex/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/svelte-complex/dist/resources.txt", tags: ["default", "todomvc", "complex", "complex-default"], async prepare(page) { const element = await page.waitForElement(".new-todo"); @@ -663,7 +660,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Lit", url: "resources/todomvc/architecture-examples/lit/dist/index.html", - // resources: "resources/todomvc/architecture-examples/lit/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/lit/dist/resources.txt", tags: ["todomvc", "webcomponents"], async prepare(page) { await page.waitForElement("todo-app"); @@ -695,7 +692,7 @@ export const DefaultSuites = freezeSuites([ { name: "TodoMVC-Lit-Complex-DOM", url: "resources/todomvc/architecture-examples/lit-complex/dist/index.html", - // resources: "resources/todomvc/architecture-examples/lit-complex/dist/resources.txt", + resources: "resources/todomvc/architecture-examples/lit-complex/dist/resources.txt", tags: ["default", "todomvc", "webcomponents", "complex", "complex-default"], async prepare(page) { await page.waitForElement("todo-app"); @@ -727,7 +724,7 @@ export const DefaultSuites = freezeSuites([ { name: "NewsSite-Next", url: "resources/newssite/news-next/dist/index.html", - // resources: "resources/newssite/news-next/dist/resources.txt", + resources: "resources/newssite/news-next/dist/resources.txt", tags: ["default", "newssite", "language"], async prepare(page) { await page.waitForElement("#navbar-dropdown-toggle"); @@ -768,7 +765,7 @@ export const DefaultSuites = freezeSuites([ { name: "NewsSite-Nuxt", url: "resources/newssite/news-nuxt/dist/index.html", - // resources: "resources/newssite/news-nuxt/dist/resources.txt", + resources: "resources/newssite/news-nuxt/dist/resources.txt", tags: ["default", "newssite"], async prepare(page) { await page.waitForElement("#navbar-dropdown-toggle"); @@ -809,7 +806,6 @@ export const DefaultSuites = freezeSuites([ { name: "Editor-CodeMirror", url: "resources/editors/dist/codemirror.html", - // resources: "resources/editors/dist/resources.txt", tags: ["default", "editor"], async prepare(page) {}, tests: [ @@ -828,7 +824,6 @@ export const DefaultSuites = freezeSuites([ { name: "Editor-TipTap", url: "resources/editors/dist/tiptap.html", - // resources: "resources/editors/dist/resources.txt", tags: ["default", "editor"], async prepare(page) {}, tests: [ @@ -847,7 +842,6 @@ export const DefaultSuites = freezeSuites([ { name: "Charts-observable-plot", url: "resources/charts/dist/observable-plot.html", - // resources: "resources/charts/dist/resources.txt", tags: ["default", "chart"], async prepare(page) {}, tests: [ @@ -874,7 +868,6 @@ export const DefaultSuites = freezeSuites([ { name: "Charts-chartjs", url: "resources/charts/dist/chartjs.html", - // resources: "resources/charts/dist/resources.txt", tags: ["default", "chart"], async prepare(page) {}, tests: [ @@ -894,7 +887,6 @@ export const DefaultSuites = freezeSuites([ { name: "React-Stockcharts-SVG", url: "resources/react-stockcharts/build/index.html?type=svg", - // resources: "resources/react-stockcharts/build/resources.txt", tags: ["default", "chart", "svg"], async prepare(page) { await page.waitForElement("#render"); @@ -934,7 +926,6 @@ export const DefaultSuites = freezeSuites([ { name: "Perf-Dashboard", url: "resources/perf.webkit.org/public/v3/#/charts/?since=1678991819934&paneList=((55-1974-null-null-(5-2.5-500)))", - // resources: "resources/perf.webkit.org/public/v3/resources.txt", tags: ["default", "chart", "webcomponents"], async prepare(page) { await page.waitForElement("#app-is-ready"); diff --git a/resources/editors/dist/resources.txt b/resources/editors/dist/resources.txt new file mode 100644 index 000000000..9b870632a --- /dev/null +++ b/resources/editors/dist/resources.txt @@ -0,0 +1,12 @@ +assets/codemirror-521de7ab.js +assets/codemirror-521de7ab.js.map +assets/index-2feebe42.css +assets/index.es-02a92ebc.js +assets/index.es-02a92ebc.js.map +assets/main-4ed993c7.js.map +assets/tiptap-95a40ba8.js +assets/tiptap-95a40ba8.js.map +codemirror.html +index.html +tiptap.html +vite.svg diff --git a/resources/main.mjs b/resources/main.mjs index ecb9e576e..760870808 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -22,7 +22,7 @@ export class PreloadServiceWorker { return false; } - this.registration = await navigator.serviceWorker.register("/sw.mjs", { type: "module" }); + this.registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); await this.registration.update(); await navigator.serviceWorker.ready; @@ -30,7 +30,7 @@ export class PreloadServiceWorker { return true; } - async precacheSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { + async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { if (!this.sw || suites.length === 0) return; @@ -48,21 +48,36 @@ export class PreloadServiceWorker { const startTime = performance.now(); await new Promise((resolve) => { const channel = new MessageChannel(); - channel.port1.onmessage = (event) => { - if (event.data?.type === SW_MESSAGES.PRECACHE_DONE) { + let timeoutId = setTimeout(() => { + channel.port1.onmessage = null; + console.error("Service worker preloading timed out"); + resolve(); + }, 30000); + const port = channel.port1; + port.onmessage = (event) => { + if (event.data?.type === SW_MESSAGES.PRELOAD_DONE) { + port.onmessage = null; + if (timeoutId) + clearTimeout(timeoutId); const timeTakenMs = performance.now() - startTime; const { totalSize, count } = event.data; const sizeMB = (totalSize / (1024 * 1024)).toFixed(2); const timeSec = (timeTakenMs / 1000).toFixed(2); console.log(`Preloaded ${count} files (${sizeMB} MB) in ${timeSec}s`); resolve(); - } else if (event.data?.type === SW_MESSAGES.PRECACHE_PROGRESS) { - onProgress(event.data); + } else if (event.data?.type === SW_MESSAGES.PRELOAD_PROGRESS) { + if (onProgress) + onProgress(event.data); + } else if (event.data?.type === 'ERROR') { + port.onmessage = null; + if (timeoutId) + clearTimeout(timeoutId); + reject(new Error(event.data.message)); } }; this.sw.postMessage( { - type: SW_MESSAGES.PRECACHE_SUITES, + type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache: clearCache, @@ -72,9 +87,19 @@ export class PreloadServiceWorker { }); } - setState(state) { - if (this.sw) - this.sw.postMessage({ type: SW_MESSAGES.SET_STATE, state }); + async setState(state) { + if (!this.sw) return; + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + channel.port1.onmessage = (event) => { + if (event.data?.type === 'SUCCESS') { + resolve(); + } else if (event.data?.type === 'ERROR') { + reject(new Error(event.data.message)); + } + }; + this.sw.postMessage({ type: SW_MESSAGES.SET_STATE, state }, [channel.port2]); + }); } } @@ -161,7 +186,12 @@ class MainBenchmarkClient { if (!params.isDefault()) await this._cacheResources(benchmarkConfigurator); - this._setBenchmarkState(BENCHMARK_STATE.RUNNING); + try { + await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); + } catch (error) { + alert(error.message); + return false; + } const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const totalSuitesCount = enabledSuites.length; @@ -467,11 +497,11 @@ class MainBenchmarkClient { this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); try { - await this.preloadServiceWorker.precacheSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); - this._didInitialPrecache = true; + await this.preloadServiceWorker.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); + this._didInitialPreload = true; this._enableStartButtons(); } catch (error) { - console.error("Service Worker precache failed:", error); + console.error("Service Worker preload failed:", error); this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._enableStartButtons(); } @@ -496,10 +526,10 @@ class MainBenchmarkClient { }); } - _setBenchmarkState(state) { + async _setBenchmarkState(state) { document.body.setAttribute("data-benchmark-state", state); if (this.preloadServiceWorker) - this.preloadServiceWorker.setState(state); + await this.preloadServiceWorker.setState(state); const startButton = document.querySelector(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { diff --git a/resources/newssite/news-next/dist/resources.txt b/resources/newssite/news-next/dist/resources.txt new file mode 100644 index 000000000..5d8d79a5c --- /dev/null +++ b/resources/newssite/news-next/dist/resources.txt @@ -0,0 +1,359 @@ +404.html +_next/static/chunks/743-fd706aeabb7828e3.js +_next/static/chunks/framework-2c79e2a64abdb08b.js +_next/static/chunks/main-2ba37e62325cc71b.js +_next/static/chunks/pages/_app-77983e68be50f72a.js +_next/static/chunks/pages/_error-54de1933a164a1ff.js +_next/static/chunks/pages/index-99c54b3ee91525b8.js +_next/static/chunks/polyfills-c67a75d1b6f99dc8.js +_next/static/chunks/webpack-e50e9853db18b759.js +_next/static/css/2cf5163b53bb0adb.css +_next/static/css/a0dca1379a01e5cf.css +_next/static/tuwdCnX7HYK_fwpI0QvDm/_buildManifest.js +_next/static/tuwdCnX7HYK_fwpI0QvDm/_ssgManifest.js +assets/images/2h-media-lPcQhLP-b4I-unsplash_150.jpg +assets/images/aaron-huber-G7sE2S4Lab4-unsplash_336.jpg +assets/images/adam-whitlock-I9j8Rk-JYFM-unsplash_684.jpg +assets/images/aditya-chinchure-ZhQCZjr9fHo-unsplash_336.jpg +assets/images/adrian-dascal-myAz-buELXs-unsplash_448.jpg +assets/images/ahmet-kurt-5BGg2L5nhlU-unsplash_150.jpg +assets/images/ahmet-kurt-WviyUzOg4RU-unsplash_150.jpg +assets/images/alan-rodriguez-qrD-g7oc9is-unsplash_684.jpg +assets/images/albert-stoynov-fEdf0fig3os-unsplash_448.jpg +assets/images/alec-favale-dLctr-PqFys-unsplash_150.jpg +assets/images/alev-takil-fYyYz38bUkQ-unsplash_150.jpg +assets/images/alex-hudson-7AgqAZbogOQ-unsplash_448.jpg +assets/images/alexander-grey-8lnbXtxFGZw-unsplash_684.jpg +assets/images/alexander-kirov-YhDJXJjmxUQ-unsplash_336.jpg +assets/images/alexandre-debieve-FO7JIlwjOtU-unsplash_336.jpg +assets/images/alice-pasqual-Olki5QpHxts-unsplash_336.jpg +assets/images/allan-wadsworth-Lp78NT-mf9o-unsplash_150.jpg +assets/images/anastassia-anufrieva-ecHGTPfjNfA-unsplash_150.jpg +assets/images/anatol-rurac-NeSj0i6HLak-unsplash_150.jpg +assets/images/anatol-rurac-b5t2lqeCGfA-unsplash_150.jpg +assets/images/andrew-solok-LbckXdUVOlY-unsplash_448.jpg +assets/images/angela-loria-hFc0JEKD4Cc-unsplash_150.jpg +assets/images/ani-kolleshi-7jjnJ-QA9fY-unsplash_150.jpg +assets/images/anne-nygard-tcJ6sJTtTWI-unsplash_448.jpg +assets/images/annie-spratt-IT6aov1ScW0-unsplash_150.jpg +assets/images/annie-spratt-KiOHnBkLQQU-unsplash_684.jpg +assets/images/ant-rozetsky-SLIFI67jv5k-unsplash_150.jpg +assets/images/arnaud-jaegers-IBWJsMObnnU-unsplash_336.jpg +assets/images/arnel-hasanovic-MNd-Rka1o0Q-unsplash_336.jpg +assets/images/artur-voznenko-rwPIQQPz1ew-unsplash_150.jpg +assets/images/arturo-rey-5yP83RhaFGA-unsplash_336.jpg +assets/images/ashim-d-silva-ZmgJiztRHXE-unsplash_336.jpg +assets/images/astronaud23-ox3t0m3PUqA-unsplash_336.jpg +assets/images/austin-distel-97HfVpyNR1M-unsplash_448.jpg +assets/images/austin-distel-rxpThOwuVgE-unsplash_448.jpg +assets/images/axp-photography-v6pAkO31d50-unsplash_448.jpg +assets/images/azhar-j-t2hgHV1R7_g-unsplash_448.jpg +assets/images/benjamin-child-rOn57CBgyMo-unsplash_448.jpg +assets/images/bermix-studio-yUnSMBogWNI-unsplash_150.jpg +assets/images/bernd-dittrich-Xk1IfNnEhRA-unsplash_448.jpg +assets/images/bianca-ackermann-qr0-lKAOZSk-unsplash_150.jpg +assets/images/boris-stefanik-q49CgyIrLes-unsplash_150.jpg +assets/images/boxed-water-is-better-y-TpYAlcBYM-unsplash_150.jpg +assets/images/braden-collum-9HI8UJMSdZA-unsplash_336.jpg +assets/images/bram-naus-oqnVnI5ixHg-unsplash_448.jpg +assets/images/brigitte-tohm-iIupxcq-yH4-unsplash_150.jpg +assets/images/brooke-lark-GJMlSBS0FhU-unsplash_150.jpg +assets/images/brooke-lark-HjWzkqW1dgI-unsplash_336.jpg +assets/images/brooke-lark-IDTEXXXfS44-unsplash_150.jpg +assets/images/brooke-lark-lcZ9NxhOSlo-unsplash_448.jpg +assets/images/brooke-lark-oaz0raysASk-unsplash_150.jpg +assets/images/bruce-mars-xj8qrWvuOEs-unsplash_336.jpg +assets/images/bruno-nascimento-PHIgYUGQPvU-unsplash_448.jpg +assets/images/c-j-1GHqOftzYo0-unsplash_150.jpg +assets/images/caglar-oskay-d0Be8Vs9XRk-unsplash_336.jpg +assets/images/campaign-creators-pypeCEaJeZY-unsplash_448.jpg +assets/images/cardmapr-nl-s8F8yglbpjo-unsplash_336.jpg +assets/images/carl-nenzen-loven-c-pc2mP7hTs-unsplash_448.jpg +assets/images/chad-stembridge-sEHrIPpkKQY-unsplash_150.jpg +assets/images/chloe-taranto-x2zyAOmVNtM-unsplash_448.jpg +assets/images/chris-leboutillier-TUJud0AWAPI-unsplash_150.jpg +assets/images/chris-leboutillier-c7RWVGL8lPA-unsplash_150.jpg +assets/images/christian-lue-8Yw6tsB8tnc-unsplash_448.jpg +assets/images/christian-tenguan-P3gfVKhz8d0-unsplash_336.jpg +assets/images/christina-rumpf-gUU4MF87Ipw-unsplash_150.jpg +assets/images/clark-street-mercantile-P3pI6xzovu0-unsplash_336.jpg +assets/images/claudio-schwarz-3cWxxW2ggKE-unsplash_336.jpg +assets/images/clay-banks-BY-R0UNRE7w-unsplash_150.jpg +assets/images/clay-banks-cisdc-344vo-unsplash_150.jpg +assets/images/clem-onojeghuo-DoA2duXyzRM-unsplash_684.jpg +assets/images/clem-onojeghuo-RLJnH4Mt9A0-unsplash_336.jpg +assets/images/colin-lloyd-2ULmNrj44QY-unsplash_448.jpg +assets/images/colin-lloyd-NKS5gg7rWGw-unsplash_150.jpg +assets/images/colin-lloyd-uaM_Ijy_joY-unsplash_448.jpg +assets/images/craig-manners-LvJCFOW3Ma8-unsplash_448.jpg +assets/images/craig-melville-_JKymnZ1Uc4-unsplash_150.jpg +assets/images/crystal-kwok-xD5SWy7hMbw-unsplash_448.jpg +assets/images/dani-navarro-6CnGzrLwM28-unsplash_336.jpg +assets/images/daniel-staple-N320vzTBviA-unsplash_684.jpg +assets/images/david-beale--lQR8yeDzek-unsplash_336.jpg +assets/images/david-everett-strickler-igCBFrMd11I-unsplash_684.jpg +assets/images/david-lezcano-NfZiOJzZgcg-unsplash_336.jpg +assets/images/david-vives-Nzbkev7SQTg-unsplash_448.jpg +assets/images/declan-sun-misAHv6YWkI-unsplash_336.jpg +assets/images/denys-nevozhai-7nrsVjvALnA-unsplash_150.jpg +assets/images/denys-nevozhai-z0nVqfrOqWA-unsplash_150.jpg +assets/images/derick-mckinney-muhK4oeYJiU-unsplash_150.jpg +assets/images/devi-puspita-amartha-yahya-7ln0pST_O8M-unsplash_448.jpg +assets/images/dibakar-roy-K9JwokzSvrc-unsplash_150.jpg +assets/images/diego-jimenez-A-NVHPka9Rk-unsplash_336.jpg +assets/images/dillon-kydd-2keCPb73aQY-unsplash_336.jpg +assets/images/dino-reichmuth-A5rCN8626Ck-unsplash_336.jpg +assets/images/dom-hill-nimElTcTNyY-unsplash_336.jpg +assets/images/dominic-bieri-vXRt4rFr4hI-unsplash_336.jpg +assets/images/dominic-hampton-_8aRumOixtI-unsplash_684.jpg +assets/images/domino-studio-164_6wVEHfI-unsplash_336.jpg +assets/images/drew-hays-tGYrlchfObE-unsplash_336.jpg +assets/images/dyana-wing-so-Og16Foo-pd8-unsplash_150.jpg +assets/images/dylan-calluy-j9q18vvHitg-unsplash_150.jpg +assets/images/ednilson-cardoso-dos-santos-haiooWA_weo-unsplash_336.jpg +assets/images/egor-myznik-GFHKMW6KiJ0-unsplash_684.jpg +assets/images/ehimetalor-akhere-unuabona-yS0uBoF4xDo-unsplash_448.jpg +assets/images/elimende-inagella-7OxV_qDiGRI-unsplash_448.jpg +assets/images/emmanuel-ikwuegbu-ceawFbpA-14-unsplash_448.jpg +assets/images/emran-yousof-k8ZbMQWbx34-unsplash_336.jpg +assets/images/erik-mclean-ByjIzFupcHo-unsplash_336.jpg +assets/images/esther-ann-glpYh1cWf0o-unsplash_448.jpg +assets/images/etienne-girardet-RqOyRtYGhLg-unsplash_448.jpg +assets/images/evie-calder-97CO-A4P0GQ-unsplash_336.jpg +assets/images/fanga-studio-bOfCOy3_4wU-unsplash_448.jpg +assets/images/fons-heijnsbroek-vBfEZdpEr-E-unsplash_448.jpg +assets/images/freestocks-nss2eRzQwgw-unsplash_150.jpg +assets/images/gaku-suyama-VyiLZUcdJv0-unsplash_150.jpg +assets/images/geert-pieters-NbpUM86Jo8Y-unsplash_150.jpg +assets/images/geoff-scott-8lUTnkZXZSA-unsplash_336.jpg +assets/images/glenn-carstens-peters-npxXWgQ33ZQ-unsplash_336.jpg +assets/images/greg-rakozy-oMpAz-DN-9I-unsplash_684.jpg +assets/images/gregory-hayes-h5cd51KXmRQ-unsplash_684.jpg +assets/images/guillaume-perigois-HL4LEIyGEYU-unsplash_150.jpg +assets/images/hannah-busing-0V6DmTuJaIk-unsplash_448.jpg +assets/images/hansjorg-keller-CQqyv5uldW4-unsplash_448.jpg +assets/images/harley-davidson-fFbUdx80oCc-unsplash_336.jpg +assets/images/harold-mendoza-6xafY_AE1LM-unsplash_150.jpg +assets/images/harri-kuokkanen-SEtUeWL8bIQ-unsplash_336.jpg +assets/images/hassan-kibwana-fmXLB_uHIh4-unsplash_448.jpg +assets/images/heather-ford-5gkYsrH_ebY-unsplash_336.jpg +assets/images/heidi-fin-2TLREZi7BUg-unsplash_336.jpg +assets/images/heidi-kaden-L_U4jhwZ6hY-unsplash_448.jpg +assets/images/hush-naidoo-jade-photography-ZCO_5Y29s8k-unsplash_150.jpg +assets/images/hush-naidoo-jade-photography-Zp7ebyti3MU-unsplash_336.jpg +assets/images/hyttalo-souza-a1p0Z7RSkL8-unsplash_684.jpg +assets/images/ian-hutchinson-P8rgDtEFn7s-unsplash_150.jpg +assets/images/ian-taylor-kAWTCt7p7rs-unsplash_448.jpg +assets/images/ibrahim-rifath-OApHds2yEGQ-unsplash_150.jpg +assets/images/icons8-team-k5fUTay0ghw-unsplash_336.jpg +assets/images/ilaria-de-bona-RuFfpBsaRY0-unsplash_336.jpg +assets/images/ilyass-seddoug-06w8RxgSzF0-unsplash_150.jpg +assets/images/inaki-del-olmo-NIJuEQw0RKg-unsplash_336.jpg +assets/images/irwan-rbDE93-0hHs-unsplash_684.jpg +assets/images/isai-ramos-Sp70YIWtuM8-unsplash_336.jpg +assets/images/isaw-company-Oqv_bQbZgS8-unsplash_336.jpg +assets/images/ixography-05Q_XPF_YKs-unsplash_336.jpg +assets/images/jacob-licht-8nA_iHrxHIo-unsplash_150.jpg +assets/images/jakob-dalbjorn-cuKJre3nyYc-unsplash_448.jpg +assets/images/jakub-zerdzicki-VnTR3XFwxWs-unsplash_150.jpg +assets/images/jakub-zerdzicki-qcRGVZNZ5js-unsplash_336.jpg +assets/images/jannis-brandt-mmsQUgMLqUo-unsplash_150.jpg +assets/images/jaron-nix-7wWRXewYCH4-unsplash_684.jpg +assets/images/jay-clark-P3sLerH3UmM-unsplash_336.jpg +assets/images/jed-owen-ajZibDGpPew-unsplash_336.jpg +assets/images/jessica-weiller-So4eFi-d1nc-unsplash_336.jpg +assets/images/jon-tyson-0BLE1xp5HBQ-unsplash_150.jpg +assets/images/jon-tyson-ne2mqMgER8Y-unsplash_150.jpg +assets/images/jonathan-borba-lrQPTQs7nQQ-unsplash_150.jpg +assets/images/jonathan-simcoe-S9J1HqoL9ns-unsplash_150.jpg +assets/images/jordhan-madec-AD5ylD2T0UY-unsplash_336.jpg +assets/images/joseph-gonzalez-QaGDmf5tMiE-unsplash_150.jpg +assets/images/joshua-rawson-harris-YNaSz-E7Qss-unsplash_336.jpg +assets/images/juli-kosolapova-4PE3X9eKsu4-unsplash_336.jpg +assets/images/junko-nakase-Q-72wa9-7Dg-unsplash_336.jpg +assets/images/k8-uYf_C34PAao-unsplash_336.jpg +assets/images/keagan-henman-xPJYL0l5Ii8-unsplash_336.jpg +assets/images/kelli-dougal-vbiQ_7vwfrs-unsplash_448.jpg +assets/images/kelly-sikkema-A-lovieAmjA-unsplash_150.jpg +assets/images/kelly-sikkema-WIYtZU3PxsI-unsplash_448.jpg +assets/images/kenny-K72n3BHgHCg-unsplash_336.jpg +assets/images/kenny-eliason-4N3iHYmqy_E-unsplash_336.jpg +assets/images/kenny-eliason-5ddH9Y2accI-unsplash_150.jpg +assets/images/kevin-rajaram-qhixFFO8EWQ-unsplash_336.jpg +assets/images/kevin-wang-t7vEVxwGGm0-unsplash_336.jpg +assets/images/kitthitorn-chaiyuthapoom-TOH_gw5dd20-unsplash_336.jpg +assets/images/koshu-kunii-ADLj1cyFfV8-unsplash_150.jpg +assets/images/koshu-kunii-cWEGNQqcImk-unsplash_336.jpg +assets/images/koshu-kunii-v9ferChkC9A-unsplash_150.jpg +assets/images/kyle-glenn-gcw_WWu_uBQ-unsplash_336.jpg +assets/images/leah-hetteberg-kTVN2l0ZUv8-unsplash_684.jpg +assets/images/leisara-studio-EzzW1oNek-I-unsplash_336.jpg +assets/images/leon-seibert-Xs3al4NpIFQ-unsplash_336.jpg +assets/images/libre-leung-9O0Sp22DF0I-unsplash_336.jpg +assets/images/linkedin-sales-solutions--AXDunSs-n4-unsplash_150.jpg +assets/images/little-plant-TZw891-oMio-unsplash_150.jpg +assets/images/lorenzo-moschi-N7ypjB7HKIk-unsplash_150.jpg +assets/images/louis-reed-pwcKF7L4-no-unsplash_448.jpg +assets/images/luis-cortes-QrPDA15pRkM-unsplash_336.jpg +assets/images/lukasz-radziejewski-cg4MzL_eSvU-unsplash_336.jpg +assets/images/m-ZzOa5G8hSPI-unsplash_150.jpg +assets/images/maarten-van-den-heuvel-gZXx8lKAb7Y-unsplash_150.jpg +assets/images/mac-blades-jpgJSBQtw5U-unsplash_336.jpg +assets/images/maddi-bazzocco-qKbHvzXb85A-unsplash_150.jpg +assets/images/maja-kochanowska-EiJQdDI_t_Y-unsplash_150.jpg +assets/images/maksim-larin-tecILYzVAzg-unsplash_336.jpg +assets/images/maksym-harbar-okn8ZIjPMxI-unsplash_448.jpg +assets/images/maksym-kaharlytskyi-Y0z9MyDsrU0-unsplash_150.jpg +assets/images/marc-olivier-jodoin-_eclsGKsUdo-unsplash_448.jpg +assets/images/marcelo-leal-6pcGTJDuf6M-unsplash_448.jpg +assets/images/marco-oriolesi-wqLGlhjr6Og-unsplash_684.jpg +assets/images/marek-studzinski-9U9I-eVx9nI-unsplash_150.jpg +assets/images/maria-lysenko-tZvkSuBleso-unsplash_150.jpg +assets/images/marius-oprea-ySA9uj7zSmw-unsplash_150.jpg +assets/images/markus-spiske-7PMGUqYQpYc-unsplash_150.jpg +assets/images/markus-spiske-Nph1oyRsHm4-unsplash_150.jpg +assets/images/markus-spiske-WUehAgqO5hE-unsplash_684.jpg +assets/images/markus-spiske-jgOkEjVw-KM-unsplash_150.jpg +assets/images/markus-spiske-lUc5pRFB25s-unsplash_336.jpg +assets/images/markus-spiske-p2Xor4Lbrrk-unsplash_150.jpg +assets/images/markus-spiske-wIUxLHndcLw-unsplash_150.jpg +assets/images/martha-dominguez-de-gouveia-k-NnVZ-z26w-unsplash_150.jpg +assets/images/martin-jernberg-jVNWCFwdjZU-unsplash_448.jpg +assets/images/martin-podsiad-wrdtA9lew9E-unsplash_150.jpg +assets/images/mathias-reding-yfXhqAW5X0c-unsplash_150.jpg +assets/images/mathurin-napoly-matnapo-ejWJ3a92FEs-unsplash_336.jpg +assets/images/mathurin-napoly-matnapo-pIJ34ZrZEEw-unsplash_150.jpg +assets/images/matt-popovich-7mqsZsE6FaU-unsplash_336.jpg +assets/images/matthew-ball-3wW2fBjptQo-unsplash_448.jpg +assets/images/matthew-henry-0Ol8Sa2n21c-unsplash_150.jpg +assets/images/matthew-landers-v8UgmRa6UDg-unsplash_150.jpg +assets/images/matthew-tenbruggencate-0HJWobhGhJs-unsplash_150.jpg +assets/images/matthias-heil-lDOEwat_MPs-unsplash_336.jpg +assets/images/mattia-bericchia-xkD79yf4tb8-unsplash_150.jpg +assets/images/maxim-hopman-IayKLkmz6g0-unsplash_448.jpg +assets/images/maximilian-bungart-nwqfl_HtJjk-unsplash_150.jpg +assets/images/melissa-askew-8n00CqwnqO8-unsplash_448.jpg +assets/images/memento-media-XhYq-5KnxSk-unsplash_448.jpg +assets/images/mesut-kaya-eOcyhe5-9sQ-unsplash_336.jpg +assets/images/metin-ozer-hShrr0WvrQs-unsplash_336.jpg +assets/images/microsoft-365-f1zQuagWCTA-unsplash_336.jpg +assets/images/miguel-bruna-TzVN0xQhWaQ-unsplash_150.jpg +assets/images/mika-baumeister-Hm4zYX-BDxk-unsplash_150.jpg +assets/images/mika-baumeister-bGZZBDvh8s4-unsplash_150.jpg +assets/images/mika-baumeister-jXPQY1em3Ew-unsplash_150.jpg +assets/images/mike-enerio-7ryPpZK1qV8-unsplash_448.jpg +assets/images/milad-fakurian-58Z17lnVS4U-unsplash_684.jpg +assets/images/mr-cup-fabien-barral-Mwuod2cm8g4-unsplash_448.jpg +assets/images/mr-lee-f4RBYsY2hxA-unsplash_150.jpg +assets/images/mufid-majnun-J12RfFH-2ZE-unsplash_684.jpg +assets/images/mufid-majnun-tJJIGh703I4-unsplash_336.jpg +assets/images/mulyadi-JeCNRxGLSp4-unsplash_150.jpg +assets/images/myriam-zilles-KltoLK6Mk-g-unsplash_336.jpg +assets/images/nasa-Q1p7bh3SHj8-unsplash_150.jpg +assets/images/nasa-cIX5TlQ_FgM-unsplash_448.jpg +assets/images/nasa-dCgbRAQmTQA-unsplash_684.jpg +assets/images/nathan-cima-TQuq2OtLBNU-unsplash_150.jpg +assets/images/nathan-dumlao-bRdRUUtbxO0-unsplash_150.jpg +assets/images/nathan-dumlao-laCrvNG3F_I-unsplash_336.jpg +assets/images/national-cancer-institute-A2CK97sS0ns-unsplash_684.jpg +assets/images/national-cancer-institute-GcrSgHDrniY-unsplash_336.jpg +assets/images/national-cancer-institute-KrsoedfRAf4-unsplash_336.jpg +assets/images/national-cancer-institute-L7en7Lb-Ovc-unsplash_336.jpg +assets/images/national-cancer-institute-NFvdKIhxYlU-unsplash_336.jpg +assets/images/national-cancer-institute-SMxzEaidR20-unsplash_336.jpg +assets/images/national-cancer-institute-YvvFRJgWShM-unsplash_336.jpg +assets/images/national-cancer-institute-cw2Zn2ZQ9YQ-unsplash_336.jpg +assets/images/neil-thomas-SIU1Glk6v5k-unsplash_150.jpg +assets/images/nikita-karimov-lvJZhHOIJJ4-unsplash_448.jpg +assets/images/no-revisions-UhpAf0ySwuk-unsplash_336.jpg +assets/images/noaa--urO88VoCRE-unsplash_336.jpg +assets/images/noaa-FY3vXNBl1v4-unsplash_336.jpg +assets/images/noaa-I323ZqSkkn8-unsplash_448.jpg +assets/images/noaa-Led9c1SSNFo-unsplash_684.jpg +assets/images/nonresident-nizUHtSIrKM-unsplash_150.jpg +assets/images/nuno-alberto-MykFFC5zolE-unsplash_448.jpg +assets/images/oguzhan-edman-ZWPkHLRu3_4-unsplash_336.jpg +assets/images/olga-guryanova-ft7vJxwl2RY-unsplash_336.jpg +assets/images/olga-guryanova-tMFeatBSS4s-unsplash_336.jpg +assets/images/paola-chaaya-QrbuLFT6ypw-unsplash_336.jpg +assets/images/parker-johnson-v0OWc_skg0g-unsplash_336.jpg +assets/images/pascal-bullan-M8sQPAfhPdk-unsplash_336.jpg +assets/images/pat-taylor-12V36G17IbQ-unsplash_336.jpg +assets/images/pat-whelen-68OkRwuOeyQ-unsplash_150.jpg +assets/images/patti-black-FnV-PjAYHCI-unsplash_150.jpg +assets/images/paul-bill-HLuPjCa6IYw-unsplash_150.jpg +assets/images/paul-hanaoka-s0XabTAKvak-unsplash_684.jpg +assets/images/peter-conlan-LEgwEaBVGMo-unsplash_448.jpg +assets/images/peter-lawrence-rXZa4ufjoGw-unsplash_448.jpg +assets/images/peter-thomas-17EJD0QdKFI-unsplash_150.jpg +assets/images/phil-hearing-bu27Y0xg7dk-unsplash_150.jpg +assets/images/pierre-bamin-lM4_Nmcj4Xk-unsplash_150.jpg +assets/images/piron-guillaume-U4FyCp3-KzY-unsplash_150.jpg +assets/images/prince-abban-0OUHhvNIbYc-unsplash_150.jpg +assets/images/priscilla-du-preez-GgtxccOjIXE-unsplash_448.jpg +assets/images/quick-ps-sW41y3lETZk-unsplash_150.jpg +assets/images/quino-al-KydWCDJe9s0-unsplash_150.jpg +assets/images/razvan-chisu-Ua-agENjmI4-unsplash_448.jpg +assets/images/rebe-pascual-SACRQSof7Qw-unsplash_448.jpg +assets/images/red-dot-Q98X_JVRGS0-unsplash_336.jpg +assets/images/redd-f-N9CYH-H_gBE-unsplash_684.jpg +assets/images/rio-lecatompessy-cfDURuQKABk-unsplash_336.jpg +assets/images/robert-bye-EILw-nEK46k-unsplash_150.jpg +assets/images/robert-bye-xHUZuSwVJg4-unsplash_336.jpg +assets/images/roberto-junior-4fsCBcZt9H8-unsplash_150.jpg +assets/images/robina-weermeijer-Pw9aFhc92P8-unsplash_448.jpg +assets/images/ronda-darby-HbMLSB-uhQY-unsplash_336.jpg +assets/images/ross-parmly-rf6ywHVkrlY-unsplash_336.jpg +assets/images/ruben-valenzuela-JEp9cl5jfZA-unsplash_684.jpg +assets/images/rufinochka-XonjCOZZN_w-unsplash_448.jpg +assets/images/ryan-plomp-TT6Hep-JzrU-unsplash_336.jpg +assets/images/sabri-tuzcu-kxR3hh0IRHU-unsplash_336.jpg +assets/images/sam-mcghee-4siwRamtFAk-unsplash_684.jpg +assets/images/samuel-schroth-hyPt63Df3Dw-unsplash_150.jpg +assets/images/sandip-roy-4hgTlYb9jzg-unsplash_448.jpg +assets/images/sapan-patel-gmgWd0CgWQI-unsplash_150.jpg +assets/images/sara-cottle-bGjtWs8sXT0-unsplash_448.jpg +assets/images/sarah-dorweiler-gUPiTDBdRe4-unsplash_336.jpg +assets/images/scott-graham-OQMZwNd3ThU-unsplash_448.jpg +assets/images/scott-webb-U5kQvbQWoG0-unsplash_150.jpg +assets/images/sean-lee-hDqRQmcjM3s-unsplash_336.jpg +assets/images/sheelah-brennan-UOfERQF_pr4-unsplash_336.jpg +assets/images/shubham-dhage-tT6GNIFkZv4-unsplash_150.jpg +assets/images/sj-objio-8hHxO3iYuU0-unsplash_448.jpg +assets/images/stan-georgiev-pvNxRUq7O7U-unsplash_150.jpg +assets/images/stefan-stefancik-pzA7QWNCIYg-unsplash_150.jpg +assets/images/stephen-phillips-hostreviews-co-uk-em37kS8WJJQ-unsplash_150.jpg +assets/images/steven-van-bTPP3jBnOb8-unsplash_684.jpg +assets/images/sven-mieke-Lx_GDv7VA9M-unsplash_150.jpg +assets/images/tachina-lee--wjk_SSqCE4-unsplash_448.jpg +assets/images/tadeusz-lakota-Tb38UzCvKCY-unsplash_336.jpg +assets/images/tamanna-rumee-lpGm415q9JA-unsplash_336.jpg +assets/images/tamara-bellis-IwVRO3TLjLc-unsplash_336.jpg +assets/images/tania-mousinho-YlpfE9uCakE-unsplash_150.jpg +assets/images/tatjana-petkevica-iad-dMBDdoo-unsplash_150.jpg +assets/images/taylor-grote-UiVe5QvOhao-unsplash_150.jpg +assets/images/tech-daily-vxTWpu14zeM-unsplash_150.jpg +assets/images/thisisengineering-ZPeXrWxOjRQ-unsplash_336.jpg +assets/images/tierra-mallorca-rgJ1J8SDEAY-unsplash_150.jpg +assets/images/tingey-injury-law-firm-9SKhDFnw4c4-unsplash_448.jpg +assets/images/toa-heftiba--abWByT3yg4-unsplash_336.jpg +assets/images/toa-heftiba-4xe-yVFJCvw-unsplash_336.jpg +assets/images/todd-trapani-vS54KomBEJU-unsplash_684.jpg +assets/images/truckrun-XBWF6_TEsFM-unsplash_684.jpg +assets/images/trung-thanh-LgdDeuBcgIY-unsplash_684.jpg +assets/images/unseen-histories-4kYkKW8v8rY-unsplash_150.jpg +assets/images/valery-tenevoy-c0VbjkPEfmM-unsplash_336.jpg +assets/images/valtteri-laukkanen-9u9Pc0t9vKM-unsplash_150.jpg +assets/images/vanilla-bear-films-JEwNQerg3Hs-unsplash_336.jpg +assets/images/venti-views-KElJx4R4Py8-unsplash_684.jpg +assets/images/viktor-talashuk-05HLFQu8bFw-unsplash_150.jpg +assets/images/vladislav-babienko-KTpSVEcU0XU-unsplash_448.jpg +assets/images/wan-san-yip-ID1yWa1Wpx0-unsplash_336.jpg +assets/images/wesley-tingey-7BkCRNwh_V0-unsplash_150.jpg +assets/images/will-shirley-xRKcHoCOA4Y-unsplash_448.jpg +assets/images/xan-griffin-QxNkzEjB180-unsplash_150.jpg +assets/images/yucel-moran-4ndj0pATzeM-unsplash_150.jpg +assets/images/zac-gudakov-wwqZ8CM21gg-unsplash_150.jpg +favicon.ico +index.html +placeholder_light.jpg diff --git a/resources/newssite/news-nuxt/dist/resources.txt b/resources/newssite/news-nuxt/dist/resources.txt new file mode 100644 index 000000000..1129d04ea --- /dev/null +++ b/resources/newssite/news-nuxt/dist/resources.txt @@ -0,0 +1,354 @@ +404.html +_nuxt/entry.4163a698.css +_nuxt/entry.61b8a168.js +_nuxt/error-404.23f2309d.css +_nuxt/error-404.2a577bdf.js +_nuxt/error-500.aa16ed4d.css +_nuxt/error-500.c1eac2b8.js +_nuxt/error-component.2cb571d2.js +assets/images/2h-media-lPcQhLP-b4I-unsplash_150.jpg +assets/images/aaron-huber-G7sE2S4Lab4-unsplash_336.jpg +assets/images/adam-whitlock-I9j8Rk-JYFM-unsplash_684.jpg +assets/images/aditya-chinchure-ZhQCZjr9fHo-unsplash_336.jpg +assets/images/adrian-dascal-myAz-buELXs-unsplash_448.jpg +assets/images/ahmet-kurt-5BGg2L5nhlU-unsplash_150.jpg +assets/images/ahmet-kurt-WviyUzOg4RU-unsplash_150.jpg +assets/images/alan-rodriguez-qrD-g7oc9is-unsplash_684.jpg +assets/images/albert-stoynov-fEdf0fig3os-unsplash_448.jpg +assets/images/alec-favale-dLctr-PqFys-unsplash_150.jpg +assets/images/alev-takil-fYyYz38bUkQ-unsplash_150.jpg +assets/images/alex-hudson-7AgqAZbogOQ-unsplash_448.jpg +assets/images/alexander-grey-8lnbXtxFGZw-unsplash_684.jpg +assets/images/alexander-kirov-YhDJXJjmxUQ-unsplash_336.jpg +assets/images/alexandre-debieve-FO7JIlwjOtU-unsplash_336.jpg +assets/images/alice-pasqual-Olki5QpHxts-unsplash_336.jpg +assets/images/allan-wadsworth-Lp78NT-mf9o-unsplash_150.jpg +assets/images/anastassia-anufrieva-ecHGTPfjNfA-unsplash_150.jpg +assets/images/anatol-rurac-NeSj0i6HLak-unsplash_150.jpg +assets/images/anatol-rurac-b5t2lqeCGfA-unsplash_150.jpg +assets/images/andrew-solok-LbckXdUVOlY-unsplash_448.jpg +assets/images/angela-loria-hFc0JEKD4Cc-unsplash_150.jpg +assets/images/ani-kolleshi-7jjnJ-QA9fY-unsplash_150.jpg +assets/images/anne-nygard-tcJ6sJTtTWI-unsplash_448.jpg +assets/images/annie-spratt-IT6aov1ScW0-unsplash_150.jpg +assets/images/annie-spratt-KiOHnBkLQQU-unsplash_684.jpg +assets/images/ant-rozetsky-SLIFI67jv5k-unsplash_150.jpg +assets/images/arnaud-jaegers-IBWJsMObnnU-unsplash_336.jpg +assets/images/arnel-hasanovic-MNd-Rka1o0Q-unsplash_336.jpg +assets/images/artur-voznenko-rwPIQQPz1ew-unsplash_150.jpg +assets/images/arturo-rey-5yP83RhaFGA-unsplash_336.jpg +assets/images/ashim-d-silva-ZmgJiztRHXE-unsplash_336.jpg +assets/images/astronaud23-ox3t0m3PUqA-unsplash_336.jpg +assets/images/austin-distel-97HfVpyNR1M-unsplash_448.jpg +assets/images/austin-distel-rxpThOwuVgE-unsplash_448.jpg +assets/images/axp-photography-v6pAkO31d50-unsplash_448.jpg +assets/images/azhar-j-t2hgHV1R7_g-unsplash_448.jpg +assets/images/benjamin-child-rOn57CBgyMo-unsplash_448.jpg +assets/images/bermix-studio-yUnSMBogWNI-unsplash_150.jpg +assets/images/bernd-dittrich-Xk1IfNnEhRA-unsplash_448.jpg +assets/images/bianca-ackermann-qr0-lKAOZSk-unsplash_150.jpg +assets/images/boris-stefanik-q49CgyIrLes-unsplash_150.jpg +assets/images/boxed-water-is-better-y-TpYAlcBYM-unsplash_150.jpg +assets/images/braden-collum-9HI8UJMSdZA-unsplash_336.jpg +assets/images/bram-naus-oqnVnI5ixHg-unsplash_448.jpg +assets/images/brigitte-tohm-iIupxcq-yH4-unsplash_150.jpg +assets/images/brooke-lark-GJMlSBS0FhU-unsplash_150.jpg +assets/images/brooke-lark-HjWzkqW1dgI-unsplash_336.jpg +assets/images/brooke-lark-IDTEXXXfS44-unsplash_150.jpg +assets/images/brooke-lark-lcZ9NxhOSlo-unsplash_448.jpg +assets/images/brooke-lark-oaz0raysASk-unsplash_150.jpg +assets/images/bruce-mars-xj8qrWvuOEs-unsplash_336.jpg +assets/images/bruno-nascimento-PHIgYUGQPvU-unsplash_448.jpg +assets/images/c-j-1GHqOftzYo0-unsplash_150.jpg +assets/images/caglar-oskay-d0Be8Vs9XRk-unsplash_336.jpg +assets/images/campaign-creators-pypeCEaJeZY-unsplash_448.jpg +assets/images/cardmapr-nl-s8F8yglbpjo-unsplash_336.jpg +assets/images/carl-nenzen-loven-c-pc2mP7hTs-unsplash_448.jpg +assets/images/chad-stembridge-sEHrIPpkKQY-unsplash_150.jpg +assets/images/chloe-taranto-x2zyAOmVNtM-unsplash_448.jpg +assets/images/chris-leboutillier-TUJud0AWAPI-unsplash_150.jpg +assets/images/chris-leboutillier-c7RWVGL8lPA-unsplash_150.jpg +assets/images/christian-lue-8Yw6tsB8tnc-unsplash_448.jpg +assets/images/christian-tenguan-P3gfVKhz8d0-unsplash_336.jpg +assets/images/christina-rumpf-gUU4MF87Ipw-unsplash_150.jpg +assets/images/clark-street-mercantile-P3pI6xzovu0-unsplash_336.jpg +assets/images/claudio-schwarz-3cWxxW2ggKE-unsplash_336.jpg +assets/images/clay-banks-BY-R0UNRE7w-unsplash_150.jpg +assets/images/clay-banks-cisdc-344vo-unsplash_150.jpg +assets/images/clem-onojeghuo-DoA2duXyzRM-unsplash_684.jpg +assets/images/clem-onojeghuo-RLJnH4Mt9A0-unsplash_336.jpg +assets/images/colin-lloyd-2ULmNrj44QY-unsplash_448.jpg +assets/images/colin-lloyd-NKS5gg7rWGw-unsplash_150.jpg +assets/images/colin-lloyd-uaM_Ijy_joY-unsplash_448.jpg +assets/images/craig-manners-LvJCFOW3Ma8-unsplash_448.jpg +assets/images/craig-melville-_JKymnZ1Uc4-unsplash_150.jpg +assets/images/crystal-kwok-xD5SWy7hMbw-unsplash_448.jpg +assets/images/dani-navarro-6CnGzrLwM28-unsplash_336.jpg +assets/images/daniel-staple-N320vzTBviA-unsplash_684.jpg +assets/images/david-beale--lQR8yeDzek-unsplash_336.jpg +assets/images/david-everett-strickler-igCBFrMd11I-unsplash_684.jpg +assets/images/david-lezcano-NfZiOJzZgcg-unsplash_336.jpg +assets/images/david-vives-Nzbkev7SQTg-unsplash_448.jpg +assets/images/declan-sun-misAHv6YWkI-unsplash_336.jpg +assets/images/denys-nevozhai-7nrsVjvALnA-unsplash_150.jpg +assets/images/denys-nevozhai-z0nVqfrOqWA-unsplash_150.jpg +assets/images/derick-mckinney-muhK4oeYJiU-unsplash_150.jpg +assets/images/devi-puspita-amartha-yahya-7ln0pST_O8M-unsplash_448.jpg +assets/images/dibakar-roy-K9JwokzSvrc-unsplash_150.jpg +assets/images/diego-jimenez-A-NVHPka9Rk-unsplash_336.jpg +assets/images/dillon-kydd-2keCPb73aQY-unsplash_336.jpg +assets/images/dino-reichmuth-A5rCN8626Ck-unsplash_336.jpg +assets/images/dom-hill-nimElTcTNyY-unsplash_336.jpg +assets/images/dominic-bieri-vXRt4rFr4hI-unsplash_336.jpg +assets/images/dominic-hampton-_8aRumOixtI-unsplash_684.jpg +assets/images/domino-studio-164_6wVEHfI-unsplash_336.jpg +assets/images/drew-hays-tGYrlchfObE-unsplash_336.jpg +assets/images/dyana-wing-so-Og16Foo-pd8-unsplash_150.jpg +assets/images/dylan-calluy-j9q18vvHitg-unsplash_150.jpg +assets/images/ednilson-cardoso-dos-santos-haiooWA_weo-unsplash_336.jpg +assets/images/egor-myznik-GFHKMW6KiJ0-unsplash_684.jpg +assets/images/ehimetalor-akhere-unuabona-yS0uBoF4xDo-unsplash_448.jpg +assets/images/elimende-inagella-7OxV_qDiGRI-unsplash_448.jpg +assets/images/emmanuel-ikwuegbu-ceawFbpA-14-unsplash_448.jpg +assets/images/emran-yousof-k8ZbMQWbx34-unsplash_336.jpg +assets/images/erik-mclean-ByjIzFupcHo-unsplash_336.jpg +assets/images/esther-ann-glpYh1cWf0o-unsplash_448.jpg +assets/images/etienne-girardet-RqOyRtYGhLg-unsplash_448.jpg +assets/images/evie-calder-97CO-A4P0GQ-unsplash_336.jpg +assets/images/fanga-studio-bOfCOy3_4wU-unsplash_448.jpg +assets/images/fons-heijnsbroek-vBfEZdpEr-E-unsplash_448.jpg +assets/images/freestocks-nss2eRzQwgw-unsplash_150.jpg +assets/images/gaku-suyama-VyiLZUcdJv0-unsplash_150.jpg +assets/images/geert-pieters-NbpUM86Jo8Y-unsplash_150.jpg +assets/images/geoff-scott-8lUTnkZXZSA-unsplash_336.jpg +assets/images/glenn-carstens-peters-npxXWgQ33ZQ-unsplash_336.jpg +assets/images/greg-rakozy-oMpAz-DN-9I-unsplash_684.jpg +assets/images/gregory-hayes-h5cd51KXmRQ-unsplash_684.jpg +assets/images/guillaume-perigois-HL4LEIyGEYU-unsplash_150.jpg +assets/images/hannah-busing-0V6DmTuJaIk-unsplash_448.jpg +assets/images/hansjorg-keller-CQqyv5uldW4-unsplash_448.jpg +assets/images/harley-davidson-fFbUdx80oCc-unsplash_336.jpg +assets/images/harold-mendoza-6xafY_AE1LM-unsplash_150.jpg +assets/images/harri-kuokkanen-SEtUeWL8bIQ-unsplash_336.jpg +assets/images/hassan-kibwana-fmXLB_uHIh4-unsplash_448.jpg +assets/images/heather-ford-5gkYsrH_ebY-unsplash_336.jpg +assets/images/heidi-fin-2TLREZi7BUg-unsplash_336.jpg +assets/images/heidi-kaden-L_U4jhwZ6hY-unsplash_448.jpg +assets/images/hush-naidoo-jade-photography-ZCO_5Y29s8k-unsplash_150.jpg +assets/images/hush-naidoo-jade-photography-Zp7ebyti3MU-unsplash_336.jpg +assets/images/hyttalo-souza-a1p0Z7RSkL8-unsplash_684.jpg +assets/images/ian-hutchinson-P8rgDtEFn7s-unsplash_150.jpg +assets/images/ian-taylor-kAWTCt7p7rs-unsplash_448.jpg +assets/images/ibrahim-rifath-OApHds2yEGQ-unsplash_150.jpg +assets/images/icons8-team-k5fUTay0ghw-unsplash_336.jpg +assets/images/ilaria-de-bona-RuFfpBsaRY0-unsplash_336.jpg +assets/images/ilyass-seddoug-06w8RxgSzF0-unsplash_150.jpg +assets/images/inaki-del-olmo-NIJuEQw0RKg-unsplash_336.jpg +assets/images/irwan-rbDE93-0hHs-unsplash_684.jpg +assets/images/isai-ramos-Sp70YIWtuM8-unsplash_336.jpg +assets/images/isaw-company-Oqv_bQbZgS8-unsplash_336.jpg +assets/images/ixography-05Q_XPF_YKs-unsplash_336.jpg +assets/images/jacob-licht-8nA_iHrxHIo-unsplash_150.jpg +assets/images/jakob-dalbjorn-cuKJre3nyYc-unsplash_448.jpg +assets/images/jakub-zerdzicki-VnTR3XFwxWs-unsplash_150.jpg +assets/images/jakub-zerdzicki-qcRGVZNZ5js-unsplash_336.jpg +assets/images/jannis-brandt-mmsQUgMLqUo-unsplash_150.jpg +assets/images/jaron-nix-7wWRXewYCH4-unsplash_684.jpg +assets/images/jay-clark-P3sLerH3UmM-unsplash_336.jpg +assets/images/jed-owen-ajZibDGpPew-unsplash_336.jpg +assets/images/jessica-weiller-So4eFi-d1nc-unsplash_336.jpg +assets/images/jon-tyson-0BLE1xp5HBQ-unsplash_150.jpg +assets/images/jon-tyson-ne2mqMgER8Y-unsplash_150.jpg +assets/images/jonathan-borba-lrQPTQs7nQQ-unsplash_150.jpg +assets/images/jonathan-simcoe-S9J1HqoL9ns-unsplash_150.jpg +assets/images/jordhan-madec-AD5ylD2T0UY-unsplash_336.jpg +assets/images/joseph-gonzalez-QaGDmf5tMiE-unsplash_150.jpg +assets/images/joshua-rawson-harris-YNaSz-E7Qss-unsplash_336.jpg +assets/images/juli-kosolapova-4PE3X9eKsu4-unsplash_336.jpg +assets/images/junko-nakase-Q-72wa9-7Dg-unsplash_336.jpg +assets/images/k8-uYf_C34PAao-unsplash_336.jpg +assets/images/keagan-henman-xPJYL0l5Ii8-unsplash_336.jpg +assets/images/kelli-dougal-vbiQ_7vwfrs-unsplash_448.jpg +assets/images/kelly-sikkema-A-lovieAmjA-unsplash_150.jpg +assets/images/kelly-sikkema-WIYtZU3PxsI-unsplash_448.jpg +assets/images/kenny-K72n3BHgHCg-unsplash_336.jpg +assets/images/kenny-eliason-4N3iHYmqy_E-unsplash_336.jpg +assets/images/kenny-eliason-5ddH9Y2accI-unsplash_150.jpg +assets/images/kevin-rajaram-qhixFFO8EWQ-unsplash_336.jpg +assets/images/kevin-wang-t7vEVxwGGm0-unsplash_336.jpg +assets/images/kitthitorn-chaiyuthapoom-TOH_gw5dd20-unsplash_336.jpg +assets/images/koshu-kunii-ADLj1cyFfV8-unsplash_150.jpg +assets/images/koshu-kunii-cWEGNQqcImk-unsplash_336.jpg +assets/images/koshu-kunii-v9ferChkC9A-unsplash_150.jpg +assets/images/kyle-glenn-gcw_WWu_uBQ-unsplash_336.jpg +assets/images/leah-hetteberg-kTVN2l0ZUv8-unsplash_684.jpg +assets/images/leisara-studio-EzzW1oNek-I-unsplash_336.jpg +assets/images/leon-seibert-Xs3al4NpIFQ-unsplash_336.jpg +assets/images/libre-leung-9O0Sp22DF0I-unsplash_336.jpg +assets/images/linkedin-sales-solutions--AXDunSs-n4-unsplash_150.jpg +assets/images/little-plant-TZw891-oMio-unsplash_150.jpg +assets/images/lorenzo-moschi-N7ypjB7HKIk-unsplash_150.jpg +assets/images/louis-reed-pwcKF7L4-no-unsplash_448.jpg +assets/images/luis-cortes-QrPDA15pRkM-unsplash_336.jpg +assets/images/lukasz-radziejewski-cg4MzL_eSvU-unsplash_336.jpg +assets/images/m-ZzOa5G8hSPI-unsplash_150.jpg +assets/images/maarten-van-den-heuvel-gZXx8lKAb7Y-unsplash_150.jpg +assets/images/mac-blades-jpgJSBQtw5U-unsplash_336.jpg +assets/images/maddi-bazzocco-qKbHvzXb85A-unsplash_150.jpg +assets/images/maja-kochanowska-EiJQdDI_t_Y-unsplash_150.jpg +assets/images/maksim-larin-tecILYzVAzg-unsplash_336.jpg +assets/images/maksym-harbar-okn8ZIjPMxI-unsplash_448.jpg +assets/images/maksym-kaharlytskyi-Y0z9MyDsrU0-unsplash_150.jpg +assets/images/marc-olivier-jodoin-_eclsGKsUdo-unsplash_448.jpg +assets/images/marcelo-leal-6pcGTJDuf6M-unsplash_448.jpg +assets/images/marco-oriolesi-wqLGlhjr6Og-unsplash_684.jpg +assets/images/marek-studzinski-9U9I-eVx9nI-unsplash_150.jpg +assets/images/maria-lysenko-tZvkSuBleso-unsplash_150.jpg +assets/images/marius-oprea-ySA9uj7zSmw-unsplash_150.jpg +assets/images/markus-spiske-7PMGUqYQpYc-unsplash_150.jpg +assets/images/markus-spiske-Nph1oyRsHm4-unsplash_150.jpg +assets/images/markus-spiske-WUehAgqO5hE-unsplash_684.jpg +assets/images/markus-spiske-jgOkEjVw-KM-unsplash_150.jpg +assets/images/markus-spiske-lUc5pRFB25s-unsplash_336.jpg +assets/images/markus-spiske-p2Xor4Lbrrk-unsplash_150.jpg +assets/images/markus-spiske-wIUxLHndcLw-unsplash_150.jpg +assets/images/martha-dominguez-de-gouveia-k-NnVZ-z26w-unsplash_150.jpg +assets/images/martin-jernberg-jVNWCFwdjZU-unsplash_448.jpg +assets/images/martin-podsiad-wrdtA9lew9E-unsplash_150.jpg +assets/images/mathias-reding-yfXhqAW5X0c-unsplash_150.jpg +assets/images/mathurin-napoly-matnapo-ejWJ3a92FEs-unsplash_336.jpg +assets/images/mathurin-napoly-matnapo-pIJ34ZrZEEw-unsplash_150.jpg +assets/images/matt-popovich-7mqsZsE6FaU-unsplash_336.jpg +assets/images/matthew-ball-3wW2fBjptQo-unsplash_448.jpg +assets/images/matthew-henry-0Ol8Sa2n21c-unsplash_150.jpg +assets/images/matthew-landers-v8UgmRa6UDg-unsplash_150.jpg +assets/images/matthew-tenbruggencate-0HJWobhGhJs-unsplash_150.jpg +assets/images/matthias-heil-lDOEwat_MPs-unsplash_336.jpg +assets/images/mattia-bericchia-xkD79yf4tb8-unsplash_150.jpg +assets/images/maxim-hopman-IayKLkmz6g0-unsplash_448.jpg +assets/images/maximilian-bungart-nwqfl_HtJjk-unsplash_150.jpg +assets/images/melissa-askew-8n00CqwnqO8-unsplash_448.jpg +assets/images/memento-media-XhYq-5KnxSk-unsplash_448.jpg +assets/images/mesut-kaya-eOcyhe5-9sQ-unsplash_336.jpg +assets/images/metin-ozer-hShrr0WvrQs-unsplash_336.jpg +assets/images/microsoft-365-f1zQuagWCTA-unsplash_336.jpg +assets/images/miguel-bruna-TzVN0xQhWaQ-unsplash_150.jpg +assets/images/mika-baumeister-Hm4zYX-BDxk-unsplash_150.jpg +assets/images/mika-baumeister-bGZZBDvh8s4-unsplash_150.jpg +assets/images/mika-baumeister-jXPQY1em3Ew-unsplash_150.jpg +assets/images/mike-enerio-7ryPpZK1qV8-unsplash_448.jpg +assets/images/milad-fakurian-58Z17lnVS4U-unsplash_684.jpg +assets/images/mr-cup-fabien-barral-Mwuod2cm8g4-unsplash_448.jpg +assets/images/mr-lee-f4RBYsY2hxA-unsplash_150.jpg +assets/images/mufid-majnun-J12RfFH-2ZE-unsplash_684.jpg +assets/images/mufid-majnun-tJJIGh703I4-unsplash_336.jpg +assets/images/mulyadi-JeCNRxGLSp4-unsplash_150.jpg +assets/images/myriam-zilles-KltoLK6Mk-g-unsplash_336.jpg +assets/images/nasa-Q1p7bh3SHj8-unsplash_150.jpg +assets/images/nasa-cIX5TlQ_FgM-unsplash_448.jpg +assets/images/nasa-dCgbRAQmTQA-unsplash_684.jpg +assets/images/nathan-cima-TQuq2OtLBNU-unsplash_150.jpg +assets/images/nathan-dumlao-bRdRUUtbxO0-unsplash_150.jpg +assets/images/nathan-dumlao-laCrvNG3F_I-unsplash_336.jpg +assets/images/national-cancer-institute-A2CK97sS0ns-unsplash_684.jpg +assets/images/national-cancer-institute-GcrSgHDrniY-unsplash_336.jpg +assets/images/national-cancer-institute-KrsoedfRAf4-unsplash_336.jpg +assets/images/national-cancer-institute-L7en7Lb-Ovc-unsplash_336.jpg +assets/images/national-cancer-institute-NFvdKIhxYlU-unsplash_336.jpg +assets/images/national-cancer-institute-SMxzEaidR20-unsplash_336.jpg +assets/images/national-cancer-institute-YvvFRJgWShM-unsplash_336.jpg +assets/images/national-cancer-institute-cw2Zn2ZQ9YQ-unsplash_336.jpg +assets/images/neil-thomas-SIU1Glk6v5k-unsplash_150.jpg +assets/images/nikita-karimov-lvJZhHOIJJ4-unsplash_448.jpg +assets/images/no-revisions-UhpAf0ySwuk-unsplash_336.jpg +assets/images/noaa--urO88VoCRE-unsplash_336.jpg +assets/images/noaa-FY3vXNBl1v4-unsplash_336.jpg +assets/images/noaa-I323ZqSkkn8-unsplash_448.jpg +assets/images/noaa-Led9c1SSNFo-unsplash_684.jpg +assets/images/nonresident-nizUHtSIrKM-unsplash_150.jpg +assets/images/nuno-alberto-MykFFC5zolE-unsplash_448.jpg +assets/images/oguzhan-edman-ZWPkHLRu3_4-unsplash_336.jpg +assets/images/olga-guryanova-ft7vJxwl2RY-unsplash_336.jpg +assets/images/olga-guryanova-tMFeatBSS4s-unsplash_336.jpg +assets/images/paola-chaaya-QrbuLFT6ypw-unsplash_336.jpg +assets/images/parker-johnson-v0OWc_skg0g-unsplash_336.jpg +assets/images/pascal-bullan-M8sQPAfhPdk-unsplash_336.jpg +assets/images/pat-taylor-12V36G17IbQ-unsplash_336.jpg +assets/images/pat-whelen-68OkRwuOeyQ-unsplash_150.jpg +assets/images/patti-black-FnV-PjAYHCI-unsplash_150.jpg +assets/images/paul-bill-HLuPjCa6IYw-unsplash_150.jpg +assets/images/paul-hanaoka-s0XabTAKvak-unsplash_684.jpg +assets/images/peter-conlan-LEgwEaBVGMo-unsplash_448.jpg +assets/images/peter-lawrence-rXZa4ufjoGw-unsplash_448.jpg +assets/images/peter-thomas-17EJD0QdKFI-unsplash_150.jpg +assets/images/phil-hearing-bu27Y0xg7dk-unsplash_150.jpg +assets/images/pierre-bamin-lM4_Nmcj4Xk-unsplash_150.jpg +assets/images/piron-guillaume-U4FyCp3-KzY-unsplash_150.jpg +assets/images/prince-abban-0OUHhvNIbYc-unsplash_150.jpg +assets/images/priscilla-du-preez-GgtxccOjIXE-unsplash_448.jpg +assets/images/quick-ps-sW41y3lETZk-unsplash_150.jpg +assets/images/quino-al-KydWCDJe9s0-unsplash_150.jpg +assets/images/razvan-chisu-Ua-agENjmI4-unsplash_448.jpg +assets/images/rebe-pascual-SACRQSof7Qw-unsplash_448.jpg +assets/images/red-dot-Q98X_JVRGS0-unsplash_336.jpg +assets/images/redd-f-N9CYH-H_gBE-unsplash_684.jpg +assets/images/rio-lecatompessy-cfDURuQKABk-unsplash_336.jpg +assets/images/robert-bye-EILw-nEK46k-unsplash_150.jpg +assets/images/robert-bye-xHUZuSwVJg4-unsplash_336.jpg +assets/images/roberto-junior-4fsCBcZt9H8-unsplash_150.jpg +assets/images/robina-weermeijer-Pw9aFhc92P8-unsplash_448.jpg +assets/images/ronda-darby-HbMLSB-uhQY-unsplash_336.jpg +assets/images/ross-parmly-rf6ywHVkrlY-unsplash_336.jpg +assets/images/ruben-valenzuela-JEp9cl5jfZA-unsplash_684.jpg +assets/images/rufinochka-XonjCOZZN_w-unsplash_448.jpg +assets/images/ryan-plomp-TT6Hep-JzrU-unsplash_336.jpg +assets/images/sabri-tuzcu-kxR3hh0IRHU-unsplash_336.jpg +assets/images/sam-mcghee-4siwRamtFAk-unsplash_684.jpg +assets/images/samuel-schroth-hyPt63Df3Dw-unsplash_150.jpg +assets/images/sandip-roy-4hgTlYb9jzg-unsplash_448.jpg +assets/images/sapan-patel-gmgWd0CgWQI-unsplash_150.jpg +assets/images/sara-cottle-bGjtWs8sXT0-unsplash_448.jpg +assets/images/sarah-dorweiler-gUPiTDBdRe4-unsplash_336.jpg +assets/images/scott-graham-OQMZwNd3ThU-unsplash_448.jpg +assets/images/scott-webb-U5kQvbQWoG0-unsplash_150.jpg +assets/images/sean-lee-hDqRQmcjM3s-unsplash_336.jpg +assets/images/sheelah-brennan-UOfERQF_pr4-unsplash_336.jpg +assets/images/shubham-dhage-tT6GNIFkZv4-unsplash_150.jpg +assets/images/sj-objio-8hHxO3iYuU0-unsplash_448.jpg +assets/images/stan-georgiev-pvNxRUq7O7U-unsplash_150.jpg +assets/images/stefan-stefancik-pzA7QWNCIYg-unsplash_150.jpg +assets/images/stephen-phillips-hostreviews-co-uk-em37kS8WJJQ-unsplash_150.jpg +assets/images/steven-van-bTPP3jBnOb8-unsplash_684.jpg +assets/images/sven-mieke-Lx_GDv7VA9M-unsplash_150.jpg +assets/images/tachina-lee--wjk_SSqCE4-unsplash_448.jpg +assets/images/tadeusz-lakota-Tb38UzCvKCY-unsplash_336.jpg +assets/images/tamanna-rumee-lpGm415q9JA-unsplash_336.jpg +assets/images/tamara-bellis-IwVRO3TLjLc-unsplash_336.jpg +assets/images/tania-mousinho-YlpfE9uCakE-unsplash_150.jpg +assets/images/tatjana-petkevica-iad-dMBDdoo-unsplash_150.jpg +assets/images/taylor-grote-UiVe5QvOhao-unsplash_150.jpg +assets/images/tech-daily-vxTWpu14zeM-unsplash_150.jpg +assets/images/thisisengineering-ZPeXrWxOjRQ-unsplash_336.jpg +assets/images/tierra-mallorca-rgJ1J8SDEAY-unsplash_150.jpg +assets/images/tingey-injury-law-firm-9SKhDFnw4c4-unsplash_448.jpg +assets/images/toa-heftiba--abWByT3yg4-unsplash_336.jpg +assets/images/toa-heftiba-4xe-yVFJCvw-unsplash_336.jpg +assets/images/todd-trapani-vS54KomBEJU-unsplash_684.jpg +assets/images/truckrun-XBWF6_TEsFM-unsplash_684.jpg +assets/images/trung-thanh-LgdDeuBcgIY-unsplash_684.jpg +assets/images/unseen-histories-4kYkKW8v8rY-unsplash_150.jpg +assets/images/valery-tenevoy-c0VbjkPEfmM-unsplash_336.jpg +assets/images/valtteri-laukkanen-9u9Pc0t9vKM-unsplash_150.jpg +assets/images/vanilla-bear-films-JEwNQerg3Hs-unsplash_336.jpg +assets/images/venti-views-KElJx4R4Py8-unsplash_684.jpg +assets/images/viktor-talashuk-05HLFQu8bFw-unsplash_150.jpg +assets/images/vladislav-babienko-KTpSVEcU0XU-unsplash_448.jpg +assets/images/wan-san-yip-ID1yWa1Wpx0-unsplash_336.jpg +assets/images/wesley-tingey-7BkCRNwh_V0-unsplash_150.jpg +assets/images/will-shirley-xRKcHoCOA4Y-unsplash_448.jpg +assets/images/xan-griffin-QxNkzEjB180-unsplash_150.jpg +assets/images/yucel-moran-4ndj0pATzeM-unsplash_150.jpg +assets/images/zac-gudakov-wwqZ8CM21gg-unsplash_150.jpg +favicon.ico +index.html +placeholder_light.jpg diff --git a/resources/react-stockcharts/build/resources.txt b/resources/react-stockcharts/build/resources.txt new file mode 100644 index 000000000..318ab6b64 --- /dev/null +++ b/resources/react-stockcharts/build/resources.txt @@ -0,0 +1,9 @@ +asset-manifest.json +index.html +static/js/2.8e539c84.chunk.js +static/js/2.8e539c84.chunk.js.LICENSE.txt +static/js/2.8e539c84.chunk.js.map +static/js/main.d5117271.chunk.js +static/js/main.d5117271.chunk.js.map +static/js/runtime-main.177cc4f8.js +static/js/runtime-main.177cc4f8.js.map diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs index 076a84733..b4536f713 100644 --- a/resources/shared/sw-messages.mjs +++ b/resources/shared/sw-messages.mjs @@ -1,6 +1,6 @@ export const SW_MESSAGES = Object.freeze({ SET_STATE: "SET_STATE", - PRECACHE_SUITES: "PRECACHE_SUITES", - PRECACHE_PROGRESS: "PRECACHE_PROGRESS", - PRECACHE_DONE: "PRECACHE_DONE", + PRELOAD_SUITES: "PRELOAD_SUITES", + PRELOAD_PROGRESS: "PRELOAD_PROGRESS", + PRELOAD_DONE: "PRELOAD_DONE", }); diff --git a/resources/todomvc/architecture-examples/angular-complex/dist/resources.txt b/resources/todomvc/architecture-examples/angular-complex/dist/resources.txt new file mode 100644 index 000000000..5d76449cd --- /dev/null +++ b/resources/todomvc/architecture-examples/angular-complex/dist/resources.txt @@ -0,0 +1,13 @@ +3rdpartylicenses.txt +big-dom-with-stacking-context-scrollable.css +favicon.ico +index.html +logo.png +main.db97198c6e808d8a.js +main.db97198c6e808d8a.js.map +polyfills.35f89f9aebee2e5c.js +polyfills.35f89f9aebee2e5c.js.map +runtime.557aa15b5f22cb0a.js +runtime.557aa15b5f22cb0a.js.map +styles.746a93d9fd4de753.css +styles.746a93d9fd4de753.css.map diff --git a/resources/todomvc/architecture-examples/angular/dist/resources.txt b/resources/todomvc/architecture-examples/angular/dist/resources.txt new file mode 100644 index 000000000..777969f77 --- /dev/null +++ b/resources/todomvc/architecture-examples/angular/dist/resources.txt @@ -0,0 +1,11 @@ +3rdpartylicenses.txt +favicon.ico +index.html +main.db97198c6e808d8a.js +main.db97198c6e808d8a.js.map +polyfills.35f89f9aebee2e5c.js +polyfills.35f89f9aebee2e5c.js.map +runtime.557aa15b5f22cb0a.js +runtime.557aa15b5f22cb0a.js.map +styles.746a93d9fd4de753.css +styles.746a93d9fd4de753.css.map diff --git a/resources/todomvc/architecture-examples/backbone-complex/dist/resources.txt b/resources/todomvc/architecture-examples/backbone-complex/dist/resources.txt new file mode 100644 index 000000000..33f3faff5 --- /dev/null +++ b/resources/todomvc/architecture-examples/backbone-complex/dist/resources.txt @@ -0,0 +1,16 @@ +app.js +backbone-min.js +backbone-min.js.map +base.css +big-dom-with-stacking-context-scrollable.css +collections/todos.js +index.css +index.html +jquery.min.js +logo.png +models/todo.js +routers/router.js +sync/backbone.sync.js +underscore-min.js +views/app-view.js +views/todo-view.js diff --git a/resources/todomvc/architecture-examples/backbone/dist/resources.txt b/resources/todomvc/architecture-examples/backbone/dist/resources.txt new file mode 100644 index 000000000..706e1950b --- /dev/null +++ b/resources/todomvc/architecture-examples/backbone/dist/resources.txt @@ -0,0 +1,14 @@ +app.js +backbone-min.js +backbone-min.js.map +base.css +collections/todos.js +index.css +index.html +jquery.min.js +models/todo.js +routers/router.js +sync/backbone.sync.js +underscore-min.js +views/app-view.js +views/todo-view.js diff --git a/resources/todomvc/architecture-examples/jquery-complex/dist/resources.txt b/resources/todomvc/architecture-examples/jquery-complex/dist/resources.txt new file mode 100644 index 000000000..5d9142684 --- /dev/null +++ b/resources/todomvc/architecture-examples/jquery-complex/dist/resources.txt @@ -0,0 +1,10 @@ +app.css +app.js +base.css +big-dom-with-stacking-context-scrollable.css +director.min.js +handlebars.min.js +index.css +index.html +jquery.min.js +logo.png diff --git a/resources/todomvc/architecture-examples/jquery/dist/resources.txt b/resources/todomvc/architecture-examples/jquery/dist/resources.txt new file mode 100644 index 000000000..326d36af5 --- /dev/null +++ b/resources/todomvc/architecture-examples/jquery/dist/resources.txt @@ -0,0 +1,8 @@ +app.css +app.js +base.css +director.min.js +handlebars.min.js +index.css +index.html +jquery.min.js diff --git a/resources/todomvc/architecture-examples/lit-complex/dist/resources.txt b/resources/todomvc/architecture-examples/lit-complex/dist/resources.txt new file mode 100644 index 000000000..7f235eaf7 --- /dev/null +++ b/resources/todomvc/architecture-examples/lit-complex/dist/resources.txt @@ -0,0 +1,8 @@ +app.css +big-dom-with-stacking-context-scrollable.css +default-variables.css +index.html +index.js +logo.png +todo-item-extra-css.js +todo-list-extra-css.js diff --git a/resources/todomvc/architecture-examples/lit/dist/resources.txt b/resources/todomvc/architecture-examples/lit/dist/resources.txt new file mode 100644 index 000000000..bd88e6e22 --- /dev/null +++ b/resources/todomvc/architecture-examples/lit/dist/resources.txt @@ -0,0 +1,2 @@ +index.html +index.js diff --git a/resources/todomvc/architecture-examples/preact-complex/dist/resources.txt b/resources/todomvc/architecture-examples/preact-complex/dist/resources.txt new file mode 100644 index 000000000..a75067fbb --- /dev/null +++ b/resources/todomvc/architecture-examples/preact-complex/dist/resources.txt @@ -0,0 +1,6 @@ +app.css +app.js +app.js.map +big-dom-with-stacking-context-scrollable.css +index.html +logo.png diff --git a/resources/todomvc/architecture-examples/preact/dist/resources.txt b/resources/todomvc/architecture-examples/preact/dist/resources.txt new file mode 100644 index 000000000..ba4690c7b --- /dev/null +++ b/resources/todomvc/architecture-examples/preact/dist/resources.txt @@ -0,0 +1,4 @@ +app.css +app.js +app.js.map +index.html diff --git a/resources/todomvc/architecture-examples/react-complex/dist/resources.txt b/resources/todomvc/architecture-examples/react-complex/dist/resources.txt new file mode 100644 index 000000000..e22e65792 --- /dev/null +++ b/resources/todomvc/architecture-examples/react-complex/dist/resources.txt @@ -0,0 +1,8 @@ +app.bundle.js +app.bundle.js.LICENSE.txt +app.bundle.js.map +app.css +app.css.map +big-dom.css +index.html +logo.png diff --git a/resources/todomvc/architecture-examples/react-redux-complex/dist/resources.txt b/resources/todomvc/architecture-examples/react-redux-complex/dist/resources.txt new file mode 100644 index 000000000..3a3f68157 --- /dev/null +++ b/resources/todomvc/architecture-examples/react-redux-complex/dist/resources.txt @@ -0,0 +1,8 @@ +app.bundle.js +app.bundle.js.LICENSE.txt +app.bundle.js.map +app.css +app.css.map +big-dom-with-stacking-context-scrollable.css +index.html +logo.png diff --git a/resources/todomvc/architecture-examples/react-redux/dist/resources.txt b/resources/todomvc/architecture-examples/react-redux/dist/resources.txt new file mode 100644 index 000000000..1633b3d99 --- /dev/null +++ b/resources/todomvc/architecture-examples/react-redux/dist/resources.txt @@ -0,0 +1,6 @@ +app.bundle.js +app.bundle.js.LICENSE.txt +app.bundle.js.map +app.css +app.css.map +index.html diff --git a/resources/todomvc/architecture-examples/react/dist/resources.txt b/resources/todomvc/architecture-examples/react/dist/resources.txt new file mode 100644 index 000000000..1633b3d99 --- /dev/null +++ b/resources/todomvc/architecture-examples/react/dist/resources.txt @@ -0,0 +1,6 @@ +app.bundle.js +app.bundle.js.LICENSE.txt +app.bundle.js.map +app.css +app.css.map +index.html diff --git a/resources/todomvc/architecture-examples/svelte-complex/dist/resources.txt b/resources/todomvc/architecture-examples/svelte-complex/dist/resources.txt new file mode 100644 index 000000000..a75067fbb --- /dev/null +++ b/resources/todomvc/architecture-examples/svelte-complex/dist/resources.txt @@ -0,0 +1,6 @@ +app.css +app.js +app.js.map +big-dom-with-stacking-context-scrollable.css +index.html +logo.png diff --git a/resources/todomvc/architecture-examples/svelte/dist/resources.txt b/resources/todomvc/architecture-examples/svelte/dist/resources.txt new file mode 100644 index 000000000..ba4690c7b --- /dev/null +++ b/resources/todomvc/architecture-examples/svelte/dist/resources.txt @@ -0,0 +1,4 @@ +app.css +app.js +app.js.map +index.html diff --git a/resources/todomvc/architecture-examples/vue-complex/dist/resources.txt b/resources/todomvc/architecture-examples/vue-complex/dist/resources.txt new file mode 100644 index 000000000..8a1e91343 --- /dev/null +++ b/resources/todomvc/architecture-examples/vue-complex/dist/resources.txt @@ -0,0 +1,8 @@ +big-dom.css +css/app.319576e1.css +index.html +js/app.ad36df07.js +js/app.ad36df07.js.map +js/chunk-vendors.b4ac9361.js +js/chunk-vendors.b4ac9361.js.map +logo.png diff --git a/resources/todomvc/architecture-examples/vue/dist/resources.txt b/resources/todomvc/architecture-examples/vue/dist/resources.txt new file mode 100644 index 000000000..f71739756 --- /dev/null +++ b/resources/todomvc/architecture-examples/vue/dist/resources.txt @@ -0,0 +1,6 @@ +css/app.319576e1.css +index.html +js/app.ad36df07.js +js/app.ad36df07.js.map +js/chunk-vendors.b4ac9361.js +js/chunk-vendors.b4ac9361.js.map diff --git a/resources/todomvc/big-dom-generator/dist/resources.txt b/resources/todomvc/big-dom-generator/dist/resources.txt new file mode 100644 index 000000000..7cbdfb6a3 --- /dev/null +++ b/resources/todomvc/big-dom-generator/dist/resources.txt @@ -0,0 +1,4 @@ +big-dom-with-stacking-context-scrollable.css +big-dom.css +index.html +logo.png diff --git a/resources/todomvc/todomvc-css/dist/resources.txt b/resources/todomvc/todomvc-css/dist/resources.txt new file mode 100644 index 000000000..d75ef0f53 --- /dev/null +++ b/resources/todomvc/todomvc-css/dist/resources.txt @@ -0,0 +1,31 @@ +app.constructable.js +app.css +app.module.css +bottombar.constructable.js +bottombar.css +bottombar.module.css +footer.constructable.js +footer.css +footer.module.css +global.constructable.js +global.css +header.constructable.js +header.css +header.module.css +index.css +index.html +index.js +index.min.css +main.constructable.js +main.css +main.module.css +partials.css +todo-item.constructable.js +todo-item.css +todo-item.module.css +todo-list.constructable.js +todo-list.css +todo-list.module.css +topbar.constructable.js +topbar.css +topbar.module.css diff --git a/resources/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt b/resources/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt new file mode 100644 index 000000000..bbe806d24 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt @@ -0,0 +1,12 @@ +app.js +base.css +big-dom-with-stacking-context-scrollable.css +controller.js +helpers.js +index.css +index.html +logo.png +model.js +store.js +template.js +view.js diff --git a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt new file mode 100644 index 000000000..c4d7a7c4b --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt @@ -0,0 +1,7 @@ +app.bundle.js +app.bundle.js.map +app.css +app.css.map +big-dom.css +index.html +logo.png diff --git a/resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt b/resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt new file mode 100644 index 000000000..bd159144e --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt @@ -0,0 +1,5 @@ +app.bundle.js +app.bundle.js.map +app.css +app.css.map +index.html diff --git a/resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt b/resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt new file mode 100644 index 000000000..589e9482c --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt @@ -0,0 +1,31 @@ +app.css +big-dom.css +components/todo-app/todo-app.component.js +components/todo-app/todo-app.template.js +components/todo-bottombar/todo-bottombar.component.js +components/todo-bottombar/todo-bottombar.template.js +components/todo-item/todo-item.component.js +components/todo-item/todo-item.template.js +components/todo-list/todo-list.component.js +components/todo-list/todo-list.template.js +components/todo-topbar/todo-topbar.component.js +components/todo-topbar/todo-topbar.template.js +default-variables.css +hooks/useDoubleClick.js +hooks/useKeyListener.js +hooks/useRouter.js +index.html +logo.png +styles/app.constructable.js +styles/bottombar.constructable.js +styles/footer.css +styles/global.constructable.js +styles/global.css +styles/header.css +styles/main.constructable.js +styles/todo-item.constructable.js +styles/todo-list.constructable.js +styles/topbar.constructable.js +todo-item-extra-css.js +todo-list-extra-css.js +utils/nanoid.js diff --git a/resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt b/resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt new file mode 100644 index 000000000..d217710b6 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt @@ -0,0 +1,34 @@ +index.html +src/components/todo-app/todo-app.component.js +src/components/todo-app/todo-app.template.js +src/components/todo-bottombar/todo-bottombar.component.js +src/components/todo-bottombar/todo-bottombar.template.js +src/components/todo-item/todo-item.component.js +src/components/todo-item/todo-item.template.js +src/components/todo-list/todo-list.component.js +src/components/todo-list/todo-list.template.js +src/components/todo-topbar/todo-topbar.component.js +src/components/todo-topbar/todo-topbar.template.js +src/hooks/useDoubleClick.js +src/hooks/useKeyListener.js +src/hooks/useRouter.js +src/index.mjs +src/speedometer-utils/benchmark.mjs +src/speedometer-utils/helpers.mjs +src/speedometer-utils/params.mjs +src/speedometer-utils/test-invoker.mjs +src/speedometer-utils/test-runner.mjs +src/speedometer-utils/todomvc-utils.mjs +src/speedometer-utils/translations.mjs +src/utils/nanoid.js +src/workload-test.mjs +styles/app.constructable.js +styles/bottombar.constructable.js +styles/footer.css +styles/global.constructable.js +styles/global.css +styles/header.css +styles/main.constructable.js +styles/todo-item.constructable.js +styles/todo-list.constructable.js +styles/topbar.constructable.js diff --git a/sw.mjs b/sw.mjs index 470efbf73..ec84afe4c 100644 --- a/sw.mjs +++ b/sw.mjs @@ -1,4 +1,6 @@ const CACHE_NAME = "speedometer-cache-v4.0"; +const DB_NAME = "SpeedometerStateDB"; +const STORE_NAME = "stateStore"; const BENCHMARK_STATE = { IDLE: "IDLE", @@ -7,9 +9,66 @@ const BENCHMARK_STATE = { import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; -let currentState = BENCHMARK_STATE.IDLE; -let cachedUrls = new Set(); -let cachedSuitesPrefixes = new Set(); +let currentState = null; +let cachedSuitesPrefixes = null; + +function openDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1); + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +async function ensureState() { + if (currentState !== null) return; + try { + const db = await openDB(); + const data = await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readonly"); + const req = tx.objectStore(STORE_NAME).get('stateData'); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + if (data) { + currentState = data.state; + cachedSuitesPrefixes = new Set(data.prefixes); + } else { + currentState = BENCHMARK_STATE.IDLE; + cachedSuitesPrefixes = new Set(); + } + } catch (e) { + console.warn("IndexedDB read failed, falling back to IDLE", e); + currentState = BENCHMARK_STATE.IDLE; + cachedSuitesPrefixes = new Set(); + } +} + +async function saveState(state, prefixes) { + currentState = state; + if (prefixes) cachedSuitesPrefixes = new Set(prefixes); + + try { + const db = await openDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readwrite"); + const req = tx.objectStore(STORE_NAME).put({ + state: currentState, + prefixes: Array.from(cachedSuitesPrefixes || []) + }, 'stateData'); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch (e) { + console.warn("IndexedDB write failed", e); + } +} function replyToClient(event, msg) { event.ports[0].postMessage(msg); @@ -19,45 +78,71 @@ function delayAsync(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function handlePrecache(event, { suites = [], delay = 0, clearCache = true }) { +/** + * Limits concurrent promise executions. + * Returns a function that queues tasks and runs at most `limit` simultaneously. + */ +function createConcurrencyLimiter(limit) { + let active = 0; + const queue = []; + const next = () => { + if (active >= limit || queue.length === 0) return; + + active++; + const task = queue.shift(); + task().finally(() => { + active--; + next(); + }); + }; + return (fn) => new Promise((resolve, reject) => { + queue.push(() => fn().then(resolve, reject)); + next(); + }); +} + +const MAX_CONCURRENT_REQUESTS = 20; +const requestLimiter = createConcurrencyLimiter(MAX_CONCURRENT_REQUESTS); + +async function handlePreload(event, { suites = [], delay = 0, clearCache = true }) { if (clearCache) { await caches.delete(CACHE_NAME); - cachedSuitesPrefixes.clear(); + await saveState(BENCHMARK_STATE.IDLE, new Set()); } const cache = await caches.open(CACHE_NAME); + await ensureState(); + let loaded = 0; let totalSize = 0; const urlsToCache = []; + for (const suite of suites) { - if (!suite.resources) - continue; + if (!suite.resources) continue; const prefix = new URL(".", suite.url).href; cachedSuitesPrefixes.add(prefix); urlsToCache.push(...await parseSuiteResources(suite)); } + await saveState(currentState, cachedSuitesPrefixes); + const total = urlsToCache.length; const promises = urlsToCache.map(async (item, index) => { const size = await fetchAndCache(cache, item.url, delay * index); totalSize += size; loaded++; - replyToClient(event, { type: SW_MESSAGES.PRECACHE_PROGRESS, loaded, total, url: item.url, suiteName: item.suiteName }); + replyToClient(event, { type: SW_MESSAGES.PRELOAD_PROGRESS, loaded, total, url: item.url, suiteName: item.suiteName }); }); await Promise.all(promises); - for (const item of urlsToCache) - cachedUrls.add(item.url); - - replyToClient(event, { type: SW_MESSAGES.PRECACHE_DONE, totalSize, count: urlsToCache.length }); + replyToClient(event, { type: SW_MESSAGES.PRELOAD_DONE, totalSize, count: urlsToCache.length }); } async function parseSuiteResources(suite) { try { const response = await fetch(suite.resources); - if (!response.ok) - return []; + if (!response.ok) return []; const text = await response.text(); return text .trim() @@ -73,83 +158,82 @@ async function parseSuiteResources(suite) { } async function fetchAndCache(cache, url, delayMs) { - const request = new Request(url, { cache: "no-cache" }); - const existing = await cache.match(request); - if (existing) { - const blob = await existing.blob(); - return blob.size; - } - - if (delayMs) - await delayAsync(delayMs); + if (delayMs) await delayAsync(delayMs); + + return requestLimiter(async () => { + const request = new Request(url, { cache: "no-cache" }); + const existing = await cache.match(request); + if (existing) { + const blob = await existing.blob(); + return blob.size; + } - try { - await cache.add(request); - const cachedResponse = await cache.match(request); - if (!cachedResponse) + try { + await cache.add(request); + const cachedResponse = await cache.match(request); + if (!cachedResponse) return 0; + const blob = await cachedResponse.blob(); + return blob.size; + } catch (e) { + console.warn("Cache failed for", url, e); return 0; - const blob = await cachedResponse.blob(); - return blob.size; - } catch (e) { - console.warn("Cache failed for", url, e); - return 0; - } + } + }); } -self.addEventListener("install", function () { +self.addEventListener("install", () => { self.skipWaiting(); }); -self.addEventListener("activate", function (event) { +self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); -self.addEventListener("message", function (event) { +self.addEventListener("message", (event) => { const { data } = event; - if (!data) - return; - - if (data.type === SW_MESSAGES.SET_STATE) - currentState = data.state; - else if (data.type === SW_MESSAGES.PRECACHE_SUITES) - event.waitUntil(handlePrecache(event, data)); + if (!data) return; + + if (data.type === SW_MESSAGES.SET_STATE) { + event.waitUntil((async () => { + await ensureState(); + if (currentState === BENCHMARK_STATE.RUNNING && data.state === BENCHMARK_STATE.RUNNING) { + if (event.ports[0]) event.ports[0].postMessage({ type: 'ERROR', message: "Another Speedometer instance is already running." }); + return; + } + await saveState(data.state, cachedSuitesPrefixes); + if (event.ports[0]) event.ports[0].postMessage({ type: 'SUCCESS' }); + })()); + } else if (data.type === SW_MESSAGES.PRELOAD_SUITES) { + event.waitUntil((async () => { + await ensureState(); + if (currentState === BENCHMARK_STATE.RUNNING) { + if (event.ports[0]) event.ports[0].postMessage({ type: 'ERROR', message: "Another Speedometer instance is already running." }); + return; + } + await handlePreload(event, data); + })()); + } }); -self.addEventListener("fetch", function (event) { - const urlObj = new URL(event.request.url); - const cleanUrl = urlObj.origin + urlObj.pathname; - const isCached = cachedUrls.has(cleanUrl) || cachedUrls.has(event.request.url); - - if (isCached) { - event.respondWith(handleFetch(event.request)); - return; - } +self.addEventListener("fetch", (event) => { + event.respondWith((async () => { + const cache = await caches.open(CACHE_NAME); + const cachedResponse = await cache.match(event.request, { ignoreSearch: true }); + if (cachedResponse) return cachedResponse; - // We only enforce strict blocking when the benchmark is actively RUNNING - // to allow runner resources to be loaded. - if (currentState !== BENCHMARK_STATE.RUNNING) - return; + await ensureState(); - let isCachedSuite = false; - for (const prefix of cachedSuitesPrefixes) { - if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { - isCachedSuite = true; - break; + if (currentState !== BENCHMARK_STATE.RUNNING) { + return fetch(event.request); } - } - if (isCachedSuite) { - console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); - event.respondWith(Promise.resolve(Response.error())); - return; - } - // Bypass Service Worker for everything else + for (const prefix of cachedSuitesPrefixes) { + if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { + console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); + return Response.error(); + } + } + + return fetch(event.request); + })()); }); - -async function handleFetch(request) { - const cache = await caches.open(CACHE_NAME); - const cachedResponse = await cache.match(request, { ignoreSearch: true }); - if (cachedResponse) - return cachedResponse; - return fetch(request); -} From f66cd92b72aee6038cfb78cd2477383905b8c5e6 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 16 Jun 2026 14:26:39 +0200 Subject: [PATCH 13/69] format --- .../javascript-wc-indexeddb/scripts/build.mjs | 2 - .../todomvc-localstorage/scripts/build.mjs | 2 - resources/main.mjs | 186 ++++++---- resources/shared/sw-messages.mjs | 3 +- .../backbone/scripts/build.mjs | 2 - .../javascript-es5/scripts/build.mjs | 2 - .../scripts/build.mjs | 2 - sw.mjs | 328 +++++++++--------- 8 files changed, 284 insertions(+), 243 deletions(-) diff --git a/experimental/javascript-wc-indexeddb/scripts/build.mjs b/experimental/javascript-wc-indexeddb/scripts/build.mjs index 54b00c881..1345e23c9 100644 --- a/experimental/javascript-wc-indexeddb/scripts/build.mjs +++ b/experimental/javascript-wc-indexeddb/scripts/build.mjs @@ -3,8 +3,6 @@ import { generateResourcesFile } from "../../../resources/shared/generate-resour import fs from "fs/promises"; import { dirname } from "path"; -const __dirname = import.meta.dirname; - /** * createDirectory * diff --git a/experimental/todomvc-localstorage/scripts/build.mjs b/experimental/todomvc-localstorage/scripts/build.mjs index ad6a77de9..b49adb74f 100644 --- a/experimental/todomvc-localstorage/scripts/build.mjs +++ b/experimental/todomvc-localstorage/scripts/build.mjs @@ -2,8 +2,6 @@ import { generateResourcesFile } from "../../../resources/shared/generate-resour import fs from "fs/promises"; import path from "path"; -const __dirname = import.meta.dirname; - const rootDirectory = "./"; const sourceDirectory = "./src"; const targetDirectory = "./dist"; diff --git a/resources/main.mjs b/resources/main.mjs index 760870808..83e902425 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -5,6 +5,8 @@ import { renderMetricView } from "./metric-ui.mjs"; import { defaultParams, params } from "./shared/params.mjs"; import { createDeveloperModeContainer } from "./developer-mode.mjs"; +const PRELOAD_TIMEOUT_MS = 60_000; + export class PreloadServiceWorker { constructor() { this.registration = null; @@ -12,9 +14,7 @@ export class PreloadServiceWorker { } async setup() { - const existingRegistrations = await navigator.serviceWorker.getRegistrations(); - for (const existing of existingRegistrations) - await existing.unregister(); + await this._unregisterOldServiceWorkers(); if (!params.preload) { this.registration = null; @@ -22,12 +22,78 @@ export class PreloadServiceWorker { return false; } + await this._registerServiceWorker(); + this._setupMessageListener(); + + return true; + } + + async _unregisterOldServiceWorkers() { + const existingRegistrations = await navigator.serviceWorker.getRegistrations(); + for (const existing of existingRegistrations) + await existing.unregister(); + } + + async _registerServiceWorker() { this.registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); await this.registration.update(); await navigator.serviceWorker.ready; - this.sw = navigator.serviceWorker.controller || this.registration.active; - return true; + } + + _setupMessageListener() { + navigator.serviceWorker.addEventListener("message", (event) => { + if (event.data?.type === SW_MESSAGES.FATAL_ERROR) + globalThis.benchmarkClient?.handleError(new Error(event.data.message)); + }); + } + + _sendMessageWithReply(messageData, onProgress, timeoutMs = 0) { + if (!this.sw) + return Promise.resolve(); + + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + const port = channel.port1; + let timeoutId = null; + + const cleanup = () => { + port.onmessage = null; + if (timeoutId) + clearTimeout(timeoutId); + }; + + if (timeoutMs > 0) { + timeoutId = setTimeout(() => { + cleanup(); + console.error(`Service worker message timed out: ${messageData.type}`); + resolve(); // Resolve to not block execution + }, timeoutMs); + } + + port.onmessage = (event) => { + const data = event.data || {}; + if (data.type === SW_MESSAGES.PRELOAD_PROGRESS) { + onProgress?.(data); + return; + } + cleanup(); + if (data.type === "ERROR") { + reject(new Error(data.message || "Unknown SW Error")); + return; + } + + resolve(data); + }; + + this.sw.postMessage(messageData, [channel.port2]); + }); + } + + async clearSw() { + if (!this.sw) + return; + await this._sendMessageWithReply({ type: SW_MESSAGES.CLEAR_CACHE }); } async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { @@ -46,60 +112,14 @@ export class PreloadServiceWorker { return; const startTime = performance.now(); - await new Promise((resolve) => { - const channel = new MessageChannel(); - let timeoutId = setTimeout(() => { - channel.port1.onmessage = null; - console.error("Service worker preloading timed out"); - resolve(); - }, 30000); - const port = channel.port1; - port.onmessage = (event) => { - if (event.data?.type === SW_MESSAGES.PRELOAD_DONE) { - port.onmessage = null; - if (timeoutId) - clearTimeout(timeoutId); - const timeTakenMs = performance.now() - startTime; - const { totalSize, count } = event.data; - const sizeMB = (totalSize / (1024 * 1024)).toFixed(2); - const timeSec = (timeTakenMs / 1000).toFixed(2); - console.log(`Preloaded ${count} files (${sizeMB} MB) in ${timeSec}s`); - resolve(); - } else if (event.data?.type === SW_MESSAGES.PRELOAD_PROGRESS) { - if (onProgress) - onProgress(event.data); - } else if (event.data?.type === 'ERROR') { - port.onmessage = null; - if (timeoutId) - clearTimeout(timeoutId); - reject(new Error(event.data.message)); - } - }; - this.sw.postMessage( - { - type: SW_MESSAGES.PRELOAD_SUITES, - suites: suitesData, - delay: resourceLoadDelay, - clearCache: clearCache, - }, - [channel.port2] - ); - }); - } + const response = await this._sendMessageWithReply({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, onProgress, PRELOAD_TIMEOUT_MS); - async setState(state) { - if (!this.sw) return; - return new Promise((resolve, reject) => { - const channel = new MessageChannel(); - channel.port1.onmessage = (event) => { - if (event.data?.type === 'SUCCESS') { - resolve(); - } else if (event.data?.type === 'ERROR') { - reject(new Error(event.data.message)); - } - }; - this.sw.postMessage({ type: SW_MESSAGES.SET_STATE, state }, [channel.port2]); - }); + if (response?.type === SW_MESSAGES.PRELOAD_DONE) { + const timeTakenMs = performance.now() - startTime; + const sizeMB = (response.totalSize / (1024 * 1024)).toFixed(2); + const timeSec = (timeTakenMs / 1000).toFixed(2); + console.log(`Preloaded ${response.count} files (${sizeMB} MB) in ${timeSec}s`); + } } } @@ -123,7 +143,7 @@ class MainBenchmarkClient { _isRunning = false; _hasResults = false; _metrics = Object.create(null); - preloadServiceWorker = new PreloadServiceWorker(); + _preloadServiceWorker = new PreloadServiceWorker(); _steppingPromise = null; _steppingResolver = null; _benchmarkConfiguratorPromise = null; @@ -181,9 +201,9 @@ class MainBenchmarkClient { const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; - await this.preloadServiceWorker.setup(); + await this._preloadServiceWorker.setup(); - if (!params.isDefault()) + if (!this._didInitialPreload) await this._cacheResources(benchmarkConfigurator); try { @@ -288,7 +308,7 @@ class MainBenchmarkClient { this._hasResults = true; this._metrics = Object.create(null); this._setBenchmarkState(BENCHMARK_STATE.ERROR); - this._populateInvalidScore(); + this._populateInvalidScore(error.message); this.showResultsSummary(); throw error; } @@ -302,10 +322,20 @@ class MainBenchmarkClient { document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`; } - _populateInvalidScore() { + _populateInvalidScore(errorText) { document.getElementById("summary").className = "invalid"; document.getElementById("result-number").textContent = "Error"; document.getElementById("confidence-number").textContent = ""; + + const invalidScoreText = document.getElementById("invalid-score-text"); + if (errorText) { + invalidScoreText.textContent = errorText; + } else { + invalidScoreText.innerHTML = ` + One or more subtests produced no duration.
+ Please check your browser settings and re-run the benchmark.
+ `; + } } _computeResults(measuredValuesList, displayUnit) { @@ -410,15 +440,15 @@ class MainBenchmarkClient { document.getElementById("metrics-results").innerHTML = html; const filePrefix = `speedometer-3-${new Date().toISOString()}`; - let jsonData = this._formattedJSONResult({ modern: false }); - let jsonLink = document.getElementById("download-classic-json"); - jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" })); - jsonLink.setAttribute("download", `${filePrefix}.json`); + const classicJsonData = this._formattedJSONResult({ modern: false }); + const classicJsonLink = document.getElementById("download-classic-json"); + classicJsonLink.href = URL.createObjectURL(new Blob([classicJsonData], { type: "application/json" })); + classicJsonLink.setAttribute("download", `${filePrefix}.json`); - jsonLink = document.getElementById("download-full-json"); - jsonData = this._formattedJSONResult({ modern: true }); - jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" })); - jsonLink.setAttribute("download", `${filePrefix}.json`); + const fullJsonLink = document.getElementById("download-full-json"); + const fullJsonData = this._formattedJSONResult({ modern: true }); + fullJsonLink.href = URL.createObjectURL(new Blob([fullJsonData], { type: "application/json" })); + fullJsonLink.setAttribute("download", `${filePrefix}.json`); const csvData = this._formattedCSVResult(); const csvLink = document.getElementById("download-csv"); @@ -487,7 +517,7 @@ class MainBenchmarkClient { } async _setupServiceWorker(benchmarkConfigurator) { - await this.preloadServiceWorker.setup(); + await this._preloadServiceWorker.setup(); await this._cacheResources(benchmarkConfigurator); } @@ -497,13 +527,15 @@ class MainBenchmarkClient { this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); try { - await this.preloadServiceWorker.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); + await this._preloadServiceWorker.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); this._didInitialPreload = true; this._enableStartButtons(); } catch (error) { console.error("Service Worker preload failed:", error); this._setBenchmarkState(BENCHMARK_STATE.ERROR); - this._enableStartButtons(); + this._populateInvalidScore(error?.message); + this.showResultsSummary(); + throw error; } } @@ -528,8 +560,6 @@ class MainBenchmarkClient { async _setBenchmarkState(state) { document.body.setAttribute("data-benchmark-state", state); - if (this.preloadServiceWorker) - await this.preloadServiceWorker.setState(state); const startButton = document.querySelector(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { @@ -538,9 +568,11 @@ class MainBenchmarkClient { document.getElementById("preload-info-progress").textContent = ""; document.body.style.setProperty("--preload-progress", "0%"); startButton.innerHTML = "
Preloading
"; - } else if (state === BENCHMARK_STATE.READY || state === BENCHMARK_STATE.IDLE || state === BENCHMARK_STATE.DONE || state === BENCHMARK_STATE.ERROR) { + } else if (state !== BENCHMARK_STATE.RUNNING) { document.body.style.removeProperty("--preload-progress"); startButton.innerHTML = "
Start Test
"; + if (state !== BENCHMARK_STATE.READY) + await this._preloadServiceWorker?.clearSw(); } } diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs index b4536f713..c07c3b7ad 100644 --- a/resources/shared/sw-messages.mjs +++ b/resources/shared/sw-messages.mjs @@ -1,6 +1,7 @@ export const SW_MESSAGES = Object.freeze({ - SET_STATE: "SET_STATE", PRELOAD_SUITES: "PRELOAD_SUITES", PRELOAD_PROGRESS: "PRELOAD_PROGRESS", PRELOAD_DONE: "PRELOAD_DONE", + FATAL_ERROR: "FATAL_ERROR", + CLEAR_CACHE: "CLEAR_CACHE", }); diff --git a/resources/todomvc/architecture-examples/backbone/scripts/build.mjs b/resources/todomvc/architecture-examples/backbone/scripts/build.mjs index 9d43978b1..402cecfac 100644 --- a/resources/todomvc/architecture-examples/backbone/scripts/build.mjs +++ b/resources/todomvc/architecture-examples/backbone/scripts/build.mjs @@ -2,8 +2,6 @@ import { generateResourcesFile } from "../../../../shared/generate-resources.mjs import fs from "fs/promises"; import path from "path"; -const __dirname = import.meta.dirname; - const rootDirectory = "./"; const sourceDirectory = "./src"; const targetDirectory = "./dist"; diff --git a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs index 13dba887d..9c354a838 100644 --- a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs +++ b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs @@ -2,8 +2,6 @@ import { generateResourcesFile } from "../../../../shared/generate-resources.mjs import fs from "fs/promises"; import path from "path"; -const __dirname = import.meta.dirname; - const rootDirectory = "./"; const sourceDirectory = "./src"; const targetDirectory = "./dist"; diff --git a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs index b768a19d0..09865953b 100644 --- a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs +++ b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs @@ -3,8 +3,6 @@ import { generateResourcesFile } from "../../../../shared/generate-resources.mjs import fs from "fs/promises"; import { dirname } from "path"; -const __dirname = import.meta.dirname; - /** * createDirectory * diff --git a/sw.mjs b/sw.mjs index ec84afe4c..febca5dff 100644 --- a/sw.mjs +++ b/sw.mjs @@ -1,93 +1,112 @@ +import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; + const CACHE_NAME = "speedometer-cache-v4.0"; const DB_NAME = "SpeedometerStateDB"; -const STORE_NAME = "stateStore"; - -const BENCHMARK_STATE = { - IDLE: "IDLE", - RUNNING: "RUNNING", -}; +const STORE_NAME = "SpeedometerSWState"; -import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; +class LockStore { + constructor() { + this.activeClientId = null; + this.dbPromise = null; + } -let currentState = null; -let cachedSuitesPrefixes = null; - -function openDB() { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, 1); - request.onupgradeneeded = (event) => { - const db = event.target.result; - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME); - } - }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); -} + async _openDB() { + if (this.dbPromise) + return this.dbPromise; -async function ensureState() { - if (currentState !== null) return; - try { - const db = await openDB(); - const data = await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readonly"); - const req = tx.objectStore(STORE_NAME).get('stateData'); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); + this.dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1); + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) + db.createObjectStore(STORE_NAME); + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => { + this.dbPromise = null; + reject(request.error); + }; }); - if (data) { - currentState = data.state; - cachedSuitesPrefixes = new Set(data.prefixes); - } else { - currentState = BENCHMARK_STATE.IDLE; - cachedSuitesPrefixes = new Set(); + return this.dbPromise; + } + + async getOwner() { + if (this.activeClientId !== null) + return this.activeClientId; + try { + const db = await this._openDB(); + const data = await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readonly"); + const req = tx.objectStore(STORE_NAME).get("stateData"); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + this.activeClientId = data?.clientId || null; + } catch (e) { + console.warn("IndexedDB read failed", e); + this.activeClientId = null; } - } catch (e) { - console.warn("IndexedDB read failed, falling back to IDLE", e); - currentState = BENCHMARK_STATE.IDLE; - cachedSuitesPrefixes = new Set(); + return this.activeClientId; } -} -async function saveState(state, prefixes) { - currentState = state; - if (prefixes) cachedSuitesPrefixes = new Set(prefixes); - - try { - const db = await openDB(); - await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readwrite"); - const req = tx.objectStore(STORE_NAME).put({ - state: currentState, - prefixes: Array.from(cachedSuitesPrefixes || []) - }, 'stateData'); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); - } catch (e) { - console.warn("IndexedDB write failed", e); + async setOwner(clientId) { + this.activeClientId = clientId; + try { + const db = await this._openDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readwrite"); + const req = tx.objectStore(STORE_NAME).put({ clientId }, "stateData"); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch (e) { + console.warn("IndexedDB write failed", e); + } + } + + async clear() { + this.activeClientId = null; + try { + const db = await this._openDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readwrite"); + const req = tx.objectStore(STORE_NAME).delete("stateData"); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch (e) { + console.warn("IndexedDB clear failed", e); + } } + + hasLock(clientId) { + if (!this.activeClientId) + return true; + return this.activeClientId === clientId; + } +} + +const STORE = new LockStore(); + +function replyToClient(event, type, msg = {}) { + event.ports?.[0]?.postMessage({ type, ...msg }); } -function replyToClient(event, msg) { - event.ports[0].postMessage(msg); +function replyError(event, message) { + replyToClient(event, "ERROR", { message }); } function delayAsync(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -/** - * Limits concurrent promise executions. - * Returns a function that queues tasks and runs at most `limit` simultaneously. - */ -function createConcurrencyLimiter(limit) { +const MAX_CONCURRENT_REQUESTS = 20; +function createConcurrencyLimiter(limit = MAX_CONCURRENT_REQUESTS) { let active = 0; const queue = []; const next = () => { - if (active >= limit || queue.length === 0) return; - + if (active >= limit || queue.length === 0) + return; active++; const task = queue.shift(); task().finally(() => { @@ -95,85 +114,79 @@ function createConcurrencyLimiter(limit) { next(); }); }; - return (fn) => new Promise((resolve, reject) => { - queue.push(() => fn().then(resolve, reject)); - next(); - }); + return (fn) => + new Promise((resolve, reject) => { + queue.push(() => fn().then(resolve, reject)); + next(); + }); } - -const MAX_CONCURRENT_REQUESTS = 20; const requestLimiter = createConcurrencyLimiter(MAX_CONCURRENT_REQUESTS); -async function handlePreload(event, { suites = [], delay = 0, clearCache = true }) { - if (clearCache) { - await caches.delete(CACHE_NAME); - await saveState(BENCHMARK_STATE.IDLE, new Set()); - } - const cache = await caches.open(CACHE_NAME); +async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { + await STORE.getOwner(); + await updateActiveClient(clientId); - await ensureState(); + if (clearCache) + await caches.delete(CACHE_NAME); + const cache = await caches.open(CACHE_NAME); let loaded = 0; let totalSize = 0; const urlsToCache = []; - + for (const suite of suites) { - if (!suite.resources) continue; - const prefix = new URL(".", suite.url).href; - cachedSuitesPrefixes.add(prefix); + if (!suite.resources) + continue; urlsToCache.push(...await parseSuiteResources(suite)); } - await saveState(currentState, cachedSuitesPrefixes); - const total = urlsToCache.length; const promises = urlsToCache.map(async (item, index) => { const size = await fetchAndCache(cache, item.url, delay * index); totalSize += size; loaded++; - replyToClient(event, { type: SW_MESSAGES.PRELOAD_PROGRESS, loaded, total, url: item.url, suiteName: item.suiteName }); + replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); }); await Promise.all(promises); - replyToClient(event, { type: SW_MESSAGES.PRELOAD_DONE, totalSize, count: urlsToCache.length }); + if (!STORE.hasLock(clientId)) { + replyError(event, "Speedometer aborted: Another tab took over."); + return; + } + replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { totalSize, count: urlsToCache.length }); } async function parseSuiteResources(suite) { - try { - const response = await fetch(suite.resources); - if (!response.ok) return []; - const text = await response.text(); - return text - .trim() - .split("\n") - .map((resourceUrl) => ({ - url: new URL(resourceUrl.trim(), suite.url).href, - suiteName: suite.name, - })); - } catch (e) { - console.warn("Failed to fetch resources.txt for", suite.name); + const response = await fetch(suite.resources); + if (!response.ok) return []; - } + const text = await response.text(); + return text + .trim() + .split("\n") + .map((resourceUrl) => ({ + url: new URL(resourceUrl.trim(), suite.url).href, + suiteName: suite.name, + })); } async function fetchAndCache(cache, url, delayMs) { - if (delayMs) await delayAsync(delayMs); - + if (delayMs) + await delayAsync(delayMs); return requestLimiter(async () => { const request = new Request(url, { cache: "no-cache" }); const existing = await cache.match(request); - if (existing) { - const blob = await existing.blob(); - return blob.size; - } + if (existing) + return (await existing.blob()).size; try { - await cache.add(request); - const cachedResponse = await cache.match(request); - if (!cachedResponse) return 0; - const blob = await cachedResponse.blob(); - return blob.size; + const response = await fetch(request); + if (!response.ok) + return 0; + const size = (await response.clone().blob()).size; + await cache.put(request, response); + return size; } catch (e) { console.warn("Cache failed for", url, e); return 0; @@ -189,51 +202,56 @@ self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); +async function updateActiveClient(newClientId) { + const oldClientId = STORE.activeClientId; + if (oldClientId === newClientId) + return false; + + await STORE.setOwner(newClientId); + + if (oldClientId) { + const oldClient = await self.clients.get(oldClientId); + oldClient?.postMessage({ type: SW_MESSAGES.FATAL_ERROR, message: "Speedometer aborted: Another tab took over." }); + } + return true; +} + +async function handleClearCacheMessage(event, clientId) { + await STORE.getOwner(); + if (!STORE.hasLock(clientId)) { + replyError(event, "Cannot clear SW: You do not own the lock."); + return; + } + await STORE.clear(); + replyToClient(event, "SUCCESS"); +} + +const MESSAGE_HANDLERS = { + __proto__: null, + [SW_MESSAGES.PRELOAD_SUITES]: handlePreloadSuitesMessage, + [SW_MESSAGES.CLEAR_CACHE]: handleClearCacheMessage, +}; + self.addEventListener("message", (event) => { const { data } = event; - if (!data) return; - - if (data.type === SW_MESSAGES.SET_STATE) { - event.waitUntil((async () => { - await ensureState(); - if (currentState === BENCHMARK_STATE.RUNNING && data.state === BENCHMARK_STATE.RUNNING) { - if (event.ports[0]) event.ports[0].postMessage({ type: 'ERROR', message: "Another Speedometer instance is already running." }); - return; - } - await saveState(data.state, cachedSuitesPrefixes); - if (event.ports[0]) event.ports[0].postMessage({ type: 'SUCCESS' }); - })()); - } else if (data.type === SW_MESSAGES.PRELOAD_SUITES) { - event.waitUntil((async () => { - await ensureState(); - if (currentState === BENCHMARK_STATE.RUNNING) { - if (event.ports[0]) event.ports[0].postMessage({ type: 'ERROR', message: "Another Speedometer instance is already running." }); - return; - } - await handlePreload(event, data); - })()); - } + if (!data) + return; + const handler = MESSAGE_HANDLERS[data.type]; + if (handler) + event.waitUntil(handler(event, event.source?.id, data)); + else + console.error("Unknown service worker message type:", data.type); }); self.addEventListener("fetch", (event) => { - event.respondWith((async () => { - const cache = await caches.open(CACHE_NAME); - const cachedResponse = await cache.match(event.request, { ignoreSearch: true }); - if (cachedResponse) return cachedResponse; - - await ensureState(); + event.respondWith( + (async () => { + const cache = await caches.open(CACHE_NAME); + const cachedResponse = await cache.match(event.request, { ignoreSearch: true }); + if (cachedResponse) + return cachedResponse; - if (currentState !== BENCHMARK_STATE.RUNNING) { return fetch(event.request); - } - - for (const prefix of cachedSuitesPrefixes) { - if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { - console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); - return Response.error(); - } - } - - return fetch(event.request); - })()); + })() + ); }); From 98ae1b897c29e0790ee3dedfdbd143bae7da84d3 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 16 Jun 2026 14:30:37 +0200 Subject: [PATCH 14/69] add-test --- tests/run-end2end.mjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/run-end2end.mjs b/tests/run-end2end.mjs index 067359694..4c55c811e 100644 --- a/tests/run-end2end.mjs +++ b/tests/run-end2end.mjs @@ -97,6 +97,18 @@ async function testAll() { assert(metrics.Score.values.length === 1); } +async function testPreloadEnabled() { + const metrics = await testPage("index.html?iterationCount=1&suites=Perf-Dashboard&preload=true"); + assert("Perf-Dashboard" in metrics); + assert(metrics["Perf-Dashboard"].values.length === 1); +} + +async function testPreloadDisabled() { + const metrics = await testPage("index.html?iterationCount=1&suites=Perf-Dashboard&preload=false"); + assert("Perf-Dashboard" in metrics); + assert(metrics["Perf-Dashboard"].values.length === 1); +} + async function testDeveloperMode() { const params = ["developerMode", "iterationCount=1", "warmupBeforeSync=2", "waitBeforeSync=2", "shuffleSeed=123", "suites=Perf-Dashboard"]; const metrics = await testPage(`index.html?${params.join("&")}`); @@ -115,6 +127,8 @@ async function test() { await driver.manage().setTimeouts({ script: 60000 }); await testIterations(); await testAll(); + await testPreloadEnabled(); + await testPreloadDisabled(); await testDeveloperMode(); console.log("\nTests complete!"); } catch (e) { From aea421a814d6b7e6c91d44094dcedaa31961585b Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 13:58:58 +0200 Subject: [PATCH 15/69] fix-preload-sync --- resources/main.mjs | 68 ++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index b09f36b02..d530ecd2e 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -7,18 +7,23 @@ import { createDeveloperModeContainer } from "./developer-mode.mjs"; const PRELOAD_TIMEOUT_MS = 60_000; -export class PreloadServiceWorker { +export class ResourcePreloader { constructor() { - this.registration = null; - this.sw = null; + this._registration = null; + this._sw = null; + this._preloadParams = ""; + } + + isCached() { + return params.toSearchParams() === this._preloadParams; } async setup() { await this._unregisterOldServiceWorkers(); if (!params.preload) { - this.registration = null; - this.sw = null; + this._registration = null; + this._sw = null; return false; } @@ -35,10 +40,10 @@ export class PreloadServiceWorker { } async _registerServiceWorker() { - this.registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); - await this.registration.update(); + this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); + await this._registration.update(); await navigator.serviceWorker.ready; - this.sw = navigator.serviceWorker.controller || this.registration.active; + this._sw = navigator.serviceWorker.controller || this._registration.active; } _setupMessageListener() { @@ -49,7 +54,7 @@ export class PreloadServiceWorker { } _sendMessageWithReply(messageData, onProgress, timeoutMs = 0) { - if (!this.sw) + if (!this._sw) return Promise.resolve(); return new Promise((resolve, reject) => { @@ -86,18 +91,18 @@ export class PreloadServiceWorker { resolve(data); }; - this.sw.postMessage(messageData, [channel.port2]); + this._sw.postMessage(messageData, [channel.port2]); }); } async clearSw() { - if (!this.sw) + if (!this._sw) return; await this._sendMessageWithReply({ type: SW_MESSAGES.CLEAR_CACHE }); } async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { - if (!this.sw || suites.length === 0) + if (!this._sw || suites.length === 0) return; const suitesData = suites @@ -120,6 +125,7 @@ export class PreloadServiceWorker { const timeSec = (timeTakenMs / 1000).toFixed(2); console.log(`Preloaded ${response.count} files (${sizeMB} MB) in ${timeSec}s`); } + this._preloadParams = params.toSearchParams() } } @@ -143,7 +149,7 @@ class MainBenchmarkClient { _isRunning = false; _hasResults = false; _metrics = Object.create(null); - _preloadServiceWorker = new PreloadServiceWorker(); + _resourcePreloader = new ResourcePreloader(); _steppingPromise = null; _steppingResolver = null; _benchmarkConfiguratorPromise = null; @@ -200,18 +206,6 @@ class MainBenchmarkClient { const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; - await this._preloadServiceWorker.setup(); - - if (!this._didInitialPreload) - await this._cacheResources(benchmarkConfigurator); - - try { - await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); - } catch (error) { - alert(error.message); - return false; - } - const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const totalSuitesCount = enabledSuites.length; @@ -228,6 +222,16 @@ class MainBenchmarkClient { } if (!this._isStepping()) this._developerModeContainer?.remove(); + + await this._setupResourcePreloader(benchmarkConfigurator) + + try { + await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); + } catch (error) { + alert(error.message); + return false; + } + this._progressCompleted = document.getElementById("progress-completed"); if (params.iterationCount < 50) { const progressNode = document.getElementById("progress"); @@ -511,15 +515,16 @@ class MainBenchmarkClient { document.body.append(this._developerModeContainer); } - await this._setupServiceWorker(benchmarkConfigurator); + await this._setupResourcePreloader(benchmarkConfigurator); if (params.startAutomatically) this.start(); } - async _setupServiceWorker(benchmarkConfigurator) { - await this._preloadServiceWorker.setup(); - await this._cacheResources(benchmarkConfigurator); + async _setupResourcePreloader(benchmarkConfigurator) { + await this._resourcePreloader.setup(); + if (!this._resourcePreloader.isCached()) + await this._cacheResources(benchmarkConfigurator); } async _cacheResources(benchmarkConfigurator) { @@ -528,8 +533,7 @@ class MainBenchmarkClient { this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); try { - await this._preloadServiceWorker.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); - this._didInitialPreload = true; + await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); this._enableStartButtons(); } catch (error) { console.error("Service Worker preload failed:", error); @@ -573,7 +577,7 @@ class MainBenchmarkClient { document.body.style.removeProperty("--preload-progress"); startButton.innerHTML = "
Start Test
"; if (state !== BENCHMARK_STATE.READY) - await this._preloadServiceWorker?.clearSw(); + await this._resourcePreloader?.clearSw(); } } From f7e71b6114d6e29a55057ce5c04f7015f9983006 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 14:00:06 +0200 Subject: [PATCH 16/69] fx --- sw.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sw.mjs b/sw.mjs index febca5dff..f86d94aa3 100644 --- a/sw.mjs +++ b/sw.mjs @@ -3,6 +3,7 @@ import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; const CACHE_NAME = "speedometer-cache-v4.0"; const DB_NAME = "SpeedometerStateDB"; const STORE_NAME = "SpeedometerSWState"; +const DB_VERSION = 2; class LockStore { constructor() { @@ -15,7 +16,7 @@ class LockStore { return this.dbPromise; this.dbPromise = new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, 1); + const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) From 48937dcc2c46faaaabe43e124228c092b2a5cd5e Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 14:01:28 +0200 Subject: [PATCH 17/69] cleanup --- resources/main.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index d530ecd2e..c53b30172 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -125,7 +125,7 @@ export class ResourcePreloader { const timeSec = (timeTakenMs / 1000).toFixed(2); console.log(`Preloaded ${response.count} files (${sizeMB} MB) in ${timeSec}s`); } - this._preloadParams = params.toSearchParams() + this._preloadParams = params.toSearchParams(); } } @@ -223,7 +223,7 @@ class MainBenchmarkClient { if (!this._isStepping()) this._developerModeContainer?.remove(); - await this._setupResourcePreloader(benchmarkConfigurator) + await this._setupResourcePreloader(benchmarkConfigurator); try { await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); From f62f7ce3a9d6005b4a327cab915ce56f7223b61f Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 14:02:12 +0200 Subject: [PATCH 18/69] revert --- .../scratch/query_wikidata.py | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 resources/.gemini/jetski/brain/254a72ea-4fe5-4adf-8574-46bee9097287/scratch/query_wikidata.py diff --git a/resources/.gemini/jetski/brain/254a72ea-4fe5-4adf-8574-46bee9097287/scratch/query_wikidata.py b/resources/.gemini/jetski/brain/254a72ea-4fe5-4adf-8574-46bee9097287/scratch/query_wikidata.py deleted file mode 100644 index 2c1206345..000000000 --- a/resources/.gemini/jetski/brain/254a72ea-4fe5-4adf-8574-46bee9097287/scratch/query_wikidata.py +++ /dev/null @@ -1,22 +0,0 @@ -import urllib.request -import urllib.parse -import json - -def search_wikidata(query): - url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" + urllib.parse.quote(query) + "&language=en&format=json" - req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) - try: - with urllib.request.urlopen(req) as response: - data = json.loads(response.read().decode('utf-8')) - if not data.get('search'): - print(f"No results for {query}") - return - for result in data['search'][:10]: - entity_id = result['id'] - label = result['label'] - description = result.get('description', '') - print(f"Entity: {entity_id} ({label}) - {description}") - except Exception as e: - print(f"Error querying: {e}") - -search_wikidata("Rising Sun") From 7d8010ee994ae72bcf57702e7fc54388216feb22 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 16:34:12 +0200 Subject: [PATCH 19/69] pre-format --- resources/benchmark-configurator.mjs | 6 ++++-- resources/main.mjs | 4 ++-- suites-experimental/suites.mjs | 4 ++-- sw.mjs | 19 ++++++++++++------- tests/index.html | 1 + tests/unittests/suites.mjs | 18 +++++++++++++----- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/resources/benchmark-configurator.mjs b/resources/benchmark-configurator.mjs index 3ac62a45b..531f5fd03 100644 --- a/resources/benchmark-configurator.mjs +++ b/resources/benchmark-configurator.mjs @@ -117,8 +117,10 @@ export class BenchmarkConfigurator { } _loadSuite(suite) { - suite.tags.forEach((tag) => this.#tags.add(tag)); - this.#suites.push(suite); + if (suite.tags) + suite.tags.forEach((tag) => this.#tags.add(tag)); + + this.#suites.push({ ...suite, tags: suite.tags ? [...suite.tags] : [] }); } enableSuites(names, tags) { diff --git a/resources/main.mjs b/resources/main.mjs index c53b30172..e7393f1d0 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -121,9 +121,9 @@ export class ResourcePreloader { if (response?.type === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; - const sizeMB = (response.totalSize / (1024 * 1024)).toFixed(2); + const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); const timeSec = (timeTakenMs / 1000).toFixed(2); - console.log(`Preloaded ${response.count} files (${sizeMB} MB) in ${timeSec}s`); + console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`); } this._preloadParams = params.toSearchParams(); } diff --git a/suites-experimental/suites.mjs b/suites-experimental/suites.mjs index 98d98aab2..85ac0dbd9 100644 --- a/suites-experimental/suites.mjs +++ b/suites-experimental/suites.mjs @@ -6,8 +6,8 @@ import { freezeSuites } from "../resources/suites-helper.mjs"; export const ExperimentalSuites = freezeSuites([ { name: "TodoMVC-JavaScript-ES5-Cached", - url: "resources/todomvc/vanilla-examples/javascript-es5/dist/index.html", - resources: "resources/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", + url: "suites/todomvc/vanilla-examples/javascript-es5/dist/index.html", + resources: "suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", tags: ["todomvc", "experimental", "cached"], async prepare(page) { (await page.waitForElement(".new-todo")).focus(); diff --git a/sw.mjs b/sw.mjs index f86d94aa3..15505bdc6 100644 --- a/sw.mjs +++ b/sw.mjs @@ -132,7 +132,7 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay const cache = await caches.open(CACHE_NAME); let loaded = 0; - let totalSize = 0; + let transferredSize = 0; const urlsToCache = []; for (const suite of suites) { @@ -144,7 +144,7 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay const total = urlsToCache.length; const promises = urlsToCache.map(async (item, index) => { const size = await fetchAndCache(cache, item.url, delay * index); - totalSize += size; + transferredSize += size; loaded++; replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); }); @@ -155,7 +155,7 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay replyError(event, "Speedometer aborted: Another tab took over."); return; } - replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { totalSize, count: urlsToCache.length }); + replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); } async function parseSuiteResources(suite) { @@ -172,6 +172,11 @@ async function parseSuiteResources(suite) { })); } +function getResponseSize(response) { + const contentLength = response.headers.get("content-length"); + return contentLength ? parseInt(contentLength, 10) : 0; +} + async function fetchAndCache(cache, url, delayMs) { if (delayMs) await delayAsync(delayMs); @@ -179,13 +184,13 @@ async function fetchAndCache(cache, url, delayMs) { const request = new Request(url, { cache: "no-cache" }); const existing = await cache.match(request); if (existing) - return (await existing.blob()).size; + return getResponseSize(existing); try { const response = await fetch(request); if (!response.ok) return 0; - const size = (await response.clone().blob()).size; + const size = getResponseSize(response); await cache.put(request, response); return size; } catch (e) { @@ -227,11 +232,11 @@ async function handleClearCacheMessage(event, clientId) { replyToClient(event, "SUCCESS"); } -const MESSAGE_HANDLERS = { +const MESSAGE_HANDLERS = Object.freeze({ __proto__: null, [SW_MESSAGES.PRELOAD_SUITES]: handlePreloadSuitesMessage, [SW_MESSAGES.CLEAR_CACHE]: handleClearCacheMessage, -}; +}); self.addEventListener("message", (event) => { const { data } = event; diff --git a/tests/index.html b/tests/index.html index a6bc60a3b..821356444 100644 --- a/tests/index.html +++ b/tests/index.html @@ -28,6 +28,7 @@ }); await import("./unittests/benchmark-runner.mjs"); + await import("./unittests/benchmark-configurator.mjs"); await import("./unittests/params.mjs"); await import("./unittests/suites.mjs"); diff --git a/tests/unittests/suites.mjs b/tests/unittests/suites.mjs index 898019702..af7f96700 100644 --- a/tests/unittests/suites.mjs +++ b/tests/unittests/suites.mjs @@ -66,29 +66,37 @@ for (const [name, suites] of Object.entries(Suites)) { }); it("should have resources.txt listing only valid files", async () => { const baseUrl = `${window.location.origin}/`; + const brokenResourcesList = []; for (const suite of suites) { if (!suite.resources) continue; const resourcesUrl = new URL(suite.resources, baseUrl).href; const res = await fetch(resourcesUrl); - expect(res.ok).to.be(true); + if (!res.ok) + throw new Error(`Failed to load resources.txt for ${suite.name} at ${resourcesUrl}`); + const text = await res.text(); - expect(text.trim().length).to.be.greaterThan(0, `resources.txt for ${suite.name} is empty`); + if (text.trim().length === 0) + throw new Error(`resources.txt for ${suite.name} is empty`); const files = text.trim().split("\n"); for (const file of files) - expect(file.trim().length).to.be.greaterThan(0); + expect(file.trim().length).to.be.greaterThan(0, `Found empty line in resources.txt for ${suite.name}`); await Promise.all( files.map(async (file) => { const fileUrl = new URL(file, resourcesUrl).href; const fileRes = await fetch(fileUrl, { method: "HEAD" }); if (!fileRes.ok) - throw new Error(`Failed to load ${fileUrl} (listed in ${resourcesUrl})`); - expect(fileRes.ok).to.be(true); + brokenResourcesList.push(`${fileUrl} (listed in ${resourcesUrl})`); + else + expect(fileRes.ok).to.be(true); }) ); } + if (brokenResourcesList.length > 0) + throw new Error(`Failed to load the following resources:\n${brokenResourcesList.join("\n")}`); + }); }); } From 620b7c84866a26a523a33441d9ad3ee5c5bdf2b6 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 16:34:20 +0200 Subject: [PATCH 20/69] pre-format --- suites/newssite/news-nuxt/dist/resources.txt | 355 ++++++++++++++++++ .../javascript-es5/dist/resources.txt | 10 + tests/unittests/benchmark-configurator.mjs | 42 +++ 3 files changed, 407 insertions(+) create mode 100644 suites/newssite/news-nuxt/dist/resources.txt create mode 100644 suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt create mode 100644 tests/unittests/benchmark-configurator.mjs diff --git a/suites/newssite/news-nuxt/dist/resources.txt b/suites/newssite/news-nuxt/dist/resources.txt new file mode 100644 index 000000000..80cf9f3b0 --- /dev/null +++ b/suites/newssite/news-nuxt/dist/resources.txt @@ -0,0 +1,355 @@ +favicon.ico +index.html +placeholder_light.jpg +404.html +_nuxt/error-500.c1eac2b8.js +_nuxt/entry.61b8a168.js +_nuxt/error-500.aa16ed4d.css +_nuxt/error-component.2cb571d2.js +_nuxt/error-404.2a577bdf.js +_nuxt/error-404.23f2309d.css +_nuxt/entry.4163a698.css +assets/images/mika-baumeister-Hm4zYX-BDxk-unsplash_150.jpg +assets/images/kelly-sikkema-WIYtZU3PxsI-unsplash_448.jpg +assets/images/nathan-cima-TQuq2OtLBNU-unsplash_150.jpg +assets/images/robert-bye-EILw-nEK46k-unsplash_150.jpg +assets/images/jannis-brandt-mmsQUgMLqUo-unsplash_150.jpg +assets/images/colin-lloyd-uaM_Ijy_joY-unsplash_448.jpg +assets/images/mathurin-napoly-matnapo-ejWJ3a92FEs-unsplash_336.jpg +assets/images/shubham-dhage-tT6GNIFkZv4-unsplash_150.jpg +assets/images/jakub-zerdzicki-VnTR3XFwxWs-unsplash_150.jpg +assets/images/mulyadi-JeCNRxGLSp4-unsplash_150.jpg +assets/images/artur-voznenko-rwPIQQPz1ew-unsplash_150.jpg +assets/images/matthew-henry-0Ol8Sa2n21c-unsplash_150.jpg +assets/images/caglar-oskay-d0Be8Vs9XRk-unsplash_336.jpg +assets/images/marc-olivier-jodoin-_eclsGKsUdo-unsplash_448.jpg +assets/images/ilaria-de-bona-RuFfpBsaRY0-unsplash_336.jpg +assets/images/juli-kosolapova-4PE3X9eKsu4-unsplash_336.jpg +assets/images/mike-enerio-7ryPpZK1qV8-unsplash_448.jpg +assets/images/national-cancer-institute-KrsoedfRAf4-unsplash_336.jpg +assets/images/yucel-moran-4ndj0pATzeM-unsplash_150.jpg +assets/images/guillaume-perigois-HL4LEIyGEYU-unsplash_150.jpg +assets/images/anatol-rurac-b5t2lqeCGfA-unsplash_150.jpg +assets/images/ehimetalor-akhere-unuabona-yS0uBoF4xDo-unsplash_448.jpg +assets/images/bernd-dittrich-Xk1IfNnEhRA-unsplash_448.jpg +assets/images/rio-lecatompessy-cfDURuQKABk-unsplash_336.jpg +assets/images/alev-takil-fYyYz38bUkQ-unsplash_150.jpg +assets/images/national-cancer-institute-L7en7Lb-Ovc-unsplash_336.jpg +assets/images/azhar-j-t2hgHV1R7_g-unsplash_448.jpg +assets/images/olga-guryanova-tMFeatBSS4s-unsplash_336.jpg +assets/images/louis-reed-pwcKF7L4-no-unsplash_448.jpg +assets/images/maximilian-bungart-nwqfl_HtJjk-unsplash_150.jpg +assets/images/hannah-busing-0V6DmTuJaIk-unsplash_448.jpg +assets/images/sheelah-brennan-UOfERQF_pr4-unsplash_336.jpg +assets/images/tania-mousinho-YlpfE9uCakE-unsplash_150.jpg +assets/images/brooke-lark-IDTEXXXfS44-unsplash_150.jpg +assets/images/paul-hanaoka-s0XabTAKvak-unsplash_684.jpg +assets/images/quino-al-KydWCDJe9s0-unsplash_150.jpg +assets/images/christina-rumpf-gUU4MF87Ipw-unsplash_150.jpg +assets/images/sean-lee-hDqRQmcjM3s-unsplash_336.jpg +assets/images/albert-stoynov-fEdf0fig3os-unsplash_448.jpg +assets/images/campaign-creators-pypeCEaJeZY-unsplash_448.jpg +assets/images/kelli-dougal-vbiQ_7vwfrs-unsplash_448.jpg +assets/images/mathias-reding-yfXhqAW5X0c-unsplash_150.jpg +assets/images/valtteri-laukkanen-9u9Pc0t9vKM-unsplash_150.jpg +assets/images/noaa--urO88VoCRE-unsplash_336.jpg +assets/images/stefan-stefancik-pzA7QWNCIYg-unsplash_150.jpg +assets/images/sabri-tuzcu-kxR3hh0IRHU-unsplash_336.jpg +assets/images/nasa-cIX5TlQ_FgM-unsplash_448.jpg +assets/images/nonresident-nizUHtSIrKM-unsplash_150.jpg +assets/images/glenn-carstens-peters-npxXWgQ33ZQ-unsplash_336.jpg +assets/images/noaa-I323ZqSkkn8-unsplash_448.jpg +assets/images/melissa-askew-8n00CqwnqO8-unsplash_448.jpg +assets/images/dyana-wing-so-Og16Foo-pd8-unsplash_150.jpg +assets/images/crystal-kwok-xD5SWy7hMbw-unsplash_448.jpg +assets/images/sven-mieke-Lx_GDv7VA9M-unsplash_150.jpg +assets/images/national-cancer-institute-SMxzEaidR20-unsplash_336.jpg +assets/images/maddi-bazzocco-qKbHvzXb85A-unsplash_150.jpg +assets/images/junko-nakase-Q-72wa9-7Dg-unsplash_336.jpg +assets/images/gregory-hayes-h5cd51KXmRQ-unsplash_684.jpg +assets/images/clark-street-mercantile-P3pI6xzovu0-unsplash_336.jpg +assets/images/milad-fakurian-58Z17lnVS4U-unsplash_684.jpg +assets/images/2h-media-lPcQhLP-b4I-unsplash_150.jpg +assets/images/jaron-nix-7wWRXewYCH4-unsplash_684.jpg +assets/images/alexandre-debieve-FO7JIlwjOtU-unsplash_336.jpg +assets/images/jay-clark-P3sLerH3UmM-unsplash_336.jpg +assets/images/national-cancer-institute-GcrSgHDrniY-unsplash_336.jpg +assets/images/axp-photography-v6pAkO31d50-unsplash_448.jpg +assets/images/maja-kochanowska-EiJQdDI_t_Y-unsplash_150.jpg +assets/images/pierre-bamin-lM4_Nmcj4Xk-unsplash_150.jpg +assets/images/koshu-kunii-v9ferChkC9A-unsplash_150.jpg +assets/images/clay-banks-BY-R0UNRE7w-unsplash_150.jpg +assets/images/annie-spratt-IT6aov1ScW0-unsplash_150.jpg +assets/images/mesut-kaya-eOcyhe5-9sQ-unsplash_336.jpg +assets/images/tierra-mallorca-rgJ1J8SDEAY-unsplash_150.jpg +assets/images/tatjana-petkevica-iad-dMBDdoo-unsplash_150.jpg +assets/images/luis-cortes-QrPDA15pRkM-unsplash_336.jpg +assets/images/martha-dominguez-de-gouveia-k-NnVZ-z26w-unsplash_150.jpg +assets/images/lukasz-radziejewski-cg4MzL_eSvU-unsplash_336.jpg +assets/images/koshu-kunii-ADLj1cyFfV8-unsplash_150.jpg +assets/images/erik-mclean-ByjIzFupcHo-unsplash_336.jpg +assets/images/maksym-harbar-okn8ZIjPMxI-unsplash_448.jpg +assets/images/razvan-chisu-Ua-agENjmI4-unsplash_448.jpg +assets/images/markus-spiske-p2Xor4Lbrrk-unsplash_150.jpg +assets/images/stan-georgiev-pvNxRUq7O7U-unsplash_150.jpg +assets/images/kevin-wang-t7vEVxwGGm0-unsplash_336.jpg +assets/images/harley-davidson-fFbUdx80oCc-unsplash_336.jpg +assets/images/xan-griffin-QxNkzEjB180-unsplash_150.jpg +assets/images/jon-tyson-ne2mqMgER8Y-unsplash_150.jpg +assets/images/maksim-larin-tecILYzVAzg-unsplash_336.jpg +assets/images/linkedin-sales-solutions--AXDunSs-n4-unsplash_150.jpg +assets/images/kitthitorn-chaiyuthapoom-TOH_gw5dd20-unsplash_336.jpg +assets/images/patti-black-FnV-PjAYHCI-unsplash_150.jpg +assets/images/red-dot-Q98X_JVRGS0-unsplash_336.jpg +assets/images/mathurin-napoly-matnapo-pIJ34ZrZEEw-unsplash_150.jpg +assets/images/martin-podsiad-wrdtA9lew9E-unsplash_150.jpg +assets/images/martin-jernberg-jVNWCFwdjZU-unsplash_448.jpg +assets/images/bruce-mars-xj8qrWvuOEs-unsplash_336.jpg +assets/images/cardmapr-nl-s8F8yglbpjo-unsplash_336.jpg +assets/images/mufid-majnun-tJJIGh703I4-unsplash_336.jpg +assets/images/vladislav-babienko-KTpSVEcU0XU-unsplash_448.jpg +assets/images/matt-popovich-7mqsZsE6FaU-unsplash_336.jpg +assets/images/mac-blades-jpgJSBQtw5U-unsplash_336.jpg +assets/images/carl-nenzen-loven-c-pc2mP7hTs-unsplash_448.jpg +assets/images/arnel-hasanovic-MNd-Rka1o0Q-unsplash_336.jpg +assets/images/dani-navarro-6CnGzrLwM28-unsplash_336.jpg +assets/images/pat-taylor-12V36G17IbQ-unsplash_336.jpg +assets/images/jonathan-simcoe-S9J1HqoL9ns-unsplash_150.jpg +assets/images/leah-hetteberg-kTVN2l0ZUv8-unsplash_684.jpg +assets/images/viktor-talashuk-05HLFQu8bFw-unsplash_150.jpg +assets/images/kyle-glenn-gcw_WWu_uBQ-unsplash_336.jpg +assets/images/will-shirley-xRKcHoCOA4Y-unsplash_448.jpg +assets/images/k8-uYf_C34PAao-unsplash_336.jpg +assets/images/david-vives-Nzbkev7SQTg-unsplash_448.jpg +assets/images/anne-nygard-tcJ6sJTtTWI-unsplash_448.jpg +assets/images/scott-webb-U5kQvbQWoG0-unsplash_150.jpg +assets/images/m-ZzOa5G8hSPI-unsplash_150.jpg +assets/images/adam-whitlock-I9j8Rk-JYFM-unsplash_684.jpg +assets/images/alan-rodriguez-qrD-g7oc9is-unsplash_684.jpg +assets/images/braden-collum-9HI8UJMSdZA-unsplash_336.jpg +assets/images/hansjorg-keller-CQqyv5uldW4-unsplash_448.jpg +assets/images/denys-nevozhai-7nrsVjvALnA-unsplash_150.jpg +assets/images/samuel-schroth-hyPt63Df3Dw-unsplash_150.jpg +assets/images/prince-abban-0OUHhvNIbYc-unsplash_150.jpg +assets/images/chloe-taranto-x2zyAOmVNtM-unsplash_448.jpg +assets/images/venti-views-KElJx4R4Py8-unsplash_684.jpg +assets/images/peter-thomas-17EJD0QdKFI-unsplash_150.jpg +assets/images/hush-naidoo-jade-photography-Zp7ebyti3MU-unsplash_336.jpg +assets/images/pascal-bullan-M8sQPAfhPdk-unsplash_336.jpg +assets/images/leisara-studio-EzzW1oNek-I-unsplash_336.jpg +assets/images/arnaud-jaegers-IBWJsMObnnU-unsplash_336.jpg +assets/images/aaron-huber-G7sE2S4Lab4-unsplash_336.jpg +assets/images/todd-trapani-vS54KomBEJU-unsplash_684.jpg +assets/images/ahmet-kurt-WviyUzOg4RU-unsplash_150.jpg +assets/images/chris-leboutillier-TUJud0AWAPI-unsplash_150.jpg +assets/images/clay-banks-cisdc-344vo-unsplash_150.jpg +assets/images/marek-studzinski-9U9I-eVx9nI-unsplash_150.jpg +assets/images/miguel-bruna-TzVN0xQhWaQ-unsplash_150.jpg +assets/images/tamanna-rumee-lpGm415q9JA-unsplash_336.jpg +assets/images/redd-f-N9CYH-H_gBE-unsplash_684.jpg +assets/images/emmanuel-ikwuegbu-ceawFbpA-14-unsplash_448.jpg +assets/images/priscilla-du-preez-GgtxccOjIXE-unsplash_448.jpg +assets/images/austin-distel-97HfVpyNR1M-unsplash_448.jpg +assets/images/craig-melville-_JKymnZ1Uc4-unsplash_150.jpg +assets/images/truckrun-XBWF6_TEsFM-unsplash_684.jpg +assets/images/hush-naidoo-jade-photography-ZCO_5Y29s8k-unsplash_150.jpg +assets/images/allan-wadsworth-Lp78NT-mf9o-unsplash_150.jpg +assets/images/nathan-dumlao-bRdRUUtbxO0-unsplash_150.jpg +assets/images/hyttalo-souza-a1p0Z7RSkL8-unsplash_684.jpg +assets/images/nasa-dCgbRAQmTQA-unsplash_684.jpg +assets/images/alexander-grey-8lnbXtxFGZw-unsplash_684.jpg +assets/images/alexander-kirov-YhDJXJjmxUQ-unsplash_336.jpg +assets/images/no-revisions-UhpAf0ySwuk-unsplash_336.jpg +assets/images/fons-heijnsbroek-vBfEZdpEr-E-unsplash_448.jpg +assets/images/hassan-kibwana-fmXLB_uHIh4-unsplash_448.jpg +assets/images/anatol-rurac-NeSj0i6HLak-unsplash_150.jpg +assets/images/angela-loria-hFc0JEKD4Cc-unsplash_150.jpg +assets/images/matthew-tenbruggencate-0HJWobhGhJs-unsplash_150.jpg +assets/images/domino-studio-164_6wVEHfI-unsplash_336.jpg +assets/images/noaa-Led9c1SSNFo-unsplash_684.jpg +assets/images/tingey-injury-law-firm-9SKhDFnw4c4-unsplash_448.jpg +assets/images/robert-bye-xHUZuSwVJg4-unsplash_336.jpg +assets/images/emran-yousof-k8ZbMQWbx34-unsplash_336.jpg +assets/images/matthew-ball-3wW2fBjptQo-unsplash_448.jpg +assets/images/keagan-henman-xPJYL0l5Ii8-unsplash_336.jpg +assets/images/memento-media-XhYq-5KnxSk-unsplash_448.jpg +assets/images/brigitte-tohm-iIupxcq-yH4-unsplash_150.jpg +assets/images/vanilla-bear-films-JEwNQerg3Hs-unsplash_336.jpg +assets/images/david-beale--lQR8yeDzek-unsplash_336.jpg +assets/images/ednilson-cardoso-dos-santos-haiooWA_weo-unsplash_336.jpg +assets/images/mika-baumeister-bGZZBDvh8s4-unsplash_150.jpg +assets/images/greg-rakozy-oMpAz-DN-9I-unsplash_684.jpg +assets/images/egor-myznik-GFHKMW6KiJ0-unsplash_684.jpg +assets/images/kenny-eliason-5ddH9Y2accI-unsplash_150.jpg +assets/images/isaw-company-Oqv_bQbZgS8-unsplash_336.jpg +assets/images/geert-pieters-NbpUM86Jo8Y-unsplash_150.jpg +assets/images/boxed-water-is-better-y-TpYAlcBYM-unsplash_150.jpg +assets/images/paola-chaaya-QrbuLFT6ypw-unsplash_336.jpg +assets/images/devi-puspita-amartha-yahya-7ln0pST_O8M-unsplash_448.jpg +assets/images/inaki-del-olmo-NIJuEQw0RKg-unsplash_336.jpg +assets/images/dylan-calluy-j9q18vvHitg-unsplash_150.jpg +assets/images/oguzhan-edman-ZWPkHLRu3_4-unsplash_336.jpg +assets/images/david-lezcano-NfZiOJzZgcg-unsplash_336.jpg +assets/images/alec-favale-dLctr-PqFys-unsplash_150.jpg +assets/images/unseen-histories-4kYkKW8v8rY-unsplash_150.jpg +assets/images/freestocks-nss2eRzQwgw-unsplash_150.jpg +assets/images/steven-van-bTPP3jBnOb8-unsplash_684.jpg +assets/images/colin-lloyd-2ULmNrj44QY-unsplash_448.jpg +assets/images/libre-leung-9O0Sp22DF0I-unsplash_336.jpg +assets/images/markus-spiske-lUc5pRFB25s-unsplash_336.jpg +assets/images/olga-guryanova-ft7vJxwl2RY-unsplash_336.jpg +assets/images/noaa-FY3vXNBl1v4-unsplash_336.jpg +assets/images/kelly-sikkema-A-lovieAmjA-unsplash_150.jpg +assets/images/markus-spiske-wIUxLHndcLw-unsplash_150.jpg +assets/images/denys-nevozhai-z0nVqfrOqWA-unsplash_150.jpg +assets/images/ian-taylor-kAWTCt7p7rs-unsplash_448.jpg +assets/images/dillon-kydd-2keCPb73aQY-unsplash_336.jpg +assets/images/ross-parmly-rf6ywHVkrlY-unsplash_336.jpg +assets/images/arturo-rey-5yP83RhaFGA-unsplash_336.jpg +assets/images/lorenzo-moschi-N7ypjB7HKIk-unsplash_150.jpg +assets/images/etienne-girardet-RqOyRtYGhLg-unsplash_448.jpg +assets/images/rufinochka-XonjCOZZN_w-unsplash_448.jpg +assets/images/stephen-phillips-hostreviews-co-uk-em37kS8WJJQ-unsplash_150.jpg +assets/images/kenny-K72n3BHgHCg-unsplash_336.jpg +assets/images/trung-thanh-LgdDeuBcgIY-unsplash_684.jpg +assets/images/maarten-van-den-heuvel-gZXx8lKAb7Y-unsplash_150.jpg +assets/images/tadeusz-lakota-Tb38UzCvKCY-unsplash_336.jpg +assets/images/valery-tenevoy-c0VbjkPEfmM-unsplash_336.jpg +assets/images/dom-hill-nimElTcTNyY-unsplash_336.jpg +assets/images/sara-cottle-bGjtWs8sXT0-unsplash_448.jpg +assets/images/toa-heftiba-4xe-yVFJCvw-unsplash_336.jpg +assets/images/nuno-alberto-MykFFC5zolE-unsplash_448.jpg +assets/images/chad-stembridge-sEHrIPpkKQY-unsplash_150.jpg +assets/images/quick-ps-sW41y3lETZk-unsplash_150.jpg +assets/images/anastassia-anufrieva-ecHGTPfjNfA-unsplash_150.jpg +assets/images/bram-naus-oqnVnI5ixHg-unsplash_448.jpg +assets/images/nasa-Q1p7bh3SHj8-unsplash_150.jpg +assets/images/ani-kolleshi-7jjnJ-QA9fY-unsplash_150.jpg +assets/images/ibrahim-rifath-OApHds2yEGQ-unsplash_150.jpg +assets/images/isai-ramos-Sp70YIWtuM8-unsplash_336.jpg +assets/images/drew-hays-tGYrlchfObE-unsplash_336.jpg +assets/images/c-j-1GHqOftzYo0-unsplash_150.jpg +assets/images/brooke-lark-oaz0raysASk-unsplash_150.jpg +assets/images/daniel-staple-N320vzTBviA-unsplash_684.jpg +assets/images/heather-ford-5gkYsrH_ebY-unsplash_336.jpg +assets/images/marco-oriolesi-wqLGlhjr6Og-unsplash_684.jpg +assets/images/brooke-lark-HjWzkqW1dgI-unsplash_336.jpg +assets/images/irwan-rbDE93-0hHs-unsplash_684.jpg +assets/images/bermix-studio-yUnSMBogWNI-unsplash_150.jpg +assets/images/geoff-scott-8lUTnkZXZSA-unsplash_336.jpg +assets/images/astronaud23-ox3t0m3PUqA-unsplash_336.jpg +assets/images/national-cancer-institute-A2CK97sS0ns-unsplash_684.jpg +assets/images/clem-onojeghuo-DoA2duXyzRM-unsplash_684.jpg +assets/images/leon-seibert-Xs3al4NpIFQ-unsplash_336.jpg +assets/images/mr-lee-f4RBYsY2hxA-unsplash_150.jpg +assets/images/derick-mckinney-muhK4oeYJiU-unsplash_150.jpg +assets/images/thisisengineering-ZPeXrWxOjRQ-unsplash_336.jpg +assets/images/heidi-kaden-L_U4jhwZ6hY-unsplash_448.jpg +assets/images/jed-owen-ajZibDGpPew-unsplash_336.jpg +assets/images/tamara-bellis-IwVRO3TLjLc-unsplash_336.jpg +assets/images/maxim-hopman-IayKLkmz6g0-unsplash_448.jpg +assets/images/joshua-rawson-harris-YNaSz-E7Qss-unsplash_336.jpg +assets/images/clem-onojeghuo-RLJnH4Mt9A0-unsplash_336.jpg +assets/images/esther-ann-glpYh1cWf0o-unsplash_448.jpg +assets/images/colin-lloyd-NKS5gg7rWGw-unsplash_150.jpg +assets/images/ronda-darby-HbMLSB-uhQY-unsplash_336.jpg +assets/images/icons8-team-k5fUTay0ghw-unsplash_336.jpg +assets/images/alex-hudson-7AgqAZbogOQ-unsplash_448.jpg +assets/images/koshu-kunii-cWEGNQqcImk-unsplash_336.jpg +assets/images/maksym-kaharlytskyi-Y0z9MyDsrU0-unsplash_150.jpg +assets/images/kenny-eliason-4N3iHYmqy_E-unsplash_336.jpg +assets/images/ian-hutchinson-P8rgDtEFn7s-unsplash_150.jpg +assets/images/ahmet-kurt-5BGg2L5nhlU-unsplash_150.jpg +assets/images/jordhan-madec-AD5ylD2T0UY-unsplash_336.jpg +assets/images/dominic-hampton-_8aRumOixtI-unsplash_684.jpg +assets/images/aditya-chinchure-ZhQCZjr9fHo-unsplash_336.jpg +assets/images/robina-weermeijer-Pw9aFhc92P8-unsplash_448.jpg +assets/images/piron-guillaume-U4FyCp3-KzY-unsplash_150.jpg +assets/images/sj-objio-8hHxO3iYuU0-unsplash_448.jpg +assets/images/national-cancer-institute-YvvFRJgWShM-unsplash_336.jpg +assets/images/declan-sun-misAHv6YWkI-unsplash_336.jpg +assets/images/mufid-majnun-J12RfFH-2ZE-unsplash_684.jpg +assets/images/wan-san-yip-ID1yWa1Wpx0-unsplash_336.jpg +assets/images/jakub-zerdzicki-qcRGVZNZ5js-unsplash_336.jpg +assets/images/ryan-plomp-TT6Hep-JzrU-unsplash_336.jpg +assets/images/ixography-05Q_XPF_YKs-unsplash_336.jpg +assets/images/bianca-ackermann-qr0-lKAOZSk-unsplash_150.jpg +assets/images/sandip-roy-4hgTlYb9jzg-unsplash_448.jpg +assets/images/kevin-rajaram-qhixFFO8EWQ-unsplash_336.jpg +assets/images/jacob-licht-8nA_iHrxHIo-unsplash_150.jpg +assets/images/annie-spratt-KiOHnBkLQQU-unsplash_684.jpg +assets/images/nikita-karimov-lvJZhHOIJJ4-unsplash_448.jpg +assets/images/marius-oprea-ySA9uj7zSmw-unsplash_150.jpg +assets/images/neil-thomas-SIU1Glk6v5k-unsplash_150.jpg +assets/images/ashim-d-silva-ZmgJiztRHXE-unsplash_336.jpg +assets/images/dino-reichmuth-A5rCN8626Ck-unsplash_336.jpg +assets/images/paul-bill-HLuPjCa6IYw-unsplash_150.jpg +assets/images/microsoft-365-f1zQuagWCTA-unsplash_336.jpg +assets/images/jessica-weiller-So4eFi-d1nc-unsplash_336.jpg +assets/images/dominic-bieri-vXRt4rFr4hI-unsplash_336.jpg +assets/images/joseph-gonzalez-QaGDmf5tMiE-unsplash_150.jpg +assets/images/jonathan-borba-lrQPTQs7nQQ-unsplash_150.jpg +assets/images/christian-lue-8Yw6tsB8tnc-unsplash_448.jpg +assets/images/christian-tenguan-P3gfVKhz8d0-unsplash_336.jpg +assets/images/zac-gudakov-wwqZ8CM21gg-unsplash_150.jpg +assets/images/claudio-schwarz-3cWxxW2ggKE-unsplash_336.jpg +assets/images/adrian-dascal-myAz-buELXs-unsplash_448.jpg +assets/images/little-plant-TZw891-oMio-unsplash_150.jpg +assets/images/matthew-landers-v8UgmRa6UDg-unsplash_150.jpg +assets/images/benjamin-child-rOn57CBgyMo-unsplash_448.jpg +assets/images/phil-hearing-bu27Y0xg7dk-unsplash_150.jpg +assets/images/pat-whelen-68OkRwuOeyQ-unsplash_150.jpg +assets/images/jakob-dalbjorn-cuKJre3nyYc-unsplash_448.jpg +assets/images/tachina-lee--wjk_SSqCE4-unsplash_448.jpg +assets/images/toa-heftiba--abWByT3yg4-unsplash_336.jpg +assets/images/matthias-heil-lDOEwat_MPs-unsplash_336.jpg +assets/images/taylor-grote-UiVe5QvOhao-unsplash_150.jpg +assets/images/peter-lawrence-rXZa4ufjoGw-unsplash_448.jpg +assets/images/andrew-solok-LbckXdUVOlY-unsplash_448.jpg +assets/images/scott-graham-OQMZwNd3ThU-unsplash_448.jpg +assets/images/ant-rozetsky-SLIFI67jv5k-unsplash_150.jpg +assets/images/maria-lysenko-tZvkSuBleso-unsplash_150.jpg +assets/images/fanga-studio-bOfCOy3_4wU-unsplash_448.jpg +assets/images/sarah-dorweiler-gUPiTDBdRe4-unsplash_336.jpg +assets/images/markus-spiske-7PMGUqYQpYc-unsplash_150.jpg +assets/images/alice-pasqual-Olki5QpHxts-unsplash_336.jpg +assets/images/brooke-lark-lcZ9NxhOSlo-unsplash_448.jpg +assets/images/chris-leboutillier-c7RWVGL8lPA-unsplash_150.jpg +assets/images/jon-tyson-0BLE1xp5HBQ-unsplash_150.jpg +assets/images/nathan-dumlao-laCrvNG3F_I-unsplash_336.jpg +assets/images/diego-jimenez-A-NVHPka9Rk-unsplash_336.jpg +assets/images/mika-baumeister-jXPQY1em3Ew-unsplash_150.jpg +assets/images/rebe-pascual-SACRQSof7Qw-unsplash_448.jpg +assets/images/bruno-nascimento-PHIgYUGQPvU-unsplash_448.jpg +assets/images/myriam-zilles-KltoLK6Mk-g-unsplash_336.jpg +assets/images/boris-stefanik-q49CgyIrLes-unsplash_150.jpg +assets/images/national-cancer-institute-cw2Zn2ZQ9YQ-unsplash_336.jpg +assets/images/david-everett-strickler-igCBFrMd11I-unsplash_684.jpg +assets/images/markus-spiske-Nph1oyRsHm4-unsplash_150.jpg +assets/images/ruben-valenzuela-JEp9cl5jfZA-unsplash_684.jpg +assets/images/ilyass-seddoug-06w8RxgSzF0-unsplash_150.jpg +assets/images/heidi-fin-2TLREZi7BUg-unsplash_336.jpg +assets/images/mattia-bericchia-xkD79yf4tb8-unsplash_150.jpg +assets/images/elimende-inagella-7OxV_qDiGRI-unsplash_448.jpg +assets/images/harold-mendoza-6xafY_AE1LM-unsplash_150.jpg +assets/images/tech-daily-vxTWpu14zeM-unsplash_150.jpg +assets/images/dibakar-roy-K9JwokzSvrc-unsplash_150.jpg +assets/images/marcelo-leal-6pcGTJDuf6M-unsplash_448.jpg +assets/images/sapan-patel-gmgWd0CgWQI-unsplash_150.jpg +assets/images/evie-calder-97CO-A4P0GQ-unsplash_336.jpg +assets/images/brooke-lark-GJMlSBS0FhU-unsplash_150.jpg +assets/images/harri-kuokkanen-SEtUeWL8bIQ-unsplash_336.jpg +assets/images/roberto-junior-4fsCBcZt9H8-unsplash_150.jpg +assets/images/markus-spiske-WUehAgqO5hE-unsplash_684.jpg +assets/images/gaku-suyama-VyiLZUcdJv0-unsplash_150.jpg +assets/images/mr-cup-fabien-barral-Mwuod2cm8g4-unsplash_448.jpg +assets/images/austin-distel-rxpThOwuVgE-unsplash_448.jpg +assets/images/national-cancer-institute-NFvdKIhxYlU-unsplash_336.jpg +assets/images/sam-mcghee-4siwRamtFAk-unsplash_684.jpg +assets/images/markus-spiske-jgOkEjVw-KM-unsplash_150.jpg +assets/images/peter-conlan-LEgwEaBVGMo-unsplash_448.jpg +assets/images/metin-ozer-hShrr0WvrQs-unsplash_336.jpg +assets/images/craig-manners-LvJCFOW3Ma8-unsplash_448.jpg +assets/images/parker-johnson-v0OWc_skg0g-unsplash_336.jpg +assets/images/wesley-tingey-7BkCRNwh_V0-unsplash_150.jpg +resources.txt diff --git a/suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt b/suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt new file mode 100644 index 000000000..3a9a2b41d --- /dev/null +++ b/suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt @@ -0,0 +1,10 @@ +app.js +base.css +controller.js +helpers.js +index.css +index.html +model.js +store.js +template.js +view.js diff --git a/tests/unittests/benchmark-configurator.mjs b/tests/unittests/benchmark-configurator.mjs new file mode 100644 index 000000000..546a74a47 --- /dev/null +++ b/tests/unittests/benchmark-configurator.mjs @@ -0,0 +1,42 @@ +import { BenchmarkConfigurator } from "../../resources/benchmark-configurator.mjs"; + +describe("BenchmarkConfigurator", () => { + it("should set measurePrepare to false on processed suites if not present", async () => { + const configurator = new BenchmarkConfigurator(); + await configurator.init(); + const suites = configurator.suites; + expect(suites.length).to.be.greaterThan(0); + suites.forEach((suite) => { + expect(suite).to.have.property("measurePrepare"); + expect(suite.measurePrepare).to.be.a("boolean"); + }); + }); + + it("should validate URLs correctly in _isValidUrl", () => { + const configurator = new BenchmarkConfigurator(); + expect(configurator._isValidUrl("http://example.com")).to.be(true); + expect(configurator._isValidUrl("suites/todomvc/index.html")).to.be(true); + expect(configurator._isValidUrl("")).to.be(false); + expect(configurator._isValidUrl(null)).to.be(false); + }); + + it("should enable suites by names", async () => { + const configurator = new BenchmarkConfigurator(); + await configurator.init(); + const firstSuiteName = configurator.suites[0].name; + configurator.enableSuites([firstSuiteName], []); + expect(configurator.suites[0].enabled).to.be(true); + // Assuming there are multiple suites + expect(configurator.suites[1].enabled).to.be(false); + }); + + it("should enable suites by tags", async () => { + const configurator = new BenchmarkConfigurator(); + await configurator.init(); + configurator.enableSuites([], ["todomvc"]); + const todomvcSuites = configurator.suites.filter(suite => suite.tags.includes("todomvc")); + const otherSuites = configurator.suites.filter(suite => !suite.tags.includes("todomvc")); + todomvcSuites.forEach(suite => expect(suite.enabled).to.be(true)); + otherSuites.forEach(suite => expect(suite.enabled).to.be(false)); + }); +}); From b3ea735ba93f8b52614cb71f4de3d8ae25c65f1d Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 16:53:28 +0200 Subject: [PATCH 21/69] wrap --- resources/shared/request-limiter.mjs | 31 ++++++++++++++++++++++++++++ sw.mjs | 30 ++++++--------------------- tests/index.html | 1 + 3 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 resources/shared/request-limiter.mjs diff --git a/resources/shared/request-limiter.mjs b/resources/shared/request-limiter.mjs new file mode 100644 index 000000000..07068aa94 --- /dev/null +++ b/resources/shared/request-limiter.mjs @@ -0,0 +1,31 @@ +export class RequestLimiter { + constructor(limit = 20) { + this.limit = limit; + this.active = 0; + this.queue = []; + } + + async _processQueue() { + while (this.queue.length > 0) { + const task = this.queue.shift(); + try { + await task(); + } catch (e) { + // Individual task errors are handled by their respective promises + } + } + this.active--; + } + + schedule(fn) { + return new Promise((resolve, reject) => { + const task = () => fn().then(resolve, reject); + this.queue.push(task); + + if (this.active < this.limit) { + this.active++; + this._processQueue(); + } + }); + } +} diff --git a/sw.mjs b/sw.mjs index 15505bdc6..23896e8b1 100644 --- a/sw.mjs +++ b/sw.mjs @@ -1,4 +1,5 @@ import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; +import { RequestLimiter } from "./resources/shared/request-limiter.mjs"; const CACHE_NAME = "speedometer-cache-v4.0"; const DB_NAME = "SpeedometerStateDB"; @@ -102,26 +103,8 @@ function delayAsync(ms) { } const MAX_CONCURRENT_REQUESTS = 20; -function createConcurrencyLimiter(limit = MAX_CONCURRENT_REQUESTS) { - let active = 0; - const queue = []; - const next = () => { - if (active >= limit || queue.length === 0) - return; - active++; - const task = queue.shift(); - task().finally(() => { - active--; - next(); - }); - }; - return (fn) => - new Promise((resolve, reject) => { - queue.push(() => fn().then(resolve, reject)); - next(); - }); -} -const requestLimiter = createConcurrencyLimiter(MAX_CONCURRENT_REQUESTS); + +const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS); async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { await STORE.getOwner(); @@ -180,9 +163,9 @@ function getResponseSize(response) { async function fetchAndCache(cache, url, delayMs) { if (delayMs) await delayAsync(delayMs); - return requestLimiter(async () => { + return requestLimiter.schedule(async () => { const request = new Request(url, { cache: "no-cache" }); - const existing = await cache.match(request); + const existing = await cache.match(request, { ignoreSearch: true }); if (existing) return getResponseSize(existing); @@ -252,8 +235,7 @@ self.addEventListener("message", (event) => { self.addEventListener("fetch", (event) => { event.respondWith( (async () => { - const cache = await caches.open(CACHE_NAME); - const cachedResponse = await cache.match(event.request, { ignoreSearch: true }); + const cachedResponse = await caches.match(event.request, { cacheName: CACHE_NAME, ignoreSearch: true }); if (cachedResponse) return cachedResponse; diff --git a/tests/index.html b/tests/index.html index 821356444..251a68b1a 100644 --- a/tests/index.html +++ b/tests/index.html @@ -31,6 +31,7 @@ await import("./unittests/benchmark-configurator.mjs"); await import("./unittests/params.mjs"); await import("./unittests/suites.mjs"); + await import("./unittests/request-limiter.mjs"); globalThis.testResults = undefined; globalThis.testRunner = mocha.run(); From da2841a807050a30a1bb42aa0805d889e6cb5419 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 16:54:45 +0200 Subject: [PATCH 22/69] pre-format --- tests/unittests/request-limiter.mjs | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/unittests/request-limiter.mjs diff --git a/tests/unittests/request-limiter.mjs b/tests/unittests/request-limiter.mjs new file mode 100644 index 000000000..4434e2e5d --- /dev/null +++ b/tests/unittests/request-limiter.mjs @@ -0,0 +1,49 @@ +import { RequestLimiter } from "../../resources/shared/request-limiter.mjs"; + +describe("RequestLimiter", () => { + it("should limit the number of concurrent tasks", async () => { + const limiter = new RequestLimiter(2); + let activeCount = 0; + let maxActive = 0; + + const createTask = () => { + return limiter.schedule(async () => { + activeCount++; + if (activeCount > maxActive) { + maxActive = activeCount; + } + // Simulate async work + await new Promise((resolve) => setTimeout(resolve, 10)); + activeCount--; + }); + }; + + const promises = [createTask(), createTask(), createTask(), createTask(), createTask()]; + await Promise.all(promises); + + // Maximum concurrent tasks should not exceed the limit of 2 + expect(maxActive).to.be(2); + // All tasks should have finished + expect(activeCount).to.be(0); + }); + + it("should resolve individual promises with correct values", async () => { + const limiter = new RequestLimiter(1); + + const p1 = limiter.schedule(async () => 1); + const p2 = limiter.schedule(async () => 2); + const p3 = limiter.schedule(async () => { throw new Error("task failed"); }); + + expect(await p1).to.be(1); + expect(await p2).to.be(2); + + let errorCaught = false; + try { + await p3; + } catch (e) { + errorCaught = true; + expect(e.message).to.be("task failed"); + } + expect(errorCaught).to.be(true); + }); +}); From 201c43b3f1bc12c8f5f7f0e9b5a3a26c0e843f51 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 16:56:29 +0200 Subject: [PATCH 23/69] cleanup --- resources/shared/request-limiter.mjs | 31 ------------------ sw.mjs | 33 ++++++++++++++++++- tests/index.html | 1 - tests/unittests/request-limiter.mjs | 49 ---------------------------- 4 files changed, 32 insertions(+), 82 deletions(-) delete mode 100644 resources/shared/request-limiter.mjs delete mode 100644 tests/unittests/request-limiter.mjs diff --git a/resources/shared/request-limiter.mjs b/resources/shared/request-limiter.mjs deleted file mode 100644 index 07068aa94..000000000 --- a/resources/shared/request-limiter.mjs +++ /dev/null @@ -1,31 +0,0 @@ -export class RequestLimiter { - constructor(limit = 20) { - this.limit = limit; - this.active = 0; - this.queue = []; - } - - async _processQueue() { - while (this.queue.length > 0) { - const task = this.queue.shift(); - try { - await task(); - } catch (e) { - // Individual task errors are handled by their respective promises - } - } - this.active--; - } - - schedule(fn) { - return new Promise((resolve, reject) => { - const task = () => fn().then(resolve, reject); - this.queue.push(task); - - if (this.active < this.limit) { - this.active++; - this._processQueue(); - } - }); - } -} diff --git a/sw.mjs b/sw.mjs index 23896e8b1..c1f935a90 100644 --- a/sw.mjs +++ b/sw.mjs @@ -1,5 +1,4 @@ import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; -import { RequestLimiter } from "./resources/shared/request-limiter.mjs"; const CACHE_NAME = "speedometer-cache-v4.0"; const DB_NAME = "SpeedometerStateDB"; @@ -104,6 +103,38 @@ function delayAsync(ms) { const MAX_CONCURRENT_REQUESTS = 20; +class RequestLimiter { + constructor(limit = MAX_CONCURRENT_REQUESTS) { + this.limit = limit; + this.active = 0; + this.queue = []; + } + + async _processQueue() { + while (this.queue.length > 0) { + const task = this.queue.shift(); + try { + await task(); + } catch (e) { + // Individual task errors are handled by their respective promises + } + } + this.active--; + } + + schedule(fn) { + return new Promise((resolve, reject) => { + const task = () => fn().then(resolve, reject); + this.queue.push(task); + + if (this.active < this.limit) { + this.active++; + this._processQueue(); + } + }); + } +} + const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS); async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { diff --git a/tests/index.html b/tests/index.html index 251a68b1a..821356444 100644 --- a/tests/index.html +++ b/tests/index.html @@ -31,7 +31,6 @@ await import("./unittests/benchmark-configurator.mjs"); await import("./unittests/params.mjs"); await import("./unittests/suites.mjs"); - await import("./unittests/request-limiter.mjs"); globalThis.testResults = undefined; globalThis.testRunner = mocha.run(); diff --git a/tests/unittests/request-limiter.mjs b/tests/unittests/request-limiter.mjs deleted file mode 100644 index 4434e2e5d..000000000 --- a/tests/unittests/request-limiter.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import { RequestLimiter } from "../../resources/shared/request-limiter.mjs"; - -describe("RequestLimiter", () => { - it("should limit the number of concurrent tasks", async () => { - const limiter = new RequestLimiter(2); - let activeCount = 0; - let maxActive = 0; - - const createTask = () => { - return limiter.schedule(async () => { - activeCount++; - if (activeCount > maxActive) { - maxActive = activeCount; - } - // Simulate async work - await new Promise((resolve) => setTimeout(resolve, 10)); - activeCount--; - }); - }; - - const promises = [createTask(), createTask(), createTask(), createTask(), createTask()]; - await Promise.all(promises); - - // Maximum concurrent tasks should not exceed the limit of 2 - expect(maxActive).to.be(2); - // All tasks should have finished - expect(activeCount).to.be(0); - }); - - it("should resolve individual promises with correct values", async () => { - const limiter = new RequestLimiter(1); - - const p1 = limiter.schedule(async () => 1); - const p2 = limiter.schedule(async () => 2); - const p3 = limiter.schedule(async () => { throw new Error("task failed"); }); - - expect(await p1).to.be(1); - expect(await p2).to.be(2); - - let errorCaught = false; - try { - await p3; - } catch (e) { - errorCaught = true; - expect(e.message).to.be("task failed"); - } - expect(errorCaught).to.be(true); - }); -}); From 90d818f772be0ecb46372381123efca24132e1a7 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 18:06:39 +0200 Subject: [PATCH 24/69] Fix Chromium Service Worker race condition in e2e tests --- resources/main.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/main.mjs b/resources/main.mjs index e7393f1d0..52382f49c 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -41,7 +41,6 @@ export class ResourcePreloader { async _registerServiceWorker() { this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); - await this._registration.update(); await navigator.serviceWorker.ready; this._sw = navigator.serviceWorker.controller || this._registration.active; } From 7429e210e4da8e9bd9f3df590983ca63ea934cb8 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 18:20:36 +0200 Subject: [PATCH 25/69] pre-format --- tests/unittests/benchmark-configurator.mjs | 8 ++++---- tests/unittests/suites.mjs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/unittests/benchmark-configurator.mjs b/tests/unittests/benchmark-configurator.mjs index 546a74a47..82585db5f 100644 --- a/tests/unittests/benchmark-configurator.mjs +++ b/tests/unittests/benchmark-configurator.mjs @@ -34,9 +34,9 @@ describe("BenchmarkConfigurator", () => { const configurator = new BenchmarkConfigurator(); await configurator.init(); configurator.enableSuites([], ["todomvc"]); - const todomvcSuites = configurator.suites.filter(suite => suite.tags.includes("todomvc")); - const otherSuites = configurator.suites.filter(suite => !suite.tags.includes("todomvc")); - todomvcSuites.forEach(suite => expect(suite.enabled).to.be(true)); - otherSuites.forEach(suite => expect(suite.enabled).to.be(false)); + const todomvcSuites = configurator.suites.filter((suite) => suite.tags.includes("todomvc")); + const otherSuites = configurator.suites.filter((suite) => !suite.tags.includes("todomvc")); + todomvcSuites.forEach((suite) => expect(suite.enabled).to.be(true)); + otherSuites.forEach((suite) => expect(suite.enabled).to.be(false)); }); }); diff --git a/tests/unittests/suites.mjs b/tests/unittests/suites.mjs index af7f96700..3375d8929 100644 --- a/tests/unittests/suites.mjs +++ b/tests/unittests/suites.mjs @@ -96,7 +96,6 @@ for (const [name, suites] of Object.entries(Suites)) { } if (brokenResourcesList.length > 0) throw new Error(`Failed to load the following resources:\n${brokenResourcesList.join("\n")}`); - }); }); } From a397584bcd0ae9fe13ca08a7ba0159ab42c1a32c Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 18:22:52 +0200 Subject: [PATCH 26/69] Throw error for missing resources.txt and switch LockStore to use IndexedDB only --- sw.mjs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/sw.mjs b/sw.mjs index c1f935a90..61604e51d 100644 --- a/sw.mjs +++ b/sw.mjs @@ -7,7 +7,6 @@ const DB_VERSION = 2; class LockStore { constructor() { - this.activeClientId = null; this.dbPromise = null; } @@ -32,8 +31,6 @@ class LockStore { } async getOwner() { - if (this.activeClientId !== null) - return this.activeClientId; try { const db = await this._openDB(); const data = await new Promise((resolve, reject) => { @@ -42,16 +39,14 @@ class LockStore { req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); - this.activeClientId = data?.clientId || null; + return data?.clientId || null; } catch (e) { console.warn("IndexedDB read failed", e); - this.activeClientId = null; + return null; } - return this.activeClientId; } async setOwner(clientId) { - this.activeClientId = clientId; try { const db = await this._openDB(); await new Promise((resolve, reject) => { @@ -66,7 +61,6 @@ class LockStore { } async clear() { - this.activeClientId = null; try { const db = await this._openDB(); await new Promise((resolve, reject) => { @@ -80,10 +74,11 @@ class LockStore { } } - hasLock(clientId) { - if (!this.activeClientId) + async hasLock(clientId) { + const activeClientId = await this.getOwner(); + if (!activeClientId) return true; - return this.activeClientId === clientId; + return activeClientId === clientId; } } @@ -165,7 +160,7 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay await Promise.all(promises); - if (!STORE.hasLock(clientId)) { + if (!(await STORE.hasLock(clientId))) { replyError(event, "Speedometer aborted: Another tab took over."); return; } @@ -175,7 +170,7 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay async function parseSuiteResources(suite) { const response = await fetch(suite.resources); if (!response.ok) - return []; + throw new Error(`Failed to load resources for suite ${suite.name}: ${suite.resources} returned ${response.status}`); const text = await response.text(); return text .trim() @@ -223,7 +218,7 @@ self.addEventListener("activate", (event) => { }); async function updateActiveClient(newClientId) { - const oldClientId = STORE.activeClientId; + const oldClientId = await STORE.getOwner(); if (oldClientId === newClientId) return false; @@ -237,8 +232,7 @@ async function updateActiveClient(newClientId) { } async function handleClearCacheMessage(event, clientId) { - await STORE.getOwner(); - if (!STORE.hasLock(clientId)) { + if (!(await STORE.hasLock(clientId))) { replyError(event, "Cannot clear SW: You do not own the lock."); return; } From b81afed3404e86471ad54f08d84b0c03d32438fb Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 23:00:07 +0200 Subject: [PATCH 27/69] Apply formatting fixes for presubmit --- tests/unittests/suites.mjs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/tests/unittests/suites.mjs b/tests/unittests/suites.mjs index 3375d8929..384337bb8 100644 --- a/tests/unittests/suites.mjs +++ b/tests/unittests/suites.mjs @@ -68,34 +68,27 @@ for (const [name, suites] of Object.entries(Suites)) { const baseUrl = `${window.location.origin}/`; const brokenResourcesList = []; for (const suite of suites) { - if (!suite.resources) - continue; + if (!suite.resources) continue; const resourcesUrl = new URL(suite.resources, baseUrl).href; const res = await fetch(resourcesUrl); - if (!res.ok) - throw new Error(`Failed to load resources.txt for ${suite.name} at ${resourcesUrl}`); + if (!res.ok) throw new Error(`Failed to load resources.txt for ${suite.name} at ${resourcesUrl}`); const text = await res.text(); - if (text.trim().length === 0) - throw new Error(`resources.txt for ${suite.name} is empty`); + if (text.trim().length === 0) throw new Error(`resources.txt for ${suite.name} is empty`); const files = text.trim().split("\n"); - for (const file of files) - expect(file.trim().length).to.be.greaterThan(0, `Found empty line in resources.txt for ${suite.name}`); + for (const file of files) expect(file.trim().length).to.be.greaterThan(0, `Found empty line in resources.txt for ${suite.name}`); await Promise.all( files.map(async (file) => { const fileUrl = new URL(file, resourcesUrl).href; const fileRes = await fetch(fileUrl, { method: "HEAD" }); - if (!fileRes.ok) - brokenResourcesList.push(`${fileUrl} (listed in ${resourcesUrl})`); - else - expect(fileRes.ok).to.be(true); + if (!fileRes.ok) brokenResourcesList.push(`${fileUrl} (listed in ${resourcesUrl})`); + else expect(fileRes.ok).to.be(true); }) ); } - if (brokenResourcesList.length > 0) - throw new Error(`Failed to load the following resources:\n${brokenResourcesList.join("\n")}`); + if (brokenResourcesList.length > 0) throw new Error(`Failed to load the following resources:\n${brokenResourcesList.join("\n")}`); }); }); } From 3f695ff9a12118a69b5a7abfb2808b1eaa5e5838 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 7 Jul 2026 23:02:42 +0200 Subject: [PATCH 28/69] formatting --- sw.mjs | 4 ++-- tests/unittests/suites.mjs | 21 ++++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/sw.mjs b/sw.mjs index 61604e51d..fe60c4ad6 100644 --- a/sw.mjs +++ b/sw.mjs @@ -160,7 +160,7 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay await Promise.all(promises); - if (!(await STORE.hasLock(clientId))) { + if (!await STORE.hasLock(clientId)) { replyError(event, "Speedometer aborted: Another tab took over."); return; } @@ -232,7 +232,7 @@ async function updateActiveClient(newClientId) { } async function handleClearCacheMessage(event, clientId) { - if (!(await STORE.hasLock(clientId))) { + if (!await STORE.hasLock(clientId)) { replyError(event, "Cannot clear SW: You do not own the lock."); return; } diff --git a/tests/unittests/suites.mjs b/tests/unittests/suites.mjs index 384337bb8..3375d8929 100644 --- a/tests/unittests/suites.mjs +++ b/tests/unittests/suites.mjs @@ -68,27 +68,34 @@ for (const [name, suites] of Object.entries(Suites)) { const baseUrl = `${window.location.origin}/`; const brokenResourcesList = []; for (const suite of suites) { - if (!suite.resources) continue; + if (!suite.resources) + continue; const resourcesUrl = new URL(suite.resources, baseUrl).href; const res = await fetch(resourcesUrl); - if (!res.ok) throw new Error(`Failed to load resources.txt for ${suite.name} at ${resourcesUrl}`); + if (!res.ok) + throw new Error(`Failed to load resources.txt for ${suite.name} at ${resourcesUrl}`); const text = await res.text(); - if (text.trim().length === 0) throw new Error(`resources.txt for ${suite.name} is empty`); + if (text.trim().length === 0) + throw new Error(`resources.txt for ${suite.name} is empty`); const files = text.trim().split("\n"); - for (const file of files) expect(file.trim().length).to.be.greaterThan(0, `Found empty line in resources.txt for ${suite.name}`); + for (const file of files) + expect(file.trim().length).to.be.greaterThan(0, `Found empty line in resources.txt for ${suite.name}`); await Promise.all( files.map(async (file) => { const fileUrl = new URL(file, resourcesUrl).href; const fileRes = await fetch(fileUrl, { method: "HEAD" }); - if (!fileRes.ok) brokenResourcesList.push(`${fileUrl} (listed in ${resourcesUrl})`); - else expect(fileRes.ok).to.be(true); + if (!fileRes.ok) + brokenResourcesList.push(`${fileUrl} (listed in ${resourcesUrl})`); + else + expect(fileRes.ok).to.be(true); }) ); } - if (brokenResourcesList.length > 0) throw new Error(`Failed to load the following resources:\n${brokenResourcesList.join("\n")}`); + if (brokenResourcesList.length > 0) + throw new Error(`Failed to load the following resources:\n${brokenResourcesList.join("\n")}`); }); }); } From 4b484433802abab7de6be68799f373b558fc1daa Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 09:40:23 +0200 Subject: [PATCH 29/69] fix-complexity --- suites-experimental/suites.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/suites-experimental/suites.mjs b/suites-experimental/suites.mjs index eff95c78d..2a7bc765b 100644 --- a/suites-experimental/suites.mjs +++ b/suites-experimental/suites.mjs @@ -13,7 +13,8 @@ export const ExperimentalSuites = freezeSuites([ (await page.waitForElement(".new-todo")).focus(); }, tests: [ - new BenchmarkTestStep(`Adding${numberOfItemsToAdd}Items`, (page) => { + new BenchmarkTestStep(`Adding${getNumberOfItemsToAdd()}Items`, (page) => { + const numberOfItemsToAdd = getNumberOfItemsToAdd(); const newTodo = page.querySelector(".new-todo"); for (let i = 0; i < numberOfItemsToAdd; i++) { newTodo.setValue(getTodoText("ja", i)); @@ -22,11 +23,13 @@ export const ExperimentalSuites = freezeSuites([ } }), new BenchmarkTestStep("CompletingAllItems", (page) => { + const numberOfItemsToAdd = getNumberOfItemsToAdd(); const checkboxes = page.querySelectorAll(".toggle"); for (let i = 0; i < numberOfItemsToAdd; i++) checkboxes[i].click(); }), new BenchmarkTestStep("DeletingAllItems", (page) => { + const numberOfItemsToAdd = getNumberOfItemsToAdd(); const deleteButtons = page.querySelectorAll(".destroy"); for (let i = numberOfItemsToAdd - 1; i >= 0; i--) deleteButtons[i].click(); From 39bb7314233f39aab680d56231e163484da4db18 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 12:13:17 +0200 Subject: [PATCH 30/69] update --- sw.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sw.mjs b/sw.mjs index fe60c4ad6..47ad8c4c8 100644 --- a/sw.mjs +++ b/sw.mjs @@ -191,7 +191,7 @@ async function fetchAndCache(cache, url, delayMs) { await delayAsync(delayMs); return requestLimiter.schedule(async () => { const request = new Request(url, { cache: "no-cache" }); - const existing = await cache.match(request, { ignoreSearch: true }); + const existing = await cache.match(request, { ignoreSearch: true, ignoreVary: true }); if (existing) return getResponseSize(existing); @@ -260,7 +260,7 @@ self.addEventListener("message", (event) => { self.addEventListener("fetch", (event) => { event.respondWith( (async () => { - const cachedResponse = await caches.match(event.request, { cacheName: CACHE_NAME, ignoreSearch: true }); + const cachedResponse = await caches.match(event.request, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); if (cachedResponse) return cachedResponse; From 5d8235503cde7823a109705fd56e853f22d71272 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 14:41:17 +0200 Subject: [PATCH 31/69] Fix Service Worker PRELOAD_PROGRESS flakiness and UI synchronization --- resources/main.css | 5 ++ resources/main.mjs | 65 +++++++++++++++++++----- resources/shared/sw-messages.mjs | 3 ++ suites/default-suites.mjs | 3 +- sw.mjs | 87 ++++++++++++++++++++++++++++++-- 5 files changed, 145 insertions(+), 18 deletions(-) diff --git a/resources/main.css b/resources/main.css index 674ce4ecc..41678b76a 100644 --- a/resources/main.css +++ b/resources/main.css @@ -989,6 +989,11 @@ section#about .note { pointer-events: none; } +[data-benchmark-state="PRELOADING"] .developer-mode .start-tests-button { + pointer-events: auto; + cursor: pointer; +} + [data-benchmark-state="PRELOADING"] .start-tests-button > div { background-color: var(--background); border-radius: 10px; diff --git a/resources/main.mjs b/resources/main.mjs index 3bee0f953..ce9922489 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -82,6 +82,10 @@ export class ResourcePreloader { return; } cleanup(); + if (data.type === "PRELOAD_ABORTED") { + resolve({ type: "PRELOAD_ABORTED" }); + return; + } if (data.type === "ERROR") { reject(new Error(data.message || "Unknown SW Error")); return; @@ -100,6 +104,12 @@ export class ResourcePreloader { await this._sendMessageWithReply({ type: SW_MESSAGES.CLEAR_CACHE }); } + async resetPreloading() { + if (!this._sw) + return; + await this._sendMessageWithReply({ type: SW_MESSAGES.RESET_PRELOADING }); + } + async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { if (!this._sw || suites.length === 0) return; @@ -118,6 +128,10 @@ export class ResourcePreloader { const startTime = performance.now(); const response = await this._sendMessageWithReply({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, onProgress, PRELOAD_TIMEOUT_MS); + if (response?.type === "PRELOAD_ABORTED") { + return "ABORTED"; + } + if (response?.type === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); @@ -222,7 +236,9 @@ class MainBenchmarkClient { if (!this._isStepping()) this._developerModeContainer?.remove(); - await this._setupResourcePreloader(benchmarkConfigurator); + await this._resourcePreloader.resetPreloading(); + const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); + if (preloadResult === "ABORTED") return false; try { await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); @@ -285,9 +301,17 @@ class MainBenchmarkClient { this._finishedTestCount = 0; } - didFinishLastIteration(metrics) { + async didFinishLastIteration(metrics) { console.assert(this._isRunning); this._isRunning = false; + + const response = await this._resourcePreloader._sendMessageWithReply({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); + if (response?.requests && response.requests.length > 0) { + console.warn("The following requests failed during the benchmark and bypassed the cache:"); + console.warn(response.requests.join("\n")); + alert("Warning: The benchmark had missing resources that were not cached. Check the console for details."); + } + this._hasResults = true; this._metrics = metrics; this._setBenchmarkState(BENCHMARK_STATE.DONE); @@ -514,7 +538,9 @@ class MainBenchmarkClient { document.body.append(this._developerModeContainer); } - await this._setupResourcePreloader(benchmarkConfigurator); + await this._resourcePreloader.resetPreloading(); + const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); + if (preloadResult === "ABORTED") return; if (params.startAutomatically) this.start(); @@ -523,16 +549,24 @@ class MainBenchmarkClient { async _setupResourcePreloader(benchmarkConfigurator) { await this._resourcePreloader.setup(); if (!this._resourcePreloader.isCached()) - await this._cacheResources(benchmarkConfigurator); + return await this._cacheResources(benchmarkConfigurator); } async _cacheResources(benchmarkConfigurator) { const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const clearCache = !params.isDefault(); this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); + + this._preloadGeneration = (this._preloadGeneration || 0) + 1; + const currentGeneration = this._preloadGeneration; + const onProgress = (data) => { + if (this._preloadGeneration === currentGeneration) + this._updateCacheProgress(data); + }; try { - await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this)); + const result = await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, onProgress); + if (result === "ABORTED") return "ABORTED"; this._enableStartButtons(); } catch (error) { console.error("Service Worker preload failed:", error); @@ -543,13 +577,21 @@ class MainBenchmarkClient { } } + _resetPreloadUI() { + document.getElementById("preload-progress-completed").value = 0; + document.getElementById("preload-info-label").textContent = ""; + document.getElementById("preload-info-progress").textContent = ""; + document.body.style.setProperty("--preload-progress", "0%"); + } + _updateCacheProgress(progressData) { const { loaded, total, url, suiteName } = progressData; document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); const progress = document.getElementById("preload-progress-completed"); progress.max = total; progress.value = loaded; - const filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; + let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; + if (!filename && url) filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); const labelText = suiteName ? `${suiteName}: ${filename}` : filename; document.getElementById("preload-info-label").textContent = labelText; document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; @@ -565,16 +607,13 @@ class MainBenchmarkClient { async _setBenchmarkState(state) { document.body.setAttribute("data-benchmark-state", state); - const startButton = document.querySelector(".start-tests-button"); + const startButtons = document.querySelectorAll(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { - document.getElementById("preload-progress-completed").value = 0; - document.getElementById("preload-info-label").textContent = ""; - document.getElementById("preload-info-progress").textContent = ""; - document.body.style.setProperty("--preload-progress", "0%"); - startButton.innerHTML = "
Preloading
"; + this._resetPreloadUI(); + startButtons.forEach((btn) => btn.innerHTML = "
Preloading
"); } else if (state !== BENCHMARK_STATE.RUNNING) { document.body.style.removeProperty("--preload-progress"); - startButton.innerHTML = "
Start Test
"; + startButtons.forEach((btn) => btn.innerHTML = "
Start Test
"); if (state !== BENCHMARK_STATE.READY) await this._resourcePreloader?.clearSw(); } diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs index c07c3b7ad..0ec39ff53 100644 --- a/resources/shared/sw-messages.mjs +++ b/resources/shared/sw-messages.mjs @@ -2,6 +2,9 @@ export const SW_MESSAGES = Object.freeze({ PRELOAD_SUITES: "PRELOAD_SUITES", PRELOAD_PROGRESS: "PRELOAD_PROGRESS", PRELOAD_DONE: "PRELOAD_DONE", + RESET_PRELOADING: "RESET_PRELOADING", FATAL_ERROR: "FATAL_ERROR", CLEAR_CACHE: "CLEAR_CACHE", + GET_FAILED_REQUESTS: "GET_FAILED_REQUESTS", + FAILED_REQUESTS: "FAILED_REQUESTS", }); diff --git a/suites/default-suites.mjs b/suites/default-suites.mjs index 2e9522bc6..a4091e7b2 100644 --- a/suites/default-suites.mjs +++ b/suites/default-suites.mjs @@ -1006,7 +1006,8 @@ export const DefaultSuites = freezeSuites([ }, { name: "Perf-Dashboard", - url: "suites/perf.webkit.org/public/v3/#/charts/?since=1678991819934&paneList=((55-1974-null-null-(5-2.5-500)))", + url: "suites/perf.webkit.org/public/v3/index.html#/charts/?since=1678991819934&paneList=((55-1974-null-null-(5-2.5-500)))", + resources: "suites/perf.webkit.org/public/v3/resources.txt", tags: ["default", "chart", "webcomponents"], async prepare(page) { await page.waitForElement("#app-is-ready"); diff --git a/sw.mjs b/sw.mjs index 47ad8c4c8..acdd7b58c 100644 --- a/sw.mjs +++ b/sw.mjs @@ -4,6 +4,7 @@ const CACHE_NAME = "speedometer-cache-v4.0"; const DB_NAME = "SpeedometerStateDB"; const STORE_NAME = "SpeedometerSWState"; const DB_VERSION = 2; +let failedRequests = new Set(); class LockStore { constructor() { @@ -113,6 +114,7 @@ class RequestLimiter { } catch (e) { // Individual task errors are handled by their respective promises } + await new Promise(r => setTimeout(r, 1)); } this.active--; } @@ -132,10 +134,31 @@ class RequestLimiter { const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS); +let currentPreloadEvent = null; +let currentPreloadId = 0; + +function handleResetPreloadingMessage(event) { + if (currentPreloadEvent) { + replyToClient(currentPreloadEvent, "PRELOAD_ABORTED"); + } + currentPreloadEvent = null; + currentPreloadId++; + if (event) { + replyToClient(event, SW_MESSAGES.RESET_PRELOADING); + } +} + async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { await STORE.getOwner(); await updateActiveClient(clientId); + handleResetPreloadingMessage(); // Call it without event to avoid replying to the PRELOAD_SUITES channel! + + currentPreloadEvent = event; + const preloadId = currentPreloadId; + + failedRequests.clear(); + if (clearCache) await caches.delete(CACHE_NAME); @@ -152,7 +175,9 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay const total = urlsToCache.length; const promises = urlsToCache.map(async (item, index) => { + if (preloadId !== currentPreloadId) return; const size = await fetchAndCache(cache, item.url, delay * index); + if (preloadId !== currentPreloadId) return; transferredSize += size; loaded++; replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); @@ -160,17 +185,23 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay await Promise.all(promises); + if (preloadId !== currentPreloadId) return; + if (!await STORE.hasLock(clientId)) { replyError(event, "Speedometer aborted: Another tab took over."); + if (currentPreloadEvent === event) currentPreloadEvent = null; return; } replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); + if (currentPreloadEvent === event) currentPreloadEvent = null; } async function parseSuiteResources(suite) { const response = await fetch(suite.resources); - if (!response.ok) + if (!response.ok) { + console.error(`Failed to load resources for suite ${suite.name}: ${suite.resources} returned ${response.status}`); throw new Error(`Failed to load resources for suite ${suite.name}: ${suite.resources} returned ${response.status}`); + } const text = await response.text(); return text .trim() @@ -240,10 +271,15 @@ async function handleClearCacheMessage(event, clientId) { replyToClient(event, "SUCCESS"); } +async function handleGetFailedRequestsMessage(event, clientId) { + replyToClient(event, SW_MESSAGES.FAILED_REQUESTS, { requests: Array.from(failedRequests) }); +} + const MESSAGE_HANDLERS = Object.freeze({ - __proto__: null, [SW_MESSAGES.PRELOAD_SUITES]: handlePreloadSuitesMessage, + [SW_MESSAGES.RESET_PRELOADING]: handleResetPreloadingMessage, [SW_MESSAGES.CLEAR_CACHE]: handleClearCacheMessage, + [SW_MESSAGES.GET_FAILED_REQUESTS]: handleGetFailedRequestsMessage, }); self.addEventListener("message", (event) => { @@ -258,13 +294,56 @@ self.addEventListener("message", (event) => { }); self.addEventListener("fetch", (event) => { + if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") { + return; + } + event.respondWith( (async () => { - const cachedResponse = await caches.match(event.request, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); + let requestToMatch = event.request; + const urlObj = new URL(event.request.url); + if (urlObj.pathname.endsWith("/")) { + urlObj.pathname += "index.html"; + requestToMatch = urlObj.href; + } + + const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); if (cachedResponse) return cachedResponse; - return fetch(event.request); + if (event.request.url.includes("/suites")) { + if (event.request.url.includes("perf.webkit.org/public/v3/index.html")) { + const cache = await caches.open(CACHE_NAME); + const keys = await cache.keys(); + console.error("Cache MISS for index.html! Dump of cache keys containing 'index.html':"); + for (const req of keys) { + if (req.url.includes("index.html")) { + console.error("In cache:", req.url); + } + } + } + if (event.request.url.includes("/suites") && + !event.request.url.includes("default-suites.mjs") && + !event.request.url.includes("experimental-suites.mjs") && + !event.request.url.includes("suites-helper.mjs")) { + failedRequests.add(event.request.url); + return new Response("Not found in cache", { + status: 404, + statusText: "Not Found", + headers: { + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin" + } + }); + } + } + + try { + return await fetch(event.request); + } catch (error) { + console.error(`SW fetch failed for: ${event.request.url}`, error); + return new Response("Network error or aborted request", { status: 503 }); + } })() ); }); From 8b1ac810ad7a5266303d1f2d367ae45be97927e2 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 14:51:15 +0200 Subject: [PATCH 32/69] Revert overzealous SW fetch blocking that broke suites-experimental loading --- sw.mjs | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/sw.mjs b/sw.mjs index acdd7b58c..bb8215609 100644 --- a/sw.mjs +++ b/sw.mjs @@ -311,21 +311,20 @@ self.addEventListener("fetch", (event) => { if (cachedResponse) return cachedResponse; - if (event.request.url.includes("/suites")) { - if (event.request.url.includes("perf.webkit.org/public/v3/index.html")) { - const cache = await caches.open(CACHE_NAME); - const keys = await cache.keys(); - console.error("Cache MISS for index.html! Dump of cache keys containing 'index.html':"); - for (const req of keys) { - if (req.url.includes("index.html")) { - console.error("In cache:", req.url); - } - } - } - if (event.request.url.includes("/suites") && - !event.request.url.includes("default-suites.mjs") && - !event.request.url.includes("experimental-suites.mjs") && - !event.request.url.includes("suites-helper.mjs")) { + await ensureState(); + + if (currentState !== BENCHMARK_STATE.RUNNING) { + try { + return await fetch(event.request); + } catch (error) { + console.error(`SW fetch failed for: ${event.request.url}`, error); + return new Response("Network error or aborted request", { status: 503 }); + } + } + + for (const prefix of cachedSuitesPrefixes) { + if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { + console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); failedRequests.add(event.request.url); return new Response("Not found in cache", { status: 404, @@ -335,15 +334,9 @@ self.addEventListener("fetch", (event) => { "Cross-Origin-Opener-Policy": "same-origin" } }); - } } - - try { - return await fetch(event.request); - } catch (error) { - console.error(`SW fetch failed for: ${event.request.url}`, error); - return new Response("Network error or aborted request", { status: 503 }); - } - })() - ); + } + + return fetch(event.request); + })()); }); From b3deb0d0b17b7af82cb63ae165fca7f35e76bbeb Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 14:54:44 +0200 Subject: [PATCH 33/69] Wait for active preload to abort and force UI reset when restarting --- resources/main.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/main.mjs b/resources/main.mjs index ce9922489..ddf9cb4ab 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -108,6 +108,9 @@ export class ResourcePreloader { if (!this._sw) return; await this._sendMessageWithReply({ type: SW_MESSAGES.RESET_PRELOADING }); + if (this._activePreloadPromise) { + await this._activePreloadPromise; + } } async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { @@ -126,7 +129,9 @@ export class ResourcePreloader { return; const startTime = performance.now(); - const response = await this._sendMessageWithReply({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, onProgress, PRELOAD_TIMEOUT_MS); + this._activePreloadPromise = this._sendMessageWithReply({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, onProgress, PRELOAD_TIMEOUT_MS); + const response = await this._activePreloadPromise; + this._activePreloadPromise = null; if (response?.type === "PRELOAD_ABORTED") { return "ABORTED"; @@ -555,6 +560,10 @@ class MainBenchmarkClient { async _cacheResources(benchmarkConfigurator) { const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const clearCache = !params.isDefault(); + + if (this._state === BENCHMARK_STATE.PRELOADING) { + this._resetPreloadUI(); + } this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); this._preloadGeneration = (this._preloadGeneration || 0) + 1; From c95a1e898fea3e2ac1ceca703c590b0b6cddf198 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 14:55:44 +0200 Subject: [PATCH 34/69] Fix fetch interceptor to avoid ERR_FAILED --- sw.mjs | 57 +++++++++++++++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/sw.mjs b/sw.mjs index bb8215609..7b992a7eb 100644 --- a/sw.mjs +++ b/sw.mjs @@ -298,42 +298,35 @@ self.addEventListener("fetch", (event) => { return; } - event.respondWith( - (async () => { - let requestToMatch = event.request; - const urlObj = new URL(event.request.url); - if (urlObj.pathname.endsWith("/")) { - urlObj.pathname += "index.html"; - requestToMatch = urlObj.href; - } + event.respondWith((async () => { + let requestToMatch = event.request; + const urlObj = new URL(event.request.url); + if (urlObj.pathname.endsWith("/")) { + urlObj.pathname += "index.html"; + requestToMatch = urlObj.href; + } - const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); - if (cachedResponse) - return cachedResponse; + const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); + if (cachedResponse) { + return cachedResponse; + } await ensureState(); - if (currentState !== BENCHMARK_STATE.RUNNING) { - try { - return await fetch(event.request); - } catch (error) { - console.error(`SW fetch failed for: ${event.request.url}`, error); - return new Response("Network error or aborted request", { status: 503 }); - } - } - - for (const prefix of cachedSuitesPrefixes) { - if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { - console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); - failedRequests.add(event.request.url); - return new Response("Not found in cache", { - status: 404, - statusText: "Not Found", - headers: { - "Cross-Origin-Embedder-Policy": "require-corp", - "Cross-Origin-Opener-Policy": "same-origin" - } - }); + if (currentState === BENCHMARK_STATE.RUNNING) { + for (const prefix of cachedSuitesPrefixes) { + if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { + console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); + failedRequests.add(event.request.url); + return new Response("Not found in cache", { + status: 404, + statusText: "Not Found", + headers: { + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin" + } + }); + } } } From 44b57c711ee4cc853309f0462ff172e8802b9e98 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 14:56:30 +0200 Subject: [PATCH 35/69] Set default iterationCount to 2 --- resources/shared/params.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index 698e80f5d..87adf4719 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -8,7 +8,7 @@ export class Params { // Enable a detailed developer menu to change the current Params. developerMode = false; startAutomatically = false; - iterationCount = 10; + iterationCount = 2; suites = []; // A list of tags to filter suites tags = ["default"]; From f2d7e54ee315a8798bd9254b3a66dc4dbf7a3681 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 14:58:10 +0200 Subject: [PATCH 36/69] Revert default iterationCount --- resources/shared/params.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index 87adf4719..698e80f5d 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -8,7 +8,7 @@ export class Params { // Enable a detailed developer menu to change the current Params. developerMode = false; startAutomatically = false; - iterationCount = 2; + iterationCount = 10; suites = []; // A list of tags to filter suites tags = ["default"]; From f52913c078f85fb202d5389837de86271fe4a70b Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 14:59:32 +0200 Subject: [PATCH 37/69] Fix ReferenceError for ensureState in sw.mjs --- sw.mjs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/sw.mjs b/sw.mjs index 7b992a7eb..88a71c0be 100644 --- a/sw.mjs +++ b/sw.mjs @@ -311,25 +311,6 @@ self.addEventListener("fetch", (event) => { return cachedResponse; } - await ensureState(); - - if (currentState === BENCHMARK_STATE.RUNNING) { - for (const prefix of cachedSuitesPrefixes) { - if (event.request.url.startsWith(prefix) || event.request.referrer.startsWith(prefix)) { - console.warn(`Blocked uncached request for cached suite: ${event.request.url} (referrer: ${event.request.referrer})`); - failedRequests.add(event.request.url); - return new Response("Not found in cache", { - status: 404, - statusText: "Not Found", - headers: { - "Cross-Origin-Embedder-Policy": "require-corp", - "Cross-Origin-Opener-Policy": "same-origin" - } - }); - } - } - } - return fetch(event.request); })()); }); From 38ad1e8cebb4ff3be923f7aa84429832db0ed0fc Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 15:12:38 +0200 Subject: [PATCH 38/69] Fix preload UI synchronization and progress rendering --- resources/main.mjs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index ddf9cb4ab..f2fa7a638 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -561,10 +561,9 @@ class MainBenchmarkClient { const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const clearCache = !params.isDefault(); - if (this._state === BENCHMARK_STATE.PRELOADING) { - this._resetPreloadUI(); - } + this._resetPreloadUI(); this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); + await new Promise(resolve => requestAnimationFrame(resolve)); this._preloadGeneration = (this._preloadGeneration || 0) + 1; const currentGeneration = this._preloadGeneration; @@ -594,16 +593,26 @@ class MainBenchmarkClient { } _updateCacheProgress(progressData) { - const { loaded, total, url, suiteName } = progressData; - document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); - const progress = document.getElementById("preload-progress-completed"); - progress.max = total; - progress.value = loaded; - let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; - if (!filename && url) filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); - const labelText = suiteName ? `${suiteName}: ${filename}` : filename; - document.getElementById("preload-info-label").textContent = labelText; - document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; + this._latestProgressData = progressData; + if (this._progressUpdateScheduled) return; + this._progressUpdateScheduled = true; + + requestAnimationFrame(() => { + this._progressUpdateScheduled = false; + const data = this._latestProgressData; + if (!data) return; + const { loaded, total, url, suiteName } = data; + + document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); + const progress = document.getElementById("preload-progress-completed"); + progress.max = total; + progress.value = loaded; + let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; + if (!filename && url) filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); + const labelText = suiteName ? `${suiteName}: ${filename}` : filename; + document.getElementById("preload-info-label").textContent = labelText; + document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; + }); } _enableStartButtons() { From 83bcc5aa67adb3d891f8ab3426f70c653ae614e1 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 15:15:27 +0200 Subject: [PATCH 39/69] Fix auto-kicking of preload when it is disabled and ensure start buttons are enabled --- resources/main.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/main.mjs b/resources/main.mjs index f2fa7a638..016abd60d 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -15,6 +15,8 @@ export class ResourcePreloader { } isCached() { + if (!params.preload) + return true; return params.toSearchParams() === this._preloadParams; } @@ -553,8 +555,11 @@ class MainBenchmarkClient { async _setupResourcePreloader(benchmarkConfigurator) { await this._resourcePreloader.setup(); - if (!this._resourcePreloader.isCached()) + if (!this._resourcePreloader.isCached()) { return await this._cacheResources(benchmarkConfigurator); + } else { + this._enableStartButtons(); + } } async _cacheResources(benchmarkConfigurator) { From 8cead7226fbf1e8f8356151de7b5c3740a13b1c0 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 15:47:22 +0200 Subject: [PATCH 40/69] Fix race conditions in UI progress when restarting preload --- resources/main.mjs | 42 ++++++++++++++++------------ sw.mjs | 68 ++++++++++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 016abd60d..8f90c83d6 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -110,9 +110,9 @@ export class ResourcePreloader { if (!this._sw) return; await this._sendMessageWithReply({ type: SW_MESSAGES.RESET_PRELOADING }); - if (this._activePreloadPromise) { + if (this._activePreloadPromise) await this._activePreloadPromise; - } + } async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { @@ -135,9 +135,8 @@ export class ResourcePreloader { const response = await this._activePreloadPromise; this._activePreloadPromise = null; - if (response?.type === "PRELOAD_ABORTED") { + if (response?.type === "PRELOAD_ABORTED") return "ABORTED"; - } if (response?.type === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; @@ -245,7 +244,8 @@ class MainBenchmarkClient { await this._resourcePreloader.resetPreloading(); const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); - if (preloadResult === "ABORTED") return false; + if (preloadResult === "ABORTED") + return false; try { await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); @@ -547,7 +547,8 @@ class MainBenchmarkClient { await this._resourcePreloader.resetPreloading(); const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); - if (preloadResult === "ABORTED") return; + if (preloadResult === "ABORTED") + return; if (params.startAutomatically) this.start(); @@ -555,21 +556,21 @@ class MainBenchmarkClient { async _setupResourcePreloader(benchmarkConfigurator) { await this._resourcePreloader.setup(); - if (!this._resourcePreloader.isCached()) { + if (!this._resourcePreloader.isCached()) return await this._cacheResources(benchmarkConfigurator); - } else { + else this._enableStartButtons(); - } + } async _cacheResources(benchmarkConfigurator) { const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const clearCache = !params.isDefault(); - + this._resetPreloadUI(); this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); - await new Promise(resolve => requestAnimationFrame(resolve)); - + await new Promise((resolve) => requestAnimationFrame(resolve)); + this._preloadGeneration = (this._preloadGeneration || 0) + 1; const currentGeneration = this._preloadGeneration; const onProgress = (data) => { @@ -579,7 +580,8 @@ class MainBenchmarkClient { try { const result = await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, onProgress); - if (result === "ABORTED") return "ABORTED"; + if (result === "ABORTED") + return "ABORTED"; this._enableStartButtons(); } catch (error) { console.error("Service Worker preload failed:", error); @@ -591,6 +593,7 @@ class MainBenchmarkClient { } _resetPreloadUI() { + this._latestProgressData = null; document.getElementById("preload-progress-completed").value = 0; document.getElementById("preload-info-label").textContent = ""; document.getElementById("preload-info-progress").textContent = ""; @@ -599,21 +602,24 @@ class MainBenchmarkClient { _updateCacheProgress(progressData) { this._latestProgressData = progressData; - if (this._progressUpdateScheduled) return; + if (this._progressUpdateScheduled) + return; this._progressUpdateScheduled = true; - + requestAnimationFrame(() => { this._progressUpdateScheduled = false; const data = this._latestProgressData; - if (!data) return; + if (!data) + return; const { loaded, total, url, suiteName } = data; - + document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); const progress = document.getElementById("preload-progress-completed"); progress.max = total; progress.value = loaded; let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; - if (!filename && url) filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); + if (!filename && url) + filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); const labelText = suiteName ? `${suiteName}: ${filename}` : filename; document.getElementById("preload-info-label").textContent = labelText; document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; diff --git a/sw.mjs b/sw.mjs index 88a71c0be..3ff312311 100644 --- a/sw.mjs +++ b/sw.mjs @@ -114,7 +114,7 @@ class RequestLimiter { } catch (e) { // Individual task errors are handled by their respective promises } - await new Promise(r => setTimeout(r, 1)); + await new Promise((r) => setTimeout(r, 1)); } this.active--; } @@ -122,6 +122,7 @@ class RequestLimiter { schedule(fn) { return new Promise((resolve, reject) => { const task = () => fn().then(resolve, reject); + task.resolve = resolve; this.queue.push(task); if (this.active < this.limit) { @@ -130,6 +131,15 @@ class RequestLimiter { } }); } + + clear() { + for (const task of this.queue) { + if (task.resolve) + task.resolve(0); + } + + this.queue = []; + } } const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS); @@ -138,14 +148,15 @@ let currentPreloadEvent = null; let currentPreloadId = 0; function handleResetPreloadingMessage(event) { - if (currentPreloadEvent) { + if (currentPreloadEvent) replyToClient(currentPreloadEvent, "PRELOAD_ABORTED"); - } + currentPreloadEvent = null; currentPreloadId++; - if (event) { + requestLimiter.clear(); + if (event) replyToClient(event, SW_MESSAGES.RESET_PRELOADING); - } + } async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { @@ -175,9 +186,11 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay const total = urlsToCache.length; const promises = urlsToCache.map(async (item, index) => { - if (preloadId !== currentPreloadId) return; + if (preloadId !== currentPreloadId) + return; const size = await fetchAndCache(cache, item.url, delay * index); - if (preloadId !== currentPreloadId) return; + if (preloadId !== currentPreloadId) + return; transferredSize += size; loaded++; replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); @@ -185,15 +198,18 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay await Promise.all(promises); - if (preloadId !== currentPreloadId) return; + if (preloadId !== currentPreloadId) + return; if (!await STORE.hasLock(clientId)) { replyError(event, "Speedometer aborted: Another tab took over."); - if (currentPreloadEvent === event) currentPreloadEvent = null; + if (currentPreloadEvent === event) + currentPreloadEvent = null; return; } replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); - if (currentPreloadEvent === event) currentPreloadEvent = null; + if (currentPreloadEvent === event) + currentPreloadEvent = null; } async function parseSuiteResources(suite) { @@ -294,23 +310,23 @@ self.addEventListener("message", (event) => { }); self.addEventListener("fetch", (event) => { - if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") { + if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") return; - } - - event.respondWith((async () => { - let requestToMatch = event.request; - const urlObj = new URL(event.request.url); - if (urlObj.pathname.endsWith("/")) { - urlObj.pathname += "index.html"; - requestToMatch = urlObj.href; - } - const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); - if (cachedResponse) { - return cachedResponse; - } + event.respondWith( + (async () => { + let requestToMatch = event.request; + const urlObj = new URL(event.request.url); + if (urlObj.pathname.endsWith("/")) { + urlObj.pathname += "index.html"; + requestToMatch = urlObj.href; + } + + const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); + if (cachedResponse) + return cachedResponse; - return fetch(event.request); - })()); + return fetch(event.request); + })() + ); }); From fb4548292932f2307d1c2d61c7076f102411fbea Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 15:48:15 +0200 Subject: [PATCH 41/69] Fix ESLint errors in main.mjs --- resources/main.mjs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 8f90c83d6..9ebb2ee1d 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -145,6 +145,7 @@ export class ResourcePreloader { console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`); } this._preloadParams = params.toSearchParams(); + return undefined; } } @@ -556,11 +557,12 @@ class MainBenchmarkClient { async _setupResourcePreloader(benchmarkConfigurator) { await this._resourcePreloader.setup(); - if (!this._resourcePreloader.isCached()) - return await this._cacheResources(benchmarkConfigurator); - else + if (!this._resourcePreloader.isCached()) { + return this._cacheResources(benchmarkConfigurator); + } else { this._enableStartButtons(); - + } + return undefined; } async _cacheResources(benchmarkConfigurator) { @@ -583,6 +585,7 @@ class MainBenchmarkClient { if (result === "ABORTED") return "ABORTED"; this._enableStartButtons(); + return undefined; } catch (error) { console.error("Service Worker preload failed:", error); this._setBenchmarkState(BENCHMARK_STATE.ERROR); @@ -639,10 +642,10 @@ class MainBenchmarkClient { const startButtons = document.querySelectorAll(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { this._resetPreloadUI(); - startButtons.forEach((btn) => btn.innerHTML = "
Preloading
"); + startButtons.forEach((btn) => { btn.innerHTML = "
Preloading
"; }); } else if (state !== BENCHMARK_STATE.RUNNING) { document.body.style.removeProperty("--preload-progress"); - startButtons.forEach((btn) => btn.innerHTML = "
Start Test
"); + startButtons.forEach((btn) => { btn.innerHTML = "
Start Test
"; }); if (state !== BENCHMARK_STATE.READY) await this._resourcePreloader?.clearSw(); } From 70f82c48e188b4fd29ed71c5a16b591448812441 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 16:15:20 +0200 Subject: [PATCH 42/69] fx --- resources/main.mjs | 19 ++-- suites/default-suites.mjs | 2 +- suites/perf.webkit.org/resources.txt | 144 +++++++++++++++++++++++++++ sw.mjs | 1 - 4 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 suites/perf.webkit.org/resources.txt diff --git a/resources/main.mjs b/resources/main.mjs index 9ebb2ee1d..673ec5085 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -112,12 +112,11 @@ export class ResourcePreloader { await this._sendMessageWithReply({ type: SW_MESSAGES.RESET_PRELOADING }); if (this._activePreloadPromise) await this._activePreloadPromise; - } async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { if (!this._sw || suites.length === 0) - return; + return undefined; const suitesData = suites .filter((s) => s.resources) @@ -128,7 +127,7 @@ export class ResourcePreloader { })); if (suitesData.length === 0) - return; + return undefined; const startTime = performance.now(); this._activePreloadPromise = this._sendMessageWithReply({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, onProgress, PRELOAD_TIMEOUT_MS); @@ -557,11 +556,11 @@ class MainBenchmarkClient { async _setupResourcePreloader(benchmarkConfigurator) { await this._resourcePreloader.setup(); - if (!this._resourcePreloader.isCached()) { + if (!this._resourcePreloader.isCached()) return this._cacheResources(benchmarkConfigurator); - } else { + else this._enableStartButtons(); - } + return undefined; } @@ -642,10 +641,14 @@ class MainBenchmarkClient { const startButtons = document.querySelectorAll(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { this._resetPreloadUI(); - startButtons.forEach((btn) => { btn.innerHTML = "
Preloading
"; }); + startButtons.forEach((btn) => { + btn.innerHTML = "
Preloading
"; + }); } else if (state !== BENCHMARK_STATE.RUNNING) { document.body.style.removeProperty("--preload-progress"); - startButtons.forEach((btn) => { btn.innerHTML = "
Start Test
"; }); + startButtons.forEach((btn) => { + btn.innerHTML = "
Start Test
"; + }); if (state !== BENCHMARK_STATE.READY) await this._resourcePreloader?.clearSw(); } diff --git a/suites/default-suites.mjs b/suites/default-suites.mjs index a4091e7b2..7ffede005 100644 --- a/suites/default-suites.mjs +++ b/suites/default-suites.mjs @@ -1007,7 +1007,7 @@ export const DefaultSuites = freezeSuites([ { name: "Perf-Dashboard", url: "suites/perf.webkit.org/public/v3/index.html#/charts/?since=1678991819934&paneList=((55-1974-null-null-(5-2.5-500)))", - resources: "suites/perf.webkit.org/public/v3/resources.txt", + resources: "suites/perf.webkit.org/resources.txt", tags: ["default", "chart", "webcomponents"], async prepare(page) { await page.waitForElement("#app-is-ready"); diff --git a/suites/perf.webkit.org/resources.txt b/suites/perf.webkit.org/resources.txt new file mode 100644 index 000000000..661dcb374 --- /dev/null +++ b/suites/perf.webkit.org/resources.txt @@ -0,0 +1,144 @@ +public/api/analysis-tasks-platform=55-metric=1006 +public/api/analysis-tasks-platform=55-metric=1407 +public/api/analysis-tasks-platform=55-metric=1648 +public/api/analysis-tasks-platform=55-metric=1649 +public/api/analysis-tasks-platform=55-metric=1974 +public/api/measurement-set-platform=16-metric=1 +public/api/measurement-set-platform=16-metric=13 +public/api/measurement-set-platform=16-metric=17 +public/api/measurement-set-platform=16-metric=1726 +public/api/measurement-set-platform=16-metric=6 +public/api/measurement-set-platform=16-metric=7 +public/api/measurement-set-platform=55-metric=1 +public/api/measurement-set-platform=55-metric=1006 +public/api/measurement-set-platform=55-metric=13 +public/api/measurement-set-platform=55-metric=1407 +public/api/measurement-set-platform=55-metric=1648 +public/api/measurement-set-platform=55-metric=1649 +public/api/measurement-set-platform=55-metric=17 +public/api/measurement-set-platform=55-metric=1726 +public/api/measurement-set-platform=55-metric=1974 +public/api/measurement-set-platform=55-metric=6 +public/api/measurement-set-platform=55-metric=7 +public/common.css +public/data/manifest.json +public/data/measurement-set-16-1-1682812800000.json +public/data/measurement-set-16-1.json +public/data/measurement-set-16-13-1682812800000.json +public/data/measurement-set-16-13.json +public/data/measurement-set-16-17-1682812800000.json +public/data/measurement-set-16-17.json +public/data/measurement-set-16-1726-1682812800000.json +public/data/measurement-set-16-1726.json +public/data/measurement-set-16-6-1682812800000.json +public/data/measurement-set-16-6.json +public/data/measurement-set-16-7-1682812800000.json +public/data/measurement-set-16-7.json +public/data/measurement-set-55-1-1682812800000.json +public/data/measurement-set-55-1.json +public/data/measurement-set-55-13-1682812800000.json +public/data/measurement-set-55-13.json +public/data/measurement-set-55-1407-1682812800000.json +public/data/measurement-set-55-1407.json +public/data/measurement-set-55-1648-1682812800000.json +public/data/measurement-set-55-1648.json +public/data/measurement-set-55-1649-1682812800000.json +public/data/measurement-set-55-1649.json +public/data/measurement-set-55-17-1682812800000.json +public/data/measurement-set-55-17.json +public/data/measurement-set-55-1726-1682812800000.json +public/data/measurement-set-55-1726.json +public/data/measurement-set-55-1974-1682812800000.json +public/data/measurement-set-55-1974.json +public/data/measurement-set-55-6-1682812800000.json +public/data/measurement-set-55-6.json +public/data/measurement-set-55-7-1682812800000.json +public/data/measurement-set-55-7.json +public/shared/common-component-base.js +public/shared/common-remote.js +public/shared/statistics.js +public/v3/async-task.js +public/v3/bundled-scripts.js +public/v3/commit-set-range-bisector.js +public/v3/components/analysis-results-viewer.js +public/v3/components/analysis-task-bug-list.js +public/v3/components/bar-graph-group.js +public/v3/components/base.js +public/v3/components/button-base.js +public/v3/components/chart-pane-base.js +public/v3/components/chart-revision-range.js +public/v3/components/chart-status-evaluator.js +public/v3/components/chart-styles.js +public/v3/components/close-button.js +public/v3/components/combo-box.js +public/v3/components/commit-log-viewer.js +public/v3/components/custom-analysis-task-configurator.js +public/v3/components/custom-configuration-test-group-form.js +public/v3/components/customizable-test-group-form.js +public/v3/components/dashboard-chart-status-view.js +public/v3/components/editable-text.js +public/v3/components/expand-collapse-button.js +public/v3/components/freshness-indicator.js +public/v3/components/instant-file-uploader.js +public/v3/components/interactive-time-series-chart.js +public/v3/components/minus-button.js +public/v3/components/mutable-list-view.js +public/v3/components/owned-commit-viewer.js +public/v3/components/pane-selector.js +public/v3/components/plus-button.js +public/v3/components/ratio-bar-graph.js +public/v3/components/repetition-type-selection.js +public/v3/components/results-table.js +public/v3/components/spinner-icon.js +public/v3/components/test-group-form.js +public/v3/components/test-group-results-viewer.js +public/v3/components/test-group-revision-table.js +public/v3/components/time-series-chart.js +public/v3/components/warning-icon.js +public/v3/index.html +public/v3/instrumentation.js +public/v3/lazily-evaluated-function.js +public/v3/main.js +public/v3/models/analysis-results.js +public/v3/models/analysis-task.js +public/v3/models/bug-tracker.js +public/v3/models/bug.js +public/v3/models/build-request.js +public/v3/models/builder.js +public/v3/models/commit-log.js +public/v3/models/commit-set.js +public/v3/models/data-model.js +public/v3/models/manifest.js +public/v3/models/measurement-adaptor.js +public/v3/models/measurement-cluster.js +public/v3/models/measurement-set.js +public/v3/models/metric.js +public/v3/models/platform-group.js +public/v3/models/platform.js +public/v3/models/repository.js +public/v3/models/test-group.js +public/v3/models/test.js +public/v3/models/time-series.js +public/v3/models/triggerable.js +public/v3/models/uploaded-file.js +public/v3/pages/analysis-category-page.js +public/v3/pages/analysis-category-toolbar.js +public/v3/pages/analysis-task-page.js +public/v3/pages/build-request-queue-page.js +public/v3/pages/chart-pane-status-view.js +public/v3/pages/chart-pane.js +public/v3/pages/charts-page.js +public/v3/pages/charts-toolbar.js +public/v3/pages/create-analysis-task-page.js +public/v3/pages/dashboard-page.js +public/v3/pages/dashboard-toolbar.js +public/v3/pages/domain-control-toolbar.js +public/v3/pages/heading.js +public/v3/pages/page-router.js +public/v3/pages/page-with-heading.js +public/v3/pages/page.js +public/v3/pages/summary-page.js +public/v3/pages/test-freshness-page.js +public/v3/pages/toolbar.js +public/v3/privileged-api.js +public/v3/remote.js diff --git a/sw.mjs b/sw.mjs index 3ff312311..6ea584e49 100644 --- a/sw.mjs +++ b/sw.mjs @@ -156,7 +156,6 @@ function handleResetPreloadingMessage(event) { requestLimiter.clear(); if (event) replyToClient(event, SW_MESSAGES.RESET_PRELOADING); - } async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { From d6ade9788edf71fcdb5c9ae310a33f843835b808 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Thu, 16 Jul 2026 17:05:22 +0200 Subject: [PATCH 43/69] fx --- resources/main.mjs | 28 +++++--- sw.mjs | 170 ++++++++++++++++++++++----------------------- 2 files changed, 103 insertions(+), 95 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 673ec5085..f33fbf005 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -12,6 +12,7 @@ export class ResourcePreloader { this._registration = null; this._sw = null; this._preloadParams = ""; + this._clientId = typeof crypto?.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).substring(2) + Date.now().toString(36); } isCached() { @@ -21,9 +22,8 @@ export class ResourcePreloader { } async setup() { - await this._unregisterOldServiceWorkers(); - if (!params.preload) { + await this._unregisterOldServiceWorkers(); this._registration = null; this._sw = null; return false; @@ -58,13 +58,20 @@ export class ResourcePreloader { if (!this._sw) return Promise.resolve(); + messageData.clientId = this._clientId; + return new Promise((resolve, reject) => { const channel = new MessageChannel(); const port = channel.port1; let timeoutId = null; + // Prevent GC in Safari + this._activeChannels = this._activeChannels || new Set(); + this._activeChannels.add(channel); + const cleanup = () => { port.onmessage = null; + this._activeChannels.delete(channel); if (timeoutId) clearTimeout(timeoutId); }; @@ -95,6 +102,7 @@ export class ResourcePreloader { resolve(data); }; + port.start(); this._sw.postMessage(messageData, [channel.port2]); }); @@ -115,9 +123,6 @@ export class ResourcePreloader { } async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { - if (!this._sw || suites.length === 0) - return undefined; - const suitesData = suites .filter((s) => s.resources) .map((s) => ({ @@ -126,7 +131,7 @@ export class ResourcePreloader { resources: new URL(s.resources, window.location.href).href, })); - if (suitesData.length === 0) + if (!this._sw || suitesData.length === 0) return undefined; const startTime = performance.now(); @@ -546,9 +551,13 @@ class MainBenchmarkClient { } await this._resourcePreloader.resetPreloading(); - const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); - if (preloadResult === "ABORTED") - return; + if (params.developerMode) { + this._enableStartButtons(); + } else { + const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); + if (preloadResult === "ABORTED") + return; + } if (params.startAutomatically) this.start(); @@ -596,6 +605,7 @@ class MainBenchmarkClient { _resetPreloadUI() { this._latestProgressData = null; + this._progressUpdateScheduled = false; document.getElementById("preload-progress-completed").value = 0; document.getElementById("preload-info-label").textContent = ""; document.getElementById("preload-info-progress").textContent = ""; diff --git a/sw.mjs b/sw.mjs index 6ea584e49..8365dbff7 100644 --- a/sw.mjs +++ b/sw.mjs @@ -31,48 +31,32 @@ class LockStore { return this.dbPromise; } - async getOwner() { + async _runTransaction(mode, callback) { try { const db = await this._openDB(); - const data = await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readonly"); - const req = tx.objectStore(STORE_NAME).get("stateData"); + return await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, mode); + const req = callback(tx.objectStore(STORE_NAME)); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); - return data?.clientId || null; } catch (e) { - console.warn("IndexedDB read failed", e); + console.warn(`IndexedDB ${mode} failed`, e); return null; } } + async getOwner() { + const data = await this._runTransaction("readonly", (store) => store.get("stateData")); + return data?.clientId || null; + } + async setOwner(clientId) { - try { - const db = await this._openDB(); - await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readwrite"); - const req = tx.objectStore(STORE_NAME).put({ clientId }, "stateData"); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); - } catch (e) { - console.warn("IndexedDB write failed", e); - } + await this._runTransaction("readwrite", (store) => store.put({ clientId }, "stateData")); } async clear() { - try { - const db = await this._openDB(); - await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readwrite"); - const req = tx.objectStore(STORE_NAME).delete("stateData"); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); - } catch (e) { - console.warn("IndexedDB clear failed", e); - } + await this._runTransaction("readwrite", (store) => store.delete("stateData")); } async hasLock(clientId) { @@ -86,7 +70,10 @@ class LockStore { const STORE = new LockStore(); function replyToClient(event, type, msg = {}) { - event.ports?.[0]?.postMessage({ type, ...msg }); + if (event.ports?.[0]) { + event.ports[0].start?.(); + event.ports[0].postMessage({ type, ...msg }); + } } function replyError(event, message) { @@ -114,7 +101,7 @@ class RequestLimiter { } catch (e) { // Individual task errors are handled by their respective promises } - await new Promise((r) => setTimeout(r, 1)); + await delayAsync(1); // Yield to event loop } this.active--; } @@ -127,18 +114,16 @@ class RequestLimiter { if (this.active < this.limit) { this.active++; - this._processQueue(); + this._processQueue(); // Intentionally not awaited } }); } clear() { - for (const task of this.queue) { - if (task.resolve) - task.resolve(0); - } + for (const task of this.queue) + task.resolve?.(0); - this.queue = []; + this.queue.length = 0; } } @@ -159,56 +144,64 @@ function handleResetPreloadingMessage(event) { } async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { - await STORE.getOwner(); - await updateActiveClient(clientId); - - handleResetPreloadingMessage(); // Call it without event to avoid replying to the PRELOAD_SUITES channel! - - currentPreloadEvent = event; - const preloadId = currentPreloadId; - - failedRequests.clear(); - - if (clearCache) - await caches.delete(CACHE_NAME); - - const cache = await caches.open(CACHE_NAME); - let loaded = 0; - let transferredSize = 0; - const urlsToCache = []; + try { + await STORE.getOwner(); + await updateActiveClient(clientId); + + handleResetPreloadingMessage(); // Call it without event to avoid replying to the PRELOAD_SUITES channel! + + currentPreloadEvent = event; + const preloadId = currentPreloadId; + + failedRequests.clear(); + if (clearCache) + await caches.delete(CACHE_NAME); + + const cache = await caches.open(CACHE_NAME); + const urlsToCache = await getUrlsToCache(suites); + const total = urlsToCache.length; + + let loaded = 0; + let transferredSize = 0; + + const promises = urlsToCache.map(async (item, index) => { + if (preloadId !== currentPreloadId) + return; + const size = await fetchAndCache(cache, item.url, delay * index); + if (preloadId !== currentPreloadId) + return; + transferredSize += size; + loaded++; + replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); + }); - for (const suite of suites) { - if (!suite.resources) - continue; - urlsToCache.push(...await parseSuiteResources(suite)); - } + await Promise.all(promises); - const total = urlsToCache.length; - const promises = urlsToCache.map(async (item, index) => { - if (preloadId !== currentPreloadId) - return; - const size = await fetchAndCache(cache, item.url, delay * index); if (preloadId !== currentPreloadId) return; - transferredSize += size; - loaded++; - replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); - }); - - await Promise.all(promises); - if (preloadId !== currentPreloadId) - return; - - if (!await STORE.hasLock(clientId)) { - replyError(event, "Speedometer aborted: Another tab took over."); + if (!await STORE.hasLock(clientId)) { + replyError(event, "Speedometer aborted: Another tab took over."); + return; + } + replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); + } catch (error) { + console.error("Error during preload:", error); + replyError(event, error.message || "Failed to preload resources."); + } finally { if (currentPreloadEvent === event) currentPreloadEvent = null; - return; } - replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); - if (currentPreloadEvent === event) - currentPreloadEvent = null; +} + +async function getUrlsToCache(suites) { + const urlsToCache = []; + for (const suite of suites) { + if (suite.resources) + urlsToCache.push(...await parseSuiteResources(suite)); + } + + return urlsToCache; } async function parseSuiteResources(suite) { @@ -237,7 +230,7 @@ async function fetchAndCache(cache, url, delayMs) { await delayAsync(delayMs); return requestLimiter.schedule(async () => { const request = new Request(url, { cache: "no-cache" }); - const existing = await cache.match(request, { ignoreSearch: true, ignoreVary: true }); + const existing = await cache.match(request, { ignoreSearch: true }); if (existing) return getResponseSize(existing); @@ -278,12 +271,16 @@ async function updateActiveClient(newClientId) { } async function handleClearCacheMessage(event, clientId) { - if (!await STORE.hasLock(clientId)) { - replyError(event, "Cannot clear SW: You do not own the lock."); - return; + try { + if (!await STORE.hasLock(clientId)) { + replyError(event, "Cannot clear SW: You do not own the lock."); + return; + } + await STORE.clear(); + replyToClient(event, "SUCCESS"); + } catch (e) { + replyError(event, e.message || "Failed to clear cache"); } - await STORE.clear(); - replyToClient(event, "SUCCESS"); } async function handleGetFailedRequestsMessage(event, clientId) { @@ -301,9 +298,10 @@ self.addEventListener("message", (event) => { const { data } = event; if (!data) return; + const clientId = data.clientId || event.source?.id; const handler = MESSAGE_HANDLERS[data.type]; if (handler) - event.waitUntil(handler(event, event.source?.id, data)); + event.waitUntil(handler(event, clientId, data)); else console.error("Unknown service worker message type:", data.type); }); @@ -321,7 +319,7 @@ self.addEventListener("fetch", (event) => { requestToMatch = urlObj.href; } - const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true }); + const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true }); if (cachedResponse) return cachedResponse; From 6f0ef1eac083a2f4faf640aabc8c2f84f6516e0f Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 14:23:04 +0200 Subject: [PATCH 44/69] cleanup --- resources/main.css | 1 + resources/main.mjs | 190 ++++++++++++++++++------------- resources/shared/sw-messages.mjs | 5 + sw.mjs | 27 +++-- 4 files changed, 138 insertions(+), 85 deletions(-) diff --git a/resources/main.css b/resources/main.css index 41678b76a..1c863acc2 100644 --- a/resources/main.css +++ b/resources/main.css @@ -970,6 +970,7 @@ section#about .note { [data-benchmark-state="PRELOADING"] { cursor: wait; + pointer-events: none; } [data-benchmark-state="PRELOADING"] #preload-progress, diff --git a/resources/main.mjs b/resources/main.mjs index f33fbf005..0c2e6f6a4 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -12,7 +12,8 @@ export class ResourcePreloader { this._registration = null; this._sw = null; this._preloadParams = ""; - this._clientId = typeof crypto?.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).substring(2) + Date.now().toString(36); + this._clientId = null; + this._pendingRequest = null; } isCached() { @@ -32,6 +33,9 @@ export class ResourcePreloader { await this._registerServiceWorker(); this._setupMessageListener(); + const response = await this._sendMessageWithReply({ type: SW_MESSAGES.GET_CLIENT_ID }, null, 5000); + this._clientId = response.clientId; + return true; } @@ -48,63 +52,67 @@ export class ResourcePreloader { } _setupMessageListener() { - navigator.serviceWorker.addEventListener("message", (event) => { - if (event.data?.type === SW_MESSAGES.FATAL_ERROR) - globalThis.benchmarkClient?.handleError(new Error(event.data.message)); - }); + navigator.serviceWorker.addEventListener("message", this.onMessage.bind(this)); + } + + onMessage(event) { + const data = event.data || {}; + + if (data.type === SW_MESSAGES.FATAL_ERROR) { + globalThis.benchmarkClient?.handleError(new Error(data.message)); + return; + } + + if (this._pendingRequest && this._pendingRequest.type === data.type) { + const { resolve, reject, onProgress, timeoutId } = this._pendingRequest; + + if (data.status === SW_MESSAGES.PRELOAD_PROGRESS) { + onProgress?.(data); + return; + } + + if (timeoutId) + clearTimeout(timeoutId); + this._pendingRequest = null; + + if (data.status === SW_MESSAGES.ERROR) + reject(new Error(data.message || "Unknown SW Error")); + else if (data.status === SW_MESSAGES.PRELOAD_ABORTED) + resolve({ status: SW_MESSAGES.PRELOAD_ABORTED }); + else + resolve(data); + + } } _sendMessageWithReply(messageData, onProgress, timeoutMs = 0) { if (!this._sw) return Promise.resolve(); - messageData.clientId = this._clientId; + if (this._clientId) + messageData.clientId = this._clientId; + + if (this._pendingRequest) { + console.warn(`Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`); + this._pendingRequest.reject(new Error("Overridden by new request")); + this._pendingRequest = null; + } return new Promise((resolve, reject) => { - const channel = new MessageChannel(); - const port = channel.port1; let timeoutId = null; - // Prevent GC in Safari - this._activeChannels = this._activeChannels || new Set(); - this._activeChannels.add(channel); - - const cleanup = () => { - port.onmessage = null; - this._activeChannels.delete(channel); - if (timeoutId) - clearTimeout(timeoutId); - }; - if (timeoutMs > 0) { timeoutId = setTimeout(() => { - cleanup(); - console.error(`Service worker message timed out: ${messageData.type}`); - resolve(); // Resolve to not block execution + if (this._pendingRequest?.type === messageData.type) { + this._pendingRequest = null; + console.error(`Service worker message timed out: ${messageData.type}`); + resolve({}); // Resolve to not block execution + } }, timeoutMs); } - port.onmessage = (event) => { - const data = event.data || {}; - if (data.type === SW_MESSAGES.PRELOAD_PROGRESS) { - onProgress?.(data); - return; - } - cleanup(); - if (data.type === "PRELOAD_ABORTED") { - resolve({ type: "PRELOAD_ABORTED" }); - return; - } - if (data.type === "ERROR") { - reject(new Error(data.message || "Unknown SW Error")); - return; - } - - resolve(data); - }; - port.start(); - - this._sw.postMessage(messageData, [channel.port2]); + this._pendingRequest = { type: messageData.type, resolve, reject, onProgress, timeoutId }; + this._sw.postMessage(messageData); }); } @@ -123,6 +131,8 @@ export class ResourcePreloader { } async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { + if (!this._sw) + return undefined; const suitesData = suites .filter((s) => s.resources) .map((s) => ({ @@ -131,7 +141,7 @@ export class ResourcePreloader { resources: new URL(s.resources, window.location.href).href, })); - if (!this._sw || suitesData.length === 0) + if (suitesData.length === 0) return undefined; const startTime = performance.now(); @@ -139,10 +149,10 @@ export class ResourcePreloader { const response = await this._activePreloadPromise; this._activePreloadPromise = null; - if (response?.type === "PRELOAD_ABORTED") + if (response.type === SW_MESSAGES.PRELOAD_ABORTED) return "ABORTED"; - if (response?.type === SW_MESSAGES.PRELOAD_DONE) { + if (response.type === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); const timeSec = (timeTakenMs / 1000).toFixed(2); @@ -162,6 +172,58 @@ const BENCHMARK_STATE = Object.freeze({ ERROR: "ERROR", }); +class PreloadStatusUpdater { + constructor() { + this._latestData = null; + this._rafId = null; + this._updateFn = this._updateUI.bind(this); + + this._progressCompletedElement = document.getElementById("preload-progress-completed"); + this._infoLabelElement = document.getElementById("preload-info-label"); + this._infoProgressElement = document.getElementById("preload-info-progress"); + } + + update(data) { + this._latestData = data; + if (!this._rafId) + this._rafId = requestAnimationFrame(this._updateFn); + } + + _updateUI() { + this._rafId = null; + const data = this._latestData; + if (!data) + return; + + const { loaded, total, url, suiteName } = data; + document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); + this._progressCompletedElement.max = total; + this._progressCompletedElement.value = loaded; + let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; + if (!filename && url) + filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); + const labelText = suiteName ? `${suiteName}: ${filename}` : filename; + this._infoLabelElement.textContent = labelText; + this._infoProgressElement.textContent = `${loaded} / ${total}`; + } + + cancel() { + if (this._rafId) { + cancelAnimationFrame(this._rafId); + this._rafId = null; + } + this._latestData = null; + } + + resetUI() { + this.cancel(); + this._progressCompletedElement.value = 0; + this._infoLabelElement.textContent = ""; + this._infoProgressElement.textContent = ""; + document.body.style.setProperty("--preload-progress", "0%"); + } +} + // FIXME(camillobruni): Add base class class MainBenchmarkClient { developerMode = false; @@ -174,6 +236,7 @@ class MainBenchmarkClient { _hasResults = false; _metrics = Object.create(null); _resourcePreloader = new ResourcePreloader(); + _preloadStatusUpdater = new PreloadStatusUpdater(); _steppingPromise = null; _steppingResolver = null; _benchmarkConfiguratorPromise = null; @@ -318,7 +381,7 @@ class MainBenchmarkClient { this._isRunning = false; const response = await this._resourcePreloader._sendMessageWithReply({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); - if (response?.requests && response.requests.length > 0) { + if (response.requests && response.requests.length > 0) { console.warn("The following requests failed during the benchmark and bypassed the cache:"); console.warn(response.requests.join("\n")); alert("Warning: The benchmark had missing resources that were not cached. Check the console for details."); @@ -590,11 +653,13 @@ class MainBenchmarkClient { try { const result = await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, onProgress); + this._preloadStatusUpdater.cancel(); if (result === "ABORTED") return "ABORTED"; this._enableStartButtons(); return undefined; } catch (error) { + this._preloadStatusUpdater.cancel(); console.error("Service Worker preload failed:", error); this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._populateInvalidScore(error?.message); @@ -604,38 +669,11 @@ class MainBenchmarkClient { } _resetPreloadUI() { - this._latestProgressData = null; - this._progressUpdateScheduled = false; - document.getElementById("preload-progress-completed").value = 0; - document.getElementById("preload-info-label").textContent = ""; - document.getElementById("preload-info-progress").textContent = ""; - document.body.style.setProperty("--preload-progress", "0%"); + this._preloadStatusUpdater.resetUI(); } _updateCacheProgress(progressData) { - this._latestProgressData = progressData; - if (this._progressUpdateScheduled) - return; - this._progressUpdateScheduled = true; - - requestAnimationFrame(() => { - this._progressUpdateScheduled = false; - const data = this._latestProgressData; - if (!data) - return; - const { loaded, total, url, suiteName } = data; - - document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); - const progress = document.getElementById("preload-progress-completed"); - progress.max = total; - progress.value = loaded; - let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; - if (!filename && url) - filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); - const labelText = suiteName ? `${suiteName}: ${filename}` : filename; - document.getElementById("preload-info-label").textContent = labelText; - document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`; - }); + this._preloadStatusUpdater.update(progressData); } _enableStartButtons() { diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs index 0ec39ff53..8f326513a 100644 --- a/resources/shared/sw-messages.mjs +++ b/resources/shared/sw-messages.mjs @@ -2,9 +2,14 @@ export const SW_MESSAGES = Object.freeze({ PRELOAD_SUITES: "PRELOAD_SUITES", PRELOAD_PROGRESS: "PRELOAD_PROGRESS", PRELOAD_DONE: "PRELOAD_DONE", + PRELOAD_ABORTED: "PRELOAD_ABORTED", RESET_PRELOADING: "RESET_PRELOADING", FATAL_ERROR: "FATAL_ERROR", + ERROR: "ERROR", + SUCCESS: "SUCCESS", CLEAR_CACHE: "CLEAR_CACHE", GET_FAILED_REQUESTS: "GET_FAILED_REQUESTS", FAILED_REQUESTS: "FAILED_REQUESTS", + GET_CLIENT_ID: "GET_CLIENT_ID", + CLIENT_ID: "CLIENT_ID", }); diff --git a/sw.mjs b/sw.mjs index 8365dbff7..f0cf568b0 100644 --- a/sw.mjs +++ b/sw.mjs @@ -69,15 +69,13 @@ class LockStore { const STORE = new LockStore(); -function replyToClient(event, type, msg = {}) { - if (event.ports?.[0]) { - event.ports[0].start?.(); - event.ports[0].postMessage({ type, ...msg }); - } +function replyToClient(event, status, msg = {}) { + const type = event.data?.type; + event.source?.postMessage({ type, status, ...msg }); } function replyError(event, message) { - replyToClient(event, "ERROR", { message }); + replyToClient(event, SW_MESSAGES.ERROR, { message }); } function delayAsync(ms) { @@ -134,7 +132,7 @@ let currentPreloadId = 0; function handleResetPreloadingMessage(event) { if (currentPreloadEvent) - replyToClient(currentPreloadEvent, "PRELOAD_ABORTED"); + replyToClient(currentPreloadEvent, SW_MESSAGES.PRELOAD_ABORTED); currentPreloadEvent = null; currentPreloadId++; @@ -277,7 +275,7 @@ async function handleClearCacheMessage(event, clientId) { return; } await STORE.clear(); - replyToClient(event, "SUCCESS"); + replyToClient(event, SW_MESSAGES.SUCCESS); } catch (e) { replyError(event, e.message || "Failed to clear cache"); } @@ -287,11 +285,17 @@ async function handleGetFailedRequestsMessage(event, clientId) { replyToClient(event, SW_MESSAGES.FAILED_REQUESTS, { requests: Array.from(failedRequests) }); } +async function handleGetClientIdMessage(event, clientId) { + const id = event.source?.id || clientId || crypto.randomUUID(); + replyToClient(event, SW_MESSAGES.CLIENT_ID, { clientId: id }); +} + const MESSAGE_HANDLERS = Object.freeze({ [SW_MESSAGES.PRELOAD_SUITES]: handlePreloadSuitesMessage, [SW_MESSAGES.RESET_PRELOADING]: handleResetPreloadingMessage, [SW_MESSAGES.CLEAR_CACHE]: handleClearCacheMessage, [SW_MESSAGES.GET_FAILED_REQUESTS]: handleGetFailedRequestsMessage, + [SW_MESSAGES.GET_CLIENT_ID]: handleGetClientIdMessage, }); self.addEventListener("message", (event) => { @@ -323,7 +327,12 @@ self.addEventListener("fetch", (event) => { if (cachedResponse) return cachedResponse; - return fetch(event.request); + try { + return await fetch(event.request); + } catch (error) { + failedRequests.add(event.request.url); + return Response.error(); + } })() ); }); From 32f522894c497bdafb3aa5a8dac6879fc2f7f7a4 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 15:30:41 +0200 Subject: [PATCH 45/69] cleaner-shutdown --- resources/main.mjs | 234 +++++++++++++++++++++++---------------------- 1 file changed, 118 insertions(+), 116 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 0c2e6f6a4..a4a65f2d4 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -14,6 +14,7 @@ export class ResourcePreloader { this._preloadParams = ""; this._clientId = null; this._pendingRequest = null; + this._onPreloadProgress = null; } isCached() { @@ -25,24 +26,20 @@ export class ResourcePreloader { async setup() { if (!params.preload) { await this._unregisterOldServiceWorkers(); - this._registration = null; - this._sw = null; - return false; + return; } await this._registerServiceWorker(); this._setupMessageListener(); - - const response = await this._sendMessageWithReply({ type: SW_MESSAGES.GET_CLIENT_ID }, null, 5000); - this._clientId = response.clientId; - - return true; + this._clientId = await this._getClientId(); } async _unregisterOldServiceWorkers() { const existingRegistrations = await navigator.serviceWorker.getRegistrations(); for (const existing of existingRegistrations) await existing.unregister(); + this._registration = null; + this._sw = null; } async _registerServiceWorker() { @@ -51,10 +48,26 @@ export class ResourcePreloader { this._sw = navigator.serviceWorker.controller || this._registration.active; } + async _getClientId() { + const response = await this._postMessage({ type: SW_MESSAGES.GET_CLIENT_ID }, 5000); + const clientId = response.clientId; + if (!clientId) + throw new Error("Could not retrieve service worker client id"); + return clientId; + } + _setupMessageListener() { navigator.serviceWorker.addEventListener("message", this.onMessage.bind(this)); } + _validateMessageRouting(data) { + if (!this._pendingRequest) + throw new Error(`Received unexpected ServiceWorker message without any pending request: ${data.type}`); + + if (this._pendingRequest.type !== data.type) + throw new Error(`Received ServiceWorker message type '${data.type}' but expected '${this._pendingRequest.type}'`); + } + onMessage(event) { const data = event.data || {}; @@ -63,29 +76,26 @@ export class ResourcePreloader { return; } - if (this._pendingRequest && this._pendingRequest.type === data.type) { - const { resolve, reject, onProgress, timeoutId } = this._pendingRequest; + this._validateMessageRouting(data); - if (data.status === SW_MESSAGES.PRELOAD_PROGRESS) { - onProgress?.(data); - return; - } + const { resolve, reject, timeoutId } = this._pendingRequest; - if (timeoutId) - clearTimeout(timeoutId); - this._pendingRequest = null; + if (data.status === SW_MESSAGES.PRELOAD_PROGRESS) { + this._onPreloadProgress?.(data); + return; + } - if (data.status === SW_MESSAGES.ERROR) - reject(new Error(data.message || "Unknown SW Error")); - else if (data.status === SW_MESSAGES.PRELOAD_ABORTED) - resolve({ status: SW_MESSAGES.PRELOAD_ABORTED }); - else - resolve(data); + if (timeoutId) + clearTimeout(timeoutId); + this._pendingRequest = null; - } + if (data.status === SW_MESSAGES.ERROR) + reject(new Error(data.message || "Unknown SW Error")); + else + resolve(data); } - _sendMessageWithReply(messageData, onProgress, timeoutMs = 0) { + _postMessage(messageData, timeoutMs = 0) { if (!this._sw) return Promise.resolve(); @@ -93,8 +103,9 @@ export class ResourcePreloader { messageData.clientId = this._clientId; if (this._pendingRequest) { - console.warn(`Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`); - this._pendingRequest.reject(new Error("Overridden by new request")); + const errorMessage = `Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`; + console.warn(errorMessage); + this._pendingRequest.reject(new Error(errorMessage)); this._pendingRequest = null; } @@ -105,27 +116,38 @@ export class ResourcePreloader { timeoutId = setTimeout(() => { if (this._pendingRequest?.type === messageData.type) { this._pendingRequest = null; - console.error(`Service worker message timed out: ${messageData.type}`); - resolve({}); // Resolve to not block execution + const errorMessage = `Service worker message timed out: ${messageData.type}`; + console.error(errorMessage); + reject(new Error(errorMessage)); } }, timeoutMs); } - this._pendingRequest = { type: messageData.type, resolve, reject, onProgress, timeoutId }; + this._pendingRequest = { type: messageData.type, resolve, reject, timeoutId }; this._sw.postMessage(messageData); }); } - async clearSw() { + async teardown() { + const response = await this.getFailedRequests(); + if (response.requests?.length > 0) { + console.warn("The following requests failed during the benchmark and bypassed the cache:"); + console.warn(response.requests.join("\n")); + throw new Error("The benchmark had missing resources that were not cached. Check the console for details."); + } + await this.clearServiceWorker(); + } + + async clearServiceWorker() { if (!this._sw) return; - await this._sendMessageWithReply({ type: SW_MESSAGES.CLEAR_CACHE }); + await this._postMessage({ type: SW_MESSAGES.CLEAR_CACHE }); } async resetPreloading() { if (!this._sw) return; - await this._sendMessageWithReply({ type: SW_MESSAGES.RESET_PRELOADING }); + await this._postMessage({ type: SW_MESSAGES.RESET_PRELOADING }); if (this._activePreloadPromise) await this._activePreloadPromise; } @@ -133,6 +155,10 @@ export class ResourcePreloader { async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { if (!this._sw) return undefined; + if (this._onPreloadProgress) + throw new Error("Preload already active"); + this._onPreloadProgress = onProgress; + const suitesData = suites .filter((s) => s.resources) .map((s) => ({ @@ -145,14 +171,14 @@ export class ResourcePreloader { return undefined; const startTime = performance.now(); - this._activePreloadPromise = this._sendMessageWithReply({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, onProgress, PRELOAD_TIMEOUT_MS); + this._activePreloadPromise = this._postMessage({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, PRELOAD_TIMEOUT_MS); const response = await this._activePreloadPromise; this._activePreloadPromise = null; - if (response.type === SW_MESSAGES.PRELOAD_ABORTED) - return "ABORTED"; + if (response.status === SW_MESSAGES.PRELOAD_ABORTED) + throw new Error("Preload aborted"); - if (response.type === SW_MESSAGES.PRELOAD_DONE) { + if (response.status === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); const timeSec = (timeTakenMs / 1000).toFixed(2); @@ -161,6 +187,11 @@ export class ResourcePreloader { this._preloadParams = params.toSearchParams(); return undefined; } + + async getFailedRequests() { + // return this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); + return { requests: ["a"] }; + } } const BENCHMARK_STATE = Object.freeze({ @@ -183,7 +214,14 @@ class PreloadStatusUpdater { this._infoProgressElement = document.getElementById("preload-info-progress"); } - update(data) { + start() { + this._progressCompletedElement.value = 0; + this._infoLabelElement.textContent = ""; + this._infoProgressElement.textContent = ""; + document.body.style.setProperty("--preload-progress", "0%"); + } + + onProgress(data) { this._latestData = data; if (!this._rafId) this._rafId = requestAnimationFrame(this._updateFn); @@ -207,21 +245,13 @@ class PreloadStatusUpdater { this._infoProgressElement.textContent = `${loaded} / ${total}`; } - cancel() { + stop() { if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = null; } this._latestData = null; } - - resetUI() { - this.cancel(); - this._progressCompletedElement.value = 0; - this._infoLabelElement.textContent = ""; - this._infoProgressElement.textContent = ""; - document.body.style.setProperty("--preload-progress", "0%"); - } } // FIXME(camillobruni): Add base class @@ -236,7 +266,6 @@ class MainBenchmarkClient { _hasResults = false; _metrics = Object.create(null); _resourcePreloader = new ResourcePreloader(); - _preloadStatusUpdater = new PreloadStatusUpdater(); _steppingPromise = null; _steppingResolver = null; _benchmarkConfiguratorPromise = null; @@ -310,17 +339,8 @@ class MainBenchmarkClient { if (!this._isStepping()) this._developerModeContainer?.remove(); - await this._resourcePreloader.resetPreloading(); - const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); - if (preloadResult === "ABORTED") - return false; - - try { - await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); - } catch (error) { - alert(error.message); - return false; - } + await this._preloadResources(benchmarkConfigurator); + await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); this._progressCompleted = document.getElementById("progress-completed"); if (params.iterationCount < 50) { @@ -380,16 +400,10 @@ class MainBenchmarkClient { console.assert(this._isRunning); this._isRunning = false; - const response = await this._resourcePreloader._sendMessageWithReply({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); - if (response.requests && response.requests.length > 0) { - console.warn("The following requests failed during the benchmark and bypassed the cache:"); - console.warn(response.requests.join("\n")); - alert("Warning: The benchmark had missing resources that were not cached. Check the console for details."); - } - this._hasResults = true; this._metrics = metrics; - this._setBenchmarkState(BENCHMARK_STATE.DONE); + await this._setBenchmarkState(BENCHMARK_STATE.DONE); + await this._resourcePreloader.teardown(); const scoreResults = this._computeResults(this._measuredValuesList, "score"); if (scoreResults.isValid) @@ -406,11 +420,11 @@ class MainBenchmarkClient { } handleError(error) { - console.assert(this._isRunning); this._isRunning = false; this._hasResults = true; this._metrics = Object.create(null); this._setBenchmarkState(BENCHMARK_STATE.ERROR); + this._resourcePreloader?.clearServiceWorker(); this._populateInvalidScore(error.message); this.showResultsSummary(); throw error; @@ -613,53 +627,33 @@ class MainBenchmarkClient { document.body.append(this._developerModeContainer); } - await this._resourcePreloader.resetPreloading(); - if (params.developerMode) { - this._enableStartButtons(); - } else { - const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator); - if (preloadResult === "ABORTED") - return; - } + await this._resourcePreloader.setup(); + if (!params.developerMode) + await this._preloadResources(benchmarkConfigurator); if (params.startAutomatically) this.start(); - } - - async _setupResourcePreloader(benchmarkConfigurator) { - await this._resourcePreloader.setup(); - if (!this._resourcePreloader.isCached()) - return this._cacheResources(benchmarkConfigurator); else this._enableStartButtons(); - - return undefined; } - async _cacheResources(benchmarkConfigurator) { + async _preloadResources(benchmarkConfigurator) { + await this._resourcePreloader.resetPreloading(); + if (this._resourcePreloader.isCached()) + return; const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const clearCache = !params.isDefault(); - this._resetPreloadUI(); - this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); - await new Promise((resolve) => requestAnimationFrame(resolve)); - - this._preloadGeneration = (this._preloadGeneration || 0) + 1; - const currentGeneration = this._preloadGeneration; - const onProgress = (data) => { - if (this._preloadGeneration === currentGeneration) - this._updateCacheProgress(data); - }; + const preloadStatusUpdater = new PreloadStatusUpdater(); + const onProgress = preloadStatusUpdater.onProgress.bind(preloadStatusUpdater); try { - const result = await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, onProgress); - this._preloadStatusUpdater.cancel(); - if (result === "ABORTED") - return "ABORTED"; - this._enableStartButtons(); - return undefined; + this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); + preloadStatusUpdater.start(); + await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, onProgress); + preloadStatusUpdater.stop(); } catch (error) { - this._preloadStatusUpdater.cancel(); + preloadStatusUpdater.stop(); console.error("Service Worker preload failed:", error); this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._populateInvalidScore(error?.message); @@ -668,16 +662,8 @@ class MainBenchmarkClient { } } - _resetPreloadUI() { - this._preloadStatusUpdater.resetUI(); - } - - _updateCacheProgress(progressData) { - this._preloadStatusUpdater.update(progressData); - } - - _enableStartButtons() { - this._setBenchmarkState(BENCHMARK_STATE.READY); + async _enableStartButtons() { + await this._setBenchmarkState(BENCHMARK_STATE.READY); document.querySelectorAll(".start-tests-button").forEach((button) => { button.disabled = false; }); @@ -685,10 +671,8 @@ class MainBenchmarkClient { async _setBenchmarkState(state) { document.body.setAttribute("data-benchmark-state", state); - const startButtons = document.querySelectorAll(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { - this._resetPreloadUI(); startButtons.forEach((btn) => { btn.innerHTML = "
Preloading
"; }); @@ -697,8 +681,6 @@ class MainBenchmarkClient { startButtons.forEach((btn) => { btn.innerHTML = "
Start Test
"; }); - if (state !== BENCHMARK_STATE.READY) - await this._resourcePreloader?.clearSw(); } } @@ -823,6 +805,26 @@ function init() { rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`); globalThis.benchmarkClient = new MainBenchmarkClient(); + + window.addEventListener("error", (event) => { + if (globalThis.benchmarkClient) { + try { + globalThis.benchmarkClient.handleError(event.error || new Error(event.message)); + } catch (e) { + // Ignore re-thrown error to prevent infinite loops + } + } + }); + + window.addEventListener("unhandledrejection", (event) => { + if (globalThis.benchmarkClient) { + try { + globalThis.benchmarkClient.handleError(event.reason || new Error("Unhandled promise rejection")); + } catch (e) { + // Ignore re-thrown error to prevent infinite loops + } + } + }); } if (document.readyState === "loading") From 28e14331bbb068651abb18c484f1a4c660a7b1a8 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 16:10:33 +0200 Subject: [PATCH 46/69] split-files --- resources/main.mjs | 258 +----------------- resources/preloader.mjs | 241 ++++++++++++++++ resources/shared/params.mjs | 3 - resources/shared/request-limiter.mjs | 38 +++ .../dist/src/speedometer-utils/params.mjs | 3 - sw.mjs | 105 +++---- tests/unittests/request-limiter.mjs | 106 +++++++ 7 files changed, 421 insertions(+), 333 deletions(-) create mode 100644 resources/preloader.mjs create mode 100644 resources/shared/request-limiter.mjs create mode 100644 tests/unittests/request-limiter.mjs diff --git a/resources/main.mjs b/resources/main.mjs index a4a65f2d4..7fc3c842b 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -5,194 +5,7 @@ import { renderMetricView } from "./metric-ui.mjs"; import { defaultParams, params } from "./shared/params.mjs"; import { createDeveloperModeContainer } from "./developer-mode.mjs"; -const PRELOAD_TIMEOUT_MS = 60_000; - -export class ResourcePreloader { - constructor() { - this._registration = null; - this._sw = null; - this._preloadParams = ""; - this._clientId = null; - this._pendingRequest = null; - this._onPreloadProgress = null; - } - - isCached() { - if (!params.preload) - return true; - return params.toSearchParams() === this._preloadParams; - } - - async setup() { - if (!params.preload) { - await this._unregisterOldServiceWorkers(); - return; - } - - await this._registerServiceWorker(); - this._setupMessageListener(); - this._clientId = await this._getClientId(); - } - - async _unregisterOldServiceWorkers() { - const existingRegistrations = await navigator.serviceWorker.getRegistrations(); - for (const existing of existingRegistrations) - await existing.unregister(); - this._registration = null; - this._sw = null; - } - - async _registerServiceWorker() { - this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); - await navigator.serviceWorker.ready; - this._sw = navigator.serviceWorker.controller || this._registration.active; - } - - async _getClientId() { - const response = await this._postMessage({ type: SW_MESSAGES.GET_CLIENT_ID }, 5000); - const clientId = response.clientId; - if (!clientId) - throw new Error("Could not retrieve service worker client id"); - return clientId; - } - - _setupMessageListener() { - navigator.serviceWorker.addEventListener("message", this.onMessage.bind(this)); - } - - _validateMessageRouting(data) { - if (!this._pendingRequest) - throw new Error(`Received unexpected ServiceWorker message without any pending request: ${data.type}`); - - if (this._pendingRequest.type !== data.type) - throw new Error(`Received ServiceWorker message type '${data.type}' but expected '${this._pendingRequest.type}'`); - } - - onMessage(event) { - const data = event.data || {}; - - if (data.type === SW_MESSAGES.FATAL_ERROR) { - globalThis.benchmarkClient?.handleError(new Error(data.message)); - return; - } - - this._validateMessageRouting(data); - - const { resolve, reject, timeoutId } = this._pendingRequest; - - if (data.status === SW_MESSAGES.PRELOAD_PROGRESS) { - this._onPreloadProgress?.(data); - return; - } - - if (timeoutId) - clearTimeout(timeoutId); - this._pendingRequest = null; - - if (data.status === SW_MESSAGES.ERROR) - reject(new Error(data.message || "Unknown SW Error")); - else - resolve(data); - } - - _postMessage(messageData, timeoutMs = 0) { - if (!this._sw) - return Promise.resolve(); - - if (this._clientId) - messageData.clientId = this._clientId; - - if (this._pendingRequest) { - const errorMessage = `Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`; - console.warn(errorMessage); - this._pendingRequest.reject(new Error(errorMessage)); - this._pendingRequest = null; - } - - return new Promise((resolve, reject) => { - let timeoutId = null; - - if (timeoutMs > 0) { - timeoutId = setTimeout(() => { - if (this._pendingRequest?.type === messageData.type) { - this._pendingRequest = null; - const errorMessage = `Service worker message timed out: ${messageData.type}`; - console.error(errorMessage); - reject(new Error(errorMessage)); - } - }, timeoutMs); - } - - this._pendingRequest = { type: messageData.type, resolve, reject, timeoutId }; - this._sw.postMessage(messageData); - }); - } - - async teardown() { - const response = await this.getFailedRequests(); - if (response.requests?.length > 0) { - console.warn("The following requests failed during the benchmark and bypassed the cache:"); - console.warn(response.requests.join("\n")); - throw new Error("The benchmark had missing resources that were not cached. Check the console for details."); - } - await this.clearServiceWorker(); - } - - async clearServiceWorker() { - if (!this._sw) - return; - await this._postMessage({ type: SW_MESSAGES.CLEAR_CACHE }); - } - - async resetPreloading() { - if (!this._sw) - return; - await this._postMessage({ type: SW_MESSAGES.RESET_PRELOADING }); - if (this._activePreloadPromise) - await this._activePreloadPromise; - } - - async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { - if (!this._sw) - return undefined; - if (this._onPreloadProgress) - throw new Error("Preload already active"); - this._onPreloadProgress = onProgress; - - const suitesData = suites - .filter((s) => s.resources) - .map((s) => ({ - name: s.name, - url: new URL(s.url, window.location.href).href, - resources: new URL(s.resources, window.location.href).href, - })); - - if (suitesData.length === 0) - return undefined; - - const startTime = performance.now(); - this._activePreloadPromise = this._postMessage({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, PRELOAD_TIMEOUT_MS); - const response = await this._activePreloadPromise; - this._activePreloadPromise = null; - - if (response.status === SW_MESSAGES.PRELOAD_ABORTED) - throw new Error("Preload aborted"); - - if (response.status === SW_MESSAGES.PRELOAD_DONE) { - const timeTakenMs = performance.now() - startTime; - const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); - const timeSec = (timeTakenMs / 1000).toFixed(2); - console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`); - } - this._preloadParams = params.toSearchParams(); - return undefined; - } - - async getFailedRequests() { - // return this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); - return { requests: ["a"] }; - } -} +import { ResourcePreloader, PreloadStatusUpdater } from "./preloader.mjs"; const BENCHMARK_STATE = Object.freeze({ IDLE: "IDLE", @@ -203,56 +16,7 @@ const BENCHMARK_STATE = Object.freeze({ ERROR: "ERROR", }); -class PreloadStatusUpdater { - constructor() { - this._latestData = null; - this._rafId = null; - this._updateFn = this._updateUI.bind(this); - - this._progressCompletedElement = document.getElementById("preload-progress-completed"); - this._infoLabelElement = document.getElementById("preload-info-label"); - this._infoProgressElement = document.getElementById("preload-info-progress"); - } - - start() { - this._progressCompletedElement.value = 0; - this._infoLabelElement.textContent = ""; - this._infoProgressElement.textContent = ""; - document.body.style.setProperty("--preload-progress", "0%"); - } - - onProgress(data) { - this._latestData = data; - if (!this._rafId) - this._rafId = requestAnimationFrame(this._updateFn); - } - - _updateUI() { - this._rafId = null; - const data = this._latestData; - if (!data) - return; - const { loaded, total, url, suiteName } = data; - document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); - this._progressCompletedElement.max = total; - this._progressCompletedElement.value = loaded; - let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; - if (!filename && url) - filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); - const labelText = suiteName ? `${suiteName}: ${filename}` : filename; - this._infoLabelElement.textContent = labelText; - this._infoProgressElement.textContent = `${loaded} / ${total}`; - } - - stop() { - if (this._rafId) { - cancelAnimationFrame(this._rafId); - this._rafId = null; - } - this._latestData = null; - } -} // FIXME(camillobruni): Add base class class MainBenchmarkClient { @@ -645,12 +409,11 @@ class MainBenchmarkClient { const clearCache = !params.isDefault(); const preloadStatusUpdater = new PreloadStatusUpdater(); - const onProgress = preloadStatusUpdater.onProgress.bind(preloadStatusUpdater); try { this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); preloadStatusUpdater.start(); - await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, onProgress); + await this._resourcePreloader.preloadSuites(enabledSuites, clearCache, preloadStatusUpdater.onProgress.bind(preloadStatusUpdater)); preloadStatusUpdater.stop(); } catch (error) { preloadStatusUpdater.stop(); @@ -805,25 +568,12 @@ function init() { rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`); globalThis.benchmarkClient = new MainBenchmarkClient(); - window.addEventListener("error", (event) => { - if (globalThis.benchmarkClient) { - try { - globalThis.benchmarkClient.handleError(event.error || new Error(event.message)); - } catch (e) { - // Ignore re-thrown error to prevent infinite loops - } - } + globalThis.benchmarkClient.handleError(event.error || new Error(event.message)); }); window.addEventListener("unhandledrejection", (event) => { - if (globalThis.benchmarkClient) { - try { - globalThis.benchmarkClient.handleError(event.reason || new Error("Unhandled promise rejection")); - } catch (e) { - // Ignore re-thrown error to prevent infinite loops - } - } + globalThis.benchmarkClient.handleError(event.reason || new Error("Unhandled promise rejection")); }); } diff --git a/resources/preloader.mjs b/resources/preloader.mjs new file mode 100644 index 000000000..3425dbb75 --- /dev/null +++ b/resources/preloader.mjs @@ -0,0 +1,241 @@ +import { SW_MESSAGES } from "./shared/sw-messages.mjs"; +import { params } from "./shared/params.mjs"; + +const PRELOAD_TIMEOUT_MS = 60_000; + +export class ResourcePreloader { + constructor() { + this._registration = null; + this._sw = null; + this._preloadParams = ""; + this._clientId = null; + this._pendingRequest = null; + this._onPreloadProgress = null; + } + + isCached() { + if (!params.preload) + return true; + return params.toSearchParams() === this._preloadParams; + } + + async setup() { + if (!params.preload) { + await this._unregisterOldServiceWorkers(); + return; + } + + await this._registerServiceWorker(); + this._setupMessageListener(); + this._clientId = await this._getClientId(); + } + + async _unregisterOldServiceWorkers() { + const existingRegistrations = await navigator.serviceWorker.getRegistrations(); + for (const existing of existingRegistrations) + await existing.unregister(); + this._registration = null; + this._sw = null; + } + + async _registerServiceWorker() { + this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); + await navigator.serviceWorker.ready; + this._sw = navigator.serviceWorker.controller || this._registration.active; + } + + async _getClientId() { + const response = await this._postMessage({ type: SW_MESSAGES.GET_CLIENT_ID }, 5000); + const clientId = response.clientId; + if (!clientId) + throw new Error("Could not retrieve service worker client id"); + return clientId; + } + + _setupMessageListener() { + navigator.serviceWorker.addEventListener("message", this.onMessage.bind(this)); + } + + _validateMessageRouting(data) { + if (!this._pendingRequest) + throw new Error(`Received unexpected ServiceWorker message without any pending request: ${data.type}`); + + if (this._pendingRequest.type !== data.type) + throw new Error(`Received ServiceWorker message type '${data.type}' but expected '${this._pendingRequest.type}'`); + } + + onMessage(event) { + const data = event.data || {}; + + if (data.type === SW_MESSAGES.FATAL_ERROR) { + globalThis.benchmarkClient?.handleError(new Error(data.message)); + return; + } + + this._validateMessageRouting(data); + + if (data.status === SW_MESSAGES.PRELOAD_PROGRESS) { + this._onPreloadProgress(data); + return; + } + + const { resolve, reject, timeoutId } = this._pendingRequest; + if (timeoutId) + clearTimeout(timeoutId); + this._pendingRequest = null; + + if (data.status === SW_MESSAGES.ERROR) + reject(new Error(data.message || "Unknown SW Error")); + else + resolve(data); + } + + _postMessage(messageData, timeoutMs = 0) { + if (!this._sw) + return Promise.resolve(); + + if (this._clientId) + messageData.clientId = this._clientId; + + if (this._pendingRequest) { + const errorMessage = `Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`; + console.warn(errorMessage); + this._pendingRequest.reject(new Error(errorMessage)); + this._pendingRequest = null; + } + + return new Promise((resolve, reject) => { + let timeoutId = null; + + if (timeoutMs > 0) { + timeoutId = setTimeout(() => { + if (this._pendingRequest?.type === messageData.type) { + this._pendingRequest = null; + const errorMessage = `Service worker message timed out: ${messageData.type}`; + console.error(errorMessage); + reject(new Error(errorMessage)); + } + }, timeoutMs); + } + + this._pendingRequest = { type: messageData.type, resolve, reject, timeoutId }; + this._sw.postMessage(messageData); + }); + } + + async teardown() { + const response = await this.getFailedRequests(); + if (response.requests?.length > 0) { + console.warn("The following requests failed during the benchmark and bypassed the cache:"); + console.warn(response.requests.join("\n")); + throw new Error("The benchmark had missing resources that were not cached. Check the console for details."); + } + await this.clearServiceWorker(); + } + + async clearServiceWorker() { + if (!this._sw) + return; + await this._postMessage({ type: SW_MESSAGES.CLEAR_CACHE }); + } + + async resetPreloading() { + if (!this._sw) + return; + await this._postMessage({ type: SW_MESSAGES.RESET_PRELOADING }); + if (this._activePreloadPromise) + await this._activePreloadPromise; + } + + async preloadSuites(suites, clearCache = true, onProgress) { + if (!this._sw) + return undefined; + if (this._onPreloadProgress) + throw new Error("Preload already active"); + this._onPreloadProgress = onProgress; + + const suitesData = suites + .filter((s) => s.resources) + .map((s) => ({ + name: s.name, + url: new URL(s.url, window.location.href).href, + resources: new URL(s.resources, window.location.href).href, + })); + + if (suitesData.length === 0) + return undefined; + + const startTime = performance.now(); + this._activePreloadPromise = this._postMessage({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, clearCache }, PRELOAD_TIMEOUT_MS); + const response = await this._activePreloadPromise; + this._activePreloadPromise = null; + + if (response.status === SW_MESSAGES.PRELOAD_ABORTED) + throw new Error("Preload aborted"); + + if (response.status === SW_MESSAGES.PRELOAD_DONE) { + const timeTakenMs = performance.now() - startTime; + const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); + const timeSec = (timeTakenMs / 1000).toFixed(2); + console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`); + } + this._preloadParams = params.toSearchParams(); + return undefined; + } + + async getFailedRequests() { + // return this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); + return { requests: ["a"] }; + } +} + +export class PreloadStatusUpdater { + constructor() { + this._latestData = null; + this._rafId = null; + this._updateFn = this._updateUI.bind(this); + + this._progressCompletedElement = document.getElementById("preload-progress-completed"); + this._infoLabelElement = document.getElementById("preload-info-label"); + this._infoProgressElement = document.getElementById("preload-info-progress"); + } + + start() { + this._progressCompletedElement.value = 0; + this._infoLabelElement.textContent = ""; + this._infoProgressElement.textContent = ""; + document.body.style.setProperty("--preload-progress", "0%"); + } + + onProgress(data) { + this._latestData = data; + if (!this._rafId) + this._rafId = requestAnimationFrame(this._updateFn); + } + + _updateUI() { + this._rafId = null; + const data = this._latestData; + if (!data) + return; + + const { loaded, total, url, suiteName } = data; + document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); + this._progressCompletedElement.max = total; + this._progressCompletedElement.value = loaded; + let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; + if (!filename && url) + filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); + const labelText = suiteName ? `${suiteName}: ${filename}` : filename; + this._infoLabelElement.textContent = labelText; + this._infoProgressElement.textContent = `${loaded} / ${total}`; + } + + stop() { + if (this._rafId) { + cancelAnimationFrame(this._rafId); + this._rafId = null; + } + this._latestData = null; + } +} diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index 698e80f5d..012d4475c 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -43,8 +43,6 @@ export class Params { measurePrepare = false; // External config url to override internal tests. config = ""; - // Resource load delay in ms for the service worker pre-caching. - resourceLoadDelay = 0; // Use service worker for resource preloading. preload = false; @@ -92,7 +90,6 @@ export class Params { this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES); this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare"); this.config = this._parseConfig(searchParams); - this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0); this.preload = this._parseBooleanParam(searchParams, "preload"); if (warnUnused) { diff --git a/resources/shared/request-limiter.mjs b/resources/shared/request-limiter.mjs new file mode 100644 index 000000000..44c2c48dd --- /dev/null +++ b/resources/shared/request-limiter.mjs @@ -0,0 +1,38 @@ +export class RequestLimiter { + constructor(limit) { + this._limit = limit; + this._active = 0; + this._queue = []; + } + + async _processQueue() { + while (this._queue.length > 0) { + const task = this._queue.shift(); + try { + await task(); + } catch (e) { + // Individual task errors are handled by their respective promises + } + } + this._active--; + } + + schedule(fn) { + return new Promise((resolve, reject) => { + const task = () => fn().then(resolve, reject); + task.resolve = resolve; + this._queue.push(task); + + if (this._active < this._limit) { + this._active++; + this._processQueue(); // Intentionally not awaited + } + }); + } + + clear() { + for (const task of this._queue) + task.resolve?.(0); + this._queue.length = 0; + } +} diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs index ee0476b4c..c34aec619 100644 --- a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs +++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs @@ -39,8 +39,6 @@ export class Params { measurePrepare = false; // External config url to override internal tests. config = ""; - // Resource load delay in ms for the service worker pre-caching. - resourceLoadDelay = 0; // Use service worker for resource preloading. preload = false; @@ -78,7 +76,6 @@ export class Params { this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES); this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare"); this.config = this._parseConfig(searchParams); - this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0); this.preload = this._parseBooleanParam(searchParams, "preload"); if (warnUnused) { diff --git a/sw.mjs b/sw.mjs index f0cf568b0..977773cca 100644 --- a/sw.mjs +++ b/sw.mjs @@ -1,4 +1,5 @@ import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; +import { RequestLimiter } from "./resources/shared/request-limiter.mjs"; const CACHE_NAME = "speedometer-cache-v4.0"; const DB_NAME = "SpeedometerStateDB"; @@ -78,78 +79,34 @@ function replyError(event, message) { replyToClient(event, SW_MESSAGES.ERROR, { message }); } -function delayAsync(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - const MAX_CONCURRENT_REQUESTS = 20; -class RequestLimiter { - constructor(limit = MAX_CONCURRENT_REQUESTS) { - this.limit = limit; - this.active = 0; - this.queue = []; - } - - async _processQueue() { - while (this.queue.length > 0) { - const task = this.queue.shift(); - try { - await task(); - } catch (e) { - // Individual task errors are handled by their respective promises - } - await delayAsync(1); // Yield to event loop - } - this.active--; - } - - schedule(fn) { - return new Promise((resolve, reject) => { - const task = () => fn().then(resolve, reject); - task.resolve = resolve; - this.queue.push(task); - - if (this.active < this.limit) { - this.active++; - this._processQueue(); // Intentionally not awaited - } - }); - } - - clear() { - for (const task of this.queue) - task.resolve?.(0); - - this.queue.length = 0; - } -} - const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS); -let currentPreloadEvent = null; -let currentPreloadId = 0; +let activePreloadController = null; function handleResetPreloadingMessage(event) { - if (currentPreloadEvent) - replyToClient(currentPreloadEvent, SW_MESSAGES.PRELOAD_ABORTED); + resetPreloadingMessage(); + replyToClient(event, SW_MESSAGES.RESET_PRELOADING); +} - currentPreloadEvent = null; - currentPreloadId++; +function resetPreloadingMessage() { + if (activePreloadController) { + activePreloadController.abort(); + activePreloadController = null; + } requestLimiter.clear(); - if (event) - replyToClient(event, SW_MESSAGES.RESET_PRELOADING); } -async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) { +async function handlePreloadSuitesMessage(event, clientId, { suites = [], clearCache = true }) { try { await STORE.getOwner(); await updateActiveClient(clientId); - handleResetPreloadingMessage(); // Call it without event to avoid replying to the PRELOAD_SUITES channel! + resetPreloadingMessage(); - currentPreloadEvent = event; - const preloadId = currentPreloadId; + activePreloadController = new AbortController(); + const signal = activePreloadController.signal; failedRequests.clear(); if (clearCache) @@ -163,32 +120,29 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay let transferredSize = 0; const promises = urlsToCache.map(async (item, index) => { - if (preloadId !== currentPreloadId) - return; - const size = await fetchAndCache(cache, item.url, delay * index); - if (preloadId !== currentPreloadId) - return; + const size = await fetchAndCache(cache, item.url, signal); transferredSize += size; loaded++; - replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); + if (!signal.aborted) + replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); }); await Promise.all(promises); - if (preloadId !== currentPreloadId) - return; - if (!await STORE.hasLock(clientId)) { replyError(event, "Speedometer aborted: Another tab took over."); return; } + replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); } catch (error) { + if (error.name === "AbortError") + return; console.error("Error during preload:", error); replyError(event, error.message || "Failed to preload resources."); } finally { - if (currentPreloadEvent === event) - currentPreloadEvent = null; + if (activePreloadController && !activePreloadController.signal.aborted) + activePreloadController = null; } } @@ -223,11 +177,14 @@ function getResponseSize(response) { return contentLength ? parseInt(contentLength, 10) : 0; } -async function fetchAndCache(cache, url, delayMs) { - if (delayMs) - await delayAsync(delayMs); +async function fetchAndCache(cache, url, signal) { + if (signal.aborted) + throw new DOMException("Aborted", "AbortError"); + return requestLimiter.schedule(async () => { - const request = new Request(url, { cache: "no-cache" }); + if (signal.aborted) + throw new DOMException("Aborted", "AbortError"); + const request = new Request(url, { cache: "no-cache", signal }); const existing = await cache.match(request, { ignoreSearch: true }); if (existing) return getResponseSize(existing); @@ -240,7 +197,9 @@ async function fetchAndCache(cache, url, delayMs) { await cache.put(request, response); return size; } catch (e) { - console.warn("Cache failed for", url, e); + if (e.name === "AbortError") + throw e; + console.error("Cache failed for", url, e); return 0; } }); diff --git a/tests/unittests/request-limiter.mjs b/tests/unittests/request-limiter.mjs new file mode 100644 index 000000000..fb3ed268e --- /dev/null +++ b/tests/unittests/request-limiter.mjs @@ -0,0 +1,106 @@ +import { RequestLimiter } from "../../resources/shared/request-limiter.mjs"; + +describe("RequestLimiter", () => { + it("should process requests within the limit concurrently", async () => { + const limiter = new RequestLimiter(2); + let active = 0; + let maxActive = 0; + + const task = async () => { + active++; + maxActive = Math.max(maxActive, active); + await new Promise(resolve => setTimeout(resolve, 10)); + active--; + }; + + const p1 = limiter.schedule(task); + const p2 = limiter.schedule(task); + const p3 = limiter.schedule(task); + + await Promise.all([p1, p2, p3]); + + expect(maxActive).to.be(2); + expect(limiter._active).to.be(0); + }); + + it("should resolve with the correct value", async () => { + const limiter = new RequestLimiter(1); + const p = limiter.schedule(async () => "result"); + const result = await p; + expect(result).to.be("result"); + }); + + it("should reject if the task throws", async () => { + const limiter = new RequestLimiter(1); + const p = limiter.schedule(async () => { + throw new Error("task failed"); + }); + + let threw = false; + try { + await p; + } catch (e) { + threw = true; + expect(e.message).to.be("task failed"); + } + expect(threw).to.be(true); + // Wait for microtasks to flush so _processQueue can decrement _active + await new Promise(r => setTimeout(r, 0)); + expect(limiter._active).to.be(0); + }); + + it("should continue processing queue if a task rejects", async () => { + const limiter = new RequestLimiter(1); + + const p1 = limiter.schedule(async () => { + throw new Error("failed"); + }); + const p2 = limiter.schedule(async () => "success"); + + try { + await p1; + } catch (e) {} + + const res2 = await p2; + expect(res2).to.be("success"); + }); + + it("should clear the queue and resolve pending requests with 0", async () => { + const limiter = new RequestLimiter(1); + let executed = 0; + + const p1 = limiter.schedule(async () => { + executed++; + await new Promise(resolve => setTimeout(resolve, 10)); + return 1; + }); + + const p2 = limiter.schedule(async () => { + executed++; + return 2; + }); + + const p3 = limiter.schedule(async () => { + executed++; + return 3; + }); + + limiter.clear(); + + const res1 = await p1; + const res2 = await p2; + const res3 = await p3; + + // p1 already started, so it finishes normally + expect(res1).to.be(1); + // p2 and p3 were cleared, so they resolve with 0 + expect(res2).to.be(0); + expect(res3).to.be(0); + + // only p1 actually ran + expect(executed).to.be(1); + + // eventually active count resets when p1 completes + expect(limiter._active).to.be(0); + }); +}); From c1852a95b65ba6b5a6d26252b6475ad283223af7 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 16:22:41 +0200 Subject: [PATCH 47/69] cleanup --- resources/main.mjs | 3 --- tests/unittests/request-limiter.mjs | 42 +++++++++++++++-------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 7fc3c842b..6d2f3250a 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -1,6 +1,5 @@ import { BenchmarkRunner } from "./benchmark-runner.mjs"; import * as Statistics from "./statistics.mjs"; -import { SW_MESSAGES } from "./shared/sw-messages.mjs"; import { renderMetricView } from "./metric-ui.mjs"; import { defaultParams, params } from "./shared/params.mjs"; import { createDeveloperModeContainer } from "./developer-mode.mjs"; @@ -16,8 +15,6 @@ const BENCHMARK_STATE = Object.freeze({ ERROR: "ERROR", }); - - // FIXME(camillobruni): Add base class class MainBenchmarkClient { developerMode = false; diff --git a/tests/unittests/request-limiter.mjs b/tests/unittests/request-limiter.mjs index fb3ed268e..d8c3cda7a 100644 --- a/tests/unittests/request-limiter.mjs +++ b/tests/unittests/request-limiter.mjs @@ -5,20 +5,20 @@ describe("RequestLimiter", () => { const limiter = new RequestLimiter(2); let active = 0; let maxActive = 0; - + const task = async () => { active++; maxActive = Math.max(maxActive, active); - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); active--; }; - + const p1 = limiter.schedule(task); const p2 = limiter.schedule(task); const p3 = limiter.schedule(task); - + await Promise.all([p1, p2, p3]); - + expect(maxActive).to.be(2); expect(limiter._active).to.be(0); }); @@ -35,7 +35,7 @@ describe("RequestLimiter", () => { const p = limiter.schedule(async () => { throw new Error("task failed"); }); - + let threw = false; try { await p; @@ -45,22 +45,24 @@ describe("RequestLimiter", () => { } expect(threw).to.be(true); // Wait for microtasks to flush so _processQueue can decrement _active - await new Promise(r => setTimeout(r, 0)); + await new Promise((r) => setTimeout(r, 0)); expect(limiter._active).to.be(0); }); it("should continue processing queue if a task rejects", async () => { const limiter = new RequestLimiter(1); - + const p1 = limiter.schedule(async () => { throw new Error("failed"); }); const p2 = limiter.schedule(async () => "success"); - + try { await p1; - } catch (e) {} - + } catch (e) { + // expected to throw + } + const res2 = await p2; expect(res2).to.be("success"); }); @@ -68,38 +70,38 @@ describe("RequestLimiter", () => { it("should clear the queue and resolve pending requests with 0", async () => { const limiter = new RequestLimiter(1); let executed = 0; - + const p1 = limiter.schedule(async () => { executed++; - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); return 1; }); - + const p2 = limiter.schedule(async () => { executed++; return 2; }); - + const p3 = limiter.schedule(async () => { executed++; return 3; }); - + limiter.clear(); - + const res1 = await p1; const res2 = await p2; const res3 = await p3; - + // p1 already started, so it finishes normally expect(res1).to.be(1); // p2 and p3 were cleared, so they resolve with 0 expect(res2).to.be(0); expect(res3).to.be(0); - + // only p1 actually ran expect(executed).to.be(1); - + // eventually active count resets when p1 completes expect(limiter._active).to.be(0); }); From d8b27f521412d03516105c53b2c94b1655c88c43 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 17:25:17 +0200 Subject: [PATCH 48/69] state-mangement --- resources/main.mjs | 69 ++++++++++++++------------------ resources/preloader.mjs | 6 +-- resources/shared/sw-messages.mjs | 1 - 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 6d2f3250a..7b61b37ca 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -23,8 +23,7 @@ class MainBenchmarkClient { _measuredValuesList = []; _finishedTestCount = 0; _progressCompleted = null; - _isRunning = false; - _hasResults = false; + _state = BENCHMARK_STATE.IDLE; _metrics = Object.create(null); _resourcePreloader = new ResourcePreloader(); _steppingPromise = null; @@ -55,7 +54,7 @@ class MainBenchmarkClient { this._steppingResolver = resolve; }); currentSteppingResolver?.(); - if (!this._isRunning) { + if (this._state !== BENCHMARK_STATE.RUNNING) { await this._startBenchmark(); this._showSection("#running"); } @@ -78,7 +77,7 @@ class MainBenchmarkClient { } async _startBenchmark() { - if (this._isRunning) + if (this._state === BENCHMARK_STATE.RUNNING) return false; const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; @@ -101,7 +100,7 @@ class MainBenchmarkClient { this._developerModeContainer?.remove(); await this._preloadResources(benchmarkConfigurator); - await this._setBenchmarkState(BENCHMARK_STATE.RUNNING); + this._setBenchmarkState(BENCHMARK_STATE.RUNNING); this._progressCompleted = document.getElementById("progress-completed"); if (params.iterationCount < 50) { @@ -114,7 +113,6 @@ class MainBenchmarkClient { } this._metrics = Object.create(null); - this._isRunning = true; this.stepCount = params.iterationCount * totalSuitesCount; this._progressCompleted.max = this.stepCount; @@ -158,12 +156,10 @@ class MainBenchmarkClient { } async didFinishLastIteration(metrics) { - console.assert(this._isRunning); - this._isRunning = false; + console.assert(this._state === BENCHMARK_STATE.RUNNING); - this._hasResults = true; this._metrics = metrics; - await this._setBenchmarkState(BENCHMARK_STATE.DONE); + this._setBenchmarkState(BENCHMARK_STATE.DONE); await this._resourcePreloader.teardown(); const scoreResults = this._computeResults(this._measuredValuesList, "score"); @@ -181,12 +177,10 @@ class MainBenchmarkClient { } handleError(error) { - this._isRunning = false; - this._hasResults = true; this._metrics = Object.create(null); this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._resourcePreloader?.clearServiceWorker(); - this._populateInvalidScore(error.message); + this._populateErrorMessage(error.message); this.showResultsSummary(); throw error; } @@ -200,20 +194,18 @@ class MainBenchmarkClient { document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`; } - _populateInvalidScore(errorText) { + _populateInvalidScore() { + this._populateErrorMessage(undefined); + } + + _populateErrorMessage(errorText) { document.getElementById("summary").className = "invalid"; document.getElementById("result-number").textContent = "Error"; document.getElementById("confidence-number").textContent = ""; - - const invalidScoreText = document.getElementById("invalid-score-text"); - if (errorText) { - invalidScoreText.textContent = errorText; - } else { - invalidScoreText.innerHTML = ` - One or more subtests produced no duration.
- Please check your browser settings and re-run the benchmark.
- `; - } + if (errorText === undefined) + return; + const errorMessage = document.getElementById("invalid-score-text"); + errorMessage.textContent = errorText; } _computeResults(measuredValuesList, displayUnit) { @@ -318,15 +310,15 @@ class MainBenchmarkClient { document.getElementById("metrics-results").innerHTML = html; const filePrefix = `speedometer-3-${new Date().toISOString()}`; - const classicJsonData = this._formattedJSONResult({ modern: false }); - const classicJsonLink = document.getElementById("download-classic-json"); - classicJsonLink.href = URL.createObjectURL(new Blob([classicJsonData], { type: "application/json" })); - classicJsonLink.setAttribute("download", `${filePrefix}.json`); + let jsonData = this._formattedJSONResult({ modern: false }); + let jsonLink = document.getElementById("download-classic-json"); + jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" })); + jsonLink.setAttribute("download", `${filePrefix}.json`); - const fullJsonLink = document.getElementById("download-full-json"); - const fullJsonData = this._formattedJSONResult({ modern: true }); - fullJsonLink.href = URL.createObjectURL(new Blob([fullJsonData], { type: "application/json" })); - fullJsonLink.setAttribute("download", `${filePrefix}.json`); + jsonLink = document.getElementById("download-full-json"); + jsonData = this._formattedJSONResult({ modern: true }); + jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" })); + jsonLink.setAttribute("download", `${filePrefix}.json`); const csvData = this._formattedCSVResult(); const csvLink = document.getElementById("download-csv"); @@ -402,9 +394,9 @@ class MainBenchmarkClient { await this._resourcePreloader.resetPreloading(); if (this._resourcePreloader.isCached()) return; + const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const clearCache = !params.isDefault(); - const preloadStatusUpdater = new PreloadStatusUpdater(); try { @@ -416,20 +408,21 @@ class MainBenchmarkClient { preloadStatusUpdater.stop(); console.error("Service Worker preload failed:", error); this._setBenchmarkState(BENCHMARK_STATE.ERROR); - this._populateInvalidScore(error?.message); + this._populateErrorMessage(error?.message); this.showResultsSummary(); throw error; } } async _enableStartButtons() { - await this._setBenchmarkState(BENCHMARK_STATE.READY); + this._setBenchmarkState(BENCHMARK_STATE.READY); document.querySelectorAll(".start-tests-button").forEach((button) => { button.disabled = false; }); } async _setBenchmarkState(state) { + this._state = state; document.body.setAttribute("data-benchmark-state", state); const startButtons = document.querySelectorAll(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { @@ -464,7 +457,7 @@ class MainBenchmarkClient { _logoClickHandler(event) { // Prevent any accidental UI changes during benchmark runs. - if (!this._isRunning) + if (this._state !== BENCHMARK_STATE.RUNNING) this._showSection("#home"); event.preventDefault(); return false; @@ -513,10 +506,10 @@ class MainBenchmarkClient { } _showSection(hash) { - if (this._isRunning) { + if (this._state === BENCHMARK_STATE.RUNNING) { this._setLocationHash("#running"); return; - } else if (this._hasResults) { + } else if (this._state === BENCHMARK_STATE.DONE || this._state === BENCHMARK_STATE.ERROR) { if (hash !== "#summary" && hash !== "#details") { this._setLocationHash("#summary"); return; diff --git a/resources/preloader.mjs b/resources/preloader.mjs index 3425dbb75..fc5b5f1fe 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -170,9 +170,6 @@ export class ResourcePreloader { const response = await this._activePreloadPromise; this._activePreloadPromise = null; - if (response.status === SW_MESSAGES.PRELOAD_ABORTED) - throw new Error("Preload aborted"); - if (response.status === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); @@ -184,8 +181,7 @@ export class ResourcePreloader { } async getFailedRequests() { - // return this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); - return { requests: ["a"] }; + return this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); } } diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs index 8f326513a..02017c055 100644 --- a/resources/shared/sw-messages.mjs +++ b/resources/shared/sw-messages.mjs @@ -2,7 +2,6 @@ export const SW_MESSAGES = Object.freeze({ PRELOAD_SUITES: "PRELOAD_SUITES", PRELOAD_PROGRESS: "PRELOAD_PROGRESS", PRELOAD_DONE: "PRELOAD_DONE", - PRELOAD_ABORTED: "PRELOAD_ABORTED", RESET_PRELOADING: "RESET_PRELOADING", FATAL_ERROR: "FATAL_ERROR", ERROR: "ERROR", From bd3f4e803b73aca93bc1ecfc2b918b4064140476 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 17:55:17 +0200 Subject: [PATCH 49/69] cleanup --- resources/main.mjs | 46 ++++++++++++++++++++--------------------- resources/preloader.mjs | 34 +++++++++++++++--------------- sw.mjs | 6 +++--- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 7b61b37ca..95b640b20 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -41,6 +41,14 @@ class MainBenchmarkClient { }); } + isRunning() { + return this._state === BENCHMARK_STATE.RUNNING; + } + + hasFinished() { + return this._state === BENCHMARK_STATE.DONE || this._state === BENCHMARK_STATE.ERROR; + } + async start() { if (this._isStepping()) this._clearStepping(); @@ -54,7 +62,7 @@ class MainBenchmarkClient { this._steppingResolver = resolve; }); currentSteppingResolver?.(); - if (this._state !== BENCHMARK_STATE.RUNNING) { + if (!this.isRunning()) { await this._startBenchmark(); this._showSection("#running"); } @@ -77,7 +85,7 @@ class MainBenchmarkClient { } async _startBenchmark() { - if (this._state === BENCHMARK_STATE.RUNNING) + if (this.isRunning()) return false; const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; @@ -100,7 +108,6 @@ class MainBenchmarkClient { this._developerModeContainer?.remove(); await this._preloadResources(benchmarkConfigurator); - this._setBenchmarkState(BENCHMARK_STATE.RUNNING); this._progressCompleted = document.getElementById("progress-completed"); if (params.iterationCount < 50) { @@ -113,10 +120,11 @@ class MainBenchmarkClient { } this._metrics = Object.create(null); - this.stepCount = params.iterationCount * totalSuitesCount; this._progressCompleted.max = this.stepCount; this.suitesCount = enabledSuites.length; + + this._setBenchmarkState(BENCHMARK_STATE.RUNNING); const runner = new BenchmarkRunner(benchmarkConfigurator.suites, this); runner.runMultipleIterations(params.iterationCount); return true; @@ -156,7 +164,7 @@ class MainBenchmarkClient { } async didFinishLastIteration(metrics) { - console.assert(this._state === BENCHMARK_STATE.RUNNING); + console.assert(this.isRunning()); this._metrics = metrics; this._setBenchmarkState(BENCHMARK_STATE.DONE); @@ -387,7 +395,7 @@ class MainBenchmarkClient { if (params.startAutomatically) this.start(); else - this._enableStartButtons(); + this._setBenchmarkState(BENCHMARK_STATE.READY); } async _preloadResources(benchmarkConfigurator) { @@ -403,24 +411,11 @@ class MainBenchmarkClient { this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); preloadStatusUpdater.start(); await this._resourcePreloader.preloadSuites(enabledSuites, clearCache, preloadStatusUpdater.onProgress.bind(preloadStatusUpdater)); + } finally { preloadStatusUpdater.stop(); - } catch (error) { - preloadStatusUpdater.stop(); - console.error("Service Worker preload failed:", error); - this._setBenchmarkState(BENCHMARK_STATE.ERROR); - this._populateErrorMessage(error?.message); - this.showResultsSummary(); - throw error; } } - async _enableStartButtons() { - this._setBenchmarkState(BENCHMARK_STATE.READY); - document.querySelectorAll(".start-tests-button").forEach((button) => { - button.disabled = false; - }); - } - async _setBenchmarkState(state) { this._state = state; document.body.setAttribute("data-benchmark-state", state); @@ -434,6 +429,11 @@ class MainBenchmarkClient { startButtons.forEach((btn) => { btn.innerHTML = "
Start Test
"; }); + if (state === BENCHMARK_STATE.READY) { + startButtons.forEach((btn) => { + btn.disabled = false; + }); + } } } @@ -457,7 +457,7 @@ class MainBenchmarkClient { _logoClickHandler(event) { // Prevent any accidental UI changes during benchmark runs. - if (this._state !== BENCHMARK_STATE.RUNNING) + if (!this.isRunning()) this._showSection("#home"); event.preventDefault(); return false; @@ -506,10 +506,10 @@ class MainBenchmarkClient { } _showSection(hash) { - if (this._state === BENCHMARK_STATE.RUNNING) { + if (this.isRunning()) { this._setLocationHash("#running"); return; - } else if (this._state === BENCHMARK_STATE.DONE || this._state === BENCHMARK_STATE.ERROR) { + } else if (this.hasFinished()) { if (hash !== "#summary" && hash !== "#details") { this._setLocationHash("#summary"); return; diff --git a/resources/preloader.mjs b/resources/preloader.mjs index fc5b5f1fe..11b0248e2 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -97,24 +97,16 @@ export class ResourcePreloader { if (this._clientId) messageData.clientId = this._clientId; - if (this._pendingRequest) { - const errorMessage = `Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`; - console.warn(errorMessage); - this._pendingRequest.reject(new Error(errorMessage)); - this._pendingRequest = null; - } + if (this._pendingRequest) + this._rejectPendingRequest(`Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`); return new Promise((resolve, reject) => { let timeoutId = null; if (timeoutMs > 0) { timeoutId = setTimeout(() => { - if (this._pendingRequest?.type === messageData.type) { - this._pendingRequest = null; - const errorMessage = `Service worker message timed out: ${messageData.type}`; - console.error(errorMessage); - reject(new Error(errorMessage)); - } + if (this._pendingRequest?.type === messageData.type) + this._rejectPendingRequest(`Service worker message timed out: ${messageData.type}`); }, timeoutMs); } @@ -123,6 +115,16 @@ export class ResourcePreloader { }); } + _rejectPendingRequest(errorMessage) { + const pendingRequest = this._pendingRequest; + this._pendingRequest = null; + if (pendingRequest.timeoutId) + clearTimeout(pendingRequest.timeoutId); + + console.warn(errorMessage); + pendingRequest.reject(new Error(errorMessage)); + } + async teardown() { const response = await this.getFailedRequests(); if (response.requests?.length > 0) { @@ -219,11 +221,9 @@ export class PreloadStatusUpdater { document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); this._progressCompletedElement.max = total; this._progressCompletedElement.value = loaded; - let filename = url ? url.substring(url.lastIndexOf("/") + 1) : ""; - if (!filename && url) - filename = url.substring(url.lastIndexOf("/", url.length - 2) + 1); - const labelText = suiteName ? `${suiteName}: ${filename}` : filename; - this._infoLabelElement.textContent = labelText; + const match = url.match(/(?:suites(?:-experimental)?|resources)\/(.+)$/); + const filename = match ? match[1] : ""; + this._infoLabelElement.textContent = `${suiteName}: ${filename}`; this._infoProgressElement.textContent = `${loaded} / ${total}`; } diff --git a/sw.mjs b/sw.mjs index 977773cca..24b97f7b8 100644 --- a/sw.mjs +++ b/sw.mjs @@ -86,11 +86,11 @@ const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS); let activePreloadController = null; function handleResetPreloadingMessage(event) { - resetPreloadingMessage(); + resetPreloading(); replyToClient(event, SW_MESSAGES.RESET_PRELOADING); } -function resetPreloadingMessage() { +function resetPreloading() { if (activePreloadController) { activePreloadController.abort(); activePreloadController = null; @@ -103,7 +103,7 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], clearC await STORE.getOwner(); await updateActiveClient(clientId); - resetPreloadingMessage(); + resetPreloading(); activePreloadController = new AbortController(); const signal = activePreloadController.signal; From c2f4513f1a6e7ff163b7f28cee67065877954333 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 18:13:38 +0200 Subject: [PATCH 50/69] pre-cleanup --- resources/main.mjs | 1 + sw.mjs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 95b640b20..2c7a6695d 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -185,6 +185,7 @@ class MainBenchmarkClient { } handleError(error) { + if (this._state === BENCHMARK_STATE.ERROR) return; this._metrics = Object.create(null); this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._resourcePreloader?.clearServiceWorker(); diff --git a/sw.mjs b/sw.mjs index 24b97f7b8..d93948e67 100644 --- a/sw.mjs +++ b/sw.mjs @@ -37,9 +37,14 @@ class LockStore { const db = await this._openDB(); return await new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, mode); + let result = null; const req = callback(tx.objectStore(STORE_NAME)); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); + if (req) { + req.onsuccess = () => { result = req.result; }; + } + tx.oncomplete = () => resolve(result); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(new Error("Transaction aborted")); }); } catch (e) { console.warn(`IndexedDB ${mode} failed`, e); From d71c2c01f0bd5bbd5c0a22e2314bb8a431c508f4 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 20 Jul 2026 18:46:37 +0200 Subject: [PATCH 51/69] hard-stop --- resources/main.mjs | 6 +- resources/preloader.mjs | 24 +--- resources/shared/sw-messages.mjs | 5 +- sw.mjs | 191 +++++++------------------------ 4 files changed, 46 insertions(+), 180 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 2c7a6695d..26eb0dfbd 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -185,10 +185,10 @@ class MainBenchmarkClient { } handleError(error) { - if (this._state === BENCHMARK_STATE.ERROR) return; + if (this._state === BENCHMARK_STATE.ERROR) + return; this._metrics = Object.create(null); this._setBenchmarkState(BENCHMARK_STATE.ERROR); - this._resourcePreloader?.clearServiceWorker(); this._populateErrorMessage(error.message); this.showResultsSummary(); throw error; @@ -400,7 +400,7 @@ class MainBenchmarkClient { } async _preloadResources(benchmarkConfigurator) { - await this._resourcePreloader.resetPreloading(); + await this._resourcePreloader.stopPreloading(); if (this._resourcePreloader.isCached()) return; diff --git a/resources/preloader.mjs b/resources/preloader.mjs index 11b0248e2..9e48d1232 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -8,7 +8,6 @@ export class ResourcePreloader { this._registration = null; this._sw = null; this._preloadParams = ""; - this._clientId = null; this._pendingRequest = null; this._onPreloadProgress = null; } @@ -27,7 +26,6 @@ export class ResourcePreloader { await this._registerServiceWorker(); this._setupMessageListener(); - this._clientId = await this._getClientId(); } async _unregisterOldServiceWorkers() { @@ -44,14 +42,6 @@ export class ResourcePreloader { this._sw = navigator.serviceWorker.controller || this._registration.active; } - async _getClientId() { - const response = await this._postMessage({ type: SW_MESSAGES.GET_CLIENT_ID }, 5000); - const clientId = response.clientId; - if (!clientId) - throw new Error("Could not retrieve service worker client id"); - return clientId; - } - _setupMessageListener() { navigator.serviceWorker.addEventListener("message", this.onMessage.bind(this)); } @@ -94,9 +84,6 @@ export class ResourcePreloader { if (!this._sw) return Promise.resolve(); - if (this._clientId) - messageData.clientId = this._clientId; - if (this._pendingRequest) this._rejectPendingRequest(`Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`); @@ -132,19 +119,12 @@ export class ResourcePreloader { console.warn(response.requests.join("\n")); throw new Error("The benchmark had missing resources that were not cached. Check the console for details."); } - await this.clearServiceWorker(); - } - - async clearServiceWorker() { - if (!this._sw) - return; - await this._postMessage({ type: SW_MESSAGES.CLEAR_CACHE }); } - async resetPreloading() { + async stopPreloading() { if (!this._sw) return; - await this._postMessage({ type: SW_MESSAGES.RESET_PRELOADING }); + await this._postMessage({ type: SW_MESSAGES.STOP_PRELOADING }); if (this._activePreloadPromise) await this._activePreloadPromise; } diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs index 02017c055..7e1cff97f 100644 --- a/resources/shared/sw-messages.mjs +++ b/resources/shared/sw-messages.mjs @@ -2,13 +2,10 @@ export const SW_MESSAGES = Object.freeze({ PRELOAD_SUITES: "PRELOAD_SUITES", PRELOAD_PROGRESS: "PRELOAD_PROGRESS", PRELOAD_DONE: "PRELOAD_DONE", - RESET_PRELOADING: "RESET_PRELOADING", + STOP_PRELOADING: "STOP_PRELOADING", FATAL_ERROR: "FATAL_ERROR", ERROR: "ERROR", SUCCESS: "SUCCESS", - CLEAR_CACHE: "CLEAR_CACHE", GET_FAILED_REQUESTS: "GET_FAILED_REQUESTS", FAILED_REQUESTS: "FAILED_REQUESTS", - GET_CLIENT_ID: "GET_CLIENT_ID", - CLIENT_ID: "CLIENT_ID", }); diff --git a/sw.mjs b/sw.mjs index d93948e67..c5a603cdf 100644 --- a/sw.mjs +++ b/sw.mjs @@ -1,79 +1,13 @@ import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs"; import { RequestLimiter } from "./resources/shared/request-limiter.mjs"; -const CACHE_NAME = "speedometer-cache-v4.0"; -const DB_NAME = "SpeedometerStateDB"; -const STORE_NAME = "SpeedometerSWState"; -const DB_VERSION = 2; -let failedRequests = new Set(); - -class LockStore { - constructor() { - this.dbPromise = null; - } - - async _openDB() { - if (this.dbPromise) - return this.dbPromise; - - this.dbPromise = new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onupgradeneeded = (event) => { - const db = event.target.result; - if (!db.objectStoreNames.contains(STORE_NAME)) - db.createObjectStore(STORE_NAME); - }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => { - this.dbPromise = null; - reject(request.error); - }; - }); - return this.dbPromise; - } - - async _runTransaction(mode, callback) { - try { - const db = await this._openDB(); - return await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, mode); - let result = null; - const req = callback(tx.objectStore(STORE_NAME)); - if (req) { - req.onsuccess = () => { result = req.result; }; - } - tx.oncomplete = () => resolve(result); - tx.onerror = () => reject(tx.error); - tx.onabort = () => reject(new Error("Transaction aborted")); - }); - } catch (e) { - console.warn(`IndexedDB ${mode} failed`, e); - return null; - } - } - - async getOwner() { - const data = await this._runTransaction("readonly", (store) => store.get("stateData")); - return data?.clientId || null; - } - - async setOwner(clientId) { - await this._runTransaction("readwrite", (store) => store.put({ clientId }, "stateData")); - } - - async clear() { - await this._runTransaction("readwrite", (store) => store.delete("stateData")); - } - - async hasLock(clientId) { - const activeClientId = await this.getOwner(); - if (!activeClientId) - return true; - return activeClientId === clientId; - } -} - -const STORE = new LockStore(); +const CACHE_NAME = "speedometer-cache-v4"; +const MAX_CONCURRENT_REQUESTS = 20; +const WORKER_STATE = { + failedRequests: new Set(), + abortController: null, + requestLimiter: new RequestLimiter(MAX_CONCURRENT_REQUESTS), +}; function replyToClient(event, status, msg = {}) { const type = event.data?.type; @@ -84,36 +18,33 @@ function replyError(event, message) { replyToClient(event, SW_MESSAGES.ERROR, { message }); } -const MAX_CONCURRENT_REQUESTS = 20; - -const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS); - -let activePreloadController = null; - -function handleResetPreloadingMessage(event) { - resetPreloading(); - replyToClient(event, SW_MESSAGES.RESET_PRELOADING); +async function handleStopPreloadingMessage(event) { + await stopPreloading(event.source?.id); + replyToClient(event, SW_MESSAGES.STOP_PRELOADING); } -function resetPreloading() { - if (activePreloadController) { - activePreloadController.abort(); - activePreloadController = null; +async function stopPreloading(currentClientId) { + if (WORKER_STATE.abortController) { + WORKER_STATE.abortController.abort(); + WORKER_STATE.abortController = null; } - requestLimiter.clear(); + const allClients = await self.clients.matchAll({ includeUncontrolled: true }); + for (const client of allClients) { + if (currentClientId && client.id !== currentClientId) + client.postMessage({ type: SW_MESSAGES.FATAL_ERROR, message: "Speedometer aborted: Another tab took over." }); + } + + WORKER_STATE.requestLimiter.clear(); } -async function handlePreloadSuitesMessage(event, clientId, { suites = [], clearCache = true }) { +async function handlePreloadSuitesMessage(event, { suites = [], clearCache = true }) { try { - await STORE.getOwner(); - await updateActiveClient(clientId); - - resetPreloading(); + await stopPreloading(event.source?.id); - activePreloadController = new AbortController(); - const signal = activePreloadController.signal; + WORKER_STATE.abortController = new AbortController(); + const signal = WORKER_STATE.abortController.signal; - failedRequests.clear(); + WORKER_STATE.failedRequests.clear(); if (clearCache) await caches.delete(CACHE_NAME); @@ -134,11 +65,6 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], clearC await Promise.all(promises); - if (!await STORE.hasLock(clientId)) { - replyError(event, "Speedometer aborted: Another tab took over."); - return; - } - replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); } catch (error) { if (error.name === "AbortError") @@ -146,8 +72,8 @@ async function handlePreloadSuitesMessage(event, clientId, { suites = [], clearC console.error("Error during preload:", error); replyError(event, error.message || "Failed to preload resources."); } finally { - if (activePreloadController && !activePreloadController.signal.aborted) - activePreloadController = null; + if (WORKER_STATE.abortController && !WORKER_STATE.abortController.signal.aborted) + WORKER_STATE.abortController = null; } } @@ -164,8 +90,9 @@ async function getUrlsToCache(suites) { async function parseSuiteResources(suite) { const response = await fetch(suite.resources); if (!response.ok) { - console.error(`Failed to load resources for suite ${suite.name}: ${suite.resources} returned ${response.status}`); - throw new Error(`Failed to load resources for suite ${suite.name}: ${suite.resources} returned ${response.status}`); + const errorMsg = `Failed to load resources for suite ${suite.name}: ${suite.resources} returned ${response.status}`; + console.error(errorMsg); + throw new Error(errorMsg); } const text = await response.text(); return text @@ -183,12 +110,10 @@ function getResponseSize(response) { } async function fetchAndCache(cache, url, signal) { - if (signal.aborted) - throw new DOMException("Aborted", "AbortError"); + signal.throwIfAborted(); - return requestLimiter.schedule(async () => { - if (signal.aborted) - throw new DOMException("Aborted", "AbortError"); + return WORKER_STATE.requestLimiter.schedule(async () => { + signal.throwIfAborted(); const request = new Request(url, { cache: "no-cache", signal }); const existing = await cache.match(request, { ignoreSearch: true }); if (existing) @@ -202,8 +127,7 @@ async function fetchAndCache(cache, url, signal) { await cache.put(request, response); return size; } catch (e) { - if (e.name === "AbortError") - throw e; + signal.throwIfAborted(); console.error("Cache failed for", url, e); return 0; } @@ -218,58 +142,23 @@ self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); -async function updateActiveClient(newClientId) { - const oldClientId = await STORE.getOwner(); - if (oldClientId === newClientId) - return false; - - await STORE.setOwner(newClientId); - - if (oldClientId) { - const oldClient = await self.clients.get(oldClientId); - oldClient?.postMessage({ type: SW_MESSAGES.FATAL_ERROR, message: "Speedometer aborted: Another tab took over." }); - } - return true; -} - -async function handleClearCacheMessage(event, clientId) { - try { - if (!await STORE.hasLock(clientId)) { - replyError(event, "Cannot clear SW: You do not own the lock."); - return; - } - await STORE.clear(); - replyToClient(event, SW_MESSAGES.SUCCESS); - } catch (e) { - replyError(event, e.message || "Failed to clear cache"); - } -} - -async function handleGetFailedRequestsMessage(event, clientId) { - replyToClient(event, SW_MESSAGES.FAILED_REQUESTS, { requests: Array.from(failedRequests) }); -} - -async function handleGetClientIdMessage(event, clientId) { - const id = event.source?.id || clientId || crypto.randomUUID(); - replyToClient(event, SW_MESSAGES.CLIENT_ID, { clientId: id }); +async function handleGetFailedRequestsMessage(event) { + replyToClient(event, SW_MESSAGES.FAILED_REQUESTS, { requests: Array.from(WORKER_STATE.failedRequests) }); } const MESSAGE_HANDLERS = Object.freeze({ [SW_MESSAGES.PRELOAD_SUITES]: handlePreloadSuitesMessage, - [SW_MESSAGES.RESET_PRELOADING]: handleResetPreloadingMessage, - [SW_MESSAGES.CLEAR_CACHE]: handleClearCacheMessage, + [SW_MESSAGES.STOP_PRELOADING]: handleStopPreloadingMessage, [SW_MESSAGES.GET_FAILED_REQUESTS]: handleGetFailedRequestsMessage, - [SW_MESSAGES.GET_CLIENT_ID]: handleGetClientIdMessage, }); self.addEventListener("message", (event) => { const { data } = event; if (!data) return; - const clientId = data.clientId || event.source?.id; const handler = MESSAGE_HANDLERS[data.type]; if (handler) - event.waitUntil(handler(event, clientId, data)); + event.waitUntil(handler(event, data)); else console.error("Unknown service worker message type:", data.type); }); @@ -294,7 +183,7 @@ self.addEventListener("fetch", (event) => { try { return await fetch(event.request); } catch (error) { - failedRequests.add(event.request.url); + WORKER_STATE.failedRequests.add(event.request.url); return Response.error(); } })() From 93864ff2cedabfdd9a0971290f2cec3f8b434833 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 21 Jul 2026 11:03:57 +0200 Subject: [PATCH 52/69] throttle-update --- sw.mjs | 102 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/sw.mjs b/sw.mjs index c5a603cdf..0beb8ba0f 100644 --- a/sw.mjs +++ b/sw.mjs @@ -28,44 +28,29 @@ async function stopPreloading(currentClientId) { WORKER_STATE.abortController.abort(); WORKER_STATE.abortController = null; } + await stopAllOtherClients(currentClientId); + WORKER_STATE.requestLimiter.clear(); +} + +async function stopAllOtherClients(currentClientId) { const allClients = await self.clients.matchAll({ includeUncontrolled: true }); for (const client of allClients) { if (currentClientId && client.id !== currentClientId) client.postMessage({ type: SW_MESSAGES.FATAL_ERROR, message: "Speedometer aborted: Another tab took over." }); } - WORKER_STATE.requestLimiter.clear(); } async function handlePreloadSuitesMessage(event, { suites = [], clearCache = true }) { try { await stopPreloading(event.source?.id); - - WORKER_STATE.abortController = new AbortController(); - const signal = WORKER_STATE.abortController.signal; - - WORKER_STATE.failedRequests.clear(); - if (clearCache) - await caches.delete(CACHE_NAME); - - const cache = await caches.open(CACHE_NAME); - const urlsToCache = await getUrlsToCache(suites); - const total = urlsToCache.length; - - let loaded = 0; - let transferredSize = 0; - - const promises = urlsToCache.map(async (item, index) => { - const size = await fetchAndCache(cache, item.url, signal); - transferredSize += size; - loaded++; - if (!signal.aborted) - replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName }); - }); - - await Promise.all(promises); - - replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length }); + // debounce on progress callbacks to limit overhead. + const MAX_RATE_MS = 100; + const onProgress = (payload) => { + replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, payload); + }; + const { transferredSize, count } = await preloadSuites(suites, clearCache, throttle(onProgress, MAX_RATE_MS)); + replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count }); } catch (error) { if (error.name === "AbortError") return; @@ -77,6 +62,45 @@ async function handlePreloadSuitesMessage(event, { suites = [], clearCache = tru } } +function throttle(callback, max_rate_ms) { + let lastCallbackTime = 0; + return (...args) => { + const now = Date.now(); + if (now - lastCallbackTime > max_rate_ms) { + lastCallbackTime = now; + callback(...args); + } + }; +} + +async function preloadSuites(suites, clearCache, onProgress) { + const abortController = new AbortController(); + const signal = abortController.signal; + WORKER_STATE.abortController = abortController; + + WORKER_STATE.failedRequests.clear(); + if (clearCache) + await caches.delete(CACHE_NAME); + + const cache = await caches.open(CACHE_NAME); + const urlsToCache = await getUrlsToCache(suites); + const total = urlsToCache.length; + + let loaded = 0; + let transferredSize = 0; + + const promises = urlsToCache.map(async (item, index) => { + const size = await fetchAndCache(cache, item.url, signal); + transferredSize += size; + loaded++; + if (!signal.aborted) + onProgress({ loaded, total, url: item.url, suiteName: item.suiteName }); + }); + + await Promise.all(promises); + return { transferredSize, count: urlsToCache.length }; +} + async function getUrlsToCache(suites) { const urlsToCache = []; for (const suite of suites) { @@ -104,11 +128,6 @@ async function parseSuiteResources(suite) { })); } -function getResponseSize(response) { - const contentLength = response.headers.get("content-length"); - return contentLength ? parseInt(contentLength, 10) : 0; -} - async function fetchAndCache(cache, url, signal) { signal.throwIfAborted(); @@ -134,13 +153,10 @@ async function fetchAndCache(cache, url, signal) { }); } -self.addEventListener("install", () => { - self.skipWaiting(); -}); - -self.addEventListener("activate", (event) => { - event.waitUntil(self.clients.claim()); -}); +function getResponseSize(response) { + const contentLength = response.headers.get("content-length"); + return contentLength ? parseInt(contentLength, 10) : 0; +} async function handleGetFailedRequestsMessage(event) { replyToClient(event, SW_MESSAGES.FAILED_REQUESTS, { requests: Array.from(WORKER_STATE.failedRequests) }); @@ -152,6 +168,14 @@ const MESSAGE_HANDLERS = Object.freeze({ [SW_MESSAGES.GET_FAILED_REQUESTS]: handleGetFailedRequestsMessage, }); +self.addEventListener("install", () => { + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil(self.clients.claim()); +}); + self.addEventListener("message", (event) => { const { data } = event; if (!data) From 39685994823f9443c930f835c8fb71c6ac8aeb41 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 21 Jul 2026 11:26:23 +0200 Subject: [PATCH 53/69] add-abort-handling --- resources/main.mjs | 7 +++++++ resources/preloader.mjs | 6 +++++- sw.mjs | 16 +++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 26eb0dfbd..3c9166bec 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -134,13 +134,20 @@ class MainBenchmarkClient { return this._metrics; } + _assertNotAborted() { + if (this._state === BENCHMARK_STATE.ERROR) + throw new Error("Benchmark aborted by another process."); + } + willAddTestFrame(frame) { + this._assertNotAborted(); frame.style.left = "50%"; frame.style.top = "50%"; frame.style.transform = "translate(-50%, -50%)"; } async willRunTest(suite, test) { + this._assertNotAborted(); document.getElementById("info-label").textContent = suite.name; document.getElementById("info-progress").textContent = `${this._finishedTestCount} / ${this.stepCount}`; if (this._steppingPromise) diff --git a/resources/preloader.mjs b/resources/preloader.mjs index 9e48d1232..f10cd62a9 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -125,8 +125,11 @@ export class ResourcePreloader { if (!this._sw) return; await this._postMessage({ type: SW_MESSAGES.STOP_PRELOADING }); - if (this._activePreloadPromise) + if (this._activePreloadPromise) { + // Await to explicitly catch abort error potentially triggered by + // by other racing tabs. await this._activePreloadPromise; + } } async preloadSuites(suites, clearCache = true, onProgress) { @@ -159,6 +162,7 @@ export class ResourcePreloader { console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`); } this._preloadParams = params.toSearchParams(); + this._onPreloadProgress = undefined; return undefined; } diff --git a/sw.mjs b/sw.mjs index 0beb8ba0f..c9163af66 100644 --- a/sw.mjs +++ b/sw.mjs @@ -36,7 +36,7 @@ async function stopAllOtherClients(currentClientId) { const allClients = await self.clients.matchAll({ includeUncontrolled: true }); for (const client of allClients) { if (currentClientId && client.id !== currentClientId) - client.postMessage({ type: SW_MESSAGES.FATAL_ERROR, message: "Speedometer aborted: Another tab took over." }); + client.postMessage({ type: SW_MESSAGES.FATAL_ERROR, message: "Aborted Run: Another tab took over." }); } } @@ -56,9 +56,6 @@ async function handlePreloadSuitesMessage(event, { suites = [], clearCache = tru return; console.error("Error during preload:", error); replyError(event, error.message || "Failed to preload resources."); - } finally { - if (WORKER_STATE.abortController && !WORKER_STATE.abortController.signal.aborted) - WORKER_STATE.abortController = null; } } @@ -83,7 +80,7 @@ async function preloadSuites(suites, clearCache, onProgress) { await caches.delete(CACHE_NAME); const cache = await caches.open(CACHE_NAME); - const urlsToCache = await getUrlsToCache(suites); + const urlsToCache = await getUrlsToCache(suites, signal); const total = urlsToCache.length; let loaded = 0; @@ -101,18 +98,19 @@ async function preloadSuites(suites, clearCache, onProgress) { return { transferredSize, count: urlsToCache.length }; } -async function getUrlsToCache(suites) { +async function getUrlsToCache(suites, signal) { const urlsToCache = []; for (const suite of suites) { + signal.throwIfAborted(); if (suite.resources) - urlsToCache.push(...await parseSuiteResources(suite)); + urlsToCache.push(...await parseSuiteResources(suite, signal)); } return urlsToCache; } -async function parseSuiteResources(suite) { - const response = await fetch(suite.resources); +async function parseSuiteResources(suite, signal) { + const response = await fetch(suite.resources, { signal }); if (!response.ok) { const errorMsg = `Failed to load resources for suite ${suite.name}: ${suite.resources} returned ${response.status}`; console.error(errorMsg); From 6d4185e01fddb981f402a88988707aefd16dfb8f Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 18 Aug 2026 11:52:04 +0200 Subject: [PATCH 54/69] fix-conflicts --- resources/main.mjs | 69 ---------------------------------------------- 1 file changed, 69 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 623abe3f6..d41778cc2 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -4,7 +4,6 @@ import { renderMetricView } from "./metric-ui.mjs"; import { defaultParams, params } from "./shared/params.mjs"; import { createDeveloperModeContainer } from "./developer-mode.mjs"; -<<<<<<< HEAD import { ResourcePreloader, PreloadStatusUpdater } from "./preloader.mjs"; const BENCHMARK_STATE = Object.freeze({ @@ -16,17 +15,6 @@ const BENCHMARK_STATE = Object.freeze({ ERROR: "ERROR", }); -||||||| 59832dd0 -======= -const BENCHMARK_STATE = Object.freeze({ - IDLE: "IDLE", - READY: "READY", - RUNNING: "RUNNING", - DONE: "DONE", - ERROR: "ERROR", -}); - ->>>>>>> main // FIXME(camillobruni): Add base class class MainBenchmarkClient { developerMode = false; @@ -35,16 +23,9 @@ class MainBenchmarkClient { _measuredValuesList = []; _finishedTestCount = 0; _progressCompleted = null; -<<<<<<< HEAD - _state = BENCHMARK_STATE.IDLE; -||||||| 59832dd0 _isRunning = false; _hasResults = false; _developerModeContainer = null; -======= - _state = BENCHMARK_STATE.IDLE; - _developerModeContainer = null; ->>>>>>> main _metrics = Object.create(null); _resourcePreloader = new ResourcePreloader(); _steppingPromise = null; @@ -141,23 +122,11 @@ class MainBenchmarkClient { } this._metrics = Object.create(null); -<<<<<<< HEAD -||||||| 59832dd0 - this._isRunning = true; - -======= - ->>>>>>> main this.stepCount = params.iterationCount * totalSuitesCount; this._progressCompleted.max = this.stepCount; this.suitesCount = enabledSuites.length; -<<<<<<< HEAD this._setBenchmarkState(BENCHMARK_STATE.RUNNING); -||||||| 59832dd0 -======= - this._setBenchmarkState(BENCHMARK_STATE.RUNNING); ->>>>>>> main const runner = new BenchmarkRunner(benchmarkConfigurator.suites, this); runner.runMultipleIterations(params.iterationCount); return true; @@ -203,28 +172,12 @@ class MainBenchmarkClient { this._finishedTestCount = 0; } -<<<<<<< HEAD async didFinishLastIteration(metrics) { console.assert(this.isRunning()); -||||||| 59832dd0 - didFinishLastIteration(metrics) { - console.assert(this._isRunning); - this._isRunning = false; - this._hasResults = true; -======= - didFinishLastIteration(metrics) { - console.assert(this.isRunning()); - ->>>>>>> main this._metrics = metrics; -<<<<<<< HEAD this._setBenchmarkState(BENCHMARK_STATE.DONE); await this._resourcePreloader.teardown(); -||||||| 59832dd0 -======= - this._setBenchmarkState(BENCHMARK_STATE.DONE); ->>>>>>> main const scoreResults = this._computeResults(this._measuredValuesList, "score"); if (scoreResults.isValid) @@ -451,7 +404,6 @@ class MainBenchmarkClient { if (params.startAutomatically) this.start(); -<<<<<<< HEAD else this._setBenchmarkState(BENCHMARK_STATE.READY); } @@ -493,27 +445,6 @@ class MainBenchmarkClient { }); } } -||||||| 59832dd0 -======= - else - this._setBenchmarkState(BENCHMARK_STATE.READY); - } - - async _setBenchmarkState(state) { - this._state = state; - document.body.setAttribute("data-benchmark-state", state); - const startButtons = document.querySelectorAll(".start-tests-button"); - if (state !== BENCHMARK_STATE.RUNNING) { - startButtons.forEach((btn) => { - btn.innerHTML = "
Start Test
"; - }); - if (state === BENCHMARK_STATE.READY) { - startButtons.forEach((btn) => { - btn.disabled = false; - }); - } - } ->>>>>>> main } _hashChangeHandler() { From 2b62dd3182259759e0fedeca2056e691bff77cd6 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 18 Aug 2026 16:34:29 +0200 Subject: [PATCH 55/69] fix-tests --- resources/preloader.mjs | 39 ++++++++++++++++++++++++++++++++------- tests/run-end2end.mjs | 29 +++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/resources/preloader.mjs b/resources/preloader.mjs index f10cd62a9..2c149aff7 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -84,8 +84,17 @@ export class ResourcePreloader { if (!this._sw) return Promise.resolve(); - if (this._pendingRequest) - this._rejectPendingRequest(`Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`); + if (this._pendingRequest) { + const isCooperativePreemption = this._pendingRequest.type === SW_MESSAGES.PRELOAD_SUITES && messageData.type === SW_MESSAGES.STOP_PRELOADING; + + const logMessage = `Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`; + if (isCooperativePreemption) { + console.warn(logMessage); + this._resolvePendingRequest(null); + } else { + this._rejectPendingRequest(logMessage); + } + } return new Promise((resolve, reject) => { let timeoutId = null; @@ -94,6 +103,7 @@ export class ResourcePreloader { timeoutId = setTimeout(() => { if (this._pendingRequest?.type === messageData.type) this._rejectPendingRequest(`Service worker message timed out: ${messageData.type}`); + }, timeoutMs); } @@ -102,6 +112,15 @@ export class ResourcePreloader { }); } + _resolvePendingRequest(data = null) { + const pendingRequest = this._pendingRequest; + this._pendingRequest = null; + if (pendingRequest.timeoutId) + clearTimeout(pendingRequest.timeoutId); + + pendingRequest.resolve(data); + } + _rejectPendingRequest(errorMessage) { const pendingRequest = this._pendingRequest; this._pendingRequest = null; @@ -113,6 +132,8 @@ export class ResourcePreloader { } async teardown() { + await this.stopPreloading(); + const response = await this.getFailedRequests(); if (response.requests?.length > 0) { console.warn("The following requests failed during the benchmark and bypassed the cache:"); @@ -124,19 +145,20 @@ export class ResourcePreloader { async stopPreloading() { if (!this._sw) return; + await this._postMessage({ type: SW_MESSAGES.STOP_PRELOADING }); - if (this._activePreloadPromise) { - // Await to explicitly catch abort error potentially triggered by - // by other racing tabs. + if (this._activePreloadPromise) await this._activePreloadPromise; - } + } async preloadSuites(suites, clearCache = true, onProgress) { if (!this._sw) return undefined; + if (this._onPreloadProgress) throw new Error("Preload already active"); + this._onPreloadProgress = onProgress; const suitesData = suites @@ -155,7 +177,7 @@ export class ResourcePreloader { const response = await this._activePreloadPromise; this._activePreloadPromise = null; - if (response.status === SW_MESSAGES.PRELOAD_DONE) { + if (response?.status === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); const timeSec = (timeTakenMs / 1000).toFixed(2); @@ -167,6 +189,9 @@ export class ResourcePreloader { } async getFailedRequests() { + if (!this._sw) + return { requests: [] }; + return this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); } } diff --git a/tests/run-end2end.mjs b/tests/run-end2end.mjs index da041304e..a3ff58316 100644 --- a/tests/run-end2end.mjs +++ b/tests/run-end2end.mjs @@ -22,6 +22,7 @@ async function testPage(url) { callback(); else globalThis.addEventListener("SpeedometerReady", () => callback(), { once: true }); + }); console.log(" - Awaiting Benchmark"); @@ -35,24 +36,35 @@ async function testPage(url) { { once: true } ); // Install error handlers to report page errors back to selenium. - globalThis.addEventListener("error", (message, source, lineno, colno, error) => + globalThis.addEventListener("error", (event) => { callback({ - error: { message, source, lineno, colno, error }, - }) - ); + error: { + message: event.message || "Unknown Runtime Error", + source: event.filename, + lineno: event.lineno, + colno: event.colno, + stack: event.error?.stack, + }, + }); + }); globalThis.addEventListener("unhandledrejection", (e) => { + const reason = e.reason; + const message = reason instanceof Error ? reason.message : typeof reason === "object" && reason !== null ? JSON.stringify(reason) : String(reason); callback({ error: { - message: e.reason.toString(), - stack: e.reason?.stack, + message: `Unhandled Rejection: ${message}`, + stack: reason?.stack, }, }); }); globalThis.benchmarkClient.start(); }); - if (error) - throw new Error(error.message + (error?.stack ?? "")); + if (error) { + const stackTrace = error.stack ? `\nBrowser Stack Trace:\n${error.stack}` : ""; + const location = error.source ? ` at ${error.source}:${error.lineno}:${error.colno}` : ""; + throw new Error(`${error.message}${location}${stackTrace}`); + } validateMetrics(metrics); return metrics; @@ -61,6 +73,7 @@ async function testPage(url) { function validateMetrics(metrics) { for (const [name, metric] of Object.entries(metrics)) validateMetric(name, metric); + assert(metrics.Geomean.mean > 0); assert(metrics.Score.mean > 0); } From 0ec2dd29d0c367fe4db94dfc48d94c72eed2f1a8 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 24 Aug 2026 15:06:22 +0200 Subject: [PATCH 56/69] fx --- resources/main.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index d41778cc2..53f2169ed 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -23,8 +23,7 @@ class MainBenchmarkClient { _measuredValuesList = []; _finishedTestCount = 0; _progressCompleted = null; - _isRunning = false; - _hasResults = false; + _state = BENCHMARK_STATE.IDLE; _developerModeContainer = null; _metrics = Object.create(null); _resourcePreloader = new ResourcePreloader(); From a36a80c10e99a3e5b146121b4898b7c47c4f1ae3 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 24 Aug 2026 15:39:33 +0200 Subject: [PATCH 57/69] formatting --- resources/preloader.mjs | 2 -- tests/run-end2end.mjs | 1 - 2 files changed, 3 deletions(-) diff --git a/resources/preloader.mjs b/resources/preloader.mjs index 2c149aff7..f59492804 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -103,7 +103,6 @@ export class ResourcePreloader { timeoutId = setTimeout(() => { if (this._pendingRequest?.type === messageData.type) this._rejectPendingRequest(`Service worker message timed out: ${messageData.type}`); - }, timeoutMs); } @@ -149,7 +148,6 @@ export class ResourcePreloader { await this._postMessage({ type: SW_MESSAGES.STOP_PRELOADING }); if (this._activePreloadPromise) await this._activePreloadPromise; - } async preloadSuites(suites, clearCache = true, onProgress) { diff --git a/tests/run-end2end.mjs b/tests/run-end2end.mjs index a3ff58316..583766e6d 100644 --- a/tests/run-end2end.mjs +++ b/tests/run-end2end.mjs @@ -22,7 +22,6 @@ async function testPage(url) { callback(); else globalThis.addEventListener("SpeedometerReady", () => callback(), { once: true }); - }); console.log(" - Awaiting Benchmark"); From ade01a9b773bdfd391d83411fdec330c89f31faf Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 24 Aug 2026 15:46:22 +0200 Subject: [PATCH 58/69] Fix race condition causing unittests to fail on Safari This fixes a race condition where SpeedometerReady would fire while _preloadResources was still in flight asynchronously. That caused the test runner to simultaneously invoke benchmarkClient.start(), leading to concurrent preloads that randomly caused the PRELOAD_SUITES message to resolve a STOP_PRELOADING promise incorrectly on slower browsers. --- resources/main.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 53f2169ed..960247684 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -37,9 +37,7 @@ class MainBenchmarkClient { this.evaluateParams(); this._showSection(window.location.hash); - this._benchmarkConfiguratorPromise.then(() => { - window.dispatchEvent(new Event("SpeedometerReady")); - }); + } isRunning() { @@ -405,6 +403,8 @@ class MainBenchmarkClient { this.start(); else this._setBenchmarkState(BENCHMARK_STATE.READY); + + window.dispatchEvent(new Event("SpeedometerReady")); } async _preloadResources(benchmarkConfigurator) { From 56c8d78ae2ea1a09c9da440fcb0dab97f03bff8c Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 24 Aug 2026 16:10:14 +0200 Subject: [PATCH 59/69] fx --- resources/main.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 960247684..6c7af31ea 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -36,8 +36,6 @@ class MainBenchmarkClient { this.prepareUI(); this.evaluateParams(); this._showSection(window.location.hash); - - } isRunning() { From 97ead476c38db01c5603394b858254b8df5c9738 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Mon, 24 Aug 2026 22:38:10 +0200 Subject: [PATCH 60/69] fx --- resources/main.mjs | 6 ++++++ tests/run-end2end.mjs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/resources/main.mjs b/resources/main.mjs index 6c7af31ea..5d9f6b784 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -30,6 +30,11 @@ class MainBenchmarkClient { _steppingPromise = null; _steppingResolver = null; _benchmarkConfiguratorPromise = null; + _isInitialized = false; + + get isInitialized() { + return this._isInitialized; + } constructor() { this._benchmarkConfiguratorPromise = import("./benchmark-configurator.mjs"); @@ -402,6 +407,7 @@ class MainBenchmarkClient { else this._setBenchmarkState(BENCHMARK_STATE.READY); + this._isInitialized = true; window.dispatchEvent(new Event("SpeedometerReady")); } diff --git a/tests/run-end2end.mjs b/tests/run-end2end.mjs index 583766e6d..349ff8e5a 100644 --- a/tests/run-end2end.mjs +++ b/tests/run-end2end.mjs @@ -18,7 +18,7 @@ async function testPage(url) { await driver.get(`http://localhost:${port}/${url}`); await driver.executeAsyncScript((callback) => { - if (globalThis.benchmarkClient) + if (globalThis.benchmarkClient?.isInitialized) callback(); else globalThis.addEventListener("SpeedometerReady", () => callback(), { once: true }); From 2e9a3ed8ac237d77a5b6f03d8f8142da074059ae Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 11:59:10 +0000 Subject: [PATCH 61/69] [preload] Fix review comments, SW synchronicity, and developer mode UI Resolve PR 540 review comments by resolving suite resources relative to suite.resources, correlating service worker messages by requestId, scoping service worker registration cleanup, and guarding non-secure contexts. Synchronize developer mode preload toggles and benchmark UI states while expanding unit and end-to-end test coverage. --- resources/main.css | 14 -- resources/main.mjs | 44 +++-- resources/preloader.mjs | 211 +++++++++++++++++------ resources/shared/params.mjs | 3 +- sw.mjs | 59 +++++-- tests/index.html | 2 + tests/run-end2end.mjs | 47 +++++- tests/unittests/params.mjs | 30 ++-- tests/unittests/preloader.mjs | 307 ++++++++++++++++++++++++++++++++++ 9 files changed, 606 insertions(+), 111 deletions(-) create mode 100644 tests/unittests/preloader.mjs diff --git a/resources/main.css b/resources/main.css index aa6e02f32..0945a2c5e 100644 --- a/resources/main.css +++ b/resources/main.css @@ -474,15 +474,6 @@ button.show-about { display: none; } -#preload-progress { - display: none; -} - -:root[data-benchmark-state="PRELOADING"] #preload-progress, -:root[data-benchmark-state="PRELOADING"] #preload-info { - display: block; -} - #progress, #preload-progress { position: absolute; @@ -1046,11 +1037,6 @@ section#about .note { pointer-events: none; } -[data-benchmark-state="PRELOADING"] .developer-mode .start-tests-button { - pointer-events: auto; - cursor: pointer; -} - [data-benchmark-state="PRELOADING"] .start-tests-button > div { background-color: var(--background); border-radius: 10px; diff --git a/resources/main.mjs b/resources/main.mjs index 49b47f581..74a7af463 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -59,14 +59,18 @@ class MainBenchmarkClient { } async step() { + if (this._state === BENCHMARK_STATE.PRELOADING) + return; const currentSteppingResolver = this._steppingResolver; this._steppingPromise = new Promise((resolve) => { this._steppingResolver = resolve; }); currentSteppingResolver?.(); if (!this.isRunning()) { - await this._startBenchmark(); - this._showSection("#running"); + if (await this._startBenchmark()) + this._showSection("#running"); + else + this._clearStepping(); } } @@ -87,7 +91,7 @@ class MainBenchmarkClient { } async _startBenchmark() { - if (this.isRunning()) + if (this.isRunning() || this._state === BENCHMARK_STATE.PRELOADING) return false; const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; @@ -110,6 +114,8 @@ class MainBenchmarkClient { this._developerModeContainer?.remove(); await this._preloadResources(benchmarkConfigurator); + if (this._state === BENCHMARK_STATE.ERROR) + return false; this._progressCompleted = document.getElementById("progress-completed"); if (params.iterationCount < 50) { @@ -405,10 +411,11 @@ class MainBenchmarkClient { if (!params.developerMode) await this._preloadResources(benchmarkConfigurator); - if (params.startAutomatically) - this.start(); - else + if (this._state !== BENCHMARK_STATE.ERROR) { this._setBenchmarkState(BENCHMARK_STATE.READY); + if (params.startAutomatically) + await this.start(); + } this._isInitialized = true; window.dispatchEvent(new Event("SpeedometerReady")); @@ -416,10 +423,14 @@ class MainBenchmarkClient { async _preloadResources(benchmarkConfigurator) { await this._resourcePreloader.stopPreloading(); - if (this._resourcePreloader.isCached()) + await this._resourcePreloader.setup(); + if (!params.preload) return; const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); + if (this._resourcePreloader.isCached(enabledSuites)) + return; + const clearCache = !params.isDefault(); const preloadStatusUpdater = new PreloadStatusUpdater(); @@ -427,6 +438,8 @@ class MainBenchmarkClient { this._setBenchmarkState(BENCHMARK_STATE.PRELOADING); preloadStatusUpdater.start(); await this._resourcePreloader.preloadSuites(enabledSuites, clearCache, preloadStatusUpdater.onProgress.bind(preloadStatusUpdater)); + } catch (error) { + this.handleError(error); } finally { preloadStatusUpdater.stop(); } @@ -438,18 +451,21 @@ class MainBenchmarkClient { const startButtons = document.querySelectorAll(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { startButtons.forEach((btn) => { + btn.disabled = true; btn.innerHTML = "
Preloading
"; }); - } else if (state !== BENCHMARK_STATE.RUNNING) { + } else if (state === BENCHMARK_STATE.RUNNING) { document.body.style.removeProperty("--preload-progress"); + startButtons.forEach((btn) => { + btn.disabled = true; + }); + } else { + document.body.style.removeProperty("--preload-progress"); + const isDisabled = state !== BENCHMARK_STATE.READY && state !== BENCHMARK_STATE.DONE && state !== BENCHMARK_STATE.IDLE; startButtons.forEach((btn) => { btn.innerHTML = "
Start Test
"; + btn.disabled = isDisabled; }); - if (state === BENCHMARK_STATE.READY) { - startButtons.forEach((btn) => { - btn.disabled = false; - }); - } } } @@ -473,7 +489,7 @@ class MainBenchmarkClient { _logoClickHandler(event) { // Prevent any accidental UI changes during benchmark runs. - if (!this.isRunning()) + if (!this.isRunning() && this._state !== BENCHMARK_STATE.PRELOADING) this._showSection("#home"); event.preventDefault(); return false; diff --git a/resources/preloader.mjs b/resources/preloader.mjs index f59492804..e2447662c 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -4,54 +4,124 @@ import { params } from "./shared/params.mjs"; const PRELOAD_TIMEOUT_MS = 60_000; export class ResourcePreloader { - constructor() { + constructor(customParams = params) { + this._params = customParams; this._registration = null; this._sw = null; - this._preloadParams = ""; + this._preloadParams = null; this._pendingRequest = null; this._onPreloadProgress = null; + this._activePreloadPromise = null; + this._nextRequestId = 1; + this._boundOnMessage = this.onMessage.bind(this); + this._messageListenerAdded = false; + } + + _isServiceWorkerSupported() { + return "serviceWorker" in navigator && Boolean(navigator.serviceWorker); + } + + _serializeParams(suitesOrParams = this._params, maybeSuites = undefined) { + const targetParams = suitesOrParams && typeof suitesOrParams.toSearchParams === "function" ? suitesOrParams : this._params; + const suites = Array.isArray(suitesOrParams) ? suitesOrParams : maybeSuites; + const searchParamsStr = targetParams.toSearchParams(); + if (!Array.isArray(suites)) + return searchParamsStr; + const suiteNames = suites + .filter((s) => typeof s === "string" ? true : s.enabled !== false) + .map((s) => typeof s === "string" ? s : s.name) + .join(","); + return `${searchParamsStr}|suites:${suiteNames}`; } - isCached() { - if (!params.preload) - return true; - return params.toSearchParams() === this._preloadParams; + isCached(enabledSuites) { + if (!this._params.preload || (!this._registration && !this._sw) || !this._preloadParams) + return false; + if (enabledSuites === undefined) + return this._preloadParams.split("|suites:")[0] === this._params.toSearchParams(); + return this._preloadParams === this._serializeParams(this._params, enabledSuites); } async setup() { - if (!params.preload) { - await this._unregisterOldServiceWorkers(); + if (!this._isServiceWorkerSupported()) + return; + + if (!this._params.preload) { + if (this._registration || this._sw) + await this._unregisterOldServiceWorkers(); return; } + if (this._registration && this._sw) + return; + await this._registerServiceWorker(); this._setupMessageListener(); } async _unregisterOldServiceWorkers() { - const existingRegistrations = await navigator.serviceWorker.getRegistrations(); - for (const existing of existingRegistrations) - await existing.unregister(); + this._removeMessageListener(); + const activeRegistration = this._registration; this._registration = null; this._sw = null; + this._preloadParams = null; + + if (!this._isServiceWorkerSupported()) + return; + + const expectedScope = new URL("./", window.location.href).href; + let unregisteredActive = false; + if (typeof navigator.serviceWorker.getRegistrations === "function") { + const existingRegistrations = await navigator.serviceWorker.getRegistrations(); + for (const existing of existingRegistrations) { + if (existing.scope === expectedScope) { + await existing.unregister(); + if (existing === activeRegistration) + unregisteredActive = true; + } + } + } + if (activeRegistration && !unregisteredActive && typeof activeRegistration.unregister === "function") { + if (!activeRegistration.scope || activeRegistration.scope === expectedScope) + await activeRegistration.unregister(); + } + } async _registerServiceWorker() { + if (!this._isServiceWorkerSupported()) + return; + this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); await navigator.serviceWorker.ready; - this._sw = navigator.serviceWorker.controller || this._registration.active; + this._sw = navigator.serviceWorker.controller || this._registration?.active || null; } _setupMessageListener() { - navigator.serviceWorker.addEventListener("message", this.onMessage.bind(this)); + if (!this._isServiceWorkerSupported() || this._messageListenerAdded) + return; + navigator.serviceWorker.addEventListener("message", this._boundOnMessage); + this._messageListenerAdded = true; + } + + _removeMessageListener() { + if (!this._isServiceWorkerSupported() || !this._messageListenerAdded) + return; + navigator.serviceWorker.removeEventListener("message", this._boundOnMessage); + this._messageListenerAdded = false; } - _validateMessageRouting(data) { + _isValidMessage(data) { if (!this._pendingRequest) - throw new Error(`Received unexpected ServiceWorker message without any pending request: ${data.type}`); + return false; + + if (data.requestId !== undefined && data.requestId !== this._pendingRequest.requestId) + return false; if (this._pendingRequest.type !== data.type) - throw new Error(`Received ServiceWorker message type '${data.type}' but expected '${this._pendingRequest.type}'`); + return false; + + return true; } onMessage(event) { @@ -62,10 +132,11 @@ export class ResourcePreloader { return; } - this._validateMessageRouting(data); + if (!this._isValidMessage(data)) + return; if (data.status === SW_MESSAGES.PRELOAD_PROGRESS) { - this._onPreloadProgress(data); + this._onPreloadProgress?.(data); return; } @@ -82,10 +153,10 @@ export class ResourcePreloader { _postMessage(messageData, timeoutMs = 0) { if (!this._sw) - return Promise.resolve(); + return Promise.resolve(null); if (this._pendingRequest) { - const isCooperativePreemption = this._pendingRequest.type === SW_MESSAGES.PRELOAD_SUITES && messageData.type === SW_MESSAGES.STOP_PRELOADING; + const isCooperativePreemption = this._pendingRequest.type === SW_MESSAGES.PRELOAD_SUITES || messageData.type === SW_MESSAGES.STOP_PRELOADING; const logMessage = `Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`; if (isCooperativePreemption) { @@ -96,18 +167,21 @@ export class ResourcePreloader { } } + const requestId = this._nextRequestId++; + const payload = { ...messageData, requestId }; + return new Promise((resolve, reject) => { let timeoutId = null; if (timeoutMs > 0) { timeoutId = setTimeout(() => { - if (this._pendingRequest?.type === messageData.type) + if (this._pendingRequest?.requestId === requestId) this._rejectPendingRequest(`Service worker message timed out: ${messageData.type}`); }, timeoutMs); } - this._pendingRequest = { type: messageData.type, resolve, reject, timeoutId }; - this._sw.postMessage(messageData); + this._pendingRequest = { type: messageData.type, requestId, resolve, reject, timeoutId }; + this._sw.postMessage(payload); }); } @@ -131,10 +205,15 @@ export class ResourcePreloader { } async teardown() { + this._preloadParams = null; + if (!this._sw && !this._registration) + return; + await this.stopPreloading(); const response = await this.getFailedRequests(); - if (response.requests?.length > 0) { + await this._unregisterOldServiceWorkers(); + if (response?.requests?.length > 0) { console.warn("The following requests failed during the benchmark and bypassed the cache:"); console.warn(response.requests.join("\n")); throw new Error("The benchmark had missing resources that were not cached. Check the console for details."); @@ -145,19 +224,34 @@ export class ResourcePreloader { if (!this._sw) return; + if (this._activePreloadPromise || this._pendingRequest?.type === SW_MESSAGES.PRELOAD_SUITES) + this._preloadParams = null; + await this._postMessage({ type: SW_MESSAGES.STOP_PRELOADING }); if (this._activePreloadPromise) await this._activePreloadPromise; } async preloadSuites(suites, clearCache = true, onProgress) { + if (!this._isServiceWorkerSupported()) + return undefined; + + if (!this._params.preload) { + if (this._registration || this._sw) + await this._unregisterOldServiceWorkers(); + return undefined; + } + + if (!this._sw && !this._registration) + await this.setup(); + if (!this._sw) return undefined; - if (this._onPreloadProgress) + if (this._activePreloadPromise || this._onPreloadProgress) throw new Error("Preload already active"); - this._onPreloadProgress = onProgress; + this._preloadParams = null; const suitesData = suites .filter((s) => s.resources) @@ -170,27 +264,32 @@ export class ResourcePreloader { if (suitesData.length === 0) return undefined; + this._onPreloadProgress = onProgress; const startTime = performance.now(); - this._activePreloadPromise = this._postMessage({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, clearCache }, PRELOAD_TIMEOUT_MS); - const response = await this._activePreloadPromise; - this._activePreloadPromise = null; - - if (response?.status === SW_MESSAGES.PRELOAD_DONE) { - const timeTakenMs = performance.now() - startTime; - const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2); - const timeSec = (timeTakenMs / 1000).toFixed(2); - console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`); + try { + this._activePreloadPromise = this._postMessage({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, clearCache }, PRELOAD_TIMEOUT_MS); + const response = await this._activePreloadPromise; + + if (response?.status === SW_MESSAGES.PRELOAD_DONE) { + const timeTakenMs = performance.now() - startTime; + const sizeMB = ((response.transferredSize || 0) / (1024 * 1024)).toFixed(2); + const timeSec = (timeTakenMs / 1000).toFixed(2); + console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`); + this._preloadParams = this._serializeParams(this._params, suites); + } + return response; + } finally { + this._activePreloadPromise = null; + this._onPreloadProgress = null; } - this._preloadParams = params.toSearchParams(); - this._onPreloadProgress = undefined; - return undefined; } async getFailedRequests() { if (!this._sw) return { requests: [] }; - return this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); + const response = await this._postMessage({ type: SW_MESSAGES.GET_FAILED_REQUESTS }); + return response || { requests: [] }; } } @@ -206,10 +305,20 @@ export class PreloadStatusUpdater { } start() { - this._progressCompletedElement.value = 0; - this._infoLabelElement.textContent = ""; - this._infoProgressElement.textContent = ""; - document.body.style.setProperty("--preload-progress", "0%"); + if (this._rafId) { + cancelAnimationFrame(this._rafId); + this._rafId = null; + } + this._latestData = null; + if (this._progressCompletedElement) { + this._progressCompletedElement.max = 1; + this._progressCompletedElement.value = 0; + } + if (this._infoLabelElement) + this._infoLabelElement.textContent = ""; + if (this._infoProgressElement) + this._infoProgressElement.textContent = ""; + document.body?.style.setProperty("--preload-progress", "0%"); } onProgress(data) { @@ -225,13 +334,17 @@ export class PreloadStatusUpdater { return; const { loaded, total, url, suiteName } = data; - document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); - this._progressCompletedElement.max = total; - this._progressCompletedElement.value = loaded; - const match = url.match(/(?:suites(?:-experimental)?|resources)\/(.+)$/); + document.body?.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); + if (this._progressCompletedElement) { + this._progressCompletedElement.max = total > 0 ? total : 1; + this._progressCompletedElement.value = total > 0 ? loaded : 0; + } + const match = typeof url === "string" ? url.match(/(?:suites(?:-experimental)?|resources)\/(.+)$/) : null; const filename = match ? match[1] : ""; - this._infoLabelElement.textContent = `${suiteName}: ${filename}`; - this._infoProgressElement.textContent = `${loaded} / ${total}`; + if (this._infoLabelElement) + this._infoLabelElement.textContent = `${suiteName}: ${filename}`; + if (this._infoProgressElement) + this._infoProgressElement.textContent = `${loaded} / ${total}`; } stop() { diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index 012d4475c..e842c2d2c 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -102,8 +102,9 @@ export class Params { _parseBooleanParam(searchParams, paramKey) { if (!searchParams.has(paramKey)) return false; + const value = searchParams.get(paramKey).toLowerCase(); searchParams.delete(paramKey); - return true; + return value !== "false" && value !== "0"; } _parserNumberParam(searchParams, paramKey, minValue) { diff --git a/sw.mjs b/sw.mjs index c9163af66..c72208160 100644 --- a/sw.mjs +++ b/sw.mjs @@ -11,7 +11,8 @@ const WORKER_STATE = { function replyToClient(event, status, msg = {}) { const type = event.data?.type; - event.source?.postMessage({ type, status, ...msg }); + const requestId = event.data?.requestId; + event.source?.postMessage({ type, status, requestId, ...msg }); } function replyError(event, message) { @@ -23,15 +24,19 @@ async function handleStopPreloadingMessage(event) { replyToClient(event, SW_MESSAGES.STOP_PRELOADING); } -async function stopPreloading(currentClientId) { +function abortActivePreload() { if (WORKER_STATE.abortController) { WORKER_STATE.abortController.abort(); WORKER_STATE.abortController = null; } - await stopAllOtherClients(currentClientId); WORKER_STATE.requestLimiter.clear(); } +async function stopPreloading(currentClientId) { + abortActivePreload(); + await stopAllOtherClients(currentClientId); +} + async function stopAllOtherClients(currentClientId) { const allClients = await self.clients.matchAll({ includeUncontrolled: true }); for (const client of allClients) { @@ -42,20 +47,31 @@ async function stopAllOtherClients(currentClientId) { } async function handlePreloadSuitesMessage(event, { suites = [], clearCache = true }) { + abortActivePreload(); + const abortController = new AbortController(); + const signal = abortController.signal; + WORKER_STATE.abortController = abortController; + try { - await stopPreloading(event.source?.id); + await stopAllOtherClients(event.source?.id); + signal.throwIfAborted(); // debounce on progress callbacks to limit overhead. const MAX_RATE_MS = 100; const onProgress = (payload) => { - replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, payload); + if (!signal.aborted) + replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, payload); }; - const { transferredSize, count } = await preloadSuites(suites, clearCache, throttle(onProgress, MAX_RATE_MS)); + const { transferredSize, count } = await preloadSuites(suites, clearCache, throttle(onProgress, MAX_RATE_MS), signal); + signal.throwIfAborted(); replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count }); } catch (error) { - if (error.name === "AbortError") + if (error.name === "AbortError" || signal.aborted) return; console.error("Error during preload:", error); replyError(event, error.message || "Failed to preload resources."); + } finally { + if (WORKER_STATE.abortController === abortController) + WORKER_STATE.abortController = null; } } @@ -70,24 +86,25 @@ function throttle(callback, max_rate_ms) { }; } -async function preloadSuites(suites, clearCache, onProgress) { - const abortController = new AbortController(); - const signal = abortController.signal; - WORKER_STATE.abortController = abortController; - +async function preloadSuites(suites, clearCache, onProgress, signal) { WORKER_STATE.failedRequests.clear(); - if (clearCache) + if (clearCache) { await caches.delete(CACHE_NAME); + signal.throwIfAborted(); + } const cache = await caches.open(CACHE_NAME); + signal.throwIfAborted(); const urlsToCache = await getUrlsToCache(suites, signal); + signal.throwIfAborted(); const total = urlsToCache.length; let loaded = 0; let transferredSize = 0; - const promises = urlsToCache.map(async (item, index) => { + const promises = urlsToCache.map(async (item) => { const size = await fetchAndCache(cache, item.url, signal); + signal.throwIfAborted(); transferredSize += size; loaded++; if (!signal.aborted) @@ -95,6 +112,7 @@ async function preloadSuites(suites, clearCache, onProgress) { }); await Promise.all(promises); + signal.throwIfAborted(); return { transferredSize, count: urlsToCache.length }; } @@ -117,11 +135,14 @@ async function parseSuiteResources(suite, signal) { throw new Error(errorMsg); } const text = await response.text(); + signal.throwIfAborted(); return text .trim() .split("\n") + .map((resourceUrl) => resourceUrl.trim()) + .filter(Boolean) .map((resourceUrl) => ({ - url: new URL(resourceUrl.trim(), suite.url).href, + url: new URL(resourceUrl, suite.resources).href, suiteName: suite.name, })); } @@ -133,19 +154,25 @@ async function fetchAndCache(cache, url, signal) { signal.throwIfAborted(); const request = new Request(url, { cache: "no-cache", signal }); const existing = await cache.match(request, { ignoreSearch: true }); + signal.throwIfAborted(); if (existing) return getResponseSize(existing); try { const response = await fetch(request); - if (!response.ok) + signal.throwIfAborted(); + if (!response.ok) { + console.error(`Cache failed for ${url}: HTTP ${response.status}`); + WORKER_STATE.failedRequests.add(url); return 0; + } const size = getResponseSize(response); await cache.put(request, response); return size; } catch (e) { signal.throwIfAborted(); console.error("Cache failed for", url, e); + WORKER_STATE.failedRequests.add(url); return 0; } }); diff --git a/tests/index.html b/tests/index.html index 821356444..2818a6f83 100644 --- a/tests/index.html +++ b/tests/index.html @@ -30,6 +30,8 @@ await import("./unittests/benchmark-runner.mjs"); await import("./unittests/benchmark-configurator.mjs"); await import("./unittests/params.mjs"); + await import("./unittests/preloader.mjs"); + await import("./unittests/request-limiter.mjs"); await import("./unittests/suites.mjs"); globalThis.testResults = undefined; diff --git a/tests/run-end2end.mjs b/tests/run-end2end.mjs index 349ff8e5a..016c6883e 100644 --- a/tests/run-end2end.mjs +++ b/tests/run-end2end.mjs @@ -17,13 +17,50 @@ async function testPage(url) { console.log(`Testing: ${url}`); await driver.get(`http://localhost:${port}/${url}`); - await driver.executeAsyncScript((callback) => { - if (globalThis.benchmarkClient?.isInitialized) - callback(); - else - globalThis.addEventListener("SpeedometerReady", () => callback(), { once: true }); + const initResult = await driver.executeAsyncScript((callback) => { + if (globalThis.benchmarkClient?.isInitialized) { + callback({}); + return; + } + if (globalThis.benchmarkClient?.hasFinished?.()) { + callback({ + error: { + message: document.getElementById("invalid-score-text")?.textContent || "Benchmark failed during initialization", + }, + }); + return; + } + globalThis.addEventListener("SpeedometerReady", () => callback({}), { once: true }); + globalThis.addEventListener("error", (event) => { + callback({ + error: { + message: event.message || "Unknown Runtime Error", + source: event.filename, + lineno: event.lineno, + colno: event.colno, + stack: event.error?.stack, + }, + }); + }); + globalThis.addEventListener("unhandledrejection", (e) => { + const reason = e.reason; + const message = reason instanceof Error ? reason.message : typeof reason === "object" && reason !== null ? JSON.stringify(reason) : String(reason); + callback({ + error: { + message: `Unhandled Rejection: ${message}`, + stack: reason?.stack, + }, + }); + }); }); + if (initResult?.error) { + const { error } = initResult; + const stackTrace = error.stack ? `\nBrowser Stack Trace:\n${error.stack}` : ""; + const location = error.source ? ` at ${error.source}:${error.lineno}:${error.colno}` : ""; + throw new Error(`${error.message}${location}${stackTrace}`); + } + console.log(" - Awaiting Benchmark"); const { error, metrics } = await driver.executeAsyncScript((callback) => { globalThis.addEventListener( diff --git a/tests/unittests/params.mjs b/tests/unittests/params.mjs index e61fe6187..b236f01e4 100644 --- a/tests/unittests/params.mjs +++ b/tests/unittests/params.mjs @@ -74,25 +74,23 @@ describe("Params", () => { ); expect(params.toSearchParams()).to.equal("suites="); }); - }); - describe("isDefault", () => { - it("should return true for defaultParams", () => { - expect(defaultParams.isDefault()).to.be(true); - }); - it("should return false for newly instantiated empty Params", () => { - const params = new Params(); - expect(params.isDefault()).to.be(false); + it("should contain preload when enabled", () => { + const params = new Params( + new URLSearchParams({ + preload: "true", + }) + ); + expect(params.toSearchParams()).to.equal("preload=true"); }); - it("should return false for custom params", () => { + it("should not contain preload when disabled", () => { const params = new Params( new URLSearchParams({ - iterationCount: "100", + preload: "false", }) ); - expect(params.isDefault()).to.be(false); + expect(params.toSearchParams()).to.equal(""); }); }); - describe("isDefault", () => { it("should return true for defaultParams", () => { expect(defaultParams.isDefault()).to.be(true); @@ -112,6 +110,14 @@ describe("Params", () => { }); describe("parse input params", () => { + it("should parse preload boolean param", () => { + expect(new Params(new URLSearchParams("preload")).preload).to.be(true); + expect(new Params(new URLSearchParams({ preload: "true" })).preload).to.be(true); + expect(new Params(new URLSearchParams({ preload: "1" })).preload).to.be(true); + expect(new Params(new URLSearchParams({ preload: "false" })).preload).to.be(false); + expect(new Params(new URLSearchParams({ preload: "FALSE" })).preload).to.be(false); + expect(new Params(new URLSearchParams({ preload: "0" })).preload).to.be(false); + }); it("should parse custom viewport", () => { const params = new Params( new URLSearchParams({ diff --git a/tests/unittests/preloader.mjs b/tests/unittests/preloader.mjs new file mode 100644 index 000000000..dd1c9f2ff --- /dev/null +++ b/tests/unittests/preloader.mjs @@ -0,0 +1,307 @@ +import { ResourcePreloader } from "../../resources/preloader.mjs"; +import { Params } from "../../resources/shared/params.mjs"; +import { SW_MESSAGES } from "../../resources/shared/sw-messages.mjs"; + +const TEST_SUITES = [ + { + name: "SuiteA", + url: "suites/suite-a/index.html", + resources: "suites/suite-a/resources.txt", + enabled: true, + }, + { + name: "SuiteB", + url: "suites/suite-b/index.html", + resources: "suites/suite-b/resources.txt", + enabled: true, + }, +]; + +function createMutableParams(search = "developerMode=true") { + return new Params(new URLSearchParams(search)); +} + +function createMockServiceWorkerEnv(scopeUrl) { + const listeners = new Map(); + const activeWorker = { + postMessage: sinon.stub(), + }; + const registration = { + scope: scopeUrl, + active: activeWorker, + unregister: sinon.stub().resolves(true), + }; + const serviceWorkerContainer = { + controller: activeWorker, + ready: Promise.resolve(registration), + register: sinon.stub().resolves(registration), + getRegistrations: sinon.stub().resolves([registration]), + addEventListener: sinon.stub().callsFake((type, fn) => { + listeners.set(type, fn); + }), + removeEventListener: sinon.stub().callsFake((type, fn) => { + if (listeners.get(type) === fn) + listeners.delete(type); + }), + emitMessage(data) { + const handler = listeners.get("message"); + if (handler) + handler({ data }); + }, + }; + return { serviceWorkerContainer, registration, activeWorker, listeners }; +} + +describe("ResourcePreloader", () => { + let createdWindow = false; + let hadOwnServiceWorker = false; + let originalServiceWorkerDescriptor; + + function setNavigatorServiceWorker(value) { + Object.defineProperty(globalThis.navigator, "serviceWorker", { + value, + configurable: true, + writable: true, + }); + } + + beforeEach(() => { + if (typeof globalThis.window === "undefined") { + globalThis.window = { + location: { + href: "http://localhost:8080/index.html", + }, + }; + createdWindow = true; + } + hadOwnServiceWorker = Object.prototype.hasOwnProperty.call(globalThis.navigator, "serviceWorker"); + originalServiceWorkerDescriptor = Object.getOwnPropertyDescriptor(globalThis.navigator, "serviceWorker"); + }); + + afterEach(() => { + if (hadOwnServiceWorker && originalServiceWorkerDescriptor) + Object.defineProperty(globalThis.navigator, "serviceWorker", originalServiceWorkerDescriptor); + else + delete globalThis.navigator.serviceWorker; + + if (createdWindow) { + delete globalThis.window; + createdWindow = false; + } + }); + + it("should not throw in non-secure contexts where navigator.serviceWorker is undefined", async () => { + setNavigatorServiceWorker(undefined); + const testParams = createMutableParams("developerMode=true&preload=true"); + const preloader = new ResourcePreloader(testParams); + + await preloader.setup(); + expect(preloader._registration).to.be(null); + expect(preloader._sw).to.be(null); + + const preloadResult = await preloader.preloadSuites(TEST_SUITES); + expect(preloadResult).to.be(undefined); + expect(preloader.isCached(TEST_SUITES)).to.be(false); + + await preloader.teardown(); + }); + + it("should not touch navigator.serviceWorker on setup() when params.preload is false", async () => { + const expectedScope = new URL("./", window.location.href).href; + const { serviceWorkerContainer } = createMockServiceWorkerEnv(expectedScope); + setNavigatorServiceWorker(serviceWorkerContainer); + + const testParams = createMutableParams("developerMode=true&preload=false"); + const preloader = new ResourcePreloader(testParams); + + await preloader.setup(); + expect(serviceWorkerContainer.register.called).to.be(false); + expect(serviceWorkerContainer.getRegistrations.called).to.be(false); + expect(serviceWorkerContainer.addEventListener.called).to.be(false); + }); + + it("should lazily set up Service Worker when params.preload is toggled from false to true, and tear down when toggled back to false", async () => { + const expectedScope = new URL("./", window.location.href).href; + const { serviceWorkerContainer, registration, activeWorker } = createMockServiceWorkerEnv(expectedScope); + setNavigatorServiceWorker(serviceWorkerContainer); + + const testParams = createMutableParams("developerMode=true&preload=false"); + const preloader = new ResourcePreloader(testParams); + + // Initial setup with preload=false does not register SW. + await preloader.setup(); + expect(serviceWorkerContainer.register.called).to.be(false); + + // Toggle preload to true (as in Developer Mode) and call preloadSuites(). + testParams.preload = true; + activeWorker.postMessage.callsFake((msg) => { + if (msg.type === SW_MESSAGES.PRELOAD_SUITES) { + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.PRELOAD_SUITES, + status: SW_MESSAGES.PRELOAD_DONE, + requestId: msg.requestId, + transferredSize: 1024, + count: 2, + }); + } + }); + + await preloader.preloadSuites(TEST_SUITES); + expect(serviceWorkerContainer.register.calledOnce).to.be(true); + expect(preloader.isCached(TEST_SUITES)).to.be(true); + + // Toggle preload back to false: preloadSuites() should tear down registration and skip preloading. + testParams.preload = false; + activeWorker.postMessage.resetHistory(); + await preloader.preloadSuites(TEST_SUITES); + + expect(registration.unregister.calledOnce).to.be(true); + expect(activeWorker.postMessage.called).to.be(false); + expect(preloader._registration).to.be(null); + expect(preloader._sw).to.be(null); + expect(preloader.isCached(TEST_SUITES)).to.be(false); + }); + + it("should only unregister Service Worker registrations matching the current scope", async () => { + const expectedScope = new URL("./", window.location.href).href; + const otherScope = new URL("/other-benchmark/", window.location.href).href; + const { serviceWorkerContainer, registration, activeWorker } = createMockServiceWorkerEnv(expectedScope); + + const otherRegistration = { + scope: otherScope, + unregister: sinon.stub().resolves(true), + }; + serviceWorkerContainer.getRegistrations.resolves([otherRegistration, registration]); + setNavigatorServiceWorker(serviceWorkerContainer); + + activeWorker.postMessage.callsFake((msg) => { + if (msg.type === SW_MESSAGES.STOP_PRELOADING) { + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.STOP_PRELOADING, + status: SW_MESSAGES.STOP_PRELOADING, + requestId: msg.requestId, + }); + } else if (msg.type === SW_MESSAGES.GET_FAILED_REQUESTS) { + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.GET_FAILED_REQUESTS, + status: SW_MESSAGES.FAILED_REQUESTS, + requestId: msg.requestId, + requests: [], + }); + } + }); + + const testParams = createMutableParams("developerMode=true&preload=true"); + const preloader = new ResourcePreloader(testParams); + await preloader.setup(); + await preloader.teardown(); + + expect(registration.unregister.calledOnce).to.be(true); + expect(otherRegistration.unregister.called).to.be(false); + }); + + it("should ignore stale PRELOAD_PROGRESS and PRELOAD_DONE messages from an earlier requestId", async () => { + const expectedScope = new URL("./", window.location.href).href; + const { serviceWorkerContainer, activeWorker } = createMockServiceWorkerEnv(expectedScope); + setNavigatorServiceWorker(serviceWorkerContainer); + + const testParams = createMutableParams("developerMode=true&preload=true"); + const preloader = new ResourcePreloader(testParams); + await preloader.setup(); + + let firstPreloadRequestId = null; + activeWorker.postMessage.callsFake((msg) => { + if (msg.type === SW_MESSAGES.PRELOAD_SUITES && firstPreloadRequestId === null) { + firstPreloadRequestId = msg.requestId; + // Do not reply immediately; simulate in-flight preload that gets stopped. + } else if (msg.type === SW_MESSAGES.STOP_PRELOADING) { + // Before STOP_PRELOADING replies, emit stale PRELOAD_PROGRESS and PRELOAD_DONE from the first request! + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.PRELOAD_SUITES, + status: SW_MESSAGES.PRELOAD_PROGRESS, + requestId: firstPreloadRequestId, + loaded: 1, + total: 2, + url: "suites/suite-a/app.js", + suiteName: "SuiteA", + }); + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.PRELOAD_SUITES, + status: SW_MESSAGES.PRELOAD_DONE, + requestId: firstPreloadRequestId, + transferredSize: 512, + count: 2, + }); + // Now reply to STOP_PRELOADING with its matching requestId. + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.STOP_PRELOADING, + status: SW_MESSAGES.STOP_PRELOADING, + requestId: msg.requestId, + }); + } + }); + + const progressSpy = sinon.spy(); + const preloadPromise = preloader.preloadSuites(TEST_SUITES, true, progressSpy); + const stopPromise = preloader.stopPreloading(); + + const [preloadResult] = await Promise.all([preloadPromise, stopPromise]); + expect(preloadResult).to.be(null); + + // Emit another late message after no request is pending: must not throw. + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.PRELOAD_SUITES, + status: SW_MESSAGES.PRELOAD_DONE, + requestId: firstPreloadRequestId, + transferredSize: 512, + count: 2, + }); + }); + + it("should return false from isCached(enabledSuites) when stopped early or when enabled suites change", async () => { + const expectedScope = new URL("./", window.location.href).href; + const { serviceWorkerContainer, activeWorker } = createMockServiceWorkerEnv(expectedScope); + setNavigatorServiceWorker(serviceWorkerContainer); + + const testParams = createMutableParams("developerMode=true&preload=true"); + const preloader = new ResourcePreloader(testParams); + await preloader.setup(); + + // 1. Preload stopped early -> isCached must be false. + activeWorker.postMessage.callsFake((msg) => { + if (msg.type === SW_MESSAGES.STOP_PRELOADING) { + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.STOP_PRELOADING, + status: SW_MESSAGES.STOP_PRELOADING, + requestId: msg.requestId, + }); + } + }); + + const abortedPreload = preloader.preloadSuites(TEST_SUITES); + await preloader.stopPreloading(); + const abortedResult = await abortedPreload; + expect(abortedResult).to.be(null); + expect(preloader.isCached(TEST_SUITES)).to.be(false); + + // 2. Full preload succeeds -> isCached is true for the same suites, false if enabled suites change. + activeWorker.postMessage.callsFake((msg) => { + if (msg.type === SW_MESSAGES.PRELOAD_SUITES) { + serviceWorkerContainer.emitMessage({ + type: SW_MESSAGES.PRELOAD_SUITES, + status: SW_MESSAGES.PRELOAD_DONE, + requestId: msg.requestId, + transferredSize: 2048, + count: 2, + }); + } + }); + + await preloader.preloadSuites(TEST_SUITES); + expect(preloader.isCached(TEST_SUITES)).to.be(true); + + // Changing enabled suites (e.g. toggling a suite checkbox in Developer Mode) invalidates cache. + const subsetSuites = [TEST_SUITES[0]]; + expect(preloader.isCached(subsetSuites)).to.be(false); + }); +}); From 8f8b4e42a66215a7ba5227037b141652c47d600e Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 15:59:58 +0000 Subject: [PATCH 62/69] [preload] Harden SW registration, stop synchronicity, and dev mode e2e Ensure Service Worker registration waits for active controller readiness, synchronize stopPreloading with preloadSuites via _activeStopPromise, and resolve suite.resources against self.location.href. Ensure the home section is visible during preloading, clear stepping state on completion, and add end-to-end coverage for enabling preload from the developer menu. --- resources/main.mjs | 8 +- resources/preloader.mjs | 27 ++++- .../dist/src/speedometer-utils/params.mjs | 3 +- sw.mjs | 3 +- tests/run-end2end.mjs | 110 ++++++++++++++++++ 5 files changed, 143 insertions(+), 8 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 74a7af463..ba25f4798 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -52,6 +52,8 @@ class MainBenchmarkClient { } async start() { + if (this._state === BENCHMARK_STATE.PRELOADING) + return; if (this._isStepping()) this._clearStepping(); else if (await this._startBenchmark()) @@ -184,6 +186,7 @@ class MainBenchmarkClient { async didFinishLastIteration(metrics) { console.assert(this.isRunning()); + this._clearStepping(); this._metrics = metrics; this._setBenchmarkState(BENCHMARK_STATE.DONE); await this._resourcePreloader.teardown(); @@ -205,6 +208,7 @@ class MainBenchmarkClient { handleError(error) { if (this._state === BENCHMARK_STATE.ERROR) return; + this._clearStepping(); this._metrics = Object.create(null); this._setBenchmarkState(BENCHMARK_STATE.ERROR); this._populateErrorMessage(error.message); @@ -445,11 +449,13 @@ class MainBenchmarkClient { } } - async _setBenchmarkState(state) { + _setBenchmarkState(state) { this._state = state; document.body.setAttribute("data-benchmark-state", state); const startButtons = document.querySelectorAll(".start-tests-button"); if (state === BENCHMARK_STATE.PRELOADING) { + if (document.documentElement.getAttribute("data-visible-section") !== "home") + this._showSection("#home"); startButtons.forEach((btn) => { btn.disabled = true; btn.innerHTML = "
Preloading
"; diff --git a/resources/preloader.mjs b/resources/preloader.mjs index e2447662c..a15b1609b 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -12,6 +12,7 @@ export class ResourcePreloader { this._pendingRequest = null; this._onPreloadProgress = null; this._activePreloadPromise = null; + this._activeStopPromise = null; this._nextRequestId = 1; this._boundOnMessage = this.onMessage.bind(this); this._messageListenerAdded = false; @@ -93,8 +94,8 @@ export class ResourcePreloader { return; this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); - await navigator.serviceWorker.ready; - this._sw = navigator.serviceWorker.controller || this._registration?.active || null; + const readyRegistration = await navigator.serviceWorker.ready; + this._sw = this._registration?.active || readyRegistration?.active || navigator.serviceWorker.controller || null; } _setupMessageListener() { @@ -224,12 +225,23 @@ export class ResourcePreloader { if (!this._sw) return; + if (this._activeStopPromise) { + await this._activeStopPromise; + return; + } + if (this._activePreloadPromise || this._pendingRequest?.type === SW_MESSAGES.PRELOAD_SUITES) this._preloadParams = null; - await this._postMessage({ type: SW_MESSAGES.STOP_PRELOADING }); - if (this._activePreloadPromise) - await this._activePreloadPromise; + const activePreloadPromise = this._activePreloadPromise; + this._activeStopPromise = this._postMessage({ type: SW_MESSAGES.STOP_PRELOADING }); + try { + await this._activeStopPromise; + if (activePreloadPromise) + await activePreloadPromise; + } finally { + this._activeStopPromise = null; + } } async preloadSuites(suites, clearCache = true, onProgress) { @@ -242,6 +254,9 @@ export class ResourcePreloader { return undefined; } + if (this._activeStopPromise) + await this._activeStopPromise; + if (!this._sw && !this._registration) await this.setup(); @@ -269,6 +284,8 @@ export class ResourcePreloader { try { this._activePreloadPromise = this._postMessage({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, clearCache }, PRELOAD_TIMEOUT_MS); const response = await this._activePreloadPromise; + if (this._activeStopPromise) + await this._activeStopPromise; if (response?.status === SW_MESSAGES.PRELOAD_DONE) { const timeTakenMs = performance.now() - startTime; diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs index c34aec619..85d37d213 100644 --- a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs +++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs @@ -88,8 +88,9 @@ export class Params { _parseBooleanParam(searchParams, paramKey) { if (!searchParams.has(paramKey)) return false; + const value = searchParams.get(paramKey).toLowerCase(); searchParams.delete(paramKey); - return true; + return value !== "false" && value !== "0"; } _parseIntParam(searchParams, paramKey, minValue) { diff --git a/sw.mjs b/sw.mjs index c72208160..90b1191a3 100644 --- a/sw.mjs +++ b/sw.mjs @@ -136,13 +136,14 @@ async function parseSuiteResources(suite, signal) { } const text = await response.text(); signal.throwIfAborted(); + const baseUrl = new URL(suite.resources, self.location.href); return text .trim() .split("\n") .map((resourceUrl) => resourceUrl.trim()) .filter(Boolean) .map((resourceUrl) => ({ - url: new URL(resourceUrl, suite.resources).href, + url: new URL(resourceUrl, baseUrl).href, suiteName: suite.name, })); } diff --git a/tests/run-end2end.mjs b/tests/run-end2end.mjs index 016c6883e..81478e64f 100644 --- a/tests/run-end2end.mjs +++ b/tests/run-end2end.mjs @@ -171,6 +171,115 @@ async function testDeveloperMode() { }); } +async function testDeveloperModePreload() { + const url = "index.html?developerMode=true&iterationCount=1&suites=Perf-Dashboard"; + console.log(`Testing: ${url} (enabling preload via Developer Mode checkbox)`); + await driver.get(`http://localhost:${port}/${url}`); + + const initResult = await driver.executeAsyncScript((callback) => { + if (globalThis.benchmarkClient?.isInitialized) { + callback({}); + return; + } + globalThis.addEventListener("SpeedometerReady", () => callback({}), { once: true }); + globalThis.addEventListener("error", (event) => { + callback({ error: { message: event.message || "Unknown Runtime Error", stack: event.error?.stack } }); + }); + globalThis.addEventListener("unhandledrejection", (e) => { + const reason = e.reason; + callback({ error: { message: `Unhandled Rejection: ${reason?.message || String(reason)}`, stack: reason?.stack } }); + }); + }); + + if (initResult?.error) + throw new Error(`${initResult.error.message}${initResult.error.stack ? `\n${initResult.error.stack}` : ""}`); + + console.log(" - Toggling preload checkbox and awaiting Benchmark"); + const result = await driver.executeAsyncScript((callback) => { + const labels = Array.from(document.querySelectorAll(".developer-mode label")); + const preloadLabel = labels.find((label) => label.textContent.includes("Use service worker for resource preloading")); + const preloadCheckbox = preloadLabel?.querySelector('input[type="checkbox"]'); + if (!preloadCheckbox || preloadCheckbox.checked) { + callback({ error: { message: `Unexpected initial preload checkbox state: ${preloadCheckbox?.checked}` } }); + return; + } + + const preloadProgressEl = document.getElementById("preload-progress"); + const preloadInfoEl = document.getElementById("preload-info"); + const initialProgressHidden = getComputedStyle(preloadProgressEl).display === "none" && getComputedStyle(preloadInfoEl).display === "none"; + + preloadCheckbox.click(); + if (!preloadCheckbox.checked || !window.location.search.includes("preload=true")) { + callback({ error: { message: `Preload checkbox click did not update state or URL: checked=${preloadCheckbox.checked}, search=${window.location.search}` } }); + return; + } + + const seenStates = []; + let preloadProgressVisibleDuringPreload = false; + let startButtonDisabledDuringPreload = false; + const observer = new MutationObserver(() => { + const state = document.body.getAttribute("data-benchmark-state"); + seenStates.push(state); + if (state === "PRELOADING") { + preloadProgressVisibleDuringPreload = getComputedStyle(preloadProgressEl).display !== "none" && getComputedStyle(preloadInfoEl).display !== "none"; + startButtonDisabledDuringPreload = document.querySelector(".start-tests-button")?.disabled === true; + } + }); + observer.observe(document.body, { attributes: true, attributeFilter: ["data-benchmark-state"] }); + + let preloadResult = null; + const preloader = globalThis.benchmarkClient._resourcePreloader; + const origPreloadSuites = preloader.preloadSuites.bind(preloader); + preloader.preloadSuites = async (...args) => { + preloadResult = await origPreloadSuites(...args); + return preloadResult; + }; + + globalThis.addEventListener( + "SpeedometerDone", + () => { + observer.disconnect(); + const preloadProgressHiddenAfterDone = getComputedStyle(preloadProgressEl).display === "none" && getComputedStyle(preloadInfoEl).display === "none"; + callback({ + metrics: globalThis.benchmarkClient.metrics, + initialProgressHidden, + seenStates, + preloadProgressVisibleDuringPreload, + startButtonDisabledDuringPreload, + preloadProgressHiddenAfterDone, + preloadResult, + }); + }, + { once: true } + ); + globalThis.addEventListener("error", (event) => { + observer.disconnect(); + callback({ error: { message: event.message || "Unknown Runtime Error", stack: event.error?.stack } }); + }); + globalThis.addEventListener("unhandledrejection", (e) => { + observer.disconnect(); + const reason = e.reason; + callback({ error: { message: `Unhandled Rejection: ${reason?.message || String(reason)}`, stack: reason?.stack } }); + }); + + globalThis.benchmarkClient.start(); + }); + + if (result?.error) + throw new Error(`${result.error.message}${result.error.stack ? `\n${result.error.stack}` : ""}`); + + assert(result.initialProgressHidden, "Expected #preload-progress and #preload-info to be hidden before preloading"); + assert(result.seenStates.includes("PRELOADING"), `Expected PRELOADING state, saw: ${result.seenStates.join(", ")}`); + assert(result.preloadProgressVisibleDuringPreload, "Expected #preload-progress and #preload-info to be visible during PRELOADING"); + assert(result.startButtonDisabledDuringPreload, "Expected .start-tests-button to be disabled during PRELOADING"); + assert(result.preloadProgressHiddenAfterDone, "Expected #preload-progress and #preload-info to be hidden after DONE"); + assert(result.preloadResult?.status === "PRELOAD_DONE" && result.preloadResult?.count > 0, "Expected Service Worker preloadSuites to finish with PRELOAD_DONE"); + + validateMetrics(result.metrics); + assert("Perf-Dashboard" in result.metrics); + assert(result.metrics["Perf-Dashboard"].values.length === 1); +} + async function test() { try { await testWithRetries(retry); @@ -204,6 +313,7 @@ async function tryTests() { await testPreloadEnabled(); await testPreloadDisabled(); await testDeveloperMode(); + await testDeveloperModePreload(); console.log("\nTests complete!"); } From b6c3ff8ca818e2fa44ba5fcd96f22a01dc0f63bb Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 16:08:29 +0000 Subject: [PATCH 63/69] [preload] Use optional chaining and simplify DOM status updates Replace verbose null and type guards across the Service Worker, preloader, configurator, and parameter parser with optional chaining, and remove redundant DOM element checks in PreloadStatusUpdater. --- resources/benchmark-configurator.mjs | 3 +- resources/preloader.mjs | 85 ++++++++----------- resources/shared/params.mjs | 2 +- .../dist/src/speedometer-utils/params.mjs | 2 +- sw.mjs | 14 ++- tests/unittests/preloader.mjs | 4 +- 6 files changed, 44 insertions(+), 66 deletions(-) diff --git a/resources/benchmark-configurator.mjs b/resources/benchmark-configurator.mjs index 531f5fd03..2e7d6729a 100644 --- a/resources/benchmark-configurator.mjs +++ b/resources/benchmark-configurator.mjs @@ -117,8 +117,7 @@ export class BenchmarkConfigurator { } _loadSuite(suite) { - if (suite.tags) - suite.tags.forEach((tag) => this.#tags.add(tag)); + suite.tags?.forEach((tag) => this.#tags.add(tag)); this.#suites.push({ ...suite, tags: suite.tags ? [...suite.tags] : [] }); } diff --git a/resources/preloader.mjs b/resources/preloader.mjs index a15b1609b..2ce8d06eb 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -19,11 +19,11 @@ export class ResourcePreloader { } _isServiceWorkerSupported() { - return "serviceWorker" in navigator && Boolean(navigator.serviceWorker); + return Boolean(globalThis.navigator?.serviceWorker); } _serializeParams(suitesOrParams = this._params, maybeSuites = undefined) { - const targetParams = suitesOrParams && typeof suitesOrParams.toSearchParams === "function" ? suitesOrParams : this._params; + const targetParams = typeof suitesOrParams?.toSearchParams === "function" ? suitesOrParams : this._params; const suites = Array.isArray(suitesOrParams) ? suitesOrParams : maybeSuites; const searchParamsStr = targetParams.toSearchParams(); if (!Array.isArray(suites)) @@ -72,43 +72,38 @@ export class ResourcePreloader { const expectedScope = new URL("./", window.location.href).href; let unregisteredActive = false; - if (typeof navigator.serviceWorker.getRegistrations === "function") { - const existingRegistrations = await navigator.serviceWorker.getRegistrations(); - for (const existing of existingRegistrations) { - if (existing.scope === expectedScope) { - await existing.unregister(); - if (existing === activeRegistration) - unregisteredActive = true; - } + const existingRegistrations = await navigator.serviceWorker?.getRegistrations?.() ?? []; + for (const existing of existingRegistrations) { + if (existing.scope === expectedScope) { + await existing.unregister?.(); + if (existing === activeRegistration) + unregisteredActive = true; } } - if (activeRegistration && !unregisteredActive && typeof activeRegistration.unregister === "function") { - if (!activeRegistration.scope || activeRegistration.scope === expectedScope) - await activeRegistration.unregister(); - } - + if (!unregisteredActive && (!activeRegistration?.scope || activeRegistration.scope === expectedScope)) + await activeRegistration?.unregister?.(); } async _registerServiceWorker() { if (!this._isServiceWorkerSupported()) return; - this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" }); - const readyRegistration = await navigator.serviceWorker.ready; - this._sw = this._registration?.active || readyRegistration?.active || navigator.serviceWorker.controller || null; + this._registration = await navigator.serviceWorker?.register("./sw.mjs", { type: "module" }); + const readyRegistration = await navigator.serviceWorker?.ready; + this._sw = this._registration?.active || readyRegistration?.active || navigator.serviceWorker?.controller || null; } _setupMessageListener() { if (!this._isServiceWorkerSupported() || this._messageListenerAdded) return; - navigator.serviceWorker.addEventListener("message", this._boundOnMessage); + navigator.serviceWorker?.addEventListener("message", this._boundOnMessage); this._messageListenerAdded = true; } _removeMessageListener() { if (!this._isServiceWorkerSupported() || !this._messageListenerAdded) return; - navigator.serviceWorker.removeEventListener("message", this._boundOnMessage); + navigator.serviceWorker?.removeEventListener("message", this._boundOnMessage); this._messageListenerAdded = false; } @@ -126,7 +121,7 @@ export class ResourcePreloader { } onMessage(event) { - const data = event.data || {}; + const data = event?.data || {}; if (data.type === SW_MESSAGES.FATAL_ERROR) { globalThis.benchmarkClient?.handleError(new Error(data.message)); @@ -182,27 +177,27 @@ export class ResourcePreloader { } this._pendingRequest = { type: messageData.type, requestId, resolve, reject, timeoutId }; - this._sw.postMessage(payload); + this._sw?.postMessage(payload); }); } _resolvePendingRequest(data = null) { const pendingRequest = this._pendingRequest; this._pendingRequest = null; - if (pendingRequest.timeoutId) + if (pendingRequest?.timeoutId) clearTimeout(pendingRequest.timeoutId); - pendingRequest.resolve(data); + pendingRequest?.resolve(data); } _rejectPendingRequest(errorMessage) { const pendingRequest = this._pendingRequest; this._pendingRequest = null; - if (pendingRequest.timeoutId) + if (pendingRequest?.timeoutId) clearTimeout(pendingRequest.timeoutId); console.warn(errorMessage); - pendingRequest.reject(new Error(errorMessage)); + pendingRequest?.reject(new Error(errorMessage)); } async teardown() { @@ -322,20 +317,12 @@ export class PreloadStatusUpdater { } start() { - if (this._rafId) { - cancelAnimationFrame(this._rafId); - this._rafId = null; - } - this._latestData = null; - if (this._progressCompletedElement) { - this._progressCompletedElement.max = 1; - this._progressCompletedElement.value = 0; - } - if (this._infoLabelElement) - this._infoLabelElement.textContent = ""; - if (this._infoProgressElement) - this._infoProgressElement.textContent = ""; - document.body?.style.setProperty("--preload-progress", "0%"); + this.stop(); + this._progressCompletedElement.max = 1; + this._progressCompletedElement.value = 0; + this._infoLabelElement.textContent = ""; + this._infoProgressElement.textContent = ""; + document.body.style.setProperty("--preload-progress", "0%"); } onProgress(data) { @@ -351,17 +338,13 @@ export class PreloadStatusUpdater { return; const { loaded, total, url, suiteName } = data; - document.body?.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); - if (this._progressCompletedElement) { - this._progressCompletedElement.max = total > 0 ? total : 1; - this._progressCompletedElement.value = total > 0 ? loaded : 0; - } - const match = typeof url === "string" ? url.match(/(?:suites(?:-experimental)?|resources)\/(.+)$/) : null; - const filename = match ? match[1] : ""; - if (this._infoLabelElement) - this._infoLabelElement.textContent = `${suiteName}: ${filename}`; - if (this._infoProgressElement) - this._infoProgressElement.textContent = `${loaded} / ${total}`; + document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`); + this._progressCompletedElement.max = total > 0 ? total : 1; + this._progressCompletedElement.value = total > 0 ? loaded : 0; + const match = url?.match(/(?:suites(?:-experimental)?|resources)\/(.+)$/); + const filename = match?.[1] ?? ""; + this._infoLabelElement.textContent = `${suiteName}: ${filename}`; + this._infoProgressElement.textContent = `${loaded} / ${total}`; } stop() { diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index e842c2d2c..3acb07d35 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -102,7 +102,7 @@ export class Params { _parseBooleanParam(searchParams, paramKey) { if (!searchParams.has(paramKey)) return false; - const value = searchParams.get(paramKey).toLowerCase(); + const value = searchParams.get(paramKey)?.toLowerCase(); searchParams.delete(paramKey); return value !== "false" && value !== "0"; } diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs index 85d37d213..4a4127950 100644 --- a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs +++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs @@ -88,7 +88,7 @@ export class Params { _parseBooleanParam(searchParams, paramKey) { if (!searchParams.has(paramKey)) return false; - const value = searchParams.get(paramKey).toLowerCase(); + const value = searchParams.get(paramKey)?.toLowerCase(); searchParams.delete(paramKey); return value !== "false" && value !== "0"; } diff --git a/sw.mjs b/sw.mjs index 90b1191a3..d288499ae 100644 --- a/sw.mjs +++ b/sw.mjs @@ -25,10 +25,8 @@ async function handleStopPreloadingMessage(event) { } function abortActivePreload() { - if (WORKER_STATE.abortController) { - WORKER_STATE.abortController.abort(); - WORKER_STATE.abortController = null; - } + WORKER_STATE.abortController?.abort(); + WORKER_STATE.abortController = null; WORKER_STATE.requestLimiter.clear(); } @@ -65,10 +63,10 @@ async function handlePreloadSuitesMessage(event, { suites = [], clearCache = tru signal.throwIfAborted(); replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count }); } catch (error) { - if (error.name === "AbortError" || signal.aborted) + if (error?.name === "AbortError" || signal.aborted) return; console.error("Error during preload:", error); - replyError(event, error.message || "Failed to preload resources."); + replyError(event, error?.message || "Failed to preload resources."); } finally { if (WORKER_STATE.abortController === abortController) WORKER_STATE.abortController = null; @@ -108,7 +106,7 @@ async function preloadSuites(suites, clearCache, onProgress, signal) { transferredSize += size; loaded++; if (!signal.aborted) - onProgress({ loaded, total, url: item.url, suiteName: item.suiteName }); + onProgress?.({ loaded, total, url: item.url, suiteName: item.suiteName }); }); await Promise.all(promises); @@ -120,7 +118,7 @@ async function getUrlsToCache(suites, signal) { const urlsToCache = []; for (const suite of suites) { signal.throwIfAborted(); - if (suite.resources) + if (suite?.resources) urlsToCache.push(...await parseSuiteResources(suite, signal)); } diff --git a/tests/unittests/preloader.mjs b/tests/unittests/preloader.mjs index dd1c9f2ff..37c90975a 100644 --- a/tests/unittests/preloader.mjs +++ b/tests/unittests/preloader.mjs @@ -44,9 +44,7 @@ function createMockServiceWorkerEnv(scopeUrl) { listeners.delete(type); }), emitMessage(data) { - const handler = listeners.get("message"); - if (handler) - handler({ data }); + listeners.get("message")?.({ data }); }, }; return { serviceWorkerContainer, registration, activeWorker, listeners }; From 790cbd64b7225b829cf45efb5a5e98d0bf49aa0f Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 16:33:49 +0000 Subject: [PATCH 64/69] [preload] Remove unintended TodoMVC-JavaScript-ES5-Cached experimental suite --- suites-experimental/suites.mjs | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/suites-experimental/suites.mjs b/suites-experimental/suites.mjs index c6409694f..5999b344b 100644 --- a/suites-experimental/suites.mjs +++ b/suites-experimental/suites.mjs @@ -35,38 +35,6 @@ function yieldTask() { } export const ExperimentalSuites = freezeSuites([ - { - name: "TodoMVC-JavaScript-ES5-Cached", - url: "suites/todomvc/vanilla-examples/javascript-es5/dist/index.html", - resources: "suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt", - tags: ["todomvc", "experimental", "cached"], - async prepare(page) { - (await page.waitForElement(".new-todo")).focus(); - }, - tests: [ - new BenchmarkTestStep(`Adding${getNumberOfItemsToAdd()}Items`, (page) => { - const numberOfItemsToAdd = getNumberOfItemsToAdd(); - const newTodo = page.querySelector(".new-todo"); - for (let i = 0; i < numberOfItemsToAdd; i++) { - newTodo.setValue(getTodoText("ja", i)); - newTodo.dispatchEvent("change"); - newTodo.enter("keypress"); - } - }), - new BenchmarkTestStep("CompletingAllItems", (page) => { - const numberOfItemsToAdd = getNumberOfItemsToAdd(); - const checkboxes = page.querySelectorAll(".toggle"); - for (let i = 0; i < numberOfItemsToAdd; i++) - checkboxes[i].click(); - }), - new BenchmarkTestStep("DeletingAllItems", (page) => { - const numberOfItemsToAdd = getNumberOfItemsToAdd(); - const deleteButtons = page.querySelectorAll(".destroy"); - for (let i = numberOfItemsToAdd - 1; i >= 0; i--) - deleteButtons[i].click(); - }), - ], - }, { name: "TodoMVC-LocalStorage", url: "suites-experimental/todomvc-localstorage/dist/index.html", From c02cdb34e1ce63b558f5f008508ce33607a88c4a Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 16:52:42 +0000 Subject: [PATCH 65/69] [preload] Add MainBenchmarkClient.isPreloading() helper --- resources/main.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index ba25f4798..10bd5d148 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -47,12 +47,16 @@ class MainBenchmarkClient { return this._state === BENCHMARK_STATE.RUNNING; } + isPreloading() { + return this._state === BENCHMARK_STATE.PRELOADING; + } + hasFinished() { return this._state === BENCHMARK_STATE.DONE || this._state === BENCHMARK_STATE.ERROR; } async start() { - if (this._state === BENCHMARK_STATE.PRELOADING) + if (this.isPreloading()) return; if (this._isStepping()) this._clearStepping(); @@ -61,7 +65,7 @@ class MainBenchmarkClient { } async step() { - if (this._state === BENCHMARK_STATE.PRELOADING) + if (this.isPreloading()) return; const currentSteppingResolver = this._steppingResolver; this._steppingPromise = new Promise((resolve) => { @@ -93,7 +97,7 @@ class MainBenchmarkClient { } async _startBenchmark() { - if (this.isRunning() || this._state === BENCHMARK_STATE.PRELOADING) + if (this.isRunning() || this.isPreloading()) return false; const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; @@ -495,7 +499,7 @@ class MainBenchmarkClient { _logoClickHandler(event) { // Prevent any accidental UI changes during benchmark runs. - if (!this.isRunning() && this._state !== BENCHMARK_STATE.PRELOADING) + if (!this.isRunning() && !this.isPreloading()) this._showSection("#home"); event.preventDefault(); return false; From ac7c9b98c32fb7c175b3c1a3b263107fac0eac17 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 16:56:35 +0000 Subject: [PATCH 66/69] [preload] Add error state helper to MainBenchmarkClient Encapsulate benchmark error state checks behind a dedicated client helper to improve readability and consistency with other lifecycle state predicates. --- resources/main.mjs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 10bd5d148..141129100 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -51,8 +51,12 @@ class MainBenchmarkClient { return this._state === BENCHMARK_STATE.PRELOADING; } + hasError() { + return this._state === BENCHMARK_STATE.ERROR; + } + hasFinished() { - return this._state === BENCHMARK_STATE.DONE || this._state === BENCHMARK_STATE.ERROR; + return this._state === BENCHMARK_STATE.DONE || this.hasError(); } async start() { @@ -120,7 +124,7 @@ class MainBenchmarkClient { this._developerModeContainer?.remove(); await this._preloadResources(benchmarkConfigurator); - if (this._state === BENCHMARK_STATE.ERROR) + if (this.hasError()) return false; this._progressCompleted = document.getElementById("progress-completed"); @@ -149,7 +153,7 @@ class MainBenchmarkClient { } _assertNotAborted() { - if (this._state === BENCHMARK_STATE.ERROR) + if (this.hasError()) throw new Error("Benchmark aborted by another process."); } @@ -210,7 +214,7 @@ class MainBenchmarkClient { } handleError(error) { - if (this._state === BENCHMARK_STATE.ERROR) + if (this.hasError()) return; this._clearStepping(); this._metrics = Object.create(null); @@ -419,7 +423,7 @@ class MainBenchmarkClient { if (!params.developerMode) await this._preloadResources(benchmarkConfigurator); - if (this._state !== BENCHMARK_STATE.ERROR) { + if (!this.hasError()) { this._setBenchmarkState(BENCHMARK_STATE.READY); if (params.startAutomatically) await this.start(); From 8dfd9b05a93e433aa48fc0a08462750589b2c2d6 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 16:58:36 +0000 Subject: [PATCH 67/69] [preload] Use state helper predicates in _setBenchmarkState Use isRunning() and isPreloading() within _setBenchmarkState to align with the rest of MainBenchmarkClient's state predicate patterns. --- resources/main.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/main.mjs b/resources/main.mjs index 141129100..6d2056106 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -461,14 +461,14 @@ class MainBenchmarkClient { this._state = state; document.body.setAttribute("data-benchmark-state", state); const startButtons = document.querySelectorAll(".start-tests-button"); - if (state === BENCHMARK_STATE.PRELOADING) { + if (this.isPreloading()) { if (document.documentElement.getAttribute("data-visible-section") !== "home") this._showSection("#home"); startButtons.forEach((btn) => { btn.disabled = true; btn.innerHTML = "
Preloading
"; }); - } else if (state === BENCHMARK_STATE.RUNNING) { + } else if (this.isRunning()) { document.body.style.removeProperty("--preload-progress"); startButtons.forEach((btn) => { btn.disabled = true; From 8349b4fb603b303087afa015ab64338f02885e4d Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 17:00:22 +0000 Subject: [PATCH 68/69] [preload] Add isDisabled() helper to MainBenchmarkClient Encapsulate disabled state check for start buttons into a dedicated helper to match other state predicates on MainBenchmarkClient. --- resources/main.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/main.mjs b/resources/main.mjs index 6d2056106..8feace701 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -59,6 +59,10 @@ class MainBenchmarkClient { return this._state === BENCHMARK_STATE.DONE || this.hasError(); } + isDisabled() { + return this._state !== BENCHMARK_STATE.READY && this._state !== BENCHMARK_STATE.DONE && this._state !== BENCHMARK_STATE.IDLE; + } + async start() { if (this.isPreloading()) return; @@ -475,7 +479,7 @@ class MainBenchmarkClient { }); } else { document.body.style.removeProperty("--preload-progress"); - const isDisabled = state !== BENCHMARK_STATE.READY && state !== BENCHMARK_STATE.DONE && state !== BENCHMARK_STATE.IDLE; + const isDisabled = this.isDisabled(); startButtons.forEach((btn) => { btn.innerHTML = "
Start Test
"; btn.disabled = isDisabled; From 068673dc465cf6af53c6c780a0cba2db9b1432c9 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Fri, 13 Nov 2026 17:09:01 +0000 Subject: [PATCH 69/69] [preload] Add isCooperativePreemption() helper to ResourcePreloader Extract cooperative preemption check into a helper method with documentation on when pending messages may be preempted. --- resources/preloader.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/preloader.mjs b/resources/preloader.mjs index 2ce8d06eb..e67d694ba 100644 --- a/resources/preloader.mjs +++ b/resources/preloader.mjs @@ -147,15 +147,22 @@ export class ResourcePreloader { resolve(data); } + // Allow preempting in-flight preloads or overriding any pending request with an explicit stop. + _isCooperativePreemption(messageData) { + return this._pendingRequest?.type === SW_MESSAGES.PRELOAD_SUITES || messageData?.type === SW_MESSAGES.STOP_PRELOADING; + } + + isCooperativePreemption(messageData) { + return this._isCooperativePreemption(messageData); + } + _postMessage(messageData, timeoutMs = 0) { if (!this._sw) return Promise.resolve(null); if (this._pendingRequest) { - const isCooperativePreemption = this._pendingRequest.type === SW_MESSAGES.PRELOAD_SUITES || messageData.type === SW_MESSAGES.STOP_PRELOADING; - const logMessage = `Already waiting for ${this._pendingRequest.type}, overriding with ${messageData.type}`; - if (isCooperativePreemption) { + if (this._isCooperativePreemption(messageData)) { console.warn(logMessage); this._resolvePendingRequest(null); } else {