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 @@
+
+
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);
+ })
+ );
+ }
+ });
});
}