diff --git a/experimental/javascript-wc-indexeddb/dist/resources.txt b/experimental/javascript-wc-indexeddb/dist/resources.txt
new file mode 100644
index 000000000..601810cf1
--- /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/step-runner.mjs
+src/speedometer-utils/step-scheduler.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/benchmark.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs
index 264145159..2ef5c7ff4 100644
--- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs
+++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs
@@ -1,5 +1,5 @@
/* eslint-disable no-case-declarations */
-import { TestRunner, AsyncTestRunner } from "./test-runner.mjs";
+import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
import { Params } from "./params.mjs";
/**
@@ -15,10 +15,10 @@ export class BenchmarkStep {
}
async runAndRecord(params, suite, test, callback) {
- const TestRunnerClass = params.useAsyncSteps ? AsyncTestRunner : TestRunner;
+ const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner;
const type = params.useAsyncSteps ? "async" : "sync";
- const testRunner = new TestRunnerClass(null, null, params, suite, test, callback, type);
- const result = await testRunner.runTest();
+ const stepRunner = new StepRunnerClass(null, null, params, suite, test, callback, type);
+ const result = await stepRunner.runStep();
return result;
}
}
diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs
index 520679d24..b54bf1277 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,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/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs
similarity index 61%
rename from experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs
rename to experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs
index 2d816e853..46187a6ba 100644
--- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs
+++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs
@@ -1,18 +1,18 @@
-import { TEST_INVOKER_LOOKUP } from "./test-invoker.mjs";
+import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs";
import { forceLayout } from "./helpers.mjs";
-export class TestRunner {
+export class StepRunner {
#frame;
#page;
#params;
#suite;
- #test;
+ #step;
#callback;
#type;
- constructor(frame, page, params, suite, test, callback, type) {
+ constructor(frame, page, params, suite, step, callback, type) {
this.#suite = suite;
- this.#test = test;
+ this.#step = step;
this.#params = params;
this.#callback = callback;
this.#page = page;
@@ -24,21 +24,21 @@ export class TestRunner {
return this.#page;
}
- get test() {
- return this.#test;
+ get step() {
+ return this.#step;
}
- _runSyncStep(test, page) {
- test.run(page);
+ _runSyncStep(step, page) {
+ step.run(page);
}
- async runTest() {
+ async runStep() {
// Prepare all mark labels outside the measuring loop.
const suiteName = this.#suite.name;
- const testName = this.#test.name;
- const syncStartLabel = `${suiteName}.${testName}-start`;
- const syncEndLabel = `${suiteName}.${testName}-sync-end`;
- const asyncEndLabel = `${suiteName}.${testName}-async-end`;
+ const stepName = this.#step.name;
+ const syncStartLabel = `${suiteName}.${stepName}-start`;
+ const syncEndLabel = `${suiteName}.${stepName}-sync-end`;
+ const asyncEndLabel = `${suiteName}.${stepName}-async-end`;
let syncTime;
let asyncStartTime;
@@ -57,9 +57,9 @@ export class TestRunner {
const syncStartTime = performance.now();
if (this.#type === "async")
- await this._runSyncStep(this.test, this.page);
+ await this._runSyncStep(this.step, this.page);
else
- this._runSyncStep(this.test, this.page);
+ this._runSyncStep(this.step, this.page);
const mark = performance.mark(syncEndLabel);
const syncEndTime = mark.startTime;
@@ -85,32 +85,32 @@ export class TestRunner {
if (this.#params.warmupBeforeSync)
performance.measure("warmup", "warmup-start", "warmup-end");
- performance.measure(`${suiteName}.${testName}-sync`, syncStartLabel, syncEndLabel);
- performance.measure(`${suiteName}.${testName}-async`, syncEndLabel, asyncEndLabel);
+ performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel);
+ performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel);
};
- const report = () => this.#callback(this.#test, syncTime, asyncTime);
- const invokerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
- const invokerClass = TEST_INVOKER_LOOKUP[invokerType];
- const invoker = new invokerClass(runSync, measureAsync, report, this.#params);
+ const report = () => this.#callback(this.#step, syncTime, asyncTime);
+ const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
+ const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType];
+ const scheduler = new schedulerClass(runSync, measureAsync, report, this.#params);
- return invoker.start();
+ return scheduler.start();
}
}
-export class AsyncTestRunner extends TestRunner {
- constructor(frame, page, params, suite, test, callback, type) {
- super(frame, page, params, suite, test, callback, type);
+export class AsyncStepRunner extends StepRunner {
+ constructor(frame, page, params, suite, step, callback, type) {
+ super(frame, page, params, suite, step, callback, type);
}
- async _runSyncStep(test, page) {
- await test.run(page);
+ async _runSyncStep(step, page) {
+ await step.run(page);
}
}
-export const TEST_RUNNER_LOOKUP = {
+export const STEP_RUNNER_LOOKUP = {
__proto__: null,
- default: TestRunner,
- async: AsyncTestRunner,
- remote: TestRunner,
+ default: StepRunner,
+ async: AsyncStepRunner,
+ remote: StepRunner,
};
diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs
similarity index 83%
rename from experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs
rename to experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs
index cb63c3de3..df7d96a21 100644
--- a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs
+++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs
@@ -1,4 +1,4 @@
-class TestInvoker {
+class StepScheduler {
constructor(syncCallback, asyncCallback, reportCallback, params) {
this._syncCallback = syncCallback;
this._asyncCallback = asyncCallback;
@@ -16,7 +16,7 @@ class TestInvoker {
}
}
-class RAFTestInvoker extends TestInvoker {
+class RAFStepScheduler extends StepScheduler {
_scheduleCallbacks(resolve) {
requestAnimationFrame(() => this._syncCallback());
requestAnimationFrame(() => {
@@ -31,7 +31,7 @@ class RAFTestInvoker extends TestInvoker {
}
}
-class AsyncRAFTestInvoker extends TestInvoker {
+class AsyncRAFStepScheduler extends StepScheduler {
static mc = new MessageChannel();
_scheduleCallbacks(resolve) {
let gotTimer = false;
@@ -62,7 +62,7 @@ class AsyncRAFTestInvoker extends TestInvoker {
tryTriggerAsyncCallback();
});
- AsyncRAFTestInvoker.mc.port1.addEventListener(
+ AsyncRAFStepScheduler.mc.port1.addEventListener(
"message",
async function () {
await Promise.resolve();
@@ -71,14 +71,14 @@ class AsyncRAFTestInvoker extends TestInvoker {
},
{ once: true }
);
- AsyncRAFTestInvoker.mc.port1.start();
- AsyncRAFTestInvoker.mc.port2.postMessage("speedometer");
+ AsyncRAFStepScheduler.mc.port1.start();
+ AsyncRAFStepScheduler.mc.port2.postMessage("speedometer");
});
}
}
-export const TEST_INVOKER_LOOKUP = {
+export const STEP_SCHEDULER_LOOKUP = {
__proto__: null,
- raf: RAFTestInvoker,
- async: AsyncRAFTestInvoker,
+ raf: RAFStepScheduler,
+ async: AsyncRAFStepScheduler,
};
diff --git a/experimental/javascript-wc-indexeddb/scripts/build.mjs b/experimental/javascript-wc-indexeddb/scripts/build.mjs
index 9db636c3a..54b00c881 100644
--- a/experimental/javascript-wc-indexeddb/scripts/build.mjs
+++ b/experimental/javascript-wc-indexeddb/scripts/build.mjs
@@ -1,6 +1,10 @@
+import path from "path";
+import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs";
import fs from "fs/promises";
import { dirname } from "path";
+const __dirname = import.meta.dirname;
+
/**
* createDirectory
*
@@ -97,7 +101,7 @@ const filesToMove = [
{ src: "node_modules/todomvc-css/dist/todo-list.constructable.js", dest: "./dist/styles/todo-list.constructable.js" },
{ src: "node_modules/todomvc-css/dist/todo-item.constructable.js", dest: "./dist/styles/todo-item.constructable.js" },
{ src: "node_modules/dexie/dist/modern/dexie.mjs", dest: "./dist/libs/dexie.mjs" },
- { src: "node_modules/speedometer-utils/step-scheduler.mjs", dest: "./dist/src/speedometer-utils/test-invoker.mjs" },
+ { src: "node_modules/speedometer-utils/step-scheduler.mjs", dest: "./dist/src/speedometer-utils/step-scheduler.mjs" },
{ src: "node_modules/speedometer-utils/step-runner.mjs", dest: "./dist/src/speedometer-utils/step-runner.mjs" },
{ src: "node_modules/speedometer-utils/params.mjs", dest: "./dist/src/speedometer-utils/params.mjs" },
{ src: "src/speedometer-utils/benchmark.mjs", dest: "./dist/src/speedometer-utils/benchmark.mjs" },
@@ -164,4 +168,5 @@ const build = async () => {
console.log("Done with building!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs b/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs
index a16f0831f..87639c816 100644
--- a/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs
+++ b/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs
@@ -1,5 +1,5 @@
/* eslint-disable no-case-declarations */
-import { TestRunner, AsyncTestRunner } from "./test-runner.mjs";
+import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
import { Params } from "/node_modules/speedometer-utils/params.mjs";
/**
@@ -15,10 +15,10 @@ export class BenchmarkStep {
}
async runAndRecord(params, suite, test, callback) {
- const TestRunnerClass = params.useAsyncSteps ? AsyncTestRunner : TestRunner;
+ const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner;
const type = params.useAsyncSteps ? "async" : "sync";
- const testRunner = new TestRunnerClass(null, null, params, suite, test, callback, type);
- const result = await testRunner.runTest();
+ const stepRunner = new StepRunnerClass(null, null, params, suite, test, callback, type);
+ const result = await stepRunner.runStep();
return result;
}
}
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.mjs b/experimental/todomvc-localstorage/scripts/build.mjs
index cc923230e..ad6a77de9 100644
--- a/experimental/todomvc-localstorage/scripts/build.mjs
+++ b/experimental/todomvc-localstorage/scripts/build.mjs
@@ -1,6 +1,9 @@
+import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
+const __dirname = import.meta.dirname;
+
const rootDirectory = "./";
const sourceDirectory = "./src";
const targetDirectory = "./dist";
@@ -53,4 +56,5 @@ const build = async () => {
console.log("done!!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.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 fdd254775..5dd02eca6 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 86c8207ed..674ce4ecc 100644
--- a/resources/main.css
+++ b/resources/main.css
@@ -419,7 +419,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;
@@ -429,7 +439,8 @@ button.show-about {
border-right: 6px solid var(--background);
}
-#progress-completed {
+#progress-completed,
+#preload-progress-completed {
position: absolute;
top: 0;
left: 0;
@@ -440,11 +451,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);
}
@@ -455,7 +468,9 @@ button.show-about {
background-color: var(--background);
}
-#info {
+#info,
+#preload-info {
+ display: none;
position: absolute;
bottom: -25px;
left: 60px;
@@ -465,11 +480,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;
@@ -945,3 +962,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 bb37318b0..c83cf90b5 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.mjs b/resources/todomvc/architecture-examples/angular-complex/scripts/build.mjs
index 0967ca52d..4cdc47f81 100644
--- a/resources/todomvc/architecture-examples/angular-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/angular-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Angular Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs
index 691f04619..2cc83bc87 100644
--- a/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/backbone-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Backbone Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/backbone/scripts/build.mjs b/resources/todomvc/architecture-examples/backbone/scripts/build.mjs
index c6ec2b793..9d43978b1 100644
--- a/resources/todomvc/architecture-examples/backbone/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/backbone/scripts/build.mjs
@@ -1,6 +1,9 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
+const __dirname = import.meta.dirname;
+
const rootDirectory = "./";
const sourceDirectory = "./src";
const targetDirectory = "./dist";
@@ -58,4 +61,5 @@ const build = async () => {
console.log("done!!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs
index 302d57cee..ddaccd1d9 100644
--- a/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/jquery-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC jQuery Complex DOM.
*/
@@ -21,4 +22,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.mjs b/resources/todomvc/architecture-examples/jquery/scripts/build.mjs
index e94df94b2..eef43065d 100644
--- a/resources/todomvc/architecture-examples/jquery/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/jquery/scripts/build.mjs
@@ -1,6 +1,9 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
+const __dirname = import.meta.dirname;
+
const rootDirectory = "./";
const sourceDirectory = "./src";
const targetDirectory = "./dist";
@@ -51,4 +54,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.mjs b/resources/todomvc/architecture-examples/lit-complex/scripts/build.mjs
index 894318a0c..0d7f34f09 100644
--- a/resources/todomvc/architecture-examples/lit-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/lit-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Lit Complex DOM.
*/
@@ -25,3 +26,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs
index 133d2924a..3ac4ef8c2 100644
--- a/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/preact-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Build the TodoMVC Preact Complex DOM example.
*/
@@ -16,3 +17,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs
index 49fdf8eaf..c6101cf1a 100644
--- a/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/react-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Build the TodoMVC: React Complex DOM example.
*/
@@ -15,3 +16,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs
index 870e615c8..5920511ec 100644
--- a/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC React-Redux Complex DOM.
*/
@@ -16,3 +17,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs
index ee7b201cd..128aa2666 100644
--- a/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/svelte-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Svelte Complex DOM.
*/
@@ -16,3 +17,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs b/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs
index b2a7e475c..b20c8b700 100644
--- a/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs
+++ b/resources/todomvc/architecture-examples/vue-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Vue Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs
index 2d75a1108..2a3bbfdad 100644
--- a/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs
+++ b/resources/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC JavaScript Es5 Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.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.mjs b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs
index cc923230e..13dba887d 100644
--- a/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs
+++ b/resources/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs
@@ -1,6 +1,9 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
+const __dirname = import.meta.dirname;
+
const rootDirectory = "./";
const sourceDirectory = "./src";
const targetDirectory = "./dist";
@@ -53,4 +56,5 @@ const build = async () => {
console.log("done!!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs
index 08c7f2bd7..4e5ec8dbc 100644
--- a/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs
+++ b/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC JavaScript Es6 Webpack Complex DOM.
*/
@@ -15,3 +16,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs
index 45dce8bd3..f5da135d8 100644
--- a/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs
+++ b/resources/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC JavaScript Web Components Complex DOM.
*/
@@ -24,3 +25,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs
index 8980583e2..b768a19d0 100644
--- a/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs
+++ b/resources/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs
@@ -1,6 +1,10 @@
+import path from "path";
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import { dirname } from "path";
+const __dirname = import.meta.dirname;
+
/**
* createDirectory
*
@@ -153,4 +157,5 @@ const build = async () => {
console.log("Done with building!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.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);
+ })
+ );
+ }
+ });
});
}