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/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/javascript-wc-indexeddb/scripts/build.js b/experimental/javascript-wc-indexeddb/scripts/build.js index 8540a774e..3ae0f1739 100644 --- a/experimental/javascript-wc-indexeddb/scripts/build.js +++ b/experimental/javascript-wc-indexeddb/scripts/build.js @@ -1,5 +1,6 @@ const fs = require("fs").promises; const { dirname } = require("path"); +const path = require("path"); /** * createDirectory @@ -164,4 +165,4 @@ const build = async () => { console.log("Done with building!"); }; -build(); +build().then(() => import("../../../resources/shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); 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/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/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/experimental/todomvc-localstorage/scripts/build.js b/experimental/todomvc-localstorage/scripts/build.js index 046de0e7e..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(); +build().then(() => import("../../../resources/shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/index.html b/index.html index 31ba85ee4..6a26fc563 100644 --- a/index.html +++ b/index.html @@ -26,13 +26,20 @@
- +
About Speedometer 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..b33553e8d 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(); @@ -35,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(); @@ -63,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"); @@ -92,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"); @@ -121,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"); @@ -153,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"); @@ -185,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"); @@ -214,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"); @@ -243,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"); @@ -271,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"); @@ -299,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"); @@ -329,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"); @@ -359,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"); @@ -388,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"); @@ -417,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"); @@ -446,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"); @@ -475,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"); @@ -502,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"); @@ -529,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"); @@ -557,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"); @@ -585,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"); @@ -613,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"); @@ -641,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"); @@ -672,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"); @@ -703,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"); @@ -743,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"); @@ -783,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: [ @@ -801,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: [ @@ -819,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: [ @@ -845,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: [ @@ -864,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"); @@ -903,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/developer-mode.mjs b/resources/developer-mode.mjs index 0d250eb57..2d8260619 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("Use service worker for resource preloading", params.preload, (isChecked) => { + params.preload = isChecked; + }); +} + function createUIForMeasurePrepare() { return createCheckboxUI("Measure Prepare", params.measurePrepare, (isChecked) => { params.measurePrepare = isChecked; diff --git a/resources/main.css b/resources/main.css index 279f2a413..00f76ea88 100644 --- a/resources/main.css +++ b/resources/main.css @@ -374,7 +374,17 @@ 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 +394,8 @@ button.show-about { border-right: 6px solid var(--background); } -#progress-completed { +#progress-completed, +#preload-progress-completed { position: absolute; top: 0; left: 0; @@ -395,11 +406,13 @@ 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 +423,9 @@ button.show-about { background-color: var(--background); } -#info { +#info, +#preload-info { + display: none; position: absolute; bottom: -25px; left: 60px; @@ -420,11 +435,13 @@ 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 +917,45 @@ 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="RUNNING"] #info { + display: block; +} + +[data-benchmark-state="PRELOADING"] .start-tests-button { + color: var(--foreground); + 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; + } + 100% { + background-position: 20px 0, 0 0; + } +} diff --git a/resources/main.mjs b/resources/main.mjs index fb287dcb3..ecb9e576e 100644 --- a/resources/main.mjs +++ b/resources/main.mjs @@ -1,9 +1,92 @@ 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(); + + 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; + + this.sw = navigator.serviceWorker.controller || this.registration.active; + return true; + } + + async precacheSuites(suites, resourceLoadDelay, clearCache = true, onProgress) { + if (!this.sw || 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(); + await 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, + clearCache: clearCache, + }, + [channel.port2] + ); + }); + } + + setState(state) { + if (this.sw) + 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; @@ -14,8 +97,8 @@ class MainBenchmarkClient { _progressCompleted = null; _isRunning = false; _hasResults = false; - _developerModeContainer = null; _metrics = Object.create(null); + preloadServiceWorker = new PreloadServiceWorker(); _steppingPromise = null; _steppingResolver = null; _benchmarkConfiguratorPromise = null; @@ -31,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; @@ -46,7 +129,7 @@ class MainBenchmarkClient { if (this._isStepping()) currentSteppingResolver(); if (!this._isRunning) { - this._startBenchmark(); + await this._startBenchmark(); this._showSection("#running"); } } @@ -73,6 +156,13 @@ class MainBenchmarkClient { const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise; + await this.preloadServiceWorker.setup(); + + if (!params.isDefault()) + await this._cacheResources(benchmarkConfigurator); + + this._setBenchmarkState(BENCHMARK_STATE.RUNNING); + const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled); const totalSuitesCount = enabledSuites.length; @@ -146,6 +236,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 +257,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 +435,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 +450,70 @@ class MainBenchmarkClient { document.body.append(this._developerModeContainer); } + await this._setupServiceWorker(benchmarkConfigurator); + if (params.startAutomatically) this.start(); } + async _setupServiceWorker(benchmarkConfigurator) { + await this.preloadServiceWorker.setup(); + await this._cacheResources(benchmarkConfigurator); + } + + 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, clearCache, this._updateCacheProgress.bind(this)); + this._didInitialPrecache = true; + this._enableStartButtons(); + } catch (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) => { + 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 = ""; + 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) { + document.body.style.removeProperty("--preload-progress"); + startButton.innerHTML = "
Start Test
"; + } + } + _hashChangeHandler() { this._showSection(window.location.hash); } diff --git a/resources/shared/generate-resources.mjs b/resources/shared/generate-resources.mjs new file mode 100644 index 000000000..8fb54f656 --- /dev/null +++ b/resources/shared/generate-resources.mjs @@ -0,0 +1,26 @@ +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/shared/params.mjs b/resources/shared/params.mjs index 520679d24..b54bf1277 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -35,6 +35,10 @@ 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; constructor(searchParams = undefined) { if (searchParams) @@ -68,6 +72,8 @@ 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"); const unused = Array.from(searchParams.keys()); if (unused.length > 0) @@ -203,6 +209,10 @@ export class Params { toSearchParams() { return this.toSearchParamsObject().toString(); } + + isDefault() { + return this === defaultParams; + } } function isValidJsonUrl(url) { diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs new file mode 100644 index 000000000..076a84733 --- /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..5f2e4cfeb 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}`); } @@ -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; }); } @@ -172,7 +174,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..cd2122f8d 100644 --- a/resources/todomvc/architecture-examples/angular-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/angular-complex/scripts/build.js @@ -17,3 +17,4 @@ const options = { }; buildComplex(options); +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 802c9f845..11bb3f4cc 100644 --- a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.js @@ -17,3 +17,4 @@ const options = { }; buildComplex(options); +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 ff06b6773..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(); +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 6f69450f0..e8b8ecb18 100644 --- a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.js @@ -19,4 +19,5 @@ const options = { cssFilesToAddLinksFor: ["big-dom-with-stacking-context-scrollable.css"], }; -buildComplex(options); \ No newline at end of file +buildComplex(options); +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 06924222d..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(); +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 a8b2448ca..32c17118d 100644 --- a/resources/todomvc/architecture-examples/lit-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/lit-complex/scripts/build.js @@ -24,3 +24,4 @@ const options = { }; buildComplex(options); +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 d9309af11..62beccabc 100644 --- a/resources/todomvc/architecture-examples/preact-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/preact-complex/scripts/build.js @@ -15,3 +15,4 @@ const options = { }; buildComplex(options); +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 814d42210..7b309f619 100644 --- a/resources/todomvc/architecture-examples/react-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/react-complex/scripts/build.js @@ -14,3 +14,4 @@ const options = { }; buildComplex(options); +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 b51b34ec0..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,3 +15,4 @@ const options = { }; buildComplex(options); +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 42fcbdefc..675be7bcc 100644 --- a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.js @@ -15,3 +15,4 @@ const options = { }; buildComplex(options); +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 cb46077d8..b72c4f84c 100644 --- a/resources/todomvc/architecture-examples/vue-complex/scripts/build.js +++ b/resources/todomvc/architecture-examples/vue-complex/scripts/build.js @@ -17,3 +17,4 @@ const options = { }; buildComplex(options); +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 2590e7875..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,3 +17,4 @@ const options = { }; buildComplex(options); +import("../../../../shared/generate-resources.mjs").then((m) => m.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..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(); +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 9e9481a91..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,3 +14,4 @@ const options = { }; buildComplex(options); +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 338594b9c..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,3 +23,4 @@ const options = { }; buildComplex(options); +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 be4499d96..7d9f8e924 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,6 @@ const fs = require("fs").promises; const { dirname } = require("path"); +const path = require("path"); /** * createDirectory @@ -153,4 +154,4 @@ const build = async () => { console.log("Done with building!"); }; -build(); +build().then(() => import("../../../../shared/generate-resources.mjs").then((m) => m.generateResourcesFile(path.join(__dirname, "../dist")))); diff --git a/sw.mjs b/sw.mjs new file mode 100644 index 000000000..470efbf73 --- /dev/null +++ b/sw.mjs @@ -0,0 +1,155 @@ +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, 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 = []; + 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); + + for (const item of urlsToCache) + cachedUrls.add(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(); + 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 []; + } +} + +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); + + 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 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; + } + + // 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, { 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 243b3a98d..37e9fbf9d 100644 --- a/tests/unittests/suites.mjs +++ b/tests/unittests/suites.mjs @@ -64,6 +64,32 @@ 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); + const text = await res.text(); + expect(text.trim().length).to.be.greaterThan(0, `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); + + 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); + }) + ); + } + }); }); }