Compare commits
373 commits
1.0
...
upstream/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86710fec5e | ||
|
|
8682ae07c4 | ||
|
|
b4b6b1d5dc | ||
|
|
c831667c22 | ||
|
|
6713c52fbd | ||
|
|
663dc26cb8 | ||
|
|
e258bb1661 | ||
|
|
9c07200b65 | ||
|
|
779ca6ffb7 | ||
|
|
8344a61b3e | ||
|
|
59832dd01d | ||
|
|
ed40d3fcaf | ||
|
|
86b99d4a1f | ||
|
|
a867a41832 | ||
|
|
80d4e409f9 | ||
|
|
8698c7839f | ||
|
|
64bbc821c8 | ||
|
|
9b4716cbe9 | ||
|
|
20c02284b7 | ||
|
|
d580beea58 | ||
|
|
9ea7e71ac2 | ||
|
|
4332004a86 | ||
|
|
56ae0346e4 | ||
|
|
a6d7f0ae29 | ||
|
|
f866d2b077 | ||
|
|
001a3926e6 | ||
|
|
58dc3ffb4c | ||
|
|
6c8c0be8f1 | ||
|
|
abe0def4f3 | ||
|
|
64656e22f3 | ||
|
|
6e175efc3c | ||
|
|
017f8a2b6e | ||
|
|
a1f4c0b3cd | ||
|
|
b274b98c33 | ||
|
|
3175f4851a | ||
|
|
0f34e5752f | ||
|
|
39469ce904 | ||
|
|
6cd6735748 | ||
|
|
e2e2538900 | ||
|
|
5744c55d13 | ||
|
|
d90b6fde07 | ||
|
|
06449bdc34 | ||
|
|
87f9ed88c8 | ||
|
|
f1eddfe695 | ||
|
|
deba93c88d | ||
|
|
c126fed439 | ||
|
|
ba41f91e48 | ||
|
|
25eb32e898 | ||
|
|
0746a31b76 | ||
|
|
bb9e3e1ba6 | ||
|
|
dd661c033a | ||
|
|
1fa5234087 | ||
|
|
cf76cb5106 | ||
|
|
c156335c1a | ||
|
|
5e7cec81a1 | ||
|
|
a5ee467351 | ||
|
|
c760d160ca | ||
|
|
f4763c3f86 | ||
|
|
763c517938 | ||
|
|
91b7adeaed | ||
|
|
fa1560af92 | ||
|
|
275043eba4 | ||
|
|
cdc058a1dd | ||
|
|
68d8f57ddf | ||
|
|
a3b0e71d97 | ||
|
|
7d7504f9bd | ||
|
|
0c3f0fb132 | ||
|
|
9f85f8e3bd | ||
|
|
2ff56c9d76 | ||
|
|
d979723ef6 | ||
|
|
d6b5ffea95 | ||
|
|
4dfa8faeea | ||
|
|
faf9fff140 | ||
|
|
a169b1d56a | ||
|
|
d87b307974 | ||
|
|
f7c3a7014a | ||
|
|
0dc0b1c50e | ||
|
|
e2e3c496f5 | ||
|
|
67bc21f1f4 | ||
|
|
e70778f1a8 | ||
|
|
a64fa26876 | ||
|
|
e2afcb6e4e | ||
|
|
66b5324d02 | ||
|
|
94f0d81686 | ||
|
|
4f87c768c4 | ||
|
|
1552769e5c | ||
|
|
04cfe65d7d | ||
|
|
617c7310fa | ||
|
|
4719e3f8bd | ||
|
|
3ce6105a3a | ||
|
|
2387b9fe89 | ||
|
|
bb4a4ad1c1 | ||
|
|
2a4d041656 | ||
|
|
c2e8d4f6bc | ||
|
|
400a5b87a0 | ||
|
|
0dcab0317e | ||
|
|
ea14d27e03 | ||
|
|
3150f41173 | ||
|
|
5a19851e54 | ||
|
|
a999ad1024 | ||
|
|
e6e8a4f969 | ||
|
|
33dec4f328 | ||
|
|
3ae822bcff | ||
|
|
13cc928140 | ||
|
|
cbf8311a76 | ||
|
|
bd9ab77462 | ||
|
|
8d67f28d02 | ||
|
|
3d2a28e2dc | ||
|
|
406042a18d | ||
|
|
7efb0ac57b | ||
|
|
309437377e | ||
|
|
88c5323a49 | ||
|
|
58e2e4e92b | ||
|
|
7dc43d27a5 | ||
|
|
ad061acd81 | ||
|
|
5654961621 | ||
|
|
377c6c7610 | ||
|
|
3ca4aed3a8 | ||
|
|
c901052f2d | ||
|
|
899c11317d | ||
|
|
82e0a46283 | ||
|
|
f6f67e0237 | ||
|
|
0c3af704a5 | ||
|
|
ca9d23c18f | ||
|
|
3d782244a5 | ||
|
|
2640affe64 | ||
|
|
0fd1528419 | ||
|
|
9837ccc768 | ||
|
|
7b1b81ee13 | ||
|
|
285e32dacb | ||
|
|
31ba222f5b | ||
|
|
7529eedcc5 | ||
|
|
638a9681cf | ||
|
|
b0a465d60c | ||
|
|
3d93f179b6 | ||
|
|
7c0931ee8c | ||
|
|
7ae836e55f | ||
|
|
c0269db7d7 | ||
|
|
6ad43c3f5c | ||
|
|
0f449aa702 | ||
|
|
4b451d8388 | ||
|
|
64a5e7c0ba | ||
|
|
2aa819faed | ||
|
|
91aaaacb42 | ||
|
|
4173e4d8a9 | ||
|
|
a268ec4391 | ||
|
|
2414364b18 | ||
|
|
208e1abee2 | ||
|
|
2ee210ca20 | ||
|
|
abea7b89b9 | ||
|
|
c19468aa56 | ||
|
|
32c7c23a7d | ||
|
|
b77b3dbf2b | ||
|
|
44c809305d | ||
|
|
4316b5fb14 | ||
|
|
81c835f6b7 | ||
|
|
8fd080fb67 | ||
|
|
b10452595e | ||
|
|
0672c38f23 | ||
|
|
790eed91c9 | ||
|
|
679cb52fdf | ||
|
|
5748e44342 | ||
|
|
c306d4237d | ||
|
|
095abdfc44 | ||
|
|
a03f1367ea | ||
|
|
f6c5098d6e | ||
|
|
f841f81faa | ||
|
|
a608e98396 | ||
|
|
fb9b966b37 | ||
|
|
b3ec8c748b | ||
|
|
d86dad330f | ||
|
|
5107c739c1 | ||
|
|
b6abd32982 | ||
|
|
0fef685dcb | ||
|
|
26531fc7f1 | ||
|
|
64798bc754 | ||
|
|
5491573d0e | ||
|
|
8f6982e71a | ||
|
|
cba2b0d2e7 | ||
|
|
36712deb57 | ||
|
|
f02d7e4b64 | ||
|
|
61ad820a5e | ||
|
|
d834dcbb07 | ||
|
|
a9c96c3bd4 | ||
|
|
e11801e3b5 | ||
|
|
e2a033d54b | ||
|
|
91f54a9c6d | ||
|
|
95af4c4956 | ||
|
|
e8624b1096 | ||
|
|
f6793b0bd5 | ||
|
|
815817b5e0 | ||
|
|
b44d98255f | ||
|
|
151d73d535 | ||
|
|
8046654389 | ||
|
|
9025eb1fdc | ||
|
|
ad36a2c483 | ||
|
|
9e692e4074 | ||
|
|
68c56ff662 | ||
|
|
1c4d9c7d10 | ||
|
|
0f6443e065 | ||
|
|
8491149024 | ||
|
|
1f48e366d0 | ||
|
|
bd4dc60b24 | ||
|
|
27c0b2cddf | ||
|
|
2281e13426 | ||
|
|
18c6ed85d9 | ||
|
|
00cac75b70 | ||
|
|
039f2e854f | ||
|
|
b4f675ef5f | ||
|
|
03f74f8b4c | ||
|
|
73fda50cb2 | ||
|
|
e3b8e93ec4 | ||
|
|
c7eef15502 | ||
|
|
42a7b3b5d6 | ||
|
|
d50264b547 | ||
|
|
4e13390704 | ||
|
|
0b89992a1a | ||
|
|
054b6342c6 | ||
|
|
7efcc89499 | ||
|
|
3b012c27f0 | ||
|
|
387720d1a7 | ||
|
|
0da3406f1b | ||
|
|
a5d8934b12 | ||
|
|
392d54feaa | ||
|
|
e295eb8596 | ||
|
|
b8b72cda46 | ||
|
|
38d5a701c0 | ||
|
|
df082e68c5 | ||
|
|
1cac2f1166 | ||
|
|
951f57b106 | ||
|
|
d19454c8e8 | ||
|
|
7465b1a9a8 | ||
|
|
28fbc651da | ||
|
|
32a381fcab | ||
|
|
a0c756901d | ||
|
|
c0d0626159 | ||
|
|
8d13cdf385 | ||
|
|
0d8bf1226d | ||
|
|
282e2b1df7 | ||
|
|
0940a7700c | ||
|
|
c46e191e52 | ||
|
|
3a678028a5 | ||
|
|
ec1326cf1a | ||
|
|
91d378683e | ||
|
|
39e9a52436 | ||
|
|
b8a24fdbb0 | ||
|
|
e1957e3919 | ||
|
|
ec51e3188b | ||
|
|
4f9ffb5e3b | ||
|
|
e2530dbed3 | ||
|
|
7dc36290eb | ||
|
|
07a4618a83 | ||
|
|
43a7448ca3 | ||
|
|
232cadb2ab | ||
|
|
1bc9ece1f5 | ||
|
|
170710fdad | ||
|
|
e0c9ed813e | ||
|
|
dca334b044 | ||
|
|
a876f0e419 | ||
|
|
34c169c724 | ||
|
|
19184dc694 | ||
|
|
36a320c98e | ||
|
|
82f7c6e2b8 | ||
|
|
7c94f49293 | ||
|
|
acbb9b442b | ||
|
|
dbc708200f | ||
|
|
e52236bfc5 | ||
|
|
372bd7a018 | ||
|
|
e2e1fcd94b | ||
|
|
cfa5eccb3b | ||
|
|
d8e35c4921 | ||
|
|
5cf8184de4 | ||
|
|
774b018832 | ||
|
|
2193fda65d | ||
|
|
40136381fb | ||
|
|
822906a474 | ||
|
|
f196015fa7 | ||
|
|
898398f07f | ||
|
|
41bdda72e8 | ||
|
|
daa720b67a | ||
|
|
ac59c3fb1e | ||
|
|
373580bec9 | ||
|
|
0c64eb67a9 | ||
|
|
e046a17583 | ||
|
|
238ffeed42 | ||
|
|
e1e156febb | ||
|
|
19ae86221b | ||
|
|
08c4983721 | ||
|
|
10a3acfb62 | ||
|
|
75cddea7ce | ||
|
|
248bfec3c4 | ||
|
|
24b447eeb3 | ||
|
|
03ea073d46 | ||
|
|
05b6f7a08d | ||
|
|
ab819fa534 | ||
|
|
652fe1bc86 | ||
|
|
9a4cf52cd6 | ||
|
|
e9565fbb1f | ||
|
|
5e6adbfe19 | ||
|
|
bb2ea5f183 | ||
|
|
c1ed921716 | ||
|
|
1d46644e9b | ||
|
|
612f748595 | ||
|
|
619b96f901 | ||
|
|
bd88f95079 | ||
|
|
0744a3eb3a | ||
|
|
d75589873f | ||
|
|
1afa78682d | ||
|
|
2e4baeefc4 | ||
|
|
90562b145f | ||
|
|
6d04874961 | ||
|
|
6c31329c83 | ||
|
|
1933672b81 | ||
|
|
09210bc05e | ||
|
|
6523f66497 | ||
|
|
359605012f | ||
|
|
f16557016d | ||
|
|
354d5039c4 | ||
|
|
2c78996286 | ||
|
|
a20855e4a8 | ||
|
|
52d094dd0e | ||
|
|
ce1b63420f | ||
|
|
ddd75e2206 | ||
|
|
999f4f676b | ||
|
|
33b5e9ae76 | ||
|
|
6f683630ef | ||
|
|
a068b53a7e | ||
|
|
24b094d1cc | ||
|
|
d8bea6b8d4 | ||
|
|
bbec60788f | ||
|
|
e884d518b6 | ||
|
|
0644776001 | ||
|
|
c64ffc8eaa | ||
|
|
661426329a | ||
|
|
cb6cc0652f | ||
|
|
d309edfe9d | ||
|
|
207416957e | ||
|
|
f4eb1ebaa5 | ||
|
|
bf927da6ba | ||
|
|
2fdc20cc2e | ||
|
|
f20363a3c4 | ||
|
|
1b06e553f9 | ||
|
|
7722c8ab06 | ||
|
|
0b592ff361 | ||
|
|
6fcb6e6359 | ||
|
|
76f4bb1e86 | ||
|
|
4c22ba45f6 | ||
|
|
480c9cee0f | ||
|
|
00db41cea3 | ||
|
|
42b6dd8a8d | ||
|
|
dbff607c76 | ||
|
|
9566af6fe6 | ||
|
|
69d5d5bf75 | ||
|
|
c0a1bc139b | ||
|
|
adc3cca860 | ||
|
|
2550aebedb | ||
|
|
ed6143a8cc | ||
|
|
ef8d58c988 | ||
|
|
dce4dfeaa0 | ||
|
|
714b5b4cb2 | ||
|
|
731c9e871d | ||
|
|
e1125d91bc | ||
|
|
52998e962d | ||
|
|
b840b30f7a | ||
|
|
8787833334 | ||
|
|
1e6af3de5f | ||
|
|
281b94e7ae | ||
|
|
694d89c47e | ||
|
|
600ccf0e64 | ||
|
|
9bae5f5ef1 | ||
|
|
3ceae4f0e9 | ||
|
|
6b63846bad | ||
|
|
a1e842164b |
50
.eslintignore
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
package-lock.json
|
||||||
|
**/.angular
|
||||||
|
|
||||||
|
/suites/todomvc/architecture-examples/*
|
||||||
|
!/suites/todomvc/architecture-examples/angular
|
||||||
|
!/suites/todomvc/architecture-examples/angular-complex
|
||||||
|
!/suites/todomvc/architecture-examples/backbone
|
||||||
|
!/suites/todomvc/architecture-examples/backbone-complex
|
||||||
|
!/suites/todomvc/architecture-examples/lit
|
||||||
|
/suites/todomvc/architecture-examples/lit/.wireit
|
||||||
|
/suites/todomvc/architecture-examples/lit/dist/index.js
|
||||||
|
!/suites/todomvc/architecture-examples/lit-complex
|
||||||
|
!/suites/todomvc/architecture-examples/preact
|
||||||
|
!/suites/todomvc/architecture-examples/preact-complex
|
||||||
|
!/suites/todomvc/architecture-examples/react
|
||||||
|
!/suites/todomvc/architecture-examples/react-complex
|
||||||
|
!/suites/todomvc/architecture-examples/react-redux
|
||||||
|
!/suites/todomvc/architecture-examples/react-redux-complex
|
||||||
|
!/suites/todomvc/architecture-examples/svelte
|
||||||
|
!/suites/todomvc/architecture-examples/svelte-complex
|
||||||
|
!/suites/todomvc/architecture-examples/vue
|
||||||
|
!/suites/todomvc/architecture-examples/vue-complex
|
||||||
|
|
||||||
|
/suites/todomvc/vanilla-examples/*
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es5
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es5-complex
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6-complex
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6-webpack
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-web-components
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||||
|
|
||||||
|
!/suites/todomvc/big-dom-generator
|
||||||
|
|
||||||
|
!/suites/editors
|
||||||
|
/suites/editors/assets
|
||||||
|
|
||||||
|
!/suites/newssite
|
||||||
|
!/suites/newssite/news-next
|
||||||
|
!/suites/newssite/news-nuxt
|
||||||
|
!/suites/newssite/news-site-css
|
||||||
|
|
||||||
|
/suites/perf.webkit.org
|
||||||
|
|
||||||
|
/suites/react-stockcharts
|
||||||
|
|
||||||
|
/suites-experimental/responsive-design/build-dev/
|
||||||
101
.eslintrc
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions
|
||||||
|
* are met:
|
||||||
|
* 1. Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
||||||
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||||
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/base"],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"worker": true,
|
||||||
|
"es2022": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
// unit test specific
|
||||||
|
"sinon": true,
|
||||||
|
"expect": true,
|
||||||
|
// injected dependencies
|
||||||
|
"testRunner": true,
|
||||||
|
"startBenchmark": true,
|
||||||
|
"PerfTestRunner": true,
|
||||||
|
"deltas": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"array-bracket-spacing": [2, "never"],
|
||||||
|
"block-spacing": [2, "always"],
|
||||||
|
"brace-style": 2,
|
||||||
|
"comma-dangle": [2, "only-multiline"],
|
||||||
|
"comma-spacing": [2, { "before": false, "after": true }],
|
||||||
|
"comma-style": [2, "last"],
|
||||||
|
"consistent-return": 2,
|
||||||
|
"curly": ["error", "multi-or-nest", "consistent"],
|
||||||
|
"dot-notation": 0,
|
||||||
|
"eol-last": 2,
|
||||||
|
"eqeqeq": 2,
|
||||||
|
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||||
|
"func-call-spacing": [2, "never"],
|
||||||
|
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
||||||
|
"keyword-spacing": [2, { "before": true, "after": true }],
|
||||||
|
"new-cap": 0,
|
||||||
|
"new-parens": 0,
|
||||||
|
"no-console": 0,
|
||||||
|
"no-constant-condition": 0,
|
||||||
|
"no-extra-bind": 2,
|
||||||
|
"no-extra-parens": [2, "all", { "nestedBinaryExpressions": false }],
|
||||||
|
"no-extra-semi": 2,
|
||||||
|
"no-global-assign": 2,
|
||||||
|
"no-inner-declarations": 0,
|
||||||
|
"no-multi-spaces": 2,
|
||||||
|
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||||
|
"no-new": 0,
|
||||||
|
"no-proto": 0,
|
||||||
|
"no-redeclare": 0,
|
||||||
|
"no-return-assign": 2,
|
||||||
|
"no-return-await": 2,
|
||||||
|
"no-self-compare": 2,
|
||||||
|
"no-shadow": 0,
|
||||||
|
"no-trailing-spaces": 2,
|
||||||
|
"no-undef": 2,
|
||||||
|
"no-underscore-dangle": 0,
|
||||||
|
"no-unused-expressions": 2,
|
||||||
|
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
|
||||||
|
"no-use-before-define": 0,
|
||||||
|
"nonblock-statement-body-position": ["error", "below"],
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"operator-linebreak": ["error", "before"],
|
||||||
|
"prefer-template": 2,
|
||||||
|
"quotes": [2, "double", { "avoidEscape": true }],
|
||||||
|
"semi": 2,
|
||||||
|
"semi-spacing": [2, { "before": false, "after": true }],
|
||||||
|
"space-infix-ops": 2,
|
||||||
|
"spaced-comment": [2, "always"],
|
||||||
|
"strict": 0,
|
||||||
|
"valid-typeof": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
97
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linters:
|
||||||
|
name: Linters
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install Node Packages
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run package.json linter
|
||||||
|
run: npm run lint:pkg
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
run: npm run format
|
||||||
|
|
||||||
|
- name: Check if anything changed
|
||||||
|
run: |
|
||||||
|
git_status="`LC_ALL=C git status --porcelain --ignore-submodules -unormal 2>&1`"
|
||||||
|
if [ -n "$git_status" ]; then
|
||||||
|
printf "Some file(s) changed as the result of formatting, this means that you need to run the formatter on your patch.\n"
|
||||||
|
printf "Here is what changed:\n"
|
||||||
|
printf -- "$git_status\n\n"
|
||||||
|
printf "And here is the diff:\n"
|
||||||
|
git diff -U8
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
test-node:
|
||||||
|
name: Node Unit Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install Node Packages
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run Node Unit Tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
browser: [chrome, firefox, safari]
|
||||||
|
steps:
|
||||||
|
- name: Install Firefox
|
||||||
|
if: ${{ matrix.browser == 'firefox' }}
|
||||||
|
run: brew install --cask firefox
|
||||||
|
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install Node Packages
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
run: |
|
||||||
|
echo "Running in $BROWSER"
|
||||||
|
npm run test:${{ matrix.browser }}
|
||||||
|
|
||||||
|
- name: Run end2end Tests
|
||||||
|
run: |
|
||||||
|
echo "Running in $BROWSER"
|
||||||
|
npm run test-e2e:${{ matrix.browser }}
|
||||||
9
.gitignore
vendored
|
|
@ -2,3 +2,12 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.directory
|
.directory
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
.agents
|
||||||
|
.gemini
|
||||||
|
.vscode
|
||||||
|
.angular
|
||||||
|
|
||||||
|
|
||||||
|
/suites-experimental/**/build-dev/
|
||||||
|
|
|
||||||
3
.npmpackagejsonlintrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "npm-package-json-lint-config-default"
|
||||||
|
}
|
||||||
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
engine-strict = true
|
||||||
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
24
|
||||||
50
.prettierignore
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
package-lock.json
|
||||||
|
**/.angular
|
||||||
|
|
||||||
|
/suites/todomvc/architecture-examples/*
|
||||||
|
!/suites/todomvc/architecture-examples/angular
|
||||||
|
!/suites/todomvc/architecture-examples/angular-complex
|
||||||
|
!/suites/todomvc/architecture-examples/backbone
|
||||||
|
!/suites/todomvc/architecture-examples/backbone-complex
|
||||||
|
!/suites/todomvc/architecture-examples/lit
|
||||||
|
/suites/todomvc/architecture-examples/lit/dist/index.js
|
||||||
|
/suites/todomvc/architecture-examples/lit/.wireit
|
||||||
|
!/suites/todomvc/architecture-examples/lit-complex
|
||||||
|
!/suites/todomvc/architecture-examples/preact
|
||||||
|
!/suites/todomvc/architecture-examples/preact-complex
|
||||||
|
!/suites/todomvc/architecture-examples/react
|
||||||
|
!/suites/todomvc/architecture-examples/react-complex
|
||||||
|
!/suites/todomvc/architecture-examples/react-redux
|
||||||
|
!/suites/todomvc/architecture-examples/react-redux-complex
|
||||||
|
!/suites/todomvc/architecture-examples/svelte
|
||||||
|
!/suites/todomvc/architecture-examples/svelte-complex
|
||||||
|
!/suites/todomvc/architecture-examples/vue
|
||||||
|
!/suites/todomvc/architecture-examples/vue-complex
|
||||||
|
|
||||||
|
/suites/todomvc/vanilla-examples/*
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es5
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es5-complex
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6-complex
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6-webpack
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-web-components
|
||||||
|
!/suites/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||||
|
|
||||||
|
!/suites/todomvc/big-dom-generator
|
||||||
|
|
||||||
|
!/suites/editors
|
||||||
|
/suites/editors/assets
|
||||||
|
|
||||||
|
!/suites/newssite
|
||||||
|
!/suites/newssite/news-next
|
||||||
|
!/suites/newssite/news-nuxt
|
||||||
|
!/suites/newssite/news-site-css
|
||||||
|
|
||||||
|
/suites/perf.webkit.org
|
||||||
|
|
||||||
|
/suites/react-stockcharts
|
||||||
|
/suites-experimental/responsive-design/build-dev/
|
||||||
|
/suites-experimental/responsive-design/**/*.generated.css
|
||||||
13
.prettierrc
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"printWidth": 250,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
34
Development.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Development Environment
|
||||||
|
|
||||||
|
## Prerequisite
|
||||||
|
|
||||||
|
Node is required for local development.
|
||||||
|
We recommend using a Node version manager ([nvm](https://github.com/nvm-sh/nvm) for example) to ensure a stable development environment is used.
|
||||||
|
Below are the required node / npm versions:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
"node": ">18.13.0",
|
||||||
|
"npm": ">8.19.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
In your working directory, open terminal and paste the following commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/WebKit/Speedometer.git
|
||||||
|
cd Speedometer
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Development Server
|
||||||
|
|
||||||
|
1. In your terminal run:
|
||||||
|
```sh
|
||||||
|
PORT=8900 npm run dev # Passing the env var PORT is optional
|
||||||
|
```
|
||||||
|
2. Open your browser of choice and navigate to [http://127.0.0.1:8900](http://127.0.0.1:8900)
|
||||||
|
|
||||||
|
## Local Server
|
||||||
|
|
||||||
|
Speedometer uses [http-server](https://github.com/http-party/http-server), which is a static HTTP server. Meaning it does not provide hot-reloading. By default, the dev script disables caching and local changes can be viewed by simply refreshing your browser window. Additional options of the http-server can be found [here](https://github.com/http-party/http-server#available-options).
|
||||||
34
Governance.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Speedometer Governance Policy
|
||||||
|
|
||||||
|
Speedometer uses multistakeholder governance.
|
||||||
|
This will allow us to share work, and build a collaborative understanding of performance on the web
|
||||||
|
in order to drive resourcing towards appropriate areas.
|
||||||
|
This also provides a structure that can endure to provide maintenance and adapt to the future web.
|
||||||
|
|
||||||
|
An eligible “browser project” is a core end-to-end web browser engine with an integrated JavaScript engine
|
||||||
|
which distributes implementations widely. The project may delegate decision making within Speedometer
|
||||||
|
to multiple representatives (for example, to review code commits or to provide consensus for major changes).
|
||||||
|
The participating browser projects at this time are Blink/V8, Gecko/SpiderMonkey, and WebKit/JavaScriptCore.
|
||||||
|
|
||||||
|
The intent is that the working team should be able to move quickly for most changes,
|
||||||
|
with a higher level of process and consensus expected based on the impact of the change.
|
||||||
|
|
||||||
|
- **Trivial change** - This is a change that has no effect on the official benchmark and includes changes
|
||||||
|
to whitespaces, comments, documentation outside policies and governance model, and unofficial test cases.
|
||||||
|
A trivial change requires approval by a reviewer, who is not the author of the change,
|
||||||
|
from one of the participating browser projects.
|
||||||
|
The intent is to ensure basic code quality & license compatibility, not to reach agreement.
|
||||||
|
For example, one participating browser project might be both writing and reviewing a new benchmark in
|
||||||
|
a subfolder to test in their own CI, or reviewing code written by an external contributor.
|
||||||
|
- **Non-trivial change** - This is a change that has small impact on the official benchmark and includes
|
||||||
|
changes to official test cases, test runners, bug fixes, and the appearance of the benchmark.
|
||||||
|
A non-trivial change requires approval by at least two of the participating browser projects
|
||||||
|
(including either authoring or reviewing the change) and none other strongly opposed to the change
|
||||||
|
within 10 business days.
|
||||||
|
- **Major change** - This is a change that has major implications on the official benchmark such as
|
||||||
|
releasing of a new version of the benchmark or any revisions to governance policies and processes,
|
||||||
|
including changes to the participating browser projects.
|
||||||
|
A major change requires a consensus, meaning approvals by each of the participating browser projects.
|
||||||
|
|
||||||
|
This governance policy and associated code will be hosted inside the Speedometer repository within
|
||||||
|
the WebKit GitHub organization under the 2-clause BSD license.
|
||||||
5
LICENSE
|
|
@ -1,4 +1,7 @@
|
||||||
Copyright (C) 2022 Apple Inc. All rights reserved.
|
Copyright (C) 2013-2024 Apple Inc. All rights reserved.
|
||||||
|
Copyright (C) 2017-2024 Google Inc. All rights reserved.
|
||||||
|
Copyright (C) 2022-2024 Mozilla Corporation. All rights reserved.
|
||||||
|
Copyright (C) 2023-2024 Microsoft Corporation. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions
|
modification, are permitted provided that the following conditions
|
||||||
|
|
|
||||||
38
README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# What is Speedometer?
|
||||||
|
|
||||||
|
Speedometer is a benchmark for web browsers that measures Web application responsiveness
|
||||||
|
by timing simulated user interactions on various workloads. Our primary goal is to make it
|
||||||
|
reflect the real-world Web as much as possible. When a browser improves its score on the
|
||||||
|
benchmark, actual users should benefit. In order to achieve this, it should:
|
||||||
|
|
||||||
|
- Test end-to-end user journeys instead of testing specific features in a tight loop. Each
|
||||||
|
test should exercise the full set of what’s needed from the engine in order for a user to
|
||||||
|
accomplish a task.
|
||||||
|
- Evolve over time, adapting to the present Web on a regular basis. This should be informed
|
||||||
|
by current usage data, and by consensus about features which are important for engines to
|
||||||
|
optimize to provide a consistent experience for users and site authors.
|
||||||
|
- Be accessible to the public and useful to browser engineers. It should run in every modern
|
||||||
|
browser by visiting a normal web page. It should run relatively quickly, while providing
|
||||||
|
enough test coverage to be reflective of the real-world Web.
|
||||||
|
|
||||||
|
# Score and Test Measurement
|
||||||
|
|
||||||
|
Each test can contain several steps who contributed to the test duration. Note that the prepare step of a test is _unmeasured_ and thus does not contribute to the score. All following test
|
||||||
|
steps are **measured** and summed up in the test step time.
|
||||||
|
|
||||||
|
<img src="./resources/measurement-timeline.svg" />
|
||||||
|
|
||||||
|
Each step consists of the following phases:
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>Sync:</dt>
|
||||||
|
<dd>Time spent in synchronous JS execution.</dd>
|
||||||
|
<dt>Async:</dt>
|
||||||
|
<dd>Time spent in asynchronous JS like Promise or setTimeout tasks scheduled by the sync-step.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
## Score
|
||||||
|
|
||||||
|
The final benchmark score is calculated based of the inverse of [geomean](https://en.wikipedia.org/wiki/Geometric_mean) of all tests. We average the score over multiple iterations with the [arithmetic mean](https://en.wikipedia.org/wiki/Arithmetic_mean). By using the geomean to combine the test durations we maintain the invariant that relative improvements are favoured equally amongst all tests, even though they can have vastly different durations.
|
||||||
|
|
||||||
|
<img src="./resources/score.svg" />
|
||||||
39
Testing.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
Speedometer uses [Selenium](https://www.selenium.dev/) for testing of the application itself.
|
||||||
|
Tests are located in the `/tests` folder.
|
||||||
|
|
||||||
|
[Sinon](https://sinonjs.org/): Standalone test spies, stubs and mocks for JavaScript.
|
||||||
|
[Mocha](https://mochajs.org/): Testing framework.
|
||||||
|
|
||||||
|
## Local Testing
|
||||||
|
|
||||||
|
### Local Node.js Unit Testing
|
||||||
|
|
||||||
|
You can run the unit tests directly in Node.js using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:node
|
||||||
|
```
|
||||||
|
|
||||||
|
### In-Browser Testing
|
||||||
|
|
||||||
|
To run the in-browser tests locally, you'll need the browsers installed along with the corresponding driver:
|
||||||
|
|
||||||
|
- [chromedriver](https://chromedriver.chromium.org/getting-started)
|
||||||
|
- [geckodriver](https://github.com/mozilla/geckodriver/releases)
|
||||||
|
- [safaridriver](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari)
|
||||||
|
- [msedgedriver](https://developer.microsoft.com/microsoft-edge/tools/webdriver).
|
||||||
|
|
||||||
|
Once installed you can run the following scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:chrome
|
||||||
|
npm run test:firefox
|
||||||
|
npm run test:safari
|
||||||
|
npm run test:edge
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automated Testing
|
||||||
|
|
||||||
|
Currently Speedometer's tests run automatically, when pushing to the main branch or when opening a pr.
|
||||||
105
about.html
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=850" />
|
||||||
|
<title>Speedometer About</title>
|
||||||
|
<link rel="stylesheet" href="resources/main.css" />
|
||||||
|
<link rel="icon" href="resources/favicon.png" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<section id="about" class="visible" data-title="About">
|
||||||
|
<a href="./" class="logo">
|
||||||
|
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||||
|
<div class="version next">next</div>
|
||||||
|
</a>
|
||||||
|
<div class="section-grid">
|
||||||
|
<h1 class="section-header">About Speedometer</h1>
|
||||||
|
<div class="section-content">
|
||||||
|
<p>Speedometer is a benchmark for web browsers that measures Web application responsiveness by timing simulated user interactions on various workloads.</p>
|
||||||
|
<p>
|
||||||
|
It's developed as an open source project, with decisions being made under a <a href="https://github.com/WebKit/Speedometer/blob/main/Governance.md">multistakeholder governance model</a> between the three widely distributed
|
||||||
|
web browser engine projects.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The following high level user journeys are implemented in the current version. Each of these journeys has one or more workloads which test important aspects of it — for example commonly used patterns, frameworks, or
|
||||||
|
technologies.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Working with a todo list</li>
|
||||||
|
<ul>
|
||||||
|
<li>Measures the time to add, complete, and remove 100 todo items in a basic list.</li>
|
||||||
|
<li>Each example implements the same todo application (TodoMVC) using different techniques and frameworks.</li>
|
||||||
|
<li>
|
||||||
|
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es5" target="_blank">TodoMVC-JavaScript-ES5</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex" target="_blank">TodoMVC-JavaScript-ES6-Webpack-Complex-DOM</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-web-components" target="_blank">TodoMVC-WebComponents</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-complex" target="_blank">TodoMVC-React-Complex-DOM</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-redux" target="_blank">TodoMVC-React-Redux</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/backbone" target="_blank">TodoMVC-Backbone</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/angular-complex" target="_blank">TodoMVC-Angular-Complex-DOM</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/vue" target="_blank">TodoMVC-Vue</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/jquery" target="_blank">TodoMVC-jQuery</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/preact-complex" target="_blank">TodoMVC-Preact-Complex-DOM</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/svelte-complex" target="_blank">TodoMVC-Svelte-Complex-DOM</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/lit-complex" target="_blank">TodoMVC-Lit-Complex-DOM</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<li>Editing rich text</li>
|
||||||
|
<ul>
|
||||||
|
<li>Loading and styling text inside WYSIWYG and code editors.</li>
|
||||||
|
<li>
|
||||||
|
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-CodeMirror</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-TipTap</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<li>Rendering charts</li>
|
||||||
|
<ul>
|
||||||
|
<li>Loading and interacting with SVG and canvas charts.</li>
|
||||||
|
<li>
|
||||||
|
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-observable-plot</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-chartjs</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/react-stockcharts" target="_blank">React-Stockcharts-SVG</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/perf.webkit.org" target="_blank">Perf-Dashboard</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<li>Reading a news site</li>
|
||||||
|
<ul>
|
||||||
|
<li>Navigating across pages and interacting with a typical looking news site.</li>
|
||||||
|
<li>
|
||||||
|
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-next" target="_blank">NewsSite-Next</a>,
|
||||||
|
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-nuxt" target="_blank">NewsSite-Nuxt</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="note"><strong>Notes about methodology</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Although user-driven actions like mouse movements and keyboard input cannot be fully emulated in JavaScript, Speedometer does its best to faithfully replay a typical workload within the demo applications.</li>
|
||||||
|
<li>To make the run time long enough to measure with the limited precision, we synchronously execute a large number of the operations, such as adding one hundred to-do items.</li>
|
||||||
|
<li>
|
||||||
|
Modern browser engines execute some work asynchronously as an optimization strategy to reduce the run time of synchronous operations. While returning control back to JavaScript execution as soon as possible is worth
|
||||||
|
pursuing, the run time cost of such an asynchronous work should still be taken into a holistic measurement of web application performance. In addition, some JavaScript frameworks call into DOM APIs asynchronously as an
|
||||||
|
optimization technique. Speedometer approximates the run time of this asynchronous work in the UI thread with a zero-second timer that is scheduled immediately after each execution of synchronous operations.
|
||||||
|
</li>
|
||||||
|
<li>Speedometer does not attempt to measure concurrent asynchronous work (e.g. in Web Workers).</li>
|
||||||
|
<li>Speedometer should not be used as a way to compare the performance of different JavaScript frameworks.</li>
|
||||||
|
<li>
|
||||||
|
The goal of all workloads is to represent a scenario that could be found on the Web. Although all workloads strive to use patterns that are commonly used, some implementation details are Speedometer specific and should
|
||||||
|
not be used as a guideline on how to implement and deploy a standalone app. For example, due to constraints within the test harness, workloads must not depend on a server infrastructure to function properly and are
|
||||||
|
built as static files ahead of time.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="buttons section-footer fixed-footer">
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="button" href="./" title="Show main section.">Home</a>
|
||||||
|
<a class="button" href="instructions.html" title="Show test instructions.">Test Instructions</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
babel.config.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@babel/plugin-proposal-decorators",
|
||||||
|
{
|
||||||
|
"legacy": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
131
index.html
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html data-visible-section="home">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=850" />
|
||||||
|
<title>Speedometer Next</title>
|
||||||
|
<link rel="stylesheet" href="resources/main.css" />
|
||||||
|
<link rel="icon" href="resources/favicon.png" />
|
||||||
|
<script src="resources/main.mjs" type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<section id="home">
|
||||||
|
<a href="#home" class="logo">
|
||||||
|
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||||
|
<div class="version next">next</div>
|
||||||
|
</a>
|
||||||
|
<div class="section-grid">
|
||||||
|
<div class="section-content content">
|
||||||
|
<p class="top-row">Speedometer is a browser benchmark that measures the responsiveness of Web applications. It uses demo web applications to simulate user actions such as adding to-do items.</p>
|
||||||
|
<p id="screen-size-warning">
|
||||||
|
<strong>
|
||||||
|
Your browser window is too small. For most accurate results, please make the view port size at least <span id="min-screen-width">850px</span> by <span id="min-screen-height">650px</span>.<br />
|
||||||
|
It's currently <span id="screen-size"></span>.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button-row">
|
||||||
|
<button class="start-tests-button primary-button">Start Test</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons section-footer fixed-footer">
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="button" href="about.html">About Speedometer</a>
|
||||||
|
<a class="button" href="instructions.html">Test Instructions</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="running">
|
||||||
|
<a href="#home" class="logo">
|
||||||
|
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||||
|
<div class="version next">next</div>
|
||||||
|
</a>
|
||||||
|
<div id="testContainer"></div>
|
||||||
|
<div id="progress">
|
||||||
|
<progress aria-label="Progress" id="progress-completed"></progress>
|
||||||
|
</div>
|
||||||
|
<div id="info">
|
||||||
|
<div id="info-label"></div>
|
||||||
|
<div id="info-progress"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="summary" data-title="Results Summary" class="valid">
|
||||||
|
<a href="#home" class="logo">
|
||||||
|
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||||
|
<div class="version next">next</div>
|
||||||
|
</a>
|
||||||
|
<h1>Score</h1>
|
||||||
|
<div class="gauge">
|
||||||
|
<div class="window"><div class="needle"></div></div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div id="result-number"></div>
|
||||||
|
<div id="confidence-number"></div>
|
||||||
|
<div id="invalid-score-text">
|
||||||
|
One or more subtests produced no duration.<br />
|
||||||
|
Please check your <a href="./instructions.html" target="_blank">browser settings</a> and re-run the benchmark.<br />
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="button primary-button" href="#details" id="show-details" title="Show detailed results data.">Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="details" data-title="Results Details">
|
||||||
|
<a href="#home" class="logo">
|
||||||
|
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||||
|
<div class="version next">next</div>
|
||||||
|
</a>
|
||||||
|
<div class="section-grid">
|
||||||
|
<h1 class="section-header">Detailed Results</h1>
|
||||||
|
<div class="section-content all-metric-results">
|
||||||
|
<div class="non-standard-params">
|
||||||
|
<h2>Non-standard Parameters</h2>
|
||||||
|
<p>
|
||||||
|
Speedometer ran with non-standard parameters.<br />
|
||||||
|
The results are likely not comparable to default runs.
|
||||||
|
</p>
|
||||||
|
<table id="non-standard-params-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Param</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>Default</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="aggregated-metric-result">
|
||||||
|
<h2>Aggregate Metric</h2>
|
||||||
|
<div id="aggregate-chart"></div>
|
||||||
|
<h2>Test Metrics Overview</h2>
|
||||||
|
<div id="tests-chart"></div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<h2>Test Metrics Details</h2>
|
||||||
|
<div id="metrics-results"></div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons section-footer">
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="button" href="#summary" title="Go back to the simplified summary view.">Summary</a>
|
||||||
|
</div>
|
||||||
|
<div class="button-row export-buttons">
|
||||||
|
<button id="copy-full-json" title="Copy full result metrics as json string.">Copy JSON</button>
|
||||||
|
<a class="button" id="download-full-json" title="Download full result metrics as json string.">Download JSON</a>
|
||||||
|
<a class="button" id="download-classic-json" title="Download backwards-compatible result metrics as json string.">Download Classic JSON</a>
|
||||||
|
<a class="button" id="download-csv" title="Download all result metrics as CSV string.">Download CSV</a>
|
||||||
|
<button id="copy-csv" title="Copy all result metrics as CSV string.">Copy CSV</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
42
instructions.html
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=850" />
|
||||||
|
<title>Speedometer Test Instructions</title>
|
||||||
|
<link rel="stylesheet" href="resources/main.css" />
|
||||||
|
<link rel="icon" href="resources/favicon.png" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<section id="instructions" class="visible" data-title="Test Instructions">
|
||||||
|
<a href="./" class="logo">
|
||||||
|
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||||
|
<div class="version next">next</div>
|
||||||
|
</a>
|
||||||
|
<div class="section-grid">
|
||||||
|
<h1 class="section-header">Speedometer General Instructions</h1>
|
||||||
|
<div class="section-content">
|
||||||
|
<p>To get reasonably accurate results your device and browser need to be focused on the benchmark. Here are some basic instructions to allow that:</p>
|
||||||
|
<ul class="compact">
|
||||||
|
<li>Ensure you are running the latest stable browser version.</li>
|
||||||
|
<li>Prefer using a separate clean browser profile — extensions and non-default browser settings can have a large impact on the score.</li>
|
||||||
|
<li>Don't run any programs in the background.</li>
|
||||||
|
<li>Restart the browser and close all other tabs and windows.</li>
|
||||||
|
<li>Keep the Speedometer page focused and do not interact with the device while running the benchmark.</li>
|
||||||
|
<li>Keep your battery-powered device plugged into a charger.</li>
|
||||||
|
<li>Some devices might need time to cool down between consecutive runs.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="buttons section-footer fixed-footer">
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="button" href="./" title="Show main section.">Home</a>
|
||||||
|
<a class="button" href="about.html" title="Show about section.">About</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
netlify.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[[headers]]
|
||||||
|
for = "/*"
|
||||||
|
[headers.values]
|
||||||
|
cross-origin-embedder-policy = "require-corp"
|
||||||
|
cross-origin-opener-policy = "same-origin"
|
||||||
|
X-Frame-Options = "SAMEORIGIN"
|
||||||
|
x-content-type-options = "nosniff"
|
||||||
|
[build.environment]
|
||||||
|
NODE_VERSION = "24"
|
||||||
10205
package-lock.json
generated
Normal file
68
package.json
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"name": "speedometer",
|
||||||
|
"version": "4.0.0-alpha",
|
||||||
|
"description": "An open source repository for the Speedometer benchmark.",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0",
|
||||||
|
"npm": ">=8.19.3"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/WebKit/Speedometer.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/WebKit/Speedometer/issues"
|
||||||
|
},
|
||||||
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node tests/server.mjs",
|
||||||
|
"lint:check": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\"",
|
||||||
|
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix",
|
||||||
|
"lint:pkg": "npmPkgJsonLint \"**/package.json\" --ignore-pattern \"**/node_modules/**\"",
|
||||||
|
"pretty:check": "prettier --check ./",
|
||||||
|
"pretty:fix": "prettier --write ./",
|
||||||
|
"format": "node tests/format.mjs",
|
||||||
|
"format:all": "node tests/format.mjs --all",
|
||||||
|
"test:chrome": "node tests/run-unittests.mjs --browser chrome",
|
||||||
|
"test:firefox": "node tests/run-unittests.mjs --browser firefox",
|
||||||
|
"test:safari": "node tests/run-unittests.mjs --browser safari",
|
||||||
|
"test:edge": "node tests/run-unittests.mjs --browser edge",
|
||||||
|
"test-e2e:chrome": "node tests/run-end2end.mjs --browser chrome",
|
||||||
|
"test-e2e:firefox": "node tests/run-end2end.mjs --browser firefox",
|
||||||
|
"test-e2e:safari": "node tests/run-end2end.mjs --browser safari",
|
||||||
|
"test-e2e:edge": "node tests/run-end2end.mjs --browser edge",
|
||||||
|
"test": "npm run test:node",
|
||||||
|
"test:node": "mocha --require ./tests/setup-node.mjs \"tests/unittests/*.mjs\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.21.3",
|
||||||
|
"@babel/eslint-parser": "^7.21.3",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||||
|
"@next/eslint-plugin-next": "^13.4.5",
|
||||||
|
"@nuxt/eslint-config": "^0.1.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||||
|
"@typescript-eslint/parser": "^5.59.0",
|
||||||
|
"command-line-args": "^5.2.1",
|
||||||
|
"command-line-usage": "^6.1.3",
|
||||||
|
"es-main": "^1.3.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-ember": "^11.4.8",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-vue": "^9.10.0",
|
||||||
|
"expect.js": "^0.3.1",
|
||||||
|
"lws": "^4.2.0",
|
||||||
|
"lws-cors": "^4.2.1",
|
||||||
|
"lws-index": "^3.1.1",
|
||||||
|
"lws-log": "^3.0.0",
|
||||||
|
"lws-static": "^3.1.1",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"normalize-package-data": "^8.0.0",
|
||||||
|
"npm-package-json-lint": "^10.4.1",
|
||||||
|
"npm-package-json-lint-config-default": "^9.0.1",
|
||||||
|
"prettier": "^2.8.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
|
"selenium-webdriver": "^4.44.0",
|
||||||
|
"sinon": "^17.0.1",
|
||||||
|
"typescript": "~5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
163
resources/benchmark-configurator.mjs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// example url for local testing:
|
||||||
|
// http://localhost:8080/?developerMode=true&config=http://localhost:8080/resources/config.json
|
||||||
|
// since the json doesn't contain a default suite, dismiss warning popups and select from the developerMenu
|
||||||
|
import { DefaultSuites } from "../suites/default-suites.mjs";
|
||||||
|
import { ExperimentalSuites } from "../suites-experimental/suites.mjs";
|
||||||
|
import { params } from "./shared/params.mjs";
|
||||||
|
|
||||||
|
const DEFAULT_TAGS = ["all", "default", "experimental"];
|
||||||
|
const DISALLOWED_DOMAINS = ["browserbench.org"];
|
||||||
|
export class BenchmarkConfigurator {
|
||||||
|
#tags = new Set(DEFAULT_TAGS);
|
||||||
|
#suites = [];
|
||||||
|
|
||||||
|
get tags() {
|
||||||
|
return this.#tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
get suites() {
|
||||||
|
return this.#suites;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given string is a valid URL, supporting both absolute and relative paths.
|
||||||
|
*
|
||||||
|
* This function attempts to construct a URL object. For relative paths, it uses
|
||||||
|
* a dummy base URL to allow the URL constructor to parse them successfully.
|
||||||
|
*
|
||||||
|
* @param {string} url The URL string to validate.
|
||||||
|
* @returns {boolean} True if the URL is valid (absolute or relative), false otherwise.
|
||||||
|
*/
|
||||||
|
_isValidUrl(url) {
|
||||||
|
if (typeof url !== "string" || url.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(url, "http://www.example.com");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports an error by showing an alert and logging a message to the console.
|
||||||
|
* @private
|
||||||
|
* @param {string} message The error message to display.
|
||||||
|
* @param {object} [debugInfo] An optional object containing additional debug information.
|
||||||
|
*/
|
||||||
|
_reportError(message, debugInfo) {
|
||||||
|
alert(message);
|
||||||
|
console.error(message, debugInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
_freezeSuites() {
|
||||||
|
Object.freeze(this.#suites);
|
||||||
|
this.#suites.forEach((suite) => {
|
||||||
|
if (!suite.tags)
|
||||||
|
suite.tags = [];
|
||||||
|
if (suite.url.startsWith("experimental/"))
|
||||||
|
suite.tags.unshift("all", "experimental");
|
||||||
|
else
|
||||||
|
suite.tags.unshift("all");
|
||||||
|
suite.enabled = suite.tags.includes("default");
|
||||||
|
Object.freeze(suite.tags);
|
||||||
|
Object.freeze(suite.steps);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_freezeTags() {
|
||||||
|
Object.freeze(this.#tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (params.config) {
|
||||||
|
try {
|
||||||
|
const benchmarkUrl = new URL(window.location);
|
||||||
|
if (DISALLOWED_DOMAINS.some((domain) => benchmarkUrl.hostname.endsWith(domain))) {
|
||||||
|
console.warn("Configuration fetch not allowed. Loading default suites.");
|
||||||
|
this._loadSuites();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(params.config);
|
||||||
|
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(`Could not fetch config: ${response.status}`);
|
||||||
|
|
||||||
|
const config = await response.json();
|
||||||
|
|
||||||
|
if (!config || !Array.isArray(config.suites))
|
||||||
|
throw new Error("Could not find a valid config structure!");
|
||||||
|
|
||||||
|
config.suites.flatMap((suite) => suite.tags || []).forEach((tag) => this.#tags.add(tag));
|
||||||
|
config.suites.forEach((suite) => {
|
||||||
|
if (suite && suite.url && this._isValidUrl(suite.url))
|
||||||
|
this.#suites.push(suite);
|
||||||
|
else
|
||||||
|
throw new Error("Invalid suite data");
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error loading custom configuration: ${error.message}. Loading default suites.`);
|
||||||
|
this._loadSuites();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._loadSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._freezeTags();
|
||||||
|
this._freezeSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSuites() {
|
||||||
|
DefaultSuites.forEach((suite) => this._loadSuite(suite));
|
||||||
|
ExperimentalSuites.forEach((suite) => this._loadSuite(suite));
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSuite(suite) {
|
||||||
|
suite.tags.forEach((tag) => this.#tags.add(tag));
|
||||||
|
this.#suites.push(suite);
|
||||||
|
}
|
||||||
|
|
||||||
|
enableSuites(names, tags) {
|
||||||
|
if (names?.length) {
|
||||||
|
const lowerCaseNames = names.map((each) => each.toLowerCase());
|
||||||
|
this.#suites.forEach((suite) => {
|
||||||
|
suite.enabled = lowerCaseNames.includes(suite.name.toLowerCase());
|
||||||
|
});
|
||||||
|
} else if (tags?.length) {
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
if (!this.#tags.has(tag))
|
||||||
|
console.error(`Unknown Suites tag: "${tag}"`);
|
||||||
|
});
|
||||||
|
const tagsSet = new Set(tags);
|
||||||
|
this.#suites.forEach((suite) => {
|
||||||
|
suite.enabled = suite.tags.some((tag) => tagsSet.has(tag));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("Neither names nor tags provided. Enabling all default suites.");
|
||||||
|
this.#suites.forEach((suite) => {
|
||||||
|
suite.enabled = suite.tags.includes("default");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.#suites.some((suite) => suite.enabled))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (names?.length) {
|
||||||
|
this._reportError(`Suites "${names}" does not match any Suite. No tests to run.`, {
|
||||||
|
providedNames: names,
|
||||||
|
validNames: this.#suites.map((each) => each.name),
|
||||||
|
});
|
||||||
|
} else if (tags?.length) {
|
||||||
|
this._reportError(`Tags "${tags}" does not match any Suite. No tests to run.`, {
|
||||||
|
providedTags: tags,
|
||||||
|
validTags: Array.from(this.#tags),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const benchmarkConfigurator = new BenchmarkConfigurator();
|
||||||
|
await benchmarkConfigurator.init();
|
||||||
|
|
||||||
|
export { benchmarkConfigurator };
|
||||||
539
resources/benchmark-runner.mjs
Normal file
|
|
@ -0,0 +1,539 @@
|
||||||
|
import { Metric } from "./metric.mjs";
|
||||||
|
import { params } from "./shared/params.mjs";
|
||||||
|
import { forceLayout } from "./shared/helpers.mjs";
|
||||||
|
import { SUITE_RUNNER_LOOKUP } from "./suite-runner.mjs";
|
||||||
|
|
||||||
|
const performance = globalThis.performance;
|
||||||
|
|
||||||
|
export class BenchmarkTestStep {
|
||||||
|
constructor(testName, testFunction) {
|
||||||
|
this.name = testName;
|
||||||
|
this.run = testFunction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParent(lookupStartNode, path) {
|
||||||
|
const parent = path.reduce((root, selector) => {
|
||||||
|
const node = root.querySelector(selector);
|
||||||
|
return node.shadowRoot ?? node;
|
||||||
|
}, lookupStartNode);
|
||||||
|
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Page {
|
||||||
|
constructor(frame) {
|
||||||
|
this._frame = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalStorage() {
|
||||||
|
return this._frame.contentWindow.localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout() {
|
||||||
|
const body = this._frame ? this._frame.contentDocument.body : document.body;
|
||||||
|
const value = forceLayout(body, params.layoutMode);
|
||||||
|
body._leakedLayoutValue = value; // Prevent dead code elimination.
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForElement(selector) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const resolveIfReady = () => {
|
||||||
|
const element = this.querySelector(selector);
|
||||||
|
let callback = resolveIfReady;
|
||||||
|
if (element)
|
||||||
|
callback = () => resolve(element);
|
||||||
|
window.requestAnimationFrame(callback);
|
||||||
|
};
|
||||||
|
resolveIfReady();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first element within the document that matches the specified selector, or group of selectors.
|
||||||
|
* If no matches are found, null is returned.
|
||||||
|
*
|
||||||
|
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
|
||||||
|
* // return PageElement(<todo-item>)
|
||||||
|
* querySelector("todo-item", ["todo-app", "todo-list"]);
|
||||||
|
*
|
||||||
|
* @param {string} selector A string containing one or more selectors to match.
|
||||||
|
* @param {string[]} [path] An array containing a path to the parent element.
|
||||||
|
* @returns PageElement | null
|
||||||
|
*/
|
||||||
|
querySelector(selector, path = []) {
|
||||||
|
const lookupStartNode = this._frame.contentDocument;
|
||||||
|
const element = getParent(lookupStartNode, path).querySelector(selector);
|
||||||
|
|
||||||
|
if (element === null)
|
||||||
|
return null;
|
||||||
|
return this._wrapElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all elements within the document that matches the specified selector, or group of selectors.
|
||||||
|
* If no matches are found, null is returned.
|
||||||
|
*
|
||||||
|
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
|
||||||
|
* // return [PageElement(<todo-item>), PageElement(<todo-item>)]
|
||||||
|
* querySelectorAll("todo-item", ["todo-app", "todo-list"]);
|
||||||
|
*
|
||||||
|
* @param {string} selector A string containing one or more selectors to match.
|
||||||
|
* @param {string[]} [path] An array containing a path to the parent element.
|
||||||
|
* @returns array
|
||||||
|
*/
|
||||||
|
querySelectorAll(selector, path = []) {
|
||||||
|
const lookupStartNode = this._frame.contentDocument;
|
||||||
|
const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector));
|
||||||
|
for (let i = 0; i < elements.length; i++)
|
||||||
|
elements[i] = this._wrapElement(elements[i]);
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementById(id) {
|
||||||
|
const element = this._frame.contentDocument.getElementById(id);
|
||||||
|
if (element === null)
|
||||||
|
return null;
|
||||||
|
return this._wrapElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
call(functionName) {
|
||||||
|
this._frame.contentWindow[functionName]();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
callAsync(functionName) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._frame.contentWindow[functionName]();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
callToGetElement(functionName) {
|
||||||
|
return this._wrapElement(this._frame.contentWindow[functionName]());
|
||||||
|
}
|
||||||
|
|
||||||
|
setWidth(widthPx) {
|
||||||
|
this._frame.style.width = `${widthPx}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wrapElement(element) {
|
||||||
|
return new PageElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(name, listener, options) {
|
||||||
|
this._frame.contentWindow.addEventListener(name, listener, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NATIVE_OPTIONS = {
|
||||||
|
bubbles: true,
|
||||||
|
cancellable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
class PageElement {
|
||||||
|
#node;
|
||||||
|
|
||||||
|
constructor(node) {
|
||||||
|
this.#node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value) {
|
||||||
|
this.#node.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
click() {
|
||||||
|
this.#node.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.#node.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementByMethod(name) {
|
||||||
|
return new PageElement(this.#node[name]());
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollIntoView(options) {
|
||||||
|
this.#node.scrollIntoView(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
|
||||||
|
if (eventName === "submit")
|
||||||
|
// FIXME FireFox doesn't like `new Event('submit')
|
||||||
|
this._dispatchSubmitEvent();
|
||||||
|
else
|
||||||
|
this.#node.dispatchEvent(new eventType(eventName, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
_dispatchSubmitEvent() {
|
||||||
|
const submitEvent = document.createEvent("Event");
|
||||||
|
submitEvent.initEvent("submit", true, true);
|
||||||
|
this.#node.dispatchEvent(submitEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
enter(type, options = undefined) {
|
||||||
|
const ENTER_KEY_CODE = 13;
|
||||||
|
return this.dispatchKeyEvent(type, ENTER_KEY_CODE, "Enter", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchKeyEvent(type, keyCode, key, options) {
|
||||||
|
let eventOptions = { bubbles: true, cancelable: true, keyCode, which: keyCode, key };
|
||||||
|
if (options !== undefined)
|
||||||
|
eventOptions = Object.assign(eventOptions, options);
|
||||||
|
const event = new KeyboardEvent(type, eventOptions);
|
||||||
|
this.#node.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchMouseEvent(type, offsetX, offsetY, options) {
|
||||||
|
const boundingRect = this.#node.getBoundingClientRect();
|
||||||
|
const clientX = offsetX + boundingRect.x;
|
||||||
|
const clientY = offsetY + boundingRect.y;
|
||||||
|
const contentWindow = this.#node.ownerDocument.defaultView;
|
||||||
|
const screenX = clientX + contentWindow.screenX;
|
||||||
|
const screenY = clientY + contentWindow.screenY;
|
||||||
|
let eventOptions = { bubbles: true, cancelable: true, clientX, clientY, screenX, screenY };
|
||||||
|
if (options !== undefined)
|
||||||
|
eventOptions = Object.assign(eventOptions, options);
|
||||||
|
const event = new contentWindow.MouseEvent(type, eventOptions);
|
||||||
|
this.#node.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first element found in a node of a PageElement that matches the specified selector, or group of selectors. If a shadow DOM is present in the node, the shadow DOM is used to query.
|
||||||
|
* If no matches are found, null is returned.
|
||||||
|
*
|
||||||
|
* @param {string} selector A string containing one or more selectors to match.
|
||||||
|
* @param {string[]} [path] An array containing a path to the parent element.
|
||||||
|
* @returns PageElement | null
|
||||||
|
*/
|
||||||
|
querySelectorInShadowRoot(selector, path = []) {
|
||||||
|
const lookupStartNode = this.#node.shadowRoot ?? this.#node;
|
||||||
|
const element = getParent(lookupStartNode, path).querySelector(selector);
|
||||||
|
|
||||||
|
if (element === null)
|
||||||
|
return null;
|
||||||
|
return new PageElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelector(selector) {
|
||||||
|
const element = this.#node.querySelector(selector);
|
||||||
|
|
||||||
|
if (element === null)
|
||||||
|
return null;
|
||||||
|
return new PageElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function geomeanToScore(geomean) {
|
||||||
|
return 1000 / geomean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The WarmupSuite is used to make sure all runner helper functions and
|
||||||
|
// classes are compiled, to avoid unnecessary pauses due to delayed
|
||||||
|
// compilation of runner methods in the middle of the measuring cycle.
|
||||||
|
export const WarmupSuite = {
|
||||||
|
name: "Warmup",
|
||||||
|
url: "warmup/index.html",
|
||||||
|
async prepare(page) {
|
||||||
|
await page.waitForElement("#testItem");
|
||||||
|
},
|
||||||
|
tests: [
|
||||||
|
// Make sure to run ever page.method once at least
|
||||||
|
new BenchmarkTestStep("WarmingUpPageMethods", (page) => {
|
||||||
|
let results = [];
|
||||||
|
results.push(page.querySelector(".testItem"));
|
||||||
|
results.push(page.querySelectorAll(".item"));
|
||||||
|
results.push(page.getElementById("testItem"));
|
||||||
|
}),
|
||||||
|
new BenchmarkTestStep("WarmingUpPageElementMethods", (page) => {
|
||||||
|
const item = page.getElementById("testItem");
|
||||||
|
item.setValue("value");
|
||||||
|
item.click();
|
||||||
|
item.focus();
|
||||||
|
item.dispatchEvent("change");
|
||||||
|
item.enter("keypress");
|
||||||
|
item.dispatchEvent("input");
|
||||||
|
item.enter("keyup");
|
||||||
|
}),
|
||||||
|
new BenchmarkTestStep("WarmingUpPageElementMouseMethods", (page) => {
|
||||||
|
const item = page.getElementById("testItem");
|
||||||
|
const mouseEventOptions = { clientX: 100, clientY: 100, bubbles: true, cancelable: true };
|
||||||
|
const wheelEventOptions = {
|
||||||
|
clientX: 200,
|
||||||
|
clientY: 200,
|
||||||
|
deltaMode: 0,
|
||||||
|
delta: -10,
|
||||||
|
deltaY: -10,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
};
|
||||||
|
item.dispatchEvent("mousedown", mouseEventOptions, MouseEvent);
|
||||||
|
item.dispatchEvent("mousemove", mouseEventOptions, MouseEvent);
|
||||||
|
item.dispatchEvent("mouseup", mouseEventOptions, MouseEvent);
|
||||||
|
item.dispatchEvent("wheel", wheelEventOptions, WheelEvent);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/47593316
|
||||||
|
function seededHashRandomNumberGenerator(a) {
|
||||||
|
return function () {
|
||||||
|
var t = a += 0x6d2b79f5;
|
||||||
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||||
|
return (t ^ (t >>> 14)) >>> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class WakeLock {
|
||||||
|
#wakeLockSentinel = undefined;
|
||||||
|
async request() {
|
||||||
|
if (!navigator.wakeLock)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
this.#wakeLockSentinel = await navigator.wakeLock.request("screen");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`${err.name}, ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async release() {
|
||||||
|
if (!this.#wakeLockSentinel)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
await this.#wakeLockSentinel.release();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`${err.name}, ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
this.#wakeLockSentinel = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BenchmarkRunner {
|
||||||
|
constructor(suites, client) {
|
||||||
|
this._suites = suites;
|
||||||
|
if (params.useWarmupSuite)
|
||||||
|
this._suites = [WarmupSuite, ...suites];
|
||||||
|
this._client = client;
|
||||||
|
this._page = null;
|
||||||
|
this._metrics = null;
|
||||||
|
this._iterationCount = params.iterationCount;
|
||||||
|
if (params.shuffleSeed !== "off")
|
||||||
|
this._suiteOrderRandomNumberGenerator = seededHashRandomNumberGenerator(params.shuffleSeed);
|
||||||
|
this._wakeLock = new WakeLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
async runMultipleIterations(iterationCount) {
|
||||||
|
this._iterationCount = iterationCount;
|
||||||
|
await this._client?.willStartFirstIteration?.(iterationCount);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._runMultipleIterations();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (this._client?.handleError) {
|
||||||
|
await this._client.handleError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._client?.didFinishLastIteration?.(this._metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _runMultipleIterations() {
|
||||||
|
const iterationStartLabel = "iteration-start";
|
||||||
|
const iterationEndLabel = "iteration-end";
|
||||||
|
for (let i = 0; i < this._iterationCount; i++) {
|
||||||
|
performance.mark(iterationStartLabel);
|
||||||
|
await this.runAllSuites();
|
||||||
|
performance.mark(iterationEndLabel);
|
||||||
|
performance.measure(`iteration-${i}`, iterationStartLabel, iterationEndLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeFrame() {
|
||||||
|
if (this._frame) {
|
||||||
|
this._frame.parentNode.removeChild(this._frame);
|
||||||
|
this._frame = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _appendFrame() {
|
||||||
|
const frame = document.createElement("iframe");
|
||||||
|
const style = frame.style;
|
||||||
|
style.width = `${params.viewport.width}px`;
|
||||||
|
style.height = `${params.viewport.height}px`;
|
||||||
|
style.border = "0px none";
|
||||||
|
style.position = "absolute";
|
||||||
|
frame.setAttribute("scrolling", "no");
|
||||||
|
frame.className = "test-runner";
|
||||||
|
style.left = "50%";
|
||||||
|
style.top = "50%";
|
||||||
|
style.transform = "translate(-50%, -50%)";
|
||||||
|
|
||||||
|
await this._client?.willAddTestFrame?.(frame);
|
||||||
|
|
||||||
|
document.body.insertBefore(frame, document.body.firstChild);
|
||||||
|
this._frame = frame;
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareAllSuites() {
|
||||||
|
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
|
||||||
|
await this._wakeLock.request();
|
||||||
|
|
||||||
|
const prepareStartLabel = "runner-prepare-start";
|
||||||
|
const prepareEndLabel = "runner-prepare-end";
|
||||||
|
|
||||||
|
performance.mark(prepareStartLabel);
|
||||||
|
let suites = [...this._suites];
|
||||||
|
if (this._suiteOrderRandomNumberGenerator)
|
||||||
|
this._shuffleSuites(suites);
|
||||||
|
|
||||||
|
performance.mark(prepareEndLabel);
|
||||||
|
performance.measure("runner-prepare", prepareStartLabel, prepareEndLabel);
|
||||||
|
|
||||||
|
return suites;
|
||||||
|
}
|
||||||
|
|
||||||
|
_shuffleSuites(suites) {
|
||||||
|
// We just do a simple Fisher-Yates shuffle based on the repeated hash of the
|
||||||
|
// seed. This is not a high quality RNG, but it's plenty good enough.
|
||||||
|
for (let i = 0; i < suites.length - 1; i++) {
|
||||||
|
const j = i + (this._suiteOrderRandomNumberGenerator() % (suites.length - i));
|
||||||
|
const tmp = suites[i];
|
||||||
|
suites[i] = suites[j];
|
||||||
|
suites[j] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAllSuites() {
|
||||||
|
const suites = await this._prepareAllSuites();
|
||||||
|
try {
|
||||||
|
for (const suite of suites) {
|
||||||
|
if (!suite.enabled)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
await this._appendFrame();
|
||||||
|
this._page = new Page(this._frame);
|
||||||
|
await this.runSuite(suite);
|
||||||
|
} finally {
|
||||||
|
this._removeFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await this._finishRunAllSuites();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _finishRunAllSuites() {
|
||||||
|
const finalizeStartLabel = "runner-finalize-start";
|
||||||
|
const finalizeEndLabel = "runner-finalize-end";
|
||||||
|
|
||||||
|
performance.mark(finalizeStartLabel);
|
||||||
|
await this._finalize();
|
||||||
|
performance.mark(finalizeEndLabel);
|
||||||
|
performance.measure("runner-finalize", finalizeStartLabel, finalizeEndLabel);
|
||||||
|
await this._wakeLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
async runSuite(suite) {
|
||||||
|
// FIXME: Encapsulate more state in the SuiteRunner.
|
||||||
|
// FIXME: Return and use measured values from SuiteRunner.
|
||||||
|
const type = suite.type ?? ((params.useAsyncSteps && "async") || "default");
|
||||||
|
const suiteRunnerClass = SUITE_RUNNER_LOOKUP[type];
|
||||||
|
const suiteRunner = new suiteRunnerClass(this._frame, this._page, params, suite, this._client, this._measuredValues);
|
||||||
|
await suiteRunner.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _finalize() {
|
||||||
|
this._appendIterationMetrics();
|
||||||
|
if (this._client?.didRunSuites) {
|
||||||
|
let product = 1;
|
||||||
|
const values = [];
|
||||||
|
for (const suiteName in this._measuredValues.tests) {
|
||||||
|
const suiteTotal = this._measuredValues.tests[suiteName].total;
|
||||||
|
product *= suiteTotal;
|
||||||
|
values.push(suiteTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
values.sort((a, b) => a - b); // Avoid the loss of significance for the sum.
|
||||||
|
const total = values.reduce((a, b) => a + b);
|
||||||
|
const geomean = Math.pow(product, 1 / values.length);
|
||||||
|
|
||||||
|
this._measuredValues.total = total;
|
||||||
|
this._measuredValues.mean = total / values.length;
|
||||||
|
this._measuredValues.geomean = geomean;
|
||||||
|
this._measuredValues.score = geomeanToScore(geomean);
|
||||||
|
await this._client.didRunSuites(this._measuredValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_appendIterationMetrics() {
|
||||||
|
const getMetric = (name, unit = "ms") => this._metrics[name] || (this._metrics[name] = new Metric(name, unit));
|
||||||
|
const iterationMetric = (i, name) => {
|
||||||
|
if (i >= params.iterationCount)
|
||||||
|
throw new Error(`Requested iteration=${i} does not exist.`);
|
||||||
|
return getMetric(`Iteration-${i}-${name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectSubMetrics = (prefix, items, parent) => {
|
||||||
|
for (let name in items) {
|
||||||
|
const results = items[name];
|
||||||
|
const metric = getMetric(prefix + name);
|
||||||
|
metric.add(results.total ?? results);
|
||||||
|
if (metric.parent !== parent)
|
||||||
|
parent.addChild(metric);
|
||||||
|
if (results.tests)
|
||||||
|
collectSubMetrics(`${metric.name}${Metric.separator}`, results.tests, metric);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const initializeMetrics = this._metrics === null;
|
||||||
|
if (initializeMetrics)
|
||||||
|
this._metrics = { __proto__: null };
|
||||||
|
|
||||||
|
const iterationResults = this._measuredValues.tests;
|
||||||
|
collectSubMetrics("", iterationResults);
|
||||||
|
|
||||||
|
if (initializeMetrics) {
|
||||||
|
// Prepare all iteration metrics so they are listed at the end of
|
||||||
|
// of the _metrics object, before "Total" and "Score".
|
||||||
|
for (let i = 0; i < this._iterationCount; i++)
|
||||||
|
iterationMetric(i, "Total").description = `Test totals for iteration ${i}`;
|
||||||
|
getMetric("Geomean", "ms").description = "Geomean of test totals";
|
||||||
|
getMetric("Score", "score").description = "Scaled inverse of the Geomean";
|
||||||
|
if (params.measurePrepare)
|
||||||
|
getMetric("Prepare", "ms").description = "Geomean of workload prepare times";
|
||||||
|
}
|
||||||
|
|
||||||
|
const geomean = getMetric("Geomean");
|
||||||
|
const iteration = geomean.length;
|
||||||
|
const iterationTotal = iterationMetric(iteration, "Total");
|
||||||
|
for (const results of Object.values(iterationResults))
|
||||||
|
iterationTotal.add(results.total);
|
||||||
|
iterationTotal.computeAggregatedMetrics();
|
||||||
|
geomean.add(iterationTotal.geomean);
|
||||||
|
getMetric("Score").add(geomeanToScore(iterationTotal.geomean));
|
||||||
|
|
||||||
|
if (params.measurePrepare) {
|
||||||
|
const iterationPrepare = iterationMetric(iteration, "Prepare");
|
||||||
|
for (const results of Object.values(iterationResults))
|
||||||
|
iterationPrepare.add(results.prepare);
|
||||||
|
iterationPrepare.computeAggregatedMetrics();
|
||||||
|
const prepare = getMetric("Prepare");
|
||||||
|
prepare.add(iterationPrepare.geomean);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const metric of Object.values(this._metrics))
|
||||||
|
metric.computeAggregatedMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initializeMetrics() {}
|
||||||
|
}
|
||||||
13
resources/config.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"suites": [
|
||||||
|
{
|
||||||
|
"name": "NewsSite-PostMessage",
|
||||||
|
"url": "resources/newssite/news-next/dist/index.html",
|
||||||
|
"tags": ["newssite", "language"],
|
||||||
|
"type": "remote",
|
||||||
|
"config": {
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
353
resources/developer-mode.mjs
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
import { params, LAYOUT_MODES, defaultParams } from "./shared/params.mjs";
|
||||||
|
import { benchmarkConfigurator } from "./benchmark-configurator.mjs";
|
||||||
|
import { handleComplexityChange } from "./shared/todomvc-utils.mjs";
|
||||||
|
|
||||||
|
const { suites, tags } = benchmarkConfigurator;
|
||||||
|
|
||||||
|
export function createDeveloperModeContainer() {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.className = "developer-mode";
|
||||||
|
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
summary.textContent = "Developer Mode";
|
||||||
|
details.append(summary);
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.className = "developer-mode-content";
|
||||||
|
|
||||||
|
content.append(createUIForSuites());
|
||||||
|
|
||||||
|
const settings = document.createElement("div");
|
||||||
|
settings.className = "settings";
|
||||||
|
settings.append(createUIForIterationCount());
|
||||||
|
settings.append(createUIForMeasurePrepare());
|
||||||
|
settings.append(createUIForWarmupSuite());
|
||||||
|
settings.append(createUIForWarmupBeforeSync());
|
||||||
|
settings.append(createUIForWaitAfterSetup());
|
||||||
|
settings.append(createUIForSyncStepDelay());
|
||||||
|
settings.append(createUIForWaitAfterSuite());
|
||||||
|
settings.append(createUIForAsyncSteps());
|
||||||
|
settings.append(createUIForLayoutMode());
|
||||||
|
settings.append(createUIForComplexity());
|
||||||
|
|
||||||
|
content.append(document.createElement("hr"));
|
||||||
|
content.append(settings);
|
||||||
|
|
||||||
|
content.append(document.createElement("hr"));
|
||||||
|
content.append(createUIForRun());
|
||||||
|
|
||||||
|
details.append(content);
|
||||||
|
container.append(details);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function span(text) {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.textContent = text;
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForWarmupSuite() {
|
||||||
|
return createCheckboxUI("Use Warmup Suite", params.useWarmupSuite, (isChecked) => {
|
||||||
|
params.useWarmupSuite = isChecked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForMeasurePrepare() {
|
||||||
|
return createCheckboxUI("Measure Prepare", params.measurePrepare, (isChecked) => {
|
||||||
|
params.measurePrepare = isChecked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForAsyncSteps() {
|
||||||
|
return createCheckboxUI("Use Async Steps", params.useAsyncSteps, (isChecked) => {
|
||||||
|
params.useAsyncSteps = isChecked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCheckboxUI(labelValue, initialValue, paramsUpdateCallback) {
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.checked = !!initialValue;
|
||||||
|
checkbox.onchange = () => {
|
||||||
|
paramsUpdateCallback(checkbox.checked);
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.append(checkbox, span(labelValue));
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForIterationCount() {
|
||||||
|
return createLinearRangeUI("Iterations: ", "iterationCount", "#", 1, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForWarmupBeforeSync() {
|
||||||
|
return createLinearRangeUI("Warmup time: ", "warmupBeforeSync");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForSyncStepDelay() {
|
||||||
|
return createLinearRangeUI("Sync step delay: ", "waitBeforeSync");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForWaitAfterSetup() {
|
||||||
|
return createLinearRangeUI("Post setup delay: ", "waitAfterSetup");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForWaitAfterSuite() {
|
||||||
|
return createLinearRangeUI("Post suite delay: ", "waitAfterSuite");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForComplexity() {
|
||||||
|
return createExpRangeUI("Relative complexity: ", "complexity", "x", 0.01, 100, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLinearRangeUI(labelText, paramKey, unit = "ms", min = 0, max = 1000, step = 1) {
|
||||||
|
const initialValue = params[paramKey];
|
||||||
|
const linearMap = (value) => value;
|
||||||
|
const { range, label } = createTimeRangeUI(labelText, paramKey, unit, linearMap, 0);
|
||||||
|
range.min = min;
|
||||||
|
range.max = max;
|
||||||
|
range.step = step;
|
||||||
|
range.value = initialValue;
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExpRangeUI(labelText, paramKey, unit = "ms", min = 0, max = 1000, step = 1) {
|
||||||
|
const defaultValue = defaultParams[paramKey];
|
||||||
|
const initialValue = params[paramKey];
|
||||||
|
const b = defaultValue - 1;
|
||||||
|
const a = -Math.log(min - b);
|
||||||
|
const logMap = (value) => Math.round((Math.exp(value * a) + b) / step) * step;
|
||||||
|
const { range, label } = createTimeRangeUI(labelText, paramKey, unit, logMap, 2);
|
||||||
|
range.min = -1;
|
||||||
|
range.max = Math.log(max - b) / a;
|
||||||
|
range.step = 0.01;
|
||||||
|
range.value = Math.log(initialValue - b) / a;
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTimeRangeUI(labelText, paramKey, unit = "ms", map, decimals) {
|
||||||
|
const range = document.createElement("input");
|
||||||
|
range.type = "range";
|
||||||
|
|
||||||
|
const rangeValueAndUnit = document.createElement("span");
|
||||||
|
rangeValueAndUnit.className = "range-label-data";
|
||||||
|
|
||||||
|
const rangeValue = document.createElement("span");
|
||||||
|
rangeValue.textContent = params[paramKey];
|
||||||
|
rangeValueAndUnit.append(rangeValue, " ", unit);
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.append(span(labelText), range, rangeValueAndUnit);
|
||||||
|
|
||||||
|
range.oninput = () => {
|
||||||
|
rangeValue.textContent = map(Number(range.value)).toFixed(decimals);
|
||||||
|
};
|
||||||
|
range.onchange = () => {
|
||||||
|
params[paramKey] = map(Number(range.value));
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { range, label };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForLayoutMode() {
|
||||||
|
return createSelectUI("Force layout mode", params.layoutMode, LAYOUT_MODES, (value) => {
|
||||||
|
params.layoutMode = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSelectUI(labelValue, initialValue, choices, paramsUpdateCallback) {
|
||||||
|
const select = document.createElement("select");
|
||||||
|
select.onchange = () => {
|
||||||
|
paramsUpdateCallback(select.value);
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
choices.forEach((choice) => {
|
||||||
|
const option = new Option(choice, choice);
|
||||||
|
select.add(option);
|
||||||
|
});
|
||||||
|
select.value = initialValue;
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.append(span(labelValue), select);
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForSuites() {
|
||||||
|
const control = document.createElement("nav");
|
||||||
|
control.className = "suites";
|
||||||
|
const checkboxes = [];
|
||||||
|
const setSuiteEnabled = (suiteIndex, enabled) => {
|
||||||
|
suites[suiteIndex].enabled = enabled;
|
||||||
|
checkboxes[suiteIndex].checked = enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
control.appendChild(createSuitesGlobalSelectButtons(setSuiteEnabled));
|
||||||
|
|
||||||
|
const ol = document.createElement("ol");
|
||||||
|
for (const suite of suites) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.id = suite.name;
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.checked = suite.enabled;
|
||||||
|
checkbox.onchange = () => {
|
||||||
|
suite.enabled = checkbox.checked;
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
checkboxes.push(checkbox);
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.append(checkbox, " ", suite.name);
|
||||||
|
li.appendChild(label);
|
||||||
|
label.onclick = (event) => {
|
||||||
|
if (event?.ctrlKey || event?.metaKey) {
|
||||||
|
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||||
|
if (suites[suiteIndex] !== suite)
|
||||||
|
setSuiteEnabled(suiteIndex, false);
|
||||||
|
else
|
||||||
|
setSuiteEnabled(suiteIndex, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = suite.url;
|
||||||
|
link.target = "_blank";
|
||||||
|
link.title = "Raw workload URL";
|
||||||
|
link.className = "workload-url-link";
|
||||||
|
link.textContent = "\u{1F517}";
|
||||||
|
li.appendChild(link);
|
||||||
|
|
||||||
|
ol.appendChild(li);
|
||||||
|
}
|
||||||
|
control.appendChild(ol);
|
||||||
|
control.appendChild(createSuitesTagsButton(setSuiteEnabled));
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSuitesGlobalSelectButtons(setSuiteEnabled) {
|
||||||
|
const buttons = document.createElement("div");
|
||||||
|
buttons.className = "button-bar";
|
||||||
|
|
||||||
|
let button = document.createElement("button");
|
||||||
|
button.className = "select-all";
|
||||||
|
button.textContent = "Select all";
|
||||||
|
button.onclick = () => {
|
||||||
|
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++)
|
||||||
|
setSuiteEnabled(suiteIndex, true);
|
||||||
|
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
buttons.appendChild(button);
|
||||||
|
|
||||||
|
button = document.createElement("button");
|
||||||
|
button.textContent = "Unselect all";
|
||||||
|
button.className = "unselect-all";
|
||||||
|
button.onclick = () => {
|
||||||
|
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++)
|
||||||
|
setSuiteEnabled(suiteIndex, false);
|
||||||
|
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
buttons.appendChild(button);
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSuitesTagsButton(setSuiteEnabled) {
|
||||||
|
let container = document.createElement("div");
|
||||||
|
let buttons = container.appendChild(document.createElement("div"));
|
||||||
|
buttons.className = "button-bar";
|
||||||
|
let i = 0;
|
||||||
|
const kTagsPerLine = 3;
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (tag === "all")
|
||||||
|
continue;
|
||||||
|
if (!(i % kTagsPerLine)) {
|
||||||
|
buttons = container.appendChild(document.createElement("div"));
|
||||||
|
buttons.className = "button-bar";
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = "tag";
|
||||||
|
button.textContent = `#${tag}`;
|
||||||
|
button.dataTag = tag;
|
||||||
|
button.onclick = (event) => {
|
||||||
|
const extendSelection = event?.shiftKey;
|
||||||
|
const invertSelection = event?.ctrlKey || event?.metaKey;
|
||||||
|
const selectedTag = event.target.dataTag;
|
||||||
|
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||||
|
let enabled = suites[suiteIndex].tags.includes(selectedTag);
|
||||||
|
if (invertSelection)
|
||||||
|
enabled = !enabled;
|
||||||
|
if (extendSelection && !enabled)
|
||||||
|
continue;
|
||||||
|
setSuiteEnabled(suiteIndex, enabled);
|
||||||
|
}
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
buttons.appendChild(button);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUIForRun() {
|
||||||
|
const stepTestButton = document.createElement("button");
|
||||||
|
stepTestButton.className = "step-button";
|
||||||
|
stepTestButton.innerHTML = "Step Test<span>\u23EF</span>";
|
||||||
|
stepTestButton.onclick = (event) => {
|
||||||
|
globalThis.benchmarkClient.step();
|
||||||
|
};
|
||||||
|
const startTestButton = document.createElement("button");
|
||||||
|
startTestButton.innerHTML = "Start Test<span>\u23F5</span>";
|
||||||
|
startTestButton.onclick = (event) => {
|
||||||
|
globalThis.benchmarkClient.start();
|
||||||
|
};
|
||||||
|
const buttons = document.createElement("div");
|
||||||
|
buttons.className = "button-bar";
|
||||||
|
buttons.appendChild(stepTestButton);
|
||||||
|
buttons.appendChild(startTestButton);
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateParamsSuitesAndTags() {
|
||||||
|
params.suites = [];
|
||||||
|
params.tags = [];
|
||||||
|
|
||||||
|
// If less than all suites are selected then change the URL "Suites" GET parameter
|
||||||
|
// to comma separate only the selected
|
||||||
|
const selectedSuites = suites.filter((suite) => suite.enabled);
|
||||||
|
if (!selectedSuites.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Try finding common tags that would result in the current suite selection.
|
||||||
|
let commonTags = new Set(selectedSuites[0].tags);
|
||||||
|
for (const suite of suites) {
|
||||||
|
if (suite.enabled)
|
||||||
|
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
|
||||||
|
else
|
||||||
|
suite.tags.forEach((tag) => commonTags.delete(tag));
|
||||||
|
}
|
||||||
|
if (selectedSuites.length > 1 && commonTags.size)
|
||||||
|
params.tags = [...commonTags];
|
||||||
|
else
|
||||||
|
params.suites = selectedSuites.map((suite) => suite.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleParamsChange() {
|
||||||
|
updateParamsSuitesAndTags();
|
||||||
|
handleComplexityChange();
|
||||||
|
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.search = params.toSearchParams();
|
||||||
|
// Only push state if changed
|
||||||
|
if (url.href !== window.location.href)
|
||||||
|
window.history.pushState({}, "", url);
|
||||||
|
}
|
||||||
BIN
resources/favicon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/gauge.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
resources/gauge@2x.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
resources/logo.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
resources/logo@2x.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
1003
resources/main.css
Normal file
547
resources/main.mjs
Normal file
|
|
@ -0,0 +1,547 @@
|
||||||
|
import { BenchmarkRunner } from "./benchmark-runner.mjs";
|
||||||
|
import * as Statistics from "./statistics.mjs";
|
||||||
|
import { renderMetricView } from "./metric-ui.mjs";
|
||||||
|
import { defaultParams, params } from "./shared/params.mjs";
|
||||||
|
import { createDeveloperModeContainer } from "./developer-mode.mjs";
|
||||||
|
|
||||||
|
const BENCHMARK_STATE = Object.freeze({
|
||||||
|
IDLE: "IDLE",
|
||||||
|
READY: "READY",
|
||||||
|
RUNNING: "RUNNING",
|
||||||
|
DONE: "DONE",
|
||||||
|
ERROR: "ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME(camillobruni): Add base class
|
||||||
|
class MainBenchmarkClient {
|
||||||
|
developerMode = false;
|
||||||
|
stepCount = null;
|
||||||
|
suitesCount = null;
|
||||||
|
_measuredValuesList = [];
|
||||||
|
_finishedTestCount = 0;
|
||||||
|
_progressCompleted = null;
|
||||||
|
_state = BENCHMARK_STATE.IDLE;
|
||||||
|
_developerModeContainer = null;
|
||||||
|
_metrics = Object.create(null);
|
||||||
|
_steppingPromise = null;
|
||||||
|
_steppingResolver = null;
|
||||||
|
_benchmarkConfiguratorPromise = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._benchmarkConfiguratorPromise = import("./benchmark-configurator.mjs");
|
||||||
|
this.prepareUI();
|
||||||
|
this.evaluateParams();
|
||||||
|
this._showSection(window.location.hash);
|
||||||
|
|
||||||
|
this._benchmarkConfiguratorPromise.then(() => {
|
||||||
|
window.dispatchEvent(new Event("SpeedometerReady"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning() {
|
||||||
|
return this._state === BENCHMARK_STATE.RUNNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFinished() {
|
||||||
|
return this._state === BENCHMARK_STATE.DONE || this._state === BENCHMARK_STATE.ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (this._isStepping())
|
||||||
|
this._clearStepping();
|
||||||
|
else if (await this._startBenchmark())
|
||||||
|
this._showSection("#running");
|
||||||
|
}
|
||||||
|
|
||||||
|
async step() {
|
||||||
|
const currentSteppingResolver = this._steppingResolver;
|
||||||
|
this._steppingPromise = new Promise((resolve) => {
|
||||||
|
this._steppingResolver = resolve;
|
||||||
|
});
|
||||||
|
currentSteppingResolver?.();
|
||||||
|
if (!this.isRunning()) {
|
||||||
|
await this._startBenchmark();
|
||||||
|
this._showSection("#running");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearStepping() {
|
||||||
|
const currentSteppingResolver = this._steppingResolver;
|
||||||
|
this._steppingPromise = null;
|
||||||
|
this._steppingResolver = null;
|
||||||
|
currentSteppingResolver?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _awaitNextStep(suite, test) {
|
||||||
|
console.log(`Next Step: ${suite.name} ${test.name}`, { suite, test });
|
||||||
|
await this._steppingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isStepping() {
|
||||||
|
return this._steppingResolver !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _startBenchmark() {
|
||||||
|
if (this.isRunning())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise;
|
||||||
|
|
||||||
|
const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled);
|
||||||
|
const totalSuitesCount = enabledSuites.length;
|
||||||
|
|
||||||
|
if (totalSuitesCount === 0) {
|
||||||
|
const message = `No suites selected - "${params.suites}" does not exist.`;
|
||||||
|
alert(message);
|
||||||
|
console.error(
|
||||||
|
message,
|
||||||
|
params.suites,
|
||||||
|
"\nValid values:",
|
||||||
|
benchmarkConfigurator.suites.map((each) => each.name)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this._isStepping())
|
||||||
|
this._developerModeContainer?.remove();
|
||||||
|
this._progressCompleted = document.getElementById("progress-completed");
|
||||||
|
if (params.iterationCount < 50) {
|
||||||
|
const progressNode = document.getElementById("progress");
|
||||||
|
for (let i = 1; i < params.iterationCount; i++) {
|
||||||
|
const iterationMarker = progressNode.appendChild(document.createElement("div"));
|
||||||
|
iterationMarker.className = "iteration-marker";
|
||||||
|
iterationMarker.style.left = `${(i / params.iterationCount) * 100}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._metrics = Object.create(null);
|
||||||
|
|
||||||
|
this.stepCount = params.iterationCount * totalSuitesCount;
|
||||||
|
this._progressCompleted.max = this.stepCount;
|
||||||
|
this.suitesCount = enabledSuites.length;
|
||||||
|
this._setBenchmarkState(BENCHMARK_STATE.RUNNING);
|
||||||
|
const runner = new BenchmarkRunner(benchmarkConfigurator.suites, this);
|
||||||
|
runner.runMultipleIterations(params.iterationCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get metrics() {
|
||||||
|
return this._metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
_assertNotAborted() {
|
||||||
|
if (this._state === BENCHMARK_STATE.ERROR)
|
||||||
|
throw new Error("Benchmark aborted by another process.");
|
||||||
|
}
|
||||||
|
|
||||||
|
willAddTestFrame(frame) {
|
||||||
|
this._assertNotAborted();
|
||||||
|
frame.style.left = "50%";
|
||||||
|
frame.style.top = "50%";
|
||||||
|
frame.style.transform = "translate(-50%, -50%)";
|
||||||
|
}
|
||||||
|
|
||||||
|
async willRunTest(suite, test) {
|
||||||
|
this._assertNotAborted();
|
||||||
|
document.getElementById("info-label").textContent = suite.name;
|
||||||
|
document.getElementById("info-progress").textContent = `${this._finishedTestCount} / ${this.stepCount}`;
|
||||||
|
if (this._steppingPromise)
|
||||||
|
await this._awaitNextStep(suite, test);
|
||||||
|
}
|
||||||
|
|
||||||
|
async didFinishSuite(suite) {
|
||||||
|
this._finishedTestCount++;
|
||||||
|
this._progressCompleted.value = this._finishedTestCount;
|
||||||
|
if (this._steppingPromise)
|
||||||
|
await this._awaitNextStep(suite, { name: "Finished" });
|
||||||
|
}
|
||||||
|
|
||||||
|
didRunSuites(measuredValues) {
|
||||||
|
this._measuredValuesList.push(measuredValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
willStartFirstIteration() {
|
||||||
|
this._measuredValuesList = [];
|
||||||
|
this._finishedTestCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
didFinishLastIteration(metrics) {
|
||||||
|
console.assert(this.isRunning());
|
||||||
|
|
||||||
|
this._metrics = metrics;
|
||||||
|
this._setBenchmarkState(BENCHMARK_STATE.DONE);
|
||||||
|
|
||||||
|
const scoreResults = this._computeResults(this._measuredValuesList, "score");
|
||||||
|
if (scoreResults.isValid)
|
||||||
|
this._populateValidScore(scoreResults);
|
||||||
|
else
|
||||||
|
this._populateInvalidScore();
|
||||||
|
|
||||||
|
this._populateDetailedResults(metrics);
|
||||||
|
if (params.developerMode)
|
||||||
|
this.showResultsDetails();
|
||||||
|
else
|
||||||
|
this.showResultsSummary();
|
||||||
|
globalThis.dispatchEvent(new Event("SpeedometerDone"));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(error) {
|
||||||
|
if (this._state === BENCHMARK_STATE.ERROR)
|
||||||
|
return;
|
||||||
|
this._metrics = Object.create(null);
|
||||||
|
this._setBenchmarkState(BENCHMARK_STATE.ERROR);
|
||||||
|
this._populateErrorMessage(error.message);
|
||||||
|
this.showResultsSummary();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateValidScore(scoreResults) {
|
||||||
|
document.getElementById("summary").className = "valid";
|
||||||
|
|
||||||
|
this._updateGaugeNeedle(scoreResults.mean);
|
||||||
|
document.getElementById("result-number").textContent = scoreResults.formattedMean;
|
||||||
|
if (scoreResults.formattedDelta)
|
||||||
|
document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateInvalidScore() {
|
||||||
|
this._populateErrorMessage(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateErrorMessage(errorText) {
|
||||||
|
document.getElementById("summary").className = "invalid";
|
||||||
|
document.getElementById("result-number").textContent = "Error";
|
||||||
|
document.getElementById("confidence-number").textContent = "";
|
||||||
|
if (errorText === undefined)
|
||||||
|
return;
|
||||||
|
const errorMessage = document.getElementById("invalid-score-text");
|
||||||
|
errorMessage.textContent = errorText;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeResults(measuredValuesList, displayUnit) {
|
||||||
|
function valueForUnit(measuredValues) {
|
||||||
|
if (displayUnit === "ms")
|
||||||
|
return measuredValues.geomean;
|
||||||
|
return measuredValues.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sigFigFromPercentDelta(percentDelta) {
|
||||||
|
return Math.ceil(-Math.log(percentDelta) / Math.log(10)) + 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSigFigPrecision(number, sigFig) {
|
||||||
|
const nonDecimalDigitCount = number < 1 ? 0 : Math.floor(Math.log(number) / Math.log(10)) + 1;
|
||||||
|
return number.toPrecision(Math.max(nonDecimalDigitCount, Math.min(6, sigFig)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = measuredValuesList.map(valueForUnit);
|
||||||
|
const sum = values.reduce((a, b) => a + b, 0);
|
||||||
|
const arithmeticMean = sum / values.length;
|
||||||
|
let meanSigFig = 4;
|
||||||
|
let formattedDelta;
|
||||||
|
let formattedPercentDelta;
|
||||||
|
const delta = Statistics.confidenceIntervalDelta(0.95, values.length, sum, Statistics.squareSum(values));
|
||||||
|
if (!isNaN(delta)) {
|
||||||
|
const percentDelta = (delta * 100) / arithmeticMean;
|
||||||
|
meanSigFig = sigFigFromPercentDelta(percentDelta);
|
||||||
|
formattedDelta = toSigFigPrecision(delta, 2);
|
||||||
|
formattedPercentDelta = `${toSigFigPrecision(percentDelta, 2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedMean = toSigFigPrecision(arithmeticMean, Math.max(meanSigFig, 3));
|
||||||
|
|
||||||
|
return {
|
||||||
|
formattedValues: values.map((value) => {
|
||||||
|
return `${toSigFigPrecision(value, 4)} ${displayUnit}`;
|
||||||
|
}),
|
||||||
|
mean: arithmeticMean,
|
||||||
|
formattedMean: formattedMean,
|
||||||
|
formattedDelta: formattedDelta,
|
||||||
|
formattedMeanAndDelta: formattedMean + (formattedDelta ? ` \xb1 ${formattedDelta} (${formattedPercentDelta})` : ""),
|
||||||
|
isValid: values.length > 0 && isFinite(sum) && sum > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_addDetailedResultsRow(table, iterationNumber, value) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
const th = document.createElement("th");
|
||||||
|
th.textContent = `Iteration ${iterationNumber + 1}`;
|
||||||
|
const td = document.createElement("td");
|
||||||
|
td.textContent = value;
|
||||||
|
row.appendChild(th);
|
||||||
|
row.appendChild(td);
|
||||||
|
table.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateGaugeNeedle(score) {
|
||||||
|
const needleAngle = Math.max(0, Math.min(score, 140)) - 70;
|
||||||
|
const needleRotationValue = `rotate(${needleAngle}deg)`;
|
||||||
|
|
||||||
|
const gaugeNeedleElement = document.querySelector("#summary > .gauge .needle");
|
||||||
|
gaugeNeedleElement.style.setProperty("-webkit-transform", needleRotationValue);
|
||||||
|
gaugeNeedleElement.style.setProperty("-moz-transform", needleRotationValue);
|
||||||
|
gaugeNeedleElement.style.setProperty("-ms-transform", needleRotationValue);
|
||||||
|
gaugeNeedleElement.style.setProperty("transform", needleRotationValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateDetailedResults(metrics) {
|
||||||
|
this._populateNonStandardParams();
|
||||||
|
const trackHeight = 24;
|
||||||
|
document.documentElement.style.setProperty("--metrics-line-height", `${trackHeight}px`);
|
||||||
|
const plotWidth = (params.viewport.width - 120) / 2;
|
||||||
|
const aggregateMetrics = [metrics.Geomean];
|
||||||
|
if (params.measurePrepare)
|
||||||
|
aggregateMetrics.push(metrics.Prepare);
|
||||||
|
document.getElementById("aggregate-chart").innerHTML = renderMetricView({
|
||||||
|
metrics: aggregateMetrics,
|
||||||
|
width: plotWidth,
|
||||||
|
trackHeight,
|
||||||
|
renderChildren: false,
|
||||||
|
colors: ["white"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const toplevelMetrics = Object.values(metrics).filter((each) => !each.parent && each.children.length > 0);
|
||||||
|
document.getElementById("tests-chart").innerHTML = renderMetricView({
|
||||||
|
metrics: toplevelMetrics,
|
||||||
|
width: plotWidth,
|
||||||
|
trackHeight,
|
||||||
|
renderChildren: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
for (const metric of toplevelMetrics) {
|
||||||
|
html += renderMetricView({
|
||||||
|
metrics: metric.children,
|
||||||
|
width: plotWidth,
|
||||||
|
trackHeight,
|
||||||
|
title: metric.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById("metrics-results").innerHTML = html;
|
||||||
|
|
||||||
|
const filePrefix = `speedometer-3-${new Date().toISOString()}`;
|
||||||
|
let jsonData = this._formattedJSONResult({ modern: false });
|
||||||
|
let jsonLink = document.getElementById("download-classic-json");
|
||||||
|
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
|
||||||
|
jsonLink.setAttribute("download", `${filePrefix}.json`);
|
||||||
|
|
||||||
|
jsonLink = document.getElementById("download-full-json");
|
||||||
|
jsonData = this._formattedJSONResult({ modern: true });
|
||||||
|
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
|
||||||
|
jsonLink.setAttribute("download", `${filePrefix}.json`);
|
||||||
|
|
||||||
|
const csvData = this._formattedCSVResult();
|
||||||
|
const csvLink = document.getElementById("download-csv");
|
||||||
|
csvLink.href = URL.createObjectURL(new Blob([csvData], { type: "text/csv" }));
|
||||||
|
csvLink.setAttribute("download", `${filePrefix}.csv`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateNonStandardParams() {
|
||||||
|
if (params.isDefault())
|
||||||
|
return;
|
||||||
|
const paramsDiff = [];
|
||||||
|
const usedSearchparams = params.toSearchParamsObject();
|
||||||
|
const defaultSearchParams = defaultParams.toCompleteSearchParamsObject(false);
|
||||||
|
for (const [key, value] of usedSearchparams.entries()) {
|
||||||
|
if (key === "developerMode")
|
||||||
|
continue;
|
||||||
|
const defaultValue = defaultSearchParams.get(key);
|
||||||
|
if (value !== defaultValue)
|
||||||
|
paramsDiff.push({ key, value, defaultValue });
|
||||||
|
}
|
||||||
|
if (paramsDiff.length === 0)
|
||||||
|
return;
|
||||||
|
const body = document.createElement("tbody");
|
||||||
|
for (const { key, value, defaultValue } of paramsDiff) {
|
||||||
|
const row = body.insertRow();
|
||||||
|
row.insertCell().textContent = key;
|
||||||
|
row.insertCell().textContent = value;
|
||||||
|
row.insertCell().textContent = defaultValue;
|
||||||
|
}
|
||||||
|
const table = document.getElementById("non-standard-params-table");
|
||||||
|
table.replaceChild(body, table.tBodies[0]);
|
||||||
|
document.querySelector(".non-standard-params").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareUI() {
|
||||||
|
window.addEventListener("hashchange", this._hashChangeHandler.bind(this));
|
||||||
|
window.addEventListener("resize", this._resizeScreeHandler.bind(this));
|
||||||
|
this._resizeScreeHandler();
|
||||||
|
|
||||||
|
document.querySelectorAll("logo").forEach((button) => {
|
||||||
|
button.onclick = this._logoClickHandler.bind(this);
|
||||||
|
});
|
||||||
|
document.getElementById("copy-full-json").onclick = this.copyJsonResults.bind(this);
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluateParams() {
|
||||||
|
const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise;
|
||||||
|
|
||||||
|
if (params.suites.length > 0 || params.tags.length > 0)
|
||||||
|
benchmarkConfigurator.enableSuites(params.suites, params.tags);
|
||||||
|
|
||||||
|
if (params.developerMode) {
|
||||||
|
this._developerModeContainer = createDeveloperModeContainer();
|
||||||
|
document.body.append(this._developerModeContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.startAutomatically)
|
||||||
|
this.start();
|
||||||
|
else
|
||||||
|
this._setBenchmarkState(BENCHMARK_STATE.READY);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _setBenchmarkState(state) {
|
||||||
|
this._state = state;
|
||||||
|
document.body.setAttribute("data-benchmark-state", state);
|
||||||
|
const startButtons = document.querySelectorAll(".start-tests-button");
|
||||||
|
if (state !== BENCHMARK_STATE.RUNNING) {
|
||||||
|
startButtons.forEach((btn) => {
|
||||||
|
btn.innerHTML = "<div>Start Test</div>";
|
||||||
|
});
|
||||||
|
if (state === BENCHMARK_STATE.READY) {
|
||||||
|
startButtons.forEach((btn) => {
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hashChangeHandler() {
|
||||||
|
this._showSection(window.location.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
_resizeScreeHandler() {
|
||||||
|
// FIXME: Detect when the window size changes during the test.
|
||||||
|
const mainSize = document.querySelector("main").getBoundingClientRect();
|
||||||
|
const screenIsTooSmall = window.innerWidth < mainSize.width || window.innerHeight < mainSize.height;
|
||||||
|
document.getElementById("min-screen-width").textContent = `${params.viewport.width + 50}px`;
|
||||||
|
document.getElementById("min-screen-height").textContent = `${params.viewport.height + 50}px`;
|
||||||
|
document.getElementById("screen-size").textContent = `${window.innerWidth}px by ${window.innerHeight}px`;
|
||||||
|
document.getElementById("screen-size-warning").style.display = screenIsTooSmall ? null : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
_startBenchmarkHandler() {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logoClickHandler(event) {
|
||||||
|
// Prevent any accidental UI changes during benchmark runs.
|
||||||
|
if (!this.isRunning())
|
||||||
|
this._showSection("#home");
|
||||||
|
event.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showResultsSummary() {
|
||||||
|
this._showSection("#summary");
|
||||||
|
}
|
||||||
|
|
||||||
|
showResultsDetails() {
|
||||||
|
this._showSection("#details");
|
||||||
|
}
|
||||||
|
|
||||||
|
_formattedJSONResult({ modern = false }) {
|
||||||
|
const indent = " ";
|
||||||
|
if (modern)
|
||||||
|
return JSON.stringify(this._metrics, undefined, indent);
|
||||||
|
return JSON.stringify(this._measuredValuesList, undefined, indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_formattedCSVResult() {
|
||||||
|
// The CSV format is similar to the details view table. Each measurement is a row with
|
||||||
|
// the name and N columns with the measurement for each iteration:
|
||||||
|
// ```
|
||||||
|
// Measurement,#1,...,#N
|
||||||
|
// TodoMVC-JavaScript-ES5/Total,num,...,num
|
||||||
|
// TodoMVC-JavaScript-ES5/Adding100Items,num,...,num
|
||||||
|
// ...
|
||||||
|
const labels = ["Name"];
|
||||||
|
for (let i = 0; i < params.iterationCount; i++)
|
||||||
|
labels.push(`#${i + 1}`);
|
||||||
|
labels.push("Mean");
|
||||||
|
const metrics = Array.from(Object.values(this._metrics)).filter((metric) => !metric.name.startsWith("Iteration-"));
|
||||||
|
const metricsValues = metrics.map((metric) => [metric.name, ...metric.values, metric.mean].join(","));
|
||||||
|
const csv = [labels.join(","), ...metricsValues];
|
||||||
|
|
||||||
|
return csv.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
copyJsonResults() {
|
||||||
|
navigator.clipboard.writeText(this._formattedJSONResult({ modern: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCSVResults() {
|
||||||
|
navigator.clipboard.writeText(this._formattedCSVResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
_showSection(hash) {
|
||||||
|
if (this.isRunning()) {
|
||||||
|
this._setLocationHash("#running");
|
||||||
|
return;
|
||||||
|
} else if (this.hasFinished()) {
|
||||||
|
if (hash !== "#summary" && hash !== "#details") {
|
||||||
|
this._setLocationHash("#summary");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (hash !== "#home" && hash !== "") {
|
||||||
|
// Redirect invalid views to #home directly.
|
||||||
|
this._setLocationHash("#home");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setLocationHash(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setLocationHash(hash) {
|
||||||
|
if (hash === "#home" || hash === "") {
|
||||||
|
if (window.location.hash !== hash)
|
||||||
|
window.location.hash = "#home";
|
||||||
|
hash = "#home";
|
||||||
|
this._removeLocationHash();
|
||||||
|
} else {
|
||||||
|
window.location.hash = hash;
|
||||||
|
}
|
||||||
|
this._updateVisibleSectionAttribute(hash);
|
||||||
|
this._updateDocumentTitle(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateVisibleSectionAttribute(hash) {
|
||||||
|
const sectionId = hash.substring(1);
|
||||||
|
document.documentElement.setAttribute("data-visible-section", sectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateDocumentTitle(hash) {
|
||||||
|
const maybeSection = document.querySelector(hash);
|
||||||
|
const sectionTitle = maybeSection?.getAttribute("data-title") ?? "";
|
||||||
|
document.title = `Speedometer 3 ${sectionTitle}`.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeLocationHash() {
|
||||||
|
const location = window.location;
|
||||||
|
window.history.pushState("", document.title, location.pathname + location.search);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const rootStyle = document.documentElement.style;
|
||||||
|
rootStyle.setProperty("--viewport-width", `${params.viewport.width}px`);
|
||||||
|
rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`);
|
||||||
|
|
||||||
|
globalThis.benchmarkClient = new MainBenchmarkClient();
|
||||||
|
window.addEventListener("error", (event) => {
|
||||||
|
globalThis.benchmarkClient.handleError(event.error || new Error(event.message));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
|
globalThis.benchmarkClient.handleError(event.reason || new Error("Unhandled promise rejection"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading")
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
else
|
||||||
|
init();
|
||||||
88
resources/measurement-timeline.svg
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
|
||||||
|
<svg fill="none" viewBox="0 0 780 140" width="780" height="140" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--foreground: rgb(235, 235, 235);
|
||||||
|
--background: rgb(46, 51, 55);
|
||||||
|
font-family: "system-ui";
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 2px 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.measured {
|
||||||
|
padding: 0 2px 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--background);
|
||||||
|
background-color: var(--foreground);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unmeasured {
|
||||||
|
padding: 0 2px 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-style: dashed !important;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.hidden {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<foreignObject width="100%" height="100%">
|
||||||
|
<table xmlns="http://www.w3.org/1999/xhtml" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Iteration:</th>
|
||||||
|
<td colspan="9">Iteration 1</td>
|
||||||
|
<td>...</td>
|
||||||
|
<td colspan="3">Iteration N</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Test:</th>
|
||||||
|
<td colspan="6">TodoMVC-JavaScript-ES5</td>
|
||||||
|
<td>...</td>
|
||||||
|
<td colspan="2">Perf-Dashboard</td>
|
||||||
|
<td class="hidden"></td>
|
||||||
|
<td colspan="2">TodoMVC-...</td>
|
||||||
|
<td>...</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Steps:</th>
|
||||||
|
<td class="unmeasured">Prepare</td>
|
||||||
|
<td colspan="2" class="measured">Adding</td>
|
||||||
|
<td colspan="2" class="measured">Completing</td>
|
||||||
|
<td class="measured">...</td>
|
||||||
|
<td class="hidden"></td>
|
||||||
|
<td class="unmeasured">Prepare</td>
|
||||||
|
<td class="measured">...</td>
|
||||||
|
<td class="hidden"></td>
|
||||||
|
<td class="unmeasured">Prepare</td>
|
||||||
|
<td class="measured">...</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<td class="hidden"></td>
|
||||||
|
<td class="measured">Sync</td>
|
||||||
|
<td class="measured">Async</td>
|
||||||
|
<td class="measured">Sync</td>
|
||||||
|
<td class="measured">Async</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</foreignObject>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
327
resources/metric-ui.mjs
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
import { Metric } from "./metric.mjs";
|
||||||
|
|
||||||
|
export const COLORS = Object.freeze(["blue", "blue-light", "green-light", "green", "yellow", "orange", "red", "magenta", "violet", "purple", "blue-dark", "green-dark", "ochre", "rust"]);
|
||||||
|
|
||||||
|
export function renderMetricView(viewParams) {
|
||||||
|
let { metrics, width = 500, trackHeight = 20, subMetricMargin = 35, title = "", colors = COLORS } = viewParams;
|
||||||
|
// Make sure subMetricMargin is set for use in renderSubMetrics.
|
||||||
|
viewParams.subMetricMargin = subMetricMargin;
|
||||||
|
const scatterPlotParams = { width, trackHeight, colors };
|
||||||
|
|
||||||
|
scatterPlotParams.xAxisPositiveOnly = false;
|
||||||
|
scatterPlotParams.xAxisShowZero = true;
|
||||||
|
scatterPlotParams.values = prepareScatterPlotValues(metrics, true);
|
||||||
|
scatterPlotParams.unit = "%";
|
||||||
|
scatterPlotParams.xAxisLabel = "Spread Normalized";
|
||||||
|
const normalizedScatterPlot = renderScatterPlot(scatterPlotParams);
|
||||||
|
|
||||||
|
scatterPlotParams.xAxisPositiveOnly = true;
|
||||||
|
scatterPlotParams.xAxisShowZero = false;
|
||||||
|
scatterPlotParams.values = prepareScatterPlotValues(metrics, false);
|
||||||
|
scatterPlotParams.unit = metrics[0].unit;
|
||||||
|
scatterPlotParams.xAxisLabel = metrics[0].unit;
|
||||||
|
const absoluteScatterPlot = renderScatterPlot(scatterPlotParams);
|
||||||
|
|
||||||
|
const legend = metrics
|
||||||
|
.map(
|
||||||
|
(metric, i) => `
|
||||||
|
<tr >
|
||||||
|
<td class="${colors[i % colors.length]} no-select" >●</td>
|
||||||
|
<td class="label">${metric.shortName}</td>
|
||||||
|
<td class="number">${metric.mean.toFixed(2)}</td>
|
||||||
|
<td>±</td>
|
||||||
|
<td>${metric.deltaString}</td>
|
||||||
|
<td>${metric.unit}</td>
|
||||||
|
</tr>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
return `
|
||||||
|
<dl class="metric">
|
||||||
|
<dt><h3>${title}<h3></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="metric-chart"">
|
||||||
|
<div onclick="document.body.classList.toggle('relative-charts')">
|
||||||
|
<div class="metric-chart-absolute">
|
||||||
|
${absoluteScatterPlot}
|
||||||
|
</div>
|
||||||
|
<div class="metric-chart-relative">
|
||||||
|
${normalizedScatterPlot}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="chart chart-legend">${legend}</table>
|
||||||
|
</div>
|
||||||
|
${renderSubMetrics(viewParams)}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubMetrics(viewParams) {
|
||||||
|
const { metrics, width, subMetricMargin, colors = COLORS, renderChildren = true } = viewParams;
|
||||||
|
const valuesTable = `
|
||||||
|
<label class="details-toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
|
||||||
|
Table
|
||||||
|
</label>
|
||||||
|
<div class="submetrics">
|
||||||
|
${renderMetricsTable(metrics)}
|
||||||
|
</div>`;
|
||||||
|
const hasChildMetric = metrics.length > 0 && metrics[0].children.length > 0;
|
||||||
|
if (!hasChildMetric || !renderChildren)
|
||||||
|
return valuesTable;
|
||||||
|
|
||||||
|
const subMetricWidth = width - subMetricMargin;
|
||||||
|
const childColors = [...colors];
|
||||||
|
|
||||||
|
const subMetrics = metrics
|
||||||
|
.map((metric) => {
|
||||||
|
// Rotate colors to get different colors for sub-plots.
|
||||||
|
for (let i = 0; i < metric.children.length; i++) {
|
||||||
|
const color = childColors.pop();
|
||||||
|
childColors.unshift(color);
|
||||||
|
}
|
||||||
|
const subMetricParams = {
|
||||||
|
...viewParams,
|
||||||
|
parentMetric: metric,
|
||||||
|
metrics: metric.children,
|
||||||
|
title: metric.name,
|
||||||
|
width: subMetricWidth,
|
||||||
|
colors: childColors,
|
||||||
|
};
|
||||||
|
return renderMetricView(subMetricParams);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
return `${valuesTable}
|
||||||
|
<label class="details-toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
|
||||||
|
Submetrics
|
||||||
|
</label>
|
||||||
|
<div class="submetrics">
|
||||||
|
${subMetrics}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMetricsTable(metrics, min, max) {
|
||||||
|
let numRows = 0;
|
||||||
|
let columnHeaders = "";
|
||||||
|
let commonPrefixes = metrics[0].name.split(Metric.separator);
|
||||||
|
for (const metric of metrics) {
|
||||||
|
const prefixes = metric.name.split(Metric.separator);
|
||||||
|
for (let i = commonPrefixes.length - 1; i >= 0; i--) {
|
||||||
|
if (commonPrefixes[i] !== prefixes[i])
|
||||||
|
commonPrefixes.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const commonPrefix = commonPrefixes.join(Metric.separator);
|
||||||
|
let commonPrefixHeader = "";
|
||||||
|
if (commonPrefix) {
|
||||||
|
commonPrefixHeader = `
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td colspan="${metrics.length}" class="prefix">${commonPrefix}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
for (const metric of metrics) {
|
||||||
|
const name = metric.name.substring(commonPrefix.length);
|
||||||
|
columnHeaders += `<th>${name} [${metric.unit}]</th>`;
|
||||||
|
numRows = Math.max(metric.values.length, numRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = "";
|
||||||
|
for (let row = 0; row < numRows; row++) {
|
||||||
|
let columns = "";
|
||||||
|
for (const metric of metrics) {
|
||||||
|
const value = metric.values[row];
|
||||||
|
if (value === undefined)
|
||||||
|
continue;
|
||||||
|
const delta = metric.max - metric.min;
|
||||||
|
const percent = Math.max(Math.min((value - metric.min) / delta, 1), 0) * 100;
|
||||||
|
const percentGradient = `background: linear-gradient(90deg, var(--foreground-alpha) ${percent}%, rgba(0,0,0,0) ${percent}%);`;
|
||||||
|
columns += `<td style="${percentGradient}">${value.toFixed(2)}</td>`;
|
||||||
|
}
|
||||||
|
body += `<tr>
|
||||||
|
<td>${row}</td>
|
||||||
|
${columns}
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
return `<table class="metrics-table" >
|
||||||
|
<thead onclick="this.classList.toggle('nowrap')" >
|
||||||
|
${commonPrefixHeader}
|
||||||
|
<tr>
|
||||||
|
<th>Iteration</th>
|
||||||
|
${columnHeaders}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${body}
|
||||||
|
<tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareScatterPlotValues(metrics, normalize = true) {
|
||||||
|
let points = [];
|
||||||
|
// Arrange child-metrics values in a single coordinate system:
|
||||||
|
// - metric 1: x values are in range [0, 1]
|
||||||
|
// - metric 2: y values are in range [1, 2]
|
||||||
|
// - ...
|
||||||
|
// This way each metric data point is on a separate track in the scatter
|
||||||
|
// plot.
|
||||||
|
// If normalize == true:
|
||||||
|
// All x values are normalized by the mean of each metric and
|
||||||
|
// centered on 0.
|
||||||
|
// Example: [90ms, 100ms, 110ms] => [-10%, 0%, +10%]
|
||||||
|
const toPercent = 100;
|
||||||
|
let unit;
|
||||||
|
for (let metricIndex = 0; metricIndex < metrics.length; metricIndex++) {
|
||||||
|
const metric = metrics[metricIndex];
|
||||||
|
// If the mean is 0 we can't normalize values properly.
|
||||||
|
const mean = metric.mean || 1;
|
||||||
|
if (!unit)
|
||||||
|
unit = metric.unit;
|
||||||
|
else if (unit !== metric.unit)
|
||||||
|
throw new Error("All metrics must have the same unit.");
|
||||||
|
let width = metric.delta || 1;
|
||||||
|
let center = mean;
|
||||||
|
if (normalize) {
|
||||||
|
width = (metric.delta / mean) * toPercent;
|
||||||
|
center = 0;
|
||||||
|
}
|
||||||
|
const left = center - width / 2;
|
||||||
|
const y = metricIndex;
|
||||||
|
const label = `Mean: ${metric.valueString}\n` + `Min: ${metric.min.toFixed(2)}${unit}\n` + `Max: ${metric.max.toFixed(2)}${unit}`;
|
||||||
|
const rect = [left, y, label, width];
|
||||||
|
// Add data for individual points:
|
||||||
|
points.push(rect);
|
||||||
|
const values = metric.values;
|
||||||
|
const length = values.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const value = values[i];
|
||||||
|
let x = value;
|
||||||
|
let normalized = (value / mean - 1) * toPercent;
|
||||||
|
if (normalize)
|
||||||
|
x = normalized;
|
||||||
|
const sign = normalized < 0 ? "-" : "+";
|
||||||
|
normalized = Math.abs(normalized);
|
||||||
|
// Each value is mapped to a y-coordinate in the range of [metricIndex, metricIndex + 1]
|
||||||
|
const valueOffsetY = length === 1 ? 0.5 : i / length;
|
||||||
|
const y = metricIndex + valueOffsetY;
|
||||||
|
let label = `Iteration ${i}: ${value.toFixed(3)}${unit}\n` + `Normalized: ${metric.mean.toFixed(3)}${unit} ${sign} ${normalized.toFixed(2)}%`;
|
||||||
|
const point = [x, y, label];
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScatterPlot({ values, width = 500, height, trackHeight, xAxisPositiveOnly = false, xAxisShowZero = false, xAxisLabel, unit = "", colors = COLORS }) {
|
||||||
|
if (!height && !trackHeight)
|
||||||
|
throw new Error("Either height or trackHeight must be specified");
|
||||||
|
let xMin = Infinity;
|
||||||
|
let xMax = 0;
|
||||||
|
let yMin = Infinity;
|
||||||
|
let yMax = 0;
|
||||||
|
for (let value of values) {
|
||||||
|
let [x, y] = value;
|
||||||
|
xMin = Math.min(xMin, x);
|
||||||
|
xMax = Math.max(xMax, x);
|
||||||
|
yMin = Math.min(yMin, y);
|
||||||
|
yMax = Math.max(yMax, y);
|
||||||
|
}
|
||||||
|
if (xAxisPositiveOnly)
|
||||||
|
xMin = Math.max(xMin, 0);
|
||||||
|
// Max delta of values across each axis:
|
||||||
|
const trackCount = Math.ceil(yMax - yMin) || 1;
|
||||||
|
const spreadX = xMax - xMin;
|
||||||
|
// Axis + labels height:
|
||||||
|
const axisHeight = 18;
|
||||||
|
const axisMarginY = 4;
|
||||||
|
const trackMargin = 2;
|
||||||
|
let markerSize = 5;
|
||||||
|
// Auto-adjust markers to [2px, 5px] for high iteration counts:
|
||||||
|
const iterationsLimit = 20;
|
||||||
|
if (values.length > iterationsLimit)
|
||||||
|
markerSize = 2 + (3 / values.length) * iterationsLimit;
|
||||||
|
// Recalculate height:
|
||||||
|
if (height)
|
||||||
|
trackHeight = (height - axisHeight - axisMarginY) / trackCount;
|
||||||
|
else
|
||||||
|
height = trackCount * trackHeight + axisHeight + axisMarginY;
|
||||||
|
|
||||||
|
// Horizontal axis position:
|
||||||
|
const axisY = height - axisHeight + axisMarginY;
|
||||||
|
const unitToPosX = width / spreadX;
|
||||||
|
const unitToPosY = trackHeight - trackMargin - markerSize / 2;
|
||||||
|
const points = values.map(renderValue).join("");
|
||||||
|
let xAxisZeroLine = "";
|
||||||
|
if (xAxisShowZero) {
|
||||||
|
const xZeroPos = (0 - xMin) * unitToPosX;
|
||||||
|
xAxisZeroLine = `<line x1="${xZeroPos}" x2="${xZeroPos}" y1="${0}" y2="${axisY}" class="axis"/>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<svg class="scatter-plot chart"
|
||||||
|
width="${width}" height="${height}"
|
||||||
|
viewBox="${`0 0 ${width} ${height}`}">
|
||||||
|
<g class="horizontal-axis no-select">
|
||||||
|
<line
|
||||||
|
x1="${0}" x2="${width}"
|
||||||
|
y1="${axisY - axisMarginY}" y2="${axisY - axisMarginY}"
|
||||||
|
class="axis" />
|
||||||
|
<text y="${axisY}" x="0" text-anchor="start">${xMin.toFixed(2)}${unit}</text>
|
||||||
|
<text y="${axisY}" x="${width / 2}" text-anchor="middle">${xAxisLabel}</text>
|
||||||
|
<text y="${axisY}" x="${width}" text-anchor="end">${xMax.toFixed(2)}${unit}</text>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<g id="marker">
|
||||||
|
<circle r="${markerSize / 2}" />
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<g class="values">
|
||||||
|
${xAxisZeroLine}
|
||||||
|
${points}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function renderValue(value) {
|
||||||
|
const [rawX, rawY, label, rawWidth = 0] = value;
|
||||||
|
const trackIndex = rawY | 0;
|
||||||
|
const y = (rawY - yMin) * unitToPosY + markerSize * trackIndex;
|
||||||
|
const cssClass = colors[trackIndex % colors.length];
|
||||||
|
|
||||||
|
if (value.length <= 3) {
|
||||||
|
// Render a simple marker:
|
||||||
|
const x = (rawX - xMin) * unitToPosX;
|
||||||
|
const adjustedY = y + markerSize / 2;
|
||||||
|
return `
|
||||||
|
<use href="#marker" x="${x}" y="${adjustedY}" class="marker ${cssClass}">
|
||||||
|
<title>${label}</title>
|
||||||
|
</use>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Render a rect with 4 input values:
|
||||||
|
const x = (rawX - xMin) * unitToPosX + rawWidth / 2;
|
||||||
|
const w = rawWidth * unitToPosX;
|
||||||
|
const centerX = x + w / 2;
|
||||||
|
const top = y;
|
||||||
|
const height = trackHeight - trackMargin;
|
||||||
|
const bottom = top + height;
|
||||||
|
return `
|
||||||
|
<g class="percentile ${cssClass}">
|
||||||
|
<rect x="${x}" y="${top}" width="${w}" height="${height}">
|
||||||
|
<title>${label}</title>
|
||||||
|
</rect>
|
||||||
|
<line x1="${x}" x2="${x}" y1="${top}" y2="${bottom}" />
|
||||||
|
<line
|
||||||
|
x1="${centerX}" x2="${centerX}"
|
||||||
|
y1="${top}" y2="${bottom}"
|
||||||
|
stroke-dasharray="${height / 3}" />
|
||||||
|
<line x1="${x + w}" x2="${x + w}" y1="${top}" y2="${bottom}" />
|
||||||
|
</g>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
resources/metric.mjs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import * as Statistics from "./statistics.mjs";
|
||||||
|
|
||||||
|
export const MILLISECONDS_PER_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
|
export class Metric {
|
||||||
|
static separator = "/";
|
||||||
|
|
||||||
|
constructor(name, unit = "ms") {
|
||||||
|
if (typeof name !== "string")
|
||||||
|
throw new Error(`Invalid metric.name=${name}, expected string.`);
|
||||||
|
this.name = name;
|
||||||
|
this.unit = unit;
|
||||||
|
this.description = "";
|
||||||
|
|
||||||
|
this.mean = 0.0;
|
||||||
|
// Make "geomean" non-enumerable so we don't serialize it with JSON.stringify
|
||||||
|
// and avoid some confusion with the top-level Geomean metric.
|
||||||
|
Object.defineProperty(this, "geomean", {
|
||||||
|
writable: true,
|
||||||
|
value: 0,
|
||||||
|
});
|
||||||
|
this.delta = 0.0;
|
||||||
|
this.percentDelta = 0.0;
|
||||||
|
|
||||||
|
this.sum = 0.0;
|
||||||
|
this.min = 0.0;
|
||||||
|
this.max = 0.0;
|
||||||
|
|
||||||
|
this.values = [];
|
||||||
|
|
||||||
|
// Mark properties which refer to other Metric objects as
|
||||||
|
// non-enumerable to avoid issue with JSON.stringify due to circular
|
||||||
|
// references.
|
||||||
|
Object.defineProperties(this, {
|
||||||
|
parent: {
|
||||||
|
writable: true,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
writable: true,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get shortName() {
|
||||||
|
return this.parent ? this.name.replace(`${this.parent.name}-`, "") : this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueString() {
|
||||||
|
const mean = this.mean.toFixed(2);
|
||||||
|
if (!this.percentDelta || !this.delta)
|
||||||
|
return `${mean} ${this.unit}`;
|
||||||
|
return `${mean} ± ${this.deltaString} ${this.unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get deltaString() {
|
||||||
|
if (!this.percentDelta || !this.delta)
|
||||||
|
return "";
|
||||||
|
return `${this.delta.toFixed(2)} (${this.percentDelta.toFixed(1)}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
addChild(metric) {
|
||||||
|
if (metric.parent)
|
||||||
|
throw new Error("Cannot re-add sub metric");
|
||||||
|
metric.parent = this;
|
||||||
|
this.children.push(metric);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value) {
|
||||||
|
if (typeof value !== "number")
|
||||||
|
throw new Error(`Adding invalid value=${value} to metric=${this.name}`);
|
||||||
|
this.values.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
computeAggregatedMetrics() {
|
||||||
|
// Avoid the loss of significance for the sum.
|
||||||
|
const sortedValues = this.values.concat().sort((a, b) => a - b);
|
||||||
|
this.sum = Statistics.sum(sortedValues);
|
||||||
|
this.min = sortedValues[0];
|
||||||
|
this.max = sortedValues[sortedValues.length - 1];
|
||||||
|
this.mean = this.sum / this.values.length;
|
||||||
|
const product = Statistics.product(sortedValues);
|
||||||
|
this.geomean = Math.pow(product, 1 / this.values.length);
|
||||||
|
if (this.values.length > 1) {
|
||||||
|
const squareSum = Statistics.squareSum(sortedValues);
|
||||||
|
this.delta = Statistics.confidenceIntervalDelta(0.95, this.values.length, this.sum, squareSum);
|
||||||
|
this.percentDelta = isNaN(this.delta) ? undefined : (this.delta * 100) / this.mean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
222
resources/score.svg
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
|
||||||
|
<svg fill="none" viewBox="0 0 550 440" width="550" height="440" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--foreground: rgb(235, 235, 235);
|
||||||
|
--background: rgb(46, 51, 55);
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
mtable {
|
||||||
|
display: table;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mtr mtd {
|
||||||
|
text-align: left;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend mtd {
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<foreignObject width="100%" height="100%">
|
||||||
|
<math xmlns="http://www.w3.org/1998/Math/MathML" width="100%" height="100%" >
|
||||||
|
<mtable displaystyle="true" >
|
||||||
|
<mtr>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mi>Score</mi>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mo>=</mo>
|
||||||
|
<mfrac>
|
||||||
|
<mn>1</mn>
|
||||||
|
<mi>n</mi>
|
||||||
|
</mfrac>
|
||||||
|
<mrow>
|
||||||
|
<munderover>
|
||||||
|
<mo stretchy="true">∑</mo>
|
||||||
|
<mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
<mo>=</mo>
|
||||||
|
<mn>1</mn>
|
||||||
|
</mrow>
|
||||||
|
<mi>n</mi>
|
||||||
|
</munderover>
|
||||||
|
</mrow>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>IterationScore</mi>
|
||||||
|
</mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
</msub>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr>
|
||||||
|
<mtd>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>IterationScore</mi>
|
||||||
|
</mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
</msub>
|
||||||
|
</mtd>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mo>=</mo>
|
||||||
|
<mfrac>
|
||||||
|
<mn>1000</mn>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>IterationGeoMeanTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
</msub>
|
||||||
|
</mfrac>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr>
|
||||||
|
<mtd>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>IterationGeoMeanTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
</msub>
|
||||||
|
</mtd>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mo>=</mo>
|
||||||
|
<mroot>
|
||||||
|
<mrow>
|
||||||
|
<mrow>
|
||||||
|
<munderover>
|
||||||
|
<mo stretchy="true">∏</mo>
|
||||||
|
<mrow>
|
||||||
|
<mi>j</mi>
|
||||||
|
<mo>=</mo>
|
||||||
|
<mn>1</mn>
|
||||||
|
</mrow>
|
||||||
|
<mi>m</mi>
|
||||||
|
</munderover>
|
||||||
|
</mrow>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>TestTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
<mi>j</mi>
|
||||||
|
</mrow>
|
||||||
|
</msub>
|
||||||
|
</mrow>
|
||||||
|
<mi>m</mi>
|
||||||
|
</mroot>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr>
|
||||||
|
<mtd>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>TestTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
<mi>j</mi>
|
||||||
|
</mrow>
|
||||||
|
</msub>
|
||||||
|
</mtd>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mo>=</mo>
|
||||||
|
<mrow>
|
||||||
|
<munder>
|
||||||
|
<mo stretchy="true">∑</mo>
|
||||||
|
<mi>k</mi>
|
||||||
|
</munder>
|
||||||
|
</mrow>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>TestStepTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
<mi>j</mi>
|
||||||
|
<mi>k</mi>
|
||||||
|
</mrow>
|
||||||
|
</msub>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr>
|
||||||
|
<mtd>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>TestStepTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
<mi>j</mi>
|
||||||
|
<mi>k</mi>
|
||||||
|
</mrow>
|
||||||
|
</msub>
|
||||||
|
</mtd>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mo>=</mo>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>TestStepSyncTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
<mi>j</mi>
|
||||||
|
<mi>k</mi>
|
||||||
|
</mrow>
|
||||||
|
</msub>
|
||||||
|
<mo>+</mo>
|
||||||
|
<msub>
|
||||||
|
<mrow>
|
||||||
|
<mi>TestStepAsyncTime</mi>
|
||||||
|
</mrow>
|
||||||
|
<mrow>
|
||||||
|
<mi>i</mi>
|
||||||
|
<mi>j</mi>
|
||||||
|
<mi>k</mi>
|
||||||
|
</mrow>
|
||||||
|
</msub>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr class="legend">
|
||||||
|
<mtd><mi>n</mi></mtd>
|
||||||
|
<mtd>= Number of Iterations (default = 10)</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr class="legend">
|
||||||
|
<mtd><mi>i</mi></mtd>
|
||||||
|
<mtd>= Iteration Index</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr class="legend">
|
||||||
|
<mtd><mi>m</mi></mtd>
|
||||||
|
<mtd>= Number of Suites</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr class="legend">
|
||||||
|
<mtd><mi>j</mi></mtd>
|
||||||
|
<mtd>= Suite Index</mtd>
|
||||||
|
</mtr>
|
||||||
|
<mtr class="legend">
|
||||||
|
<mtd><mi>k</mi></mtd>
|
||||||
|
<mtd>= Test Index</mtd>
|
||||||
|
</mtr>
|
||||||
|
</mtable>
|
||||||
|
</math>
|
||||||
|
</foreignObject>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.3 KiB |
45
resources/scripts/sanitize-language/index.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
const fs = require("fs").promises;
|
||||||
|
const { resolve } = require("path");
|
||||||
|
|
||||||
|
const dirName = "./dist";
|
||||||
|
// spelling out words that this file doesn't trigger any bad word filters.
|
||||||
|
const parts = [
|
||||||
|
["b", "l", "a", "c", "k", "l", "i", "s", "t"],
|
||||||
|
["w", "h", "i", "t", "e", "l", "i", "s", "t"],
|
||||||
|
];
|
||||||
|
const wordsToUse = ["disallowlist", "allowlist"];
|
||||||
|
|
||||||
|
const wordsToReplace = parts.map((part) => part.join(""));
|
||||||
|
const replacements = new Map();
|
||||||
|
wordsToReplace.forEach((word, index) => replacements.set(word, wordsToUse[index]));
|
||||||
|
const stringReplaceRegex = new RegExp(wordsToReplace.join("|"), "gi");
|
||||||
|
|
||||||
|
async function getFiles(dir) {
|
||||||
|
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
const files = await Promise.all(
|
||||||
|
dirents.map((dirent) => {
|
||||||
|
const res = resolve(dir, dirent.name);
|
||||||
|
return dirent.isDirectory() ? getFiles(res) : res;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Array.prototype.concat(...files);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readAndReplace(fileName) {
|
||||||
|
const contents = await fs.readFile(fileName, "utf8");
|
||||||
|
const sanitized = contents.replace(stringReplaceRegex, function (matched) {
|
||||||
|
return replacements.get(matched);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (contents !== sanitized)
|
||||||
|
await fs.writeFile(fileName, sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sanitize() {
|
||||||
|
const dir = process.env.OUTPUT_FOLDER ?? dirName;
|
||||||
|
const files = await getFiles(dir);
|
||||||
|
await Promise.all(files.map((file) => readAndReplace(file)));
|
||||||
|
console.log(`** Done sanitizing ${dir}! **`);
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitize();
|
||||||
6
resources/scripts/sanitize-language/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "sanitize-language",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A script to parse files for non-inclusive words.",
|
||||||
|
"main": "index.js"
|
||||||
|
}
|
||||||
174
resources/shared/benchmark.mjs
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
|
||||||
|
import { Params } from "./params.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BenchmarkStep
|
||||||
|
*
|
||||||
|
* A single test step, with a common interface to interact with.
|
||||||
|
*/
|
||||||
|
export class BenchmarkStep {
|
||||||
|
constructor(name, run) {
|
||||||
|
this.name = name;
|
||||||
|
this.run = run;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAndRecordStep(params, suite, step) {
|
||||||
|
const stepRunner = new StepRunner(null, null, params, suite, step);
|
||||||
|
const result = await stepRunner.runStep();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsyncBenchmarkStep extends BenchmarkStep {
|
||||||
|
async runAndRecordStep(params, suite, step) {
|
||||||
|
const stepRunner = new AsyncStepRunner(null, null, params, suite, step);
|
||||||
|
const result = await stepRunner.runStep();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BENCHMARK_SUITE_TYPE = Object.freeze({
|
||||||
|
__proto__: null,
|
||||||
|
sync: "sync",
|
||||||
|
async: "async",
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BenchmarkSuite
|
||||||
|
*
|
||||||
|
* A single test suite that contains one or more test steps.
|
||||||
|
*/
|
||||||
|
export class BenchmarkSuite {
|
||||||
|
constructor(name, steps, type = BENCHMARK_SUITE_TYPE.sync) {
|
||||||
|
this.name = name;
|
||||||
|
this.steps = steps;
|
||||||
|
this.type = type;
|
||||||
|
console.assert(this.type in BENCHMARK_SUITE_TYPE, "Invalid Type", this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAndRecordSuite(params, onProgress) {
|
||||||
|
const measuredValues = {
|
||||||
|
tests: {},
|
||||||
|
prepare: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
const suiteStartLabel = `suite-${this.name}-start`;
|
||||||
|
const suiteEndLabel = `suite-${this.name}-end`;
|
||||||
|
|
||||||
|
performance.mark(suiteStartLabel);
|
||||||
|
|
||||||
|
for (const step of this.steps) {
|
||||||
|
const rawResult = await step.runAndRecordStep(params, this, step);
|
||||||
|
console.assert(rawResult, "Missing test return value", step);
|
||||||
|
const { syncTime, asyncTime } = rawResult;
|
||||||
|
const total = syncTime + asyncTime;
|
||||||
|
const result = {
|
||||||
|
tests: { Sync: syncTime, Async: asyncTime },
|
||||||
|
total: total,
|
||||||
|
};
|
||||||
|
if (!step.ignoreResult) {
|
||||||
|
measuredValues.tests[step.name] = result;
|
||||||
|
measuredValues.total += total;
|
||||||
|
}
|
||||||
|
onProgress?.(step.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
performance.mark(suiteEndLabel);
|
||||||
|
performance.measure(`suite-${this.name}`, suiteStartLabel, suiteEndLabel);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "suite-tests-complete",
|
||||||
|
status: "success",
|
||||||
|
result: measuredValues,
|
||||||
|
suiteName: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsyncBenchmarkSuite extends BenchmarkSuite {
|
||||||
|
constructor(name, steps) {
|
||||||
|
super(name, steps, BENCHMARK_SUITE_TYPE.async);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MESSAGE_TYPE = Object.freeze({
|
||||||
|
__proto__: null,
|
||||||
|
appReady: "app-ready",
|
||||||
|
suiteStart: "suite-start",
|
||||||
|
stepComplete: "step-complete",
|
||||||
|
suiteComplete: "suite-complete",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MESSAGE_STATUS = Object.freeze({
|
||||||
|
__proto__: null,
|
||||||
|
success: "success",
|
||||||
|
error: "error",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** **********************************************************************
|
||||||
|
* BenchmarkConnector
|
||||||
|
*
|
||||||
|
* postMessage is used to communicate between app and benchmark.
|
||||||
|
* When the app is ready, an 'app-ready' message is sent to signal that the app can receive instructions.
|
||||||
|
*
|
||||||
|
* A prepare script within the apps appends window.name and window.version from the package.json file.
|
||||||
|
* The appId is build by appending name-version
|
||||||
|
* It's used as an additional safe-guard to ensure the correct app responds to a message.
|
||||||
|
*************************************************************************/
|
||||||
|
export class BenchmarkConnector {
|
||||||
|
constructor(suites, name, version) {
|
||||||
|
this.suites = suites;
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if (!name || !version)
|
||||||
|
console.warn("No name or version supplied, to create a unique appId");
|
||||||
|
|
||||||
|
this.appId = name && version ? `${name}-${version}` : -1;
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(event) {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.appId !== this.appId || message.key !== "benchmark-connector") {
|
||||||
|
console.warning("Invalid message", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case MESSAGE_TYPE.suiteStart:
|
||||||
|
const params = new Params(new URLSearchParams(window.location.search));
|
||||||
|
const { name } = message.payload;
|
||||||
|
const suite = this.suites[name];
|
||||||
|
if (!suite)
|
||||||
|
console.error(`Suite with the name of "${name}" not found!`);
|
||||||
|
const onProgress = (step) => this._sendMessage(MESSAGE_TYPE.stepComplete, { name: this.name, step });
|
||||||
|
const { result } = await suite.runAndRecordSuite(params, onProgress);
|
||||||
|
this._sendMessage(MESSAGE_TYPE.suiteComplete, { result });
|
||||||
|
this.disconnect();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Message data type not supported: ${message.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendMessage(type, payload, status = MESSAGE_STATUS.success) {
|
||||||
|
const message = {
|
||||||
|
appId: this.appId,
|
||||||
|
type: type,
|
||||||
|
payload: payload,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
window.top.postMessage(message, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
window.addEventListener("message", this.onMessage);
|
||||||
|
this._sendMessage(MESSAGE_TYPE.appReady, { appId: this.appId });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
window.removeEventListener("message", this.onMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
resources/shared/generate-resources.mjs
Normal file
|
|
@ -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}`);
|
||||||
|
}
|
||||||
61
resources/shared/helpers.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* Recursively queries the DOM for an element, traversing through shadow DOMs.
|
||||||
|
*
|
||||||
|
* @param {Element|ShadowRoot} lookupStartNode The node or shadow root to start the lookup from.
|
||||||
|
* @param {string[]} path An array of CSS selectors representing the path to the target element.
|
||||||
|
* @returns {Element|null} The target element if found, otherwise null.
|
||||||
|
*/
|
||||||
|
export function recursivelyQuerySelector(lookupStartNode, path) {
|
||||||
|
lookupStartNode = lookupStartNode.shadowRoot ?? lookupStartNode;
|
||||||
|
const target = path.reduce((root, selector) => {
|
||||||
|
const node = root.querySelector(selector);
|
||||||
|
return node.shadowRoot ?? node;
|
||||||
|
}, lookupStartNode);
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single DOM element, optionally traversing through shadow DOMs to a specified path before the final selection.
|
||||||
|
*
|
||||||
|
* @param {string} selector The CSS selector for the desired element.
|
||||||
|
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
|
||||||
|
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
|
||||||
|
* @returns {Element|null} The found element, or null if not found.
|
||||||
|
*/
|
||||||
|
export function getElement(selector, path = [], lookupStartNode = document) {
|
||||||
|
const element = recursivelyQuerySelector(lookupStartNode, path).querySelector(selector);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all DOM elements matching a selector, optionally traversing through shadow DOMs to a specified path before the final selection.
|
||||||
|
*
|
||||||
|
* @param {string} selector The CSS selector for the desired elements.
|
||||||
|
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
|
||||||
|
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
|
||||||
|
* @returns {Element[]} An array of found elements.
|
||||||
|
*/
|
||||||
|
export function getAllElements(selector, path = [], lookupStartNode = document) {
|
||||||
|
const elements = Array.from(recursivelyQuerySelector(lookupStartNode, path).querySelectorAll(selector));
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forceLayout(body, layoutMode = "getBoundingRectAndElementFromPoint") {
|
||||||
|
body ??= document.body;
|
||||||
|
const rect = body.getBoundingClientRect();
|
||||||
|
switch (layoutMode) {
|
||||||
|
case "getBoundingRectAndElementFromPoint":
|
||||||
|
return document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0);
|
||||||
|
case "getBoundingClientRect":
|
||||||
|
return rect.height;
|
||||||
|
default:
|
||||||
|
throw Error(`Invalid layoutMode: ${layoutMode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skipInShell(context) {
|
||||||
|
if (typeof window === "undefined" || typeof document === "undefined")
|
||||||
|
// Skip in non-browser environments
|
||||||
|
context.skip();
|
||||||
|
}
|
||||||
5
resources/shared/package.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "speedometer-utils",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Utility files for Speedometer & Workloads"
|
||||||
|
}
|
||||||
264
resources/shared/params.mjs
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
export const LAYOUT_MODES = Object.freeze(["getBoundingClientRect", "getBoundingRectAndElementFromPoint"]);
|
||||||
|
|
||||||
|
export class Params {
|
||||||
|
viewport = {
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
};
|
||||||
|
// Enable a detailed developer menu to change the current Params.
|
||||||
|
developerMode = false;
|
||||||
|
startAutomatically = false;
|
||||||
|
iterationCount = 10;
|
||||||
|
suites = [];
|
||||||
|
// A list of tags to filter suites
|
||||||
|
tags = ["default"];
|
||||||
|
// Toggle running a dummy suite once before the normal test suites.
|
||||||
|
useWarmupSuite = false;
|
||||||
|
// toggle async type vs default raf type.
|
||||||
|
useAsyncSteps = false;
|
||||||
|
// Change how a test measurement is triggered and async time is measured:
|
||||||
|
// "timer": The classic (as in Speedometer 2.x) way using setTimeout
|
||||||
|
// "raf": Using rAF callbacks, both for triggering the sync part and for measuring async time.
|
||||||
|
measurementMethod = "raf";
|
||||||
|
// Wait time after suite preparation in ms.
|
||||||
|
waitAfterSetup = 0;
|
||||||
|
// Wait time before the sync step in ms.
|
||||||
|
waitBeforeSync = 0;
|
||||||
|
// Wait after running a suite.
|
||||||
|
waitAfterSuite = 0;
|
||||||
|
// Warmup time before the sync step in ms.
|
||||||
|
warmupBeforeSync = 0;
|
||||||
|
// Seed for shuffling the execution order of suites.
|
||||||
|
// "off": do not shuffle
|
||||||
|
// "generate": generate a random seed
|
||||||
|
// <integer>: use the provided integer as a seed
|
||||||
|
shuffleSeed = "off";
|
||||||
|
// Param to tweak the relative complexity of all suites.
|
||||||
|
// The default is 1.0, and for suites supporting this param, the duration
|
||||||
|
// roughly scales with the complexity.
|
||||||
|
complexity = 1.0;
|
||||||
|
// Choices: "getBoundingClientRect" or "getBoundingRectAndElementFromPoint"
|
||||||
|
layoutMode = LAYOUT_MODES[0];
|
||||||
|
// Measure more workload prepare time.
|
||||||
|
measurePrepare = false;
|
||||||
|
// External config url to override internal tests.
|
||||||
|
config = "";
|
||||||
|
|
||||||
|
constructor(searchParams = undefined, warnUnused = false) {
|
||||||
|
if (searchParams)
|
||||||
|
this._copyFromSearchParams(searchParams, warnUnused);
|
||||||
|
if (!this.developerMode) {
|
||||||
|
Object.freeze(this.viewport);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseNumber(value, errorMessage, minValue = 0) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number) && errorMessage)
|
||||||
|
throw new Error(`Invalid ${errorMessage} param: '${value}', expected Number.`);
|
||||||
|
if (number < minValue)
|
||||||
|
throw new Error(`Invalid ${errorMessage} param: '${value}', value must be >= ${minValue}.`);
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseInt(value, errorMessage, minValue = 0) {
|
||||||
|
const number = this._parseNumber(value, errorMessage, minValue);
|
||||||
|
if (!Number.isInteger(number) && errorMessage)
|
||||||
|
throw new Error(`Invalid ${errorMessage} param: '${value}', expected int.`);
|
||||||
|
return parseInt(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
_copyFromSearchParams(searchParams, warnUnused) {
|
||||||
|
this.viewport = this._parseViewport(searchParams);
|
||||||
|
this.startAutomatically = this._parseBooleanParam(searchParams, "startAutomatically");
|
||||||
|
this.iterationCount = this._parseIntParam(searchParams, "iterationCount", 1);
|
||||||
|
this.suites = this._parseSuites(searchParams);
|
||||||
|
this.tags = this._parseTags(searchParams);
|
||||||
|
this.developerMode = this._parseBooleanParam(searchParams, "developerMode");
|
||||||
|
this.useWarmupSuite = this._parseBooleanParam(searchParams, "useWarmupSuite");
|
||||||
|
this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps");
|
||||||
|
this.waitAfterSetup = this._parseIntParam(searchParams, "waitAfterSetup", 0);
|
||||||
|
this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0);
|
||||||
|
this.waitAfterSuite = this._parseIntParam(searchParams, "waitAfterSuite", 0);
|
||||||
|
this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0);
|
||||||
|
this.measurementMethod = this._parseEnumParam(searchParams, "measurementMethod", ["raf"]);
|
||||||
|
this.shuffleSeed = this._parseShuffleSeed(searchParams);
|
||||||
|
this.complexity = this._parserNumberParam(searchParams, "complexity", 0);
|
||||||
|
this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES);
|
||||||
|
this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare");
|
||||||
|
this.config = this._parseConfig(searchParams);
|
||||||
|
|
||||||
|
if (warnUnused) {
|
||||||
|
const unused = Array.from(searchParams.keys());
|
||||||
|
if (unused.length > 0)
|
||||||
|
console.error(`Got unused search params: ${unused.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseBooleanParam(searchParams, paramKey) {
|
||||||
|
if (!searchParams.has(paramKey))
|
||||||
|
return false;
|
||||||
|
searchParams.delete(paramKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parserNumberParam(searchParams, paramKey, minValue) {
|
||||||
|
if (!searchParams.has(paramKey))
|
||||||
|
return defaultParams[paramKey];
|
||||||
|
const parsedValue = this._parseNumber(searchParams.get(paramKey), "waitBeforeSync", minValue);
|
||||||
|
searchParams.delete(paramKey);
|
||||||
|
return parsedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseIntParam(searchParams, paramKey, minValue) {
|
||||||
|
if (!searchParams.has(paramKey))
|
||||||
|
return defaultParams[paramKey];
|
||||||
|
const parsedValue = this._parseInt(searchParams.get(paramKey), "waitBeforeSync", minValue);
|
||||||
|
searchParams.delete(paramKey);
|
||||||
|
return parsedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseViewport(searchParams) {
|
||||||
|
if (!searchParams.has("viewport"))
|
||||||
|
return defaultParams.viewport;
|
||||||
|
const viewportParam = searchParams.get("viewport");
|
||||||
|
const [width, height] = viewportParam.split("x");
|
||||||
|
const viewport = {
|
||||||
|
width: this._parseInt(width, "viewport.width"),
|
||||||
|
height: this._parseInt(height, "viewport.height"),
|
||||||
|
};
|
||||||
|
if (this.viewport.width < 800 || this.viewport.height < 600)
|
||||||
|
throw new Error(`Invalid viewport param: ${viewportParam}`);
|
||||||
|
searchParams.delete("viewport");
|
||||||
|
return viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseSuites(searchParams) {
|
||||||
|
if (searchParams.has("suite") || searchParams.has("suites")) {
|
||||||
|
if (searchParams.has("suite") && searchParams.has("suites"))
|
||||||
|
throw new Error("Params 'suite' and 'suites' can not be used together.");
|
||||||
|
const value = searchParams.get("suite") || searchParams.get("suites");
|
||||||
|
const suites = value.split(",");
|
||||||
|
if (suites.length === 0)
|
||||||
|
throw new Error("No suites selected");
|
||||||
|
searchParams.delete("suite");
|
||||||
|
searchParams.delete("suites");
|
||||||
|
return suites;
|
||||||
|
}
|
||||||
|
return defaultParams.suites;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseTags(searchParams) {
|
||||||
|
if (!searchParams.has("tags"))
|
||||||
|
return defaultParams.tags;
|
||||||
|
if (this.suites.length)
|
||||||
|
throw new Error("'suites' and 'tags' cannot be used together.");
|
||||||
|
const tags = searchParams.get("tags").split(",");
|
||||||
|
searchParams.delete("tags");
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseEnumParam(searchParams, paramKey, enumArray) {
|
||||||
|
if (!searchParams.has(paramKey))
|
||||||
|
return defaultParams[paramKey];
|
||||||
|
const value = searchParams.get(paramKey);
|
||||||
|
if (!enumArray.includes(value))
|
||||||
|
throw new Error(`Got invalid ${paramKey}: '${value}', choices are ${enumArray}`);
|
||||||
|
searchParams.delete(paramKey);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseShuffleSeed(searchParams) {
|
||||||
|
if (!searchParams.has("shuffleSeed"))
|
||||||
|
return defaultParams.shuffleSeed;
|
||||||
|
let shuffleSeed = searchParams.get("shuffleSeed");
|
||||||
|
if (shuffleSeed !== "off") {
|
||||||
|
if (shuffleSeed === "generate") {
|
||||||
|
shuffleSeed = Math.floor((Math.random() * 1) << 16);
|
||||||
|
console.log(`Generated a random suite order seed: ${shuffleSeed}`);
|
||||||
|
} else {
|
||||||
|
shuffleSeed = parseInt(shuffleSeed);
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(shuffleSeed))
|
||||||
|
throw new Error(`Invalid shuffle seed: '${shuffleSeed}', must be either 'off', 'generate' or an integer.`);
|
||||||
|
}
|
||||||
|
searchParams.delete("shuffleSeed");
|
||||||
|
return shuffleSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseConfig(searchParams) {
|
||||||
|
const config = searchParams.get("config") ?? "";
|
||||||
|
searchParams.delete("config");
|
||||||
|
if (config && !isValidJsonUrl(config))
|
||||||
|
throw new Error("Invalid config url passed in.");
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
toCompleteSearchParamsObject() {
|
||||||
|
return this.toSearchParamsObject(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
toSearchParamsObject(filter = true) {
|
||||||
|
const rawUrlParams = { __proto__: null };
|
||||||
|
for (const [key, value] of Object.entries(this)) {
|
||||||
|
// Handle composite values separately.
|
||||||
|
if (key === "viewport" || key === "suites" || key === "tags")
|
||||||
|
continue;
|
||||||
|
// Skip over default values.
|
||||||
|
if (filter && value === defaultParams[key])
|
||||||
|
continue;
|
||||||
|
rawUrlParams[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.viewport.width !== defaultParams.viewport.width || this.viewport.height !== defaultParams.viewport.height)
|
||||||
|
rawUrlParams.viewport = `${this.viewport.width}x${this.viewport.height}`;
|
||||||
|
|
||||||
|
if (this.suites.length) {
|
||||||
|
rawUrlParams.suites = this.suites.join(",");
|
||||||
|
} else if (this.tags.length) {
|
||||||
|
if (!(this.tags.length === 1 && this.tags[0] === "default"))
|
||||||
|
rawUrlParams.tags = this.tags.join(",");
|
||||||
|
} else {
|
||||||
|
rawUrlParams.suites = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URLSearchParams(rawUrlParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
toSearchParams() {
|
||||||
|
return this.toSearchParamsObject().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
isDefault() {
|
||||||
|
return this === defaultParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidJsonUrl(url) {
|
||||||
|
if (typeof url !== "string" || url.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(url, "http://www.example.com");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParams = new Params();
|
||||||
|
|
||||||
|
export let paramsError = null;
|
||||||
|
let maybeCustomParams = defaultParams;
|
||||||
|
if (globalThis?.location?.search) {
|
||||||
|
const searchParams = new URLSearchParams(globalThis.location.search);
|
||||||
|
try {
|
||||||
|
maybeCustomParams = new Params(searchParams, true);
|
||||||
|
} catch (e) {
|
||||||
|
paramsError = e;
|
||||||
|
console.error("Invalid URL Param", e, "\nUsing defaults as fallback:", maybeCustomParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const params = maybeCustomParams;
|
||||||
109
resources/shared/step-runner.mjs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs";
|
||||||
|
import { forceLayout } from "./helpers.mjs";
|
||||||
|
|
||||||
|
export class StepRunner {
|
||||||
|
#frame;
|
||||||
|
#page;
|
||||||
|
#params;
|
||||||
|
#suite;
|
||||||
|
#step;
|
||||||
|
#type;
|
||||||
|
|
||||||
|
constructor(frame, page, params, suite, step, type) {
|
||||||
|
this.#suite = suite;
|
||||||
|
this.#step = step;
|
||||||
|
this.#params = params;
|
||||||
|
this.#page = page;
|
||||||
|
this.#frame = frame;
|
||||||
|
this.#type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get page() {
|
||||||
|
return this.#page;
|
||||||
|
}
|
||||||
|
|
||||||
|
get step() {
|
||||||
|
return this.#step;
|
||||||
|
}
|
||||||
|
|
||||||
|
_runSyncStep(step, page) {
|
||||||
|
step.run(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runStep() {
|
||||||
|
// Prepare all mark labels outside the measuring loop.
|
||||||
|
const suiteName = this.#suite.name;
|
||||||
|
const stepName = this.#step.name;
|
||||||
|
const syncStartLabel = `${suiteName}.${stepName}-start`;
|
||||||
|
const syncEndLabel = `${suiteName}.${stepName}-sync-end`;
|
||||||
|
const asyncEndLabel = `${suiteName}.${stepName}-async-end`;
|
||||||
|
|
||||||
|
let syncTime;
|
||||||
|
let asyncStartTime;
|
||||||
|
let asyncTime;
|
||||||
|
|
||||||
|
const runSync = async () => {
|
||||||
|
if (this.#params.warmupBeforeSync) {
|
||||||
|
performance.mark("warmup-start");
|
||||||
|
const startTime = performance.now();
|
||||||
|
// Infinite loop for the specified ms.
|
||||||
|
while (performance.now() - startTime < this.#params.warmupBeforeSync)
|
||||||
|
continue;
|
||||||
|
performance.mark("warmup-end");
|
||||||
|
}
|
||||||
|
performance.mark(syncStartLabel);
|
||||||
|
const syncStartTime = performance.now();
|
||||||
|
|
||||||
|
if (this.#type === "async")
|
||||||
|
await this._runSyncStep(this.step, this.page);
|
||||||
|
else
|
||||||
|
this._runSyncStep(this.step, this.page);
|
||||||
|
|
||||||
|
const mark = performance.mark(syncEndLabel);
|
||||||
|
const syncEndTime = mark.startTime;
|
||||||
|
|
||||||
|
syncTime = syncEndTime - syncStartTime;
|
||||||
|
asyncStartTime = syncEndTime;
|
||||||
|
};
|
||||||
|
const measureAsync = () => {
|
||||||
|
// Some browsers don't immediately update the layout for paint.
|
||||||
|
// Force the layout here to ensure we're measuring the layout time.
|
||||||
|
//
|
||||||
|
// Note: This matches the behavior of Page.layout() in benchmark-runner.mjs.
|
||||||
|
// Since shared code cannot depend on Page, we duplicate the logic here,
|
||||||
|
// falling back to document.body for remote workloads that don't have a frame.
|
||||||
|
const body = this.#frame?.contentDocument?.body ?? document.body;
|
||||||
|
const value = forceLayout(body, this.#params.layoutMode);
|
||||||
|
body._leakedLayoutValue = value; // Prevent dead code elimination.
|
||||||
|
|
||||||
|
const asyncEndTime = performance.now();
|
||||||
|
performance.mark(asyncEndLabel);
|
||||||
|
|
||||||
|
asyncTime = asyncEndTime - asyncStartTime;
|
||||||
|
|
||||||
|
if (this.#params.warmupBeforeSync)
|
||||||
|
performance.measure("warmup", "warmup-start", "warmup-end");
|
||||||
|
performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel);
|
||||||
|
performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
|
||||||
|
const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType];
|
||||||
|
const scheduler = new schedulerClass(runSync, measureAsync, this.#params);
|
||||||
|
await scheduler.start();
|
||||||
|
return { syncTime, asyncTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsyncStepRunner extends StepRunner {
|
||||||
|
async _runSyncStep(step, page) {
|
||||||
|
await step.run(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STEP_RUNNER_LOOKUP = {
|
||||||
|
__proto__: null,
|
||||||
|
default: StepRunner,
|
||||||
|
async: AsyncStepRunner,
|
||||||
|
remote: StepRunner,
|
||||||
|
};
|
||||||
77
resources/shared/step-scheduler.mjs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
class StepScheduler {
|
||||||
|
constructor(syncCallback, asyncCallback, params) {
|
||||||
|
this._syncCallback = syncCallback;
|
||||||
|
this._asyncCallback = asyncCallback;
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this._params.waitBeforeSync)
|
||||||
|
setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync);
|
||||||
|
else
|
||||||
|
this._scheduleCallbacks(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RAFStepScheduler extends StepScheduler {
|
||||||
|
_scheduleCallbacks(resolve) {
|
||||||
|
requestAnimationFrame(() => this._syncCallback());
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._asyncCallback();
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsyncRAFStepScheduler extends StepScheduler {
|
||||||
|
static mc = new MessageChannel();
|
||||||
|
_scheduleCallbacks(resolve) {
|
||||||
|
let gotTimer = false;
|
||||||
|
let gotMessage = false;
|
||||||
|
let gotPromise = false;
|
||||||
|
|
||||||
|
const tryTriggerAsyncCallback = () => {
|
||||||
|
if (!gotTimer || !gotMessage || !gotPromise)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._asyncCallback();
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(async () => {
|
||||||
|
await this._syncCallback();
|
||||||
|
gotPromise = true;
|
||||||
|
tryTriggerAsyncCallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
gotTimer = true;
|
||||||
|
tryTriggerAsyncCallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
AsyncRAFStepScheduler.mc.port1.addEventListener(
|
||||||
|
"message",
|
||||||
|
async function () {
|
||||||
|
await Promise.resolve();
|
||||||
|
gotMessage = true;
|
||||||
|
tryTriggerAsyncCallback();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
AsyncRAFStepScheduler.mc.port1.start();
|
||||||
|
AsyncRAFStepScheduler.mc.port2.postMessage("speedometer");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STEP_SCHEDULER_LOOKUP = {
|
||||||
|
__proto__: null,
|
||||||
|
raf: RAFStepScheduler,
|
||||||
|
async: AsyncRAFStepScheduler,
|
||||||
|
};
|
||||||
12
resources/shared/todomvc-utils.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { params } from "./params.mjs";
|
||||||
|
|
||||||
|
export const defaultNumberOfItemsToAdd = 100;
|
||||||
|
export let numberOfItemsToAdd = defaultNumberOfItemsToAdd;
|
||||||
|
|
||||||
|
export function getNumberOfItemsToAdd() {
|
||||||
|
return numberOfItemsToAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleComplexityChange() {
|
||||||
|
numberOfItemsToAdd = Math.round(defaultNumberOfItemsToAdd * params.complexity);
|
||||||
|
}
|
||||||
734
resources/shared/translations.mjs
Normal file
|
|
@ -0,0 +1,734 @@
|
||||||
|
export const todos = {
|
||||||
|
en: [
|
||||||
|
"Electronic Granite Hat",
|
||||||
|
"Bespoke Soft Table",
|
||||||
|
"Ergonomic Fresh Bike",
|
||||||
|
"Luxurious Wooden Cheese",
|
||||||
|
"Gorgeous Fresh Pizza",
|
||||||
|
"Rustic Rubber Shirt",
|
||||||
|
"Modern Rubber Soap",
|
||||||
|
"Small Bronze Ball",
|
||||||
|
"Awesome Bronze Shoes",
|
||||||
|
"Bespoke Steel Chair",
|
||||||
|
"Practical Plastic Soap",
|
||||||
|
"Incredible Granite Bacon",
|
||||||
|
"Elegant Metal Keyboard",
|
||||||
|
"Electronic Wooden Sausages",
|
||||||
|
"Tasty Wooden Gloves",
|
||||||
|
"Luxurious Metal Cheese",
|
||||||
|
"Awesome Rubber Gloves",
|
||||||
|
"Sleek Soft Car",
|
||||||
|
"Licensed Fresh Salad",
|
||||||
|
"Ergonomic Frozen Towels",
|
||||||
|
"Modern Rubber Keyboard",
|
||||||
|
"Tasty Concrete Pizza",
|
||||||
|
"Handmade Plastic Chicken",
|
||||||
|
"Luxurious Rubber Chicken",
|
||||||
|
"Practical Soft Fish",
|
||||||
|
"Ergonomic Bronze Shirt",
|
||||||
|
"Handcrafted Plastic Bacon",
|
||||||
|
"Unbranded Plastic Pants",
|
||||||
|
"Modern Wooden Sausages",
|
||||||
|
"Handmade Steel Shoes",
|
||||||
|
"Rustic Steel Bike",
|
||||||
|
"Gorgeous Frozen Salad",
|
||||||
|
"Handmade Bronze Chicken",
|
||||||
|
"Sleek Granite Bike",
|
||||||
|
"Generic Concrete Sausages",
|
||||||
|
"Incredible Plastic Tuna",
|
||||||
|
"Bespoke Fresh Cheese",
|
||||||
|
"Electronic Fresh Bacon",
|
||||||
|
"Licensed Wooden Car",
|
||||||
|
"Recycled Fresh Fish",
|
||||||
|
"Incredible Fresh Shoes",
|
||||||
|
"Practical Soft Chips",
|
||||||
|
"Small Soft Chicken",
|
||||||
|
"Intelligent Fresh Mouse",
|
||||||
|
"Modern Metal Mouse",
|
||||||
|
"Tasty Granite Gloves",
|
||||||
|
"Awesome Rubber Bike",
|
||||||
|
"Small Steel Shirt",
|
||||||
|
"Refined Concrete Computer",
|
||||||
|
"Sleek Frozen Shirt",
|
||||||
|
"Intelligent Concrete Shoes",
|
||||||
|
"Handmade Rubber Car",
|
||||||
|
"Sleek Rubber Towels",
|
||||||
|
"Unbranded Concrete Hat",
|
||||||
|
"Incredible Plastic Fish",
|
||||||
|
"Practical Soft Gloves",
|
||||||
|
"Organic Stone Pizza",
|
||||||
|
"Generic Wooden Keyboard",
|
||||||
|
"Recycled Wooden Chips",
|
||||||
|
"Incredible Rubber Chips",
|
||||||
|
"Ergonomic Granite Shirt",
|
||||||
|
"Tasty Frozen Keyboard",
|
||||||
|
"Gorgeous Steel Soap",
|
||||||
|
"Luxurious Plastic Chair",
|
||||||
|
"Elegant Frozen Bike",
|
||||||
|
"Recycled Steel Chair",
|
||||||
|
"Modern Bronze Sausages",
|
||||||
|
"Elegant Wooden Cheese",
|
||||||
|
"Small Plastic Sausages",
|
||||||
|
"Luxurious Frozen Shoes",
|
||||||
|
"Sleek Plastic Sausages",
|
||||||
|
"Handcrafted Fresh Sausages",
|
||||||
|
"Incredible Soft Chair",
|
||||||
|
"Recycled Wooden Soap",
|
||||||
|
"Soft Rubber Duck",
|
||||||
|
"Licensed Concrete Tuna",
|
||||||
|
"Luxurious Granite Pants",
|
||||||
|
"Refined Rubber Keyboard",
|
||||||
|
"Handcrafted Plastic Computer",
|
||||||
|
"Practical Steel Salad",
|
||||||
|
"Incredible Soft Bacon",
|
||||||
|
"Practical Metal Fish",
|
||||||
|
"Elegant Rubber Shirt",
|
||||||
|
"Handcrafted Rubber Table",
|
||||||
|
"Gorgeous Wooden Table",
|
||||||
|
"Fantastic Steel Sausages",
|
||||||
|
"Small Soft Keyboard",
|
||||||
|
"Generic Steel Ball",
|
||||||
|
"Electronic Frozen Hat",
|
||||||
|
"Gorgeous Fresh Chair",
|
||||||
|
"Sleek Soft Sausages",
|
||||||
|
"Gorgeous Wooden Towels",
|
||||||
|
"Bespoke Granite Pizza",
|
||||||
|
"Generic Metal Salad",
|
||||||
|
"Handmade Rubber Cheese",
|
||||||
|
"Fantastic Steel Chair",
|
||||||
|
"Handcrafted Frozen Computer",
|
||||||
|
"Rustic Rubber Mouse",
|
||||||
|
"Sleek Granite Pizza",
|
||||||
|
"Gorgeous Plastic Keyboard",
|
||||||
|
],
|
||||||
|
"zh-cn": [
|
||||||
|
"电子花岗岩帽子",
|
||||||
|
"定制软桌",
|
||||||
|
"符合人体工程学新鲜自行车",
|
||||||
|
"豪华的木制奶酪",
|
||||||
|
"华丽的新鲜披萨",
|
||||||
|
"质朴的橡胶衬衫",
|
||||||
|
"现代橡胶肥皂",
|
||||||
|
"小青铜球",
|
||||||
|
"很棒的青铜鞋",
|
||||||
|
"定制钢椅",
|
||||||
|
"实用的塑料肥皂",
|
||||||
|
"令人难以置信的花岗岩培根",
|
||||||
|
"优雅的金属键盘",
|
||||||
|
"电子木香肠",
|
||||||
|
"美味的木制手套",
|
||||||
|
"豪华的金属奶酪",
|
||||||
|
"很棒的橡胶手套",
|
||||||
|
"光滑的柔软汽车",
|
||||||
|
"有执照的新鲜沙拉",
|
||||||
|
"人体工程学的冷冻毛巾",
|
||||||
|
"现代橡胶键盘",
|
||||||
|
"美味的混凝土披萨",
|
||||||
|
"手工塑料鸡",
|
||||||
|
"豪华橡胶鸡",
|
||||||
|
"实用的软鱼",
|
||||||
|
"人体工程学的青铜衬衫",
|
||||||
|
"手工制作的塑料培根",
|
||||||
|
"未品牌的塑料裤",
|
||||||
|
"现代木香肠",
|
||||||
|
"手工钢鞋",
|
||||||
|
"质朴的钢自行车",
|
||||||
|
"华丽的冷冻沙拉",
|
||||||
|
"手工铜鸡",
|
||||||
|
"光滑的花岗岩自行车",
|
||||||
|
"通用混凝土香肠",
|
||||||
|
"令人难以置信的塑料金枪鱼",
|
||||||
|
"定制新鲜奶酪",
|
||||||
|
"电子新鲜培根",
|
||||||
|
"有执照的木车",
|
||||||
|
"回收的新鲜鱼",
|
||||||
|
"令人难以置信的新鲜鞋",
|
||||||
|
"实用的软芯片",
|
||||||
|
"小鸡肉",
|
||||||
|
"智能新鲜鼠标",
|
||||||
|
"现代金属鼠标",
|
||||||
|
"美味的花岗岩手套",
|
||||||
|
"很棒的橡胶自行车",
|
||||||
|
"小钢衬衫",
|
||||||
|
"精制混凝土计算机",
|
||||||
|
"光滑的冷冻衬衫",
|
||||||
|
"智能混凝土鞋",
|
||||||
|
"手工橡胶车",
|
||||||
|
"光滑的橡胶毛巾",
|
||||||
|
"未品牌的混凝土帽子",
|
||||||
|
"令人难以置信的塑料鱼",
|
||||||
|
"实用的软手套",
|
||||||
|
"有机石比萨",
|
||||||
|
"通用木制键盘",
|
||||||
|
"再生木芯片",
|
||||||
|
"令人难以置信的橡胶芯片",
|
||||||
|
"人体工程学花岗岩衬衫",
|
||||||
|
"美味的冷冻键盘",
|
||||||
|
"华丽的钢肥皂",
|
||||||
|
"豪华的塑料椅",
|
||||||
|
"优雅的冷冻自行车",
|
||||||
|
"再生钢椅",
|
||||||
|
"现代铜香肠",
|
||||||
|
"优雅的木制奶酪",
|
||||||
|
"小塑料香肠",
|
||||||
|
"豪华的冷冻鞋",
|
||||||
|
"光滑的塑料香肠",
|
||||||
|
"手工制作的新鲜香肠",
|
||||||
|
"令人难以置信的柔软椅子",
|
||||||
|
"回收的木制肥皂",
|
||||||
|
"软橡皮鸭",
|
||||||
|
"许可的混凝土金枪鱼",
|
||||||
|
"豪华的花岗岩裤",
|
||||||
|
"精制橡胶键盘",
|
||||||
|
"手工制作的塑料计算机",
|
||||||
|
"实用的钢沙拉",
|
||||||
|
"令人难以置信的软培根",
|
||||||
|
"实用的金属鱼",
|
||||||
|
"优雅的橡胶衬衫",
|
||||||
|
"手工橡胶桌",
|
||||||
|
"华丽的木桌",
|
||||||
|
"奇妙的钢香肠",
|
||||||
|
"小软键盘",
|
||||||
|
"通用钢球",
|
||||||
|
"电子冷冻帽子",
|
||||||
|
"华丽的新鲜椅子",
|
||||||
|
"光滑的软香肠",
|
||||||
|
"华丽的木制毛巾",
|
||||||
|
"定制的花岗岩披萨",
|
||||||
|
"通用金属沙拉",
|
||||||
|
"手工橡胶奶酪",
|
||||||
|
"奇妙的钢椅",
|
||||||
|
"手工制作的冷冻计算机",
|
||||||
|
"质朴的橡胶老鼠",
|
||||||
|
"光滑的花岗岩披萨",
|
||||||
|
"华丽的塑料键盘",
|
||||||
|
],
|
||||||
|
ja: [
|
||||||
|
"電子花崗岩の帽子",
|
||||||
|
"オーダーメイドのソフトテーブル",
|
||||||
|
"人間工学に基づいた新鮮な自転車",
|
||||||
|
"豪華な木製チーズ",
|
||||||
|
"ゴージャスな新鮮なピザ",
|
||||||
|
"素朴なラバーシャツ",
|
||||||
|
"モダンゴム石鹸",
|
||||||
|
"小さなブロンズボール",
|
||||||
|
"素晴らしいブロンズシューズ",
|
||||||
|
"オーダーメイドのスチールチェア",
|
||||||
|
"実用的なプラスチック石鹸",
|
||||||
|
"信じられないほどの花崗岩のベーコン",
|
||||||
|
"エレガントなメタルキーボード",
|
||||||
|
"電子木製ソーセージ",
|
||||||
|
"おいしい木製手袋",
|
||||||
|
"豪華なメタルチーズ",
|
||||||
|
"素晴らしいラバーグローブ",
|
||||||
|
"洗練されたソフトカー",
|
||||||
|
"ライセンスされた新鮮なサラダ",
|
||||||
|
"人間工学に基づいた冷凍タオル",
|
||||||
|
"モダンなゴムキーボード",
|
||||||
|
"おいしいコンクリートピザ",
|
||||||
|
"手作りのプラスチックチキン",
|
||||||
|
"豪華なゴム鶏",
|
||||||
|
"実用的な柔らかい魚",
|
||||||
|
"人間工学に基づいたブロンズシャツ",
|
||||||
|
"手作りのプラスチックベーコン",
|
||||||
|
"ブランドのないプラスチックパンツ",
|
||||||
|
"モダンな木製ソーセージ",
|
||||||
|
"手作りのスチールシューズ",
|
||||||
|
"素朴なスチールバイク",
|
||||||
|
"ゴージャスな冷凍サラダ",
|
||||||
|
"手作りのブロンズチキン",
|
||||||
|
"洗練された花崗岩の自転車",
|
||||||
|
"一般的なコンクリートソーセージ",
|
||||||
|
"信じられないほどのプラスチックマグロ",
|
||||||
|
"オーダーメイドのフレッシュチーズ",
|
||||||
|
"電子新鮮なベーコン",
|
||||||
|
"ライセンスされた木製車",
|
||||||
|
"新鮮な魚をリサイクルしました",
|
||||||
|
"信じられないほどの新鮮な靴",
|
||||||
|
"実用的なソフトチップ",
|
||||||
|
"小さな柔らかい鶏",
|
||||||
|
"インテリジェントな新鮮なマウス",
|
||||||
|
"現代の金属マウス",
|
||||||
|
"おいしい花崗岩の手袋",
|
||||||
|
"素晴らしいラバーバイク",
|
||||||
|
"小さなスチールシャツ",
|
||||||
|
"洗練されたコンクリートコンピューター",
|
||||||
|
"洗練された冷凍シャツ",
|
||||||
|
"インテリジェントコンクリートシューズ",
|
||||||
|
"手作りのラバーカー",
|
||||||
|
"洗練されたゴムタオル",
|
||||||
|
"ブランドのないコンクリートの帽子",
|
||||||
|
"信じられないほどのプラスチックの魚",
|
||||||
|
"実用的なソフトグローブ",
|
||||||
|
"オーガニックストーンピザ",
|
||||||
|
"一般的な木製キーボード",
|
||||||
|
"リサイクルされた木製チップ",
|
||||||
|
"信じられないほどのゴムチップ",
|
||||||
|
"人間工学に基づいた花崗岩のシャツ",
|
||||||
|
"おいしい冷凍キーボード",
|
||||||
|
"ゴージャスなスチールソープ",
|
||||||
|
"豪華なプラスチック製の椅子",
|
||||||
|
"エレガントな冷凍自転車",
|
||||||
|
"リサイクルスチールチェア",
|
||||||
|
"モダンブロンズソーセージ",
|
||||||
|
"エレガントな木製チーズ",
|
||||||
|
"小さなプラスチックソーセージ",
|
||||||
|
"豪華な冷凍靴",
|
||||||
|
"なめらかなプラスチックソーセージ",
|
||||||
|
"手作りの新鮮なソーセージ",
|
||||||
|
"信じられないほどのソフトチェア",
|
||||||
|
"リサイクルされた木製石鹸",
|
||||||
|
"柔らかいラバーアヒル",
|
||||||
|
"ライセンスされたコンクリートマグロ",
|
||||||
|
"豪華な花崗岩のズボン",
|
||||||
|
"洗練されたゴムキーボード",
|
||||||
|
"手作りのプラスチックコンピューター",
|
||||||
|
"実用的なスチールサラダ",
|
||||||
|
"信じられないほどのソフトベーコン",
|
||||||
|
"実用的な金属魚",
|
||||||
|
"エレガントなラバーシャツ",
|
||||||
|
"手作りのゴムテーブル",
|
||||||
|
"ゴージャスな木製のテーブル",
|
||||||
|
"素晴らしいスチールソーセージ",
|
||||||
|
"小さなソフトキーボード",
|
||||||
|
"ジェネリックスチールボール",
|
||||||
|
"電子冷凍帽子",
|
||||||
|
"ゴージャスな新鮮な椅子",
|
||||||
|
"洗練された柔らかいソーセージ",
|
||||||
|
"ゴージャスな木製タオル",
|
||||||
|
"オーダーメイドの花崗岩のピザ",
|
||||||
|
"ジェネリックメタルサラダ",
|
||||||
|
"手作りのゴムチーズ",
|
||||||
|
"素晴らしいスチールチェア",
|
||||||
|
"手作りの冷凍コンピューター",
|
||||||
|
"素朴なゴムマウス",
|
||||||
|
"洗練された花崗岩のピザ",
|
||||||
|
"ゴージャスなプラスチックキーボード",
|
||||||
|
],
|
||||||
|
es: [
|
||||||
|
"Sombrero de granito electrónico",
|
||||||
|
"Mesa suave a medida",
|
||||||
|
"Bicicleta ergonómica fresca",
|
||||||
|
"Lujoso queso de madera",
|
||||||
|
"Hermosa pizza fresca",
|
||||||
|
"Camisa de goma rústica",
|
||||||
|
"Jabón de goma moderno",
|
||||||
|
"Bola de bronce pequeña",
|
||||||
|
"Zapatos de bronce impresionantes",
|
||||||
|
"Silla de acero a medida",
|
||||||
|
"Jabón de plástico práctico",
|
||||||
|
"Tocino de granito increíble",
|
||||||
|
"Teclado de metal elegante",
|
||||||
|
"Salchichas de madera electrónica",
|
||||||
|
"Sabrosos guantes de madera",
|
||||||
|
"Lujoso queso de metal",
|
||||||
|
"Guantes de goma impresionantes",
|
||||||
|
"Coche suave y elegante",
|
||||||
|
"Ensalada fresca con licencia",
|
||||||
|
"Toallas congeladas ergonómicas",
|
||||||
|
"Teclado de goma moderno",
|
||||||
|
"Pizza de hormigón sabrosa",
|
||||||
|
"Pollo de plástico hecho a mano",
|
||||||
|
"Pollo de goma lujoso",
|
||||||
|
"Pescado suave práctico",
|
||||||
|
"Camisa de bronce ergonómica",
|
||||||
|
"Tocino de plástico artesanal",
|
||||||
|
"Pantalones de plástico sin marca",
|
||||||
|
"Salchichas de madera modernas",
|
||||||
|
"Zapatos de acero hechos a mano",
|
||||||
|
"Bicicleta de acero rústica",
|
||||||
|
"Hermosa ensalada congelada",
|
||||||
|
"Pollo bronce hecho a mano",
|
||||||
|
"Bicicleta de granito elegante",
|
||||||
|
"Salchichas de concreto genérico",
|
||||||
|
"Atún de plástico increíble",
|
||||||
|
"Queso fresco a medida",
|
||||||
|
"Tocino fresco electrónico",
|
||||||
|
"Coche de madera con licencia",
|
||||||
|
"Pescado reciclado",
|
||||||
|
"Increíbles zapatos frescos",
|
||||||
|
"Chips suaves prácticos",
|
||||||
|
"Pollo pequeño y suave",
|
||||||
|
"Ratón fresco inteligente",
|
||||||
|
"Ratón de metal moderno",
|
||||||
|
"Sabrosos guantes de granito",
|
||||||
|
"Impresionante bicicleta de goma",
|
||||||
|
"Camisa de acero pequeña",
|
||||||
|
"Ordenador de concreto refinada",
|
||||||
|
"Camisa elegante congelada",
|
||||||
|
"Zapatos de concreto inteligentes",
|
||||||
|
"Coche de goma hecho a mano",
|
||||||
|
"Toallas de goma elegantes",
|
||||||
|
"Sombrero de hormigón sin marca",
|
||||||
|
"Pescado de plástico increíble",
|
||||||
|
"Guantes suaves prácticos",
|
||||||
|
"Pizza de piedra orgánica",
|
||||||
|
"Teclado de madera genérico",
|
||||||
|
"Chips de madera reciclados",
|
||||||
|
"Increíbles chips de goma",
|
||||||
|
"Camisa de granito ergonómico",
|
||||||
|
"Sabroso teclado congelado",
|
||||||
|
"Hermoso jabón de acero",
|
||||||
|
"Lujosa silla de plástico",
|
||||||
|
"Bicicleta congelada elegante",
|
||||||
|
"Silla de acero reciclada",
|
||||||
|
"Salchichas de bronce modernas",
|
||||||
|
"Elegante queso de madera",
|
||||||
|
"Pequeñas salchichas de plástico",
|
||||||
|
"Zapatos congelados lujosos",
|
||||||
|
"Salchichas de plástico elegantes",
|
||||||
|
"Salchichas frescas artesanales",
|
||||||
|
"Increíble silla suave",
|
||||||
|
"Jabón de madera reciclado",
|
||||||
|
"Pato de goma suave",
|
||||||
|
"Atún de hormigón con licencia",
|
||||||
|
"Pantalones de granito lujosos",
|
||||||
|
"Teclado de goma refinado",
|
||||||
|
"Computadora de plástico hecha a mano",
|
||||||
|
"Ensalada de acero práctica",
|
||||||
|
"Bocino suave increíble",
|
||||||
|
"Pescado de metal práctico",
|
||||||
|
"Elegante camisa de goma",
|
||||||
|
"Mesa de goma artesanal",
|
||||||
|
"Hermosa mesa de madera",
|
||||||
|
"Fantásticas salchichas de acero",
|
||||||
|
"Pequeño teclado suave",
|
||||||
|
"Bola de acero genérica",
|
||||||
|
"Sombrero de congelado",
|
||||||
|
"Hermosa silla fresca",
|
||||||
|
"Salchichas suaves y elegantes",
|
||||||
|
"Hermosas toallas de madera",
|
||||||
|
"Pizza de granito a medida",
|
||||||
|
"Ensalada de metal genérica",
|
||||||
|
"Queso de goma hecho a mano",
|
||||||
|
"Fantástica silla de acero",
|
||||||
|
"Computadora congelada hecha a mano",
|
||||||
|
"Ratón de goma rústica",
|
||||||
|
"Pizza de granito elegante",
|
||||||
|
"Hermoso teclado de plástico",
|
||||||
|
],
|
||||||
|
de: [
|
||||||
|
"Elektronischer Granithut",
|
||||||
|
"Maßgeschneiderter weicher Tisch",
|
||||||
|
"Ergonomisches frisches Fahrrad",
|
||||||
|
"Luxuriöser Holzkäse",
|
||||||
|
"Wunderschöne frische Pizza",
|
||||||
|
"Rustikales Gummihemd",
|
||||||
|
"Moderne Gummiseife",
|
||||||
|
"Kleine Bronzekugel",
|
||||||
|
"Tolle Bronzeschuhe",
|
||||||
|
"Maßgeschneiderter Stahlstuhl",
|
||||||
|
"Praktische Plastikseife",
|
||||||
|
"Unglaublicher Granitspeck",
|
||||||
|
"Elegante Metall -Tastatur",
|
||||||
|
"Elektronische Holzwurst",
|
||||||
|
"Leckere Holzhandschuhe",
|
||||||
|
"Luxuriöser Metallkäse",
|
||||||
|
"Super Gummihandschuhe",
|
||||||
|
"Schlankes weiches Auto",
|
||||||
|
"Lizenzierter frischer Salat",
|
||||||
|
"Ergonomische gefrorene Handtücher",
|
||||||
|
"Moderne Gummi Tastatur",
|
||||||
|
"Leckere Betonpizza",
|
||||||
|
"Handgefertigtes Plastikhuhn",
|
||||||
|
"Luxuriöses Gummihähnchen",
|
||||||
|
"Praktischer weicher Fisch",
|
||||||
|
"Ergonomisches Bronzehemd",
|
||||||
|
"Handgefertigter Plastikspeck",
|
||||||
|
"Plastikhosen ohne Marken",
|
||||||
|
"Moderne Holz Würste",
|
||||||
|
"Handgefertigte Stahlschuhe",
|
||||||
|
"Rustikales Stahlrad",
|
||||||
|
"Wunderschöner gefrorener Salat",
|
||||||
|
"Handgefertigtes Bronze Hühnchen",
|
||||||
|
"Schlankes Granitrad",
|
||||||
|
"Generische Beton Würste",
|
||||||
|
"Unglaublicher Plastik Thunfisch",
|
||||||
|
"Maßgeschneiderter frischer Käse",
|
||||||
|
"Elektronischer frischer Speck",
|
||||||
|
"Lizenziertes Holzauto",
|
||||||
|
"Recycelter frischer Fisch",
|
||||||
|
"Unglaubliche frische Schuhe",
|
||||||
|
"Praktische weiche Chips",
|
||||||
|
"Kleines weiches Huhn",
|
||||||
|
"Intelligente frische Maus",
|
||||||
|
"Moderne Metallmaus",
|
||||||
|
"Leckere Granithandschuhe",
|
||||||
|
"Super Gummi Fahrrad",
|
||||||
|
"Kleines Stahlhemd",
|
||||||
|
"Raffinierter Betoncomputer",
|
||||||
|
"Schlankes gefrorenes Hemd",
|
||||||
|
"Intelligente Betonschuhe",
|
||||||
|
"Handgefertigtes Gummiauto",
|
||||||
|
"Schlanke Gummi Handtücher",
|
||||||
|
"Betonhut ohne Markenzeichen",
|
||||||
|
"Unglaublicher Plastikfisch",
|
||||||
|
"Praktische weiche Handschuhe",
|
||||||
|
"Bio Steinpizza",
|
||||||
|
"Generische hölzerne Tastatur",
|
||||||
|
"Recycelte Holzchips",
|
||||||
|
"Unglaubliche Gummischchips",
|
||||||
|
"Ergonomisches Granithemd",
|
||||||
|
"Leckere gefrorene Tastatur",
|
||||||
|
"Wunderschöne Stahlseife",
|
||||||
|
"Luxuriöser Plastikstuhl",
|
||||||
|
"Elegantes gefrorenes Fahrrad",
|
||||||
|
"Recycelter Stahlstuhl",
|
||||||
|
"Moderne Bronze Würste",
|
||||||
|
"Eleganter Holzkäse",
|
||||||
|
"Kleine Plastikwurst",
|
||||||
|
"Luxuriöse gefrorene Schuhe",
|
||||||
|
"Schlanke Plastikwurst",
|
||||||
|
"Handgefertigte frische Würste",
|
||||||
|
"Unglaublicher weicher Stuhl",
|
||||||
|
"Recycelte Holzseife",
|
||||||
|
"Weiche Gummi Ente",
|
||||||
|
"Lizenzierter Beton Thunfisch",
|
||||||
|
"Luxuriöse Granithosen",
|
||||||
|
"Raffinierte Gummi Tastatur",
|
||||||
|
"Handgefertigter Kunststoffcomputer",
|
||||||
|
"Praktischer Stahlsalat",
|
||||||
|
"Unglaublicher weicher Speck",
|
||||||
|
"Praktische Metallfische",
|
||||||
|
"Elegantes Gummihemd",
|
||||||
|
"Handgefertigter Gummi Tisch",
|
||||||
|
"Wunderschöner Holztisch",
|
||||||
|
"Fantastische Stahlwurst",
|
||||||
|
"Kleine weiche Tastatur",
|
||||||
|
"Generischer Stahlkugel",
|
||||||
|
"Elektronischer Gefrorenhut",
|
||||||
|
"Wunderschöner frischer Stuhl",
|
||||||
|
"Schlanke weiche Würste",
|
||||||
|
"Wunderschöne Holztücher",
|
||||||
|
"Maßgeschneiderte Granitpizza",
|
||||||
|
"Generischer Metallsalat",
|
||||||
|
"Handgefertigter Gummi Käse",
|
||||||
|
"Fantastischer Stahlstuhl",
|
||||||
|
"Handgefertigter gefrorener Computer",
|
||||||
|
"Rustikale Gummi Maus",
|
||||||
|
"Schlanke Granitpizza",
|
||||||
|
"Wunderschöne Plastiktastatur",
|
||||||
|
],
|
||||||
|
ru: [
|
||||||
|
"Электронная гранитная шляпа",
|
||||||
|
"Беспокойный мягкий стол",
|
||||||
|
"Эргономичный свежий велосипед",
|
||||||
|
"Роскошный деревянный сыр",
|
||||||
|
"Великолепная свежая пицца",
|
||||||
|
"Деревенская резиновая рубашка",
|
||||||
|
"Современное резиновое мыло",
|
||||||
|
"Маленький бронзовый мяч",
|
||||||
|
"Потрясающие бронзовые туфли",
|
||||||
|
"Стальное кресло на заказ",
|
||||||
|
"Практическое пластиковое мыло",
|
||||||
|
"Невероятный гранитный бекон",
|
||||||
|
"Элегантная металлическая клавиатура",
|
||||||
|
"Электронные деревянные колбасы",
|
||||||
|
"Вкусные деревянные перчатки",
|
||||||
|
"Роскошный металлический сыр",
|
||||||
|
"Потрясающие резиновые перчатки",
|
||||||
|
"Гладкая мягкая машина",
|
||||||
|
"Лицензированный свежий салат",
|
||||||
|
"Эргономичные замороженные полотенца",
|
||||||
|
"Современная резиновая клавиатура",
|
||||||
|
"Вкусная бетонная пицца",
|
||||||
|
"Пластиковая курица ручной работы",
|
||||||
|
"Роскошная резиновая курица",
|
||||||
|
"Практическая мягкая рыба",
|
||||||
|
"Эргономичная бронзовая рубашка",
|
||||||
|
"Пластиковый бекон из ручной работы",
|
||||||
|
"Непревзойденные пластиковые брюки",
|
||||||
|
"Современные деревянные колбасы",
|
||||||
|
"Стальные туфли ручной работы",
|
||||||
|
"Деревенский стальный велосипед",
|
||||||
|
"Великолепный замороженный салат",
|
||||||
|
"Бронзовая курица ручной работы",
|
||||||
|
"Гладкий гранитный велосипед",
|
||||||
|
"Общие бетонные колбасы",
|
||||||
|
"Невероятный пластиковый тунец",
|
||||||
|
"Созданный свежий сыр",
|
||||||
|
"Электронный свежий бекон",
|
||||||
|
"Лицензированный деревянный автомобиль",
|
||||||
|
"Переработанная свежая рыба",
|
||||||
|
"Невероятные свежие туфли",
|
||||||
|
"Практические мягкие чипсы",
|
||||||
|
"Маленькая мягкая курица",
|
||||||
|
"Интеллектуальная свежая мышь",
|
||||||
|
"Современная металлическая мышь",
|
||||||
|
"Вкусные гранитные перчатки",
|
||||||
|
"Потрясающий резиновый велосипед",
|
||||||
|
"Маленькая стальная рубашка",
|
||||||
|
"Изысканный бетонный компьютер",
|
||||||
|
"Гладкая замороженная рубашка",
|
||||||
|
"Интеллектуальные бетонные туфли",
|
||||||
|
"Резиновый автомобиль ручной работы",
|
||||||
|
"Гладкие резиновые полотенца",
|
||||||
|
"Непревзойденная бетонная шляпа",
|
||||||
|
"Невероятная пластиковая рыба",
|
||||||
|
"Практические мягкие перчатки",
|
||||||
|
"Органическая камня пицца",
|
||||||
|
"Общая деревянная клавиатура",
|
||||||
|
"Переработанные деревянные чипсы",
|
||||||
|
"Невероятные резиновые чипсы",
|
||||||
|
"Эргономичная гранитная рубашка",
|
||||||
|
"Вкусная замороженная клавиатура",
|
||||||
|
"Великолепное стальное мыло",
|
||||||
|
"Роскошное пластиковое кресло",
|
||||||
|
"Элегантный замороженный велосипед",
|
||||||
|
"Переработанное стальное кресло",
|
||||||
|
"Современные бронзовые колбасы",
|
||||||
|
"Элегантный деревянный сыр",
|
||||||
|
"Маленькие пластиковые колбаски",
|
||||||
|
"Роскошные замороженные туфли",
|
||||||
|
"Гладкие пластиковые колбаски",
|
||||||
|
"Свежие колбаски ручной работы",
|
||||||
|
"Невероятный мягкий стул",
|
||||||
|
"Переработанное деревянное мыло",
|
||||||
|
"Мягкая резиновая утка",
|
||||||
|
"Лицензированный бетонный тунец",
|
||||||
|
"Роскошные гранитные штаны",
|
||||||
|
"Рафинированная резиновая клавиатура",
|
||||||
|
"Пластиковый компьютер ручной работы",
|
||||||
|
"Практический стальной салат",
|
||||||
|
"Невероятный мягкий бекон",
|
||||||
|
"Практическая металлическая рыба",
|
||||||
|
"Элегантная резиновая рубашка",
|
||||||
|
"Ручной резиновый стол",
|
||||||
|
"Великолепный деревянный стол",
|
||||||
|
"Фантастические стальные колбаски",
|
||||||
|
"Небольшая мягкая клавиатура",
|
||||||
|
"Общий стальной шар",
|
||||||
|
"Электронная замороженная шляпа",
|
||||||
|
"Великолепный свежий стул",
|
||||||
|
"Гладкие мягкие колбаски",
|
||||||
|
"Великолепные деревянные полотенца",
|
||||||
|
"Сделанная на заказ гранитная пицца",
|
||||||
|
"Общий металлический салат",
|
||||||
|
"Резиновый сыр ручной работы",
|
||||||
|
"Фантастическое стальное кресло",
|
||||||
|
"Замороженный компьютер ручной работы",
|
||||||
|
"Деревенская резиновая мышь",
|
||||||
|
"Главная гранитная пицца",
|
||||||
|
"Великолепная пластиковая клавиатура",
|
||||||
|
],
|
||||||
|
emoji: [
|
||||||
|
"Electronic Granite Hat 👆🏻",
|
||||||
|
"Bespoke Soft 🍷 Table",
|
||||||
|
"Ergonomic Fresh Bike 😚😚",
|
||||||
|
"Luxurious 🍉 Wooden Cheese 🍮",
|
||||||
|
"Gorgeous Fresh Pizza ⛔",
|
||||||
|
"Rustic 💪🏽 Rubber Shirt",
|
||||||
|
"Modern Rubber 🍀 Soap",
|
||||||
|
"👍 Small Bronze Ball 👍",
|
||||||
|
"Awesome Bronze Shoes 😎",
|
||||||
|
"Bespoke 👈🏽 Steel Chair",
|
||||||
|
"Practical Plastic 💋 💋 Soap",
|
||||||
|
"🙌🏻 Incredible Granite Bacon",
|
||||||
|
"🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃",
|
||||||
|
"Electronic Wooden Sausages 🌷",
|
||||||
|
"Tasty 🍺 Wooden Gloves",
|
||||||
|
"🏖️ Luxurious Metal Cheese",
|
||||||
|
"Awesome Rubber 😉 Gloves",
|
||||||
|
"Sleek Soft Car 💁🏻♂️",
|
||||||
|
"Licensed 👏👏👏 Fresh Salad",
|
||||||
|
"Ergonomic Frozen Towels 🐇",
|
||||||
|
"🖐🏻 Modern Rubber Keyboard",
|
||||||
|
"Tasty Concrete Pizza ✨✨",
|
||||||
|
"Handmade 😘 Plastic 😘 Chicken 😘",
|
||||||
|
"🏁 Luxurious Rubber Chicken 🏁",
|
||||||
|
"Practical Soft Fish 🤍",
|
||||||
|
"Ergonomic Bronze Shirt 😍",
|
||||||
|
"😸😸 Handcrafted 🐻 Plastic Bacon",
|
||||||
|
"Unbranded 🐭 Plastic Pants",
|
||||||
|
"🤘 Modern 🤘 Wooden 🤘 Sausages",
|
||||||
|
"Handmade Steel Shoes 👍",
|
||||||
|
"Rustic 🧁🧁 Steel Bike",
|
||||||
|
"Gorgeous Frozen Salad 👩💻",
|
||||||
|
"Handmade Bronze Chicken 😮😸",
|
||||||
|
"Sleek 🍐 Granite Bike",
|
||||||
|
" ❌ ❌ Generic Concrete Sausages",
|
||||||
|
"Incredible 🍉 Plastic Tuna",
|
||||||
|
"Bespoke Fresh Cheese 😘",
|
||||||
|
"💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡",
|
||||||
|
"Licensed 🍐 Wooden 🍅 Car ",
|
||||||
|
"Recycled Fresh Fish 🤡",
|
||||||
|
"📞 Incredible Fresh Shoes",
|
||||||
|
"Practical 🐻🐻 Soft Chips",
|
||||||
|
"Small 💝 Soft Chicken",
|
||||||
|
" 💝 Intelligent Fresh Mouse 💝",
|
||||||
|
"Modern Metal 🧵 Mouse",
|
||||||
|
"🦈 Tasty Granite Gloves",
|
||||||
|
"Awesome Rubber Bike 😡😡😡😡",
|
||||||
|
"🚮 Small Steel Shirt 🚮",
|
||||||
|
"Refined 🌲 Concrete Computer",
|
||||||
|
"Sleek Frozen Shirt 👨🦰",
|
||||||
|
"Intelligent Concrete ➗➗➗ Shoes",
|
||||||
|
"🏅Handmade Rubber Car",
|
||||||
|
"Sleek 👨🏼🌾 Rubber 👨🏼🌾 Towels",
|
||||||
|
"Unbranded Concrete Hat🎇",
|
||||||
|
"🌀🌀Incredible Plastic Fish",
|
||||||
|
"Practical Soft Gloves🌶️ 🌶️",
|
||||||
|
"Organic 🍞 Stone 🔽 Pizza 🥴",
|
||||||
|
"Generic Wooden Keyboard 💙",
|
||||||
|
"Recycled 🔴 Wooden Chips",
|
||||||
|
"Incredible Rubber Chips 🍹",
|
||||||
|
"🌵 Ergonomic Granite Shirt",
|
||||||
|
"Tasty Frozen 🦄 Keyboard",
|
||||||
|
"🍣 Gorgeous Steel 🥯 Soap",
|
||||||
|
"Luxurious Plastic Chair 🧑🦰",
|
||||||
|
"Elegant Frozen 🧑🦰 Bike",
|
||||||
|
"Recycled 🟠🟠 Steel Chair",
|
||||||
|
"⭐⭐ Modern ⭐ Bronze ⭐ Sausages",
|
||||||
|
"Elegant Wooden Cheese🤘",
|
||||||
|
"Small 🎎 Plastic 🛩️ Sausages",
|
||||||
|
"*️⃣*️⃣*️⃣*️⃣ Luxurious Frozen Shoes",
|
||||||
|
"Sleek Plastic Sausages 🚩",
|
||||||
|
"Handcrafted Fresh 💮 Sausages",
|
||||||
|
"Incredible 🤢🤢 Soft Chair",
|
||||||
|
"🇬🇪 Recycled 🇲🇺 Wooden Soap",
|
||||||
|
"Soft 🦌 Rubber Duck 🐥",
|
||||||
|
"Licensed Concrete Tuna 👎👎",
|
||||||
|
"Luxurious Granite 💝 Pants",
|
||||||
|
"Refined Rubber Keyboard 💝",
|
||||||
|
"👌🏻👌🏻 Handcrafted Plastic Computer",
|
||||||
|
"Practical Steel 🐪 Salad",
|
||||||
|
"Incredible Soft Bacon 🌺",
|
||||||
|
"Practical Metal 🥊 Fish",
|
||||||
|
"Elegant 👩🏾❤️💋👨🏽👩🏾❤️💋👨🏽 Rubber Shirt",
|
||||||
|
"🛺 Handcrafted Rubber Table",
|
||||||
|
"Gorgeous 🦙 Wooden Table 🦙",
|
||||||
|
"🍉 Fantastic Steel Sausages",
|
||||||
|
"Small Soft Keyboard👟",
|
||||||
|
"Generic 🦙🦙 Steel Ball",
|
||||||
|
"Electronic Frozen Hat ✌🏾✌🏾✌🏾✌🏾✌🏾",
|
||||||
|
"Gorgeous 🍏 Fresh Chair",
|
||||||
|
"Sleek Soft 💧 Sausages",
|
||||||
|
"Gorgeous Wooden Towels 🍿",
|
||||||
|
"Bespoke 🌅 Granite Pizza",
|
||||||
|
"Generic Metal Salad 🎗️",
|
||||||
|
"✨ Handmade ✨ Rubber ✨ Cheese ✨",
|
||||||
|
"Fantastic 🐥 🌺 🤷🏾 Steel Chair",
|
||||||
|
"Handcrafted Frozen Computer 🛡️ 🧸 🐓",
|
||||||
|
"🐹 Rustic Rubber Mouse",
|
||||||
|
"💠 Sleek Granite Pizza 💠",
|
||||||
|
"Gorgeous 🧝🏻♂️ Plastic Keyboard",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTodoText = {
|
||||||
|
en: "Something to do",
|
||||||
|
"zh-cn": "做某事",
|
||||||
|
ja: "何かをする必要がある",
|
||||||
|
es: "Algo que hacer",
|
||||||
|
de: "Etwas zu tun",
|
||||||
|
ru: "Кое-что сделать",
|
||||||
|
emoji: "Something to do 😊",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultLanguage = "en";
|
||||||
|
|
||||||
|
export function getTodoText(lang = "en", index) {
|
||||||
|
const todosSelection = todos[lang];
|
||||||
|
const currentIndex = index % todosSelection.length;
|
||||||
|
return todosSelection[currentIndex];
|
||||||
|
}
|
||||||
2
resources/shared/workload-testing-utils.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./helpers.mjs";
|
||||||
|
export * from "./benchmark.mjs";
|
||||||
127
resources/statistics.mjs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012, 2013 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions
|
||||||
|
* are met:
|
||||||
|
* 1. Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
||||||
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||||
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function max(values) {
|
||||||
|
return Math.max.apply(Math, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function min(values) {
|
||||||
|
return Math.min.apply(Math, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sum(values) {
|
||||||
|
return values.reduce((a, b) => a + b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function product(values) {
|
||||||
|
return values.reduce((a, b) => a * b, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function squareSum(values) {
|
||||||
|
return values.reduce((sum, value) => sum + value * value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// With sum and sum of squares, we can compute the sample standard deviation in O(1).
|
||||||
|
// See https://rniwa.com/2012-11-10/sample-standard-deviation-in-terms-of-sum-and-square-sum-of-samples/
|
||||||
|
export function sampleStandardDeviation(numberOfSamples, sum, squareSum) {
|
||||||
|
if (numberOfSamples < 2)
|
||||||
|
return 0;
|
||||||
|
return Math.sqrt(squareSum / (numberOfSamples - 1) - (sum * sum) / (numberOfSamples - 1) / numberOfSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportedConfidenceLevels() {
|
||||||
|
const supportedLevels = [];
|
||||||
|
for (let quantile in tDistributionInverseCDF)
|
||||||
|
supportedLevels.push((1 - (1 - quantile) * 2).toFixed(2));
|
||||||
|
return supportedLevels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the delta d s.t. (mean - d, mean + d) is the confidence interval with the specified confidence level in O(1).
|
||||||
|
export function confidenceIntervalDelta(confidenceLevel, numberOfSamples, sum, squareSum) {
|
||||||
|
const probability = 1 - (1 - confidenceLevel) / 2;
|
||||||
|
if (!(probability in tDistributionInverseCDF)) {
|
||||||
|
const supportedIntervals = supportedConfidenceLevels().map((level) => `${level * 100}%`);
|
||||||
|
throw `We only support ${supportedIntervals.join(", ")} confidence intervals.`;
|
||||||
|
}
|
||||||
|
if (numberOfSamples - 2 < 0)
|
||||||
|
return NaN;
|
||||||
|
|
||||||
|
const cdfForProbability = tDistributionInverseCDF[probability];
|
||||||
|
let degreesOfFreedom = numberOfSamples - 1;
|
||||||
|
if (degreesOfFreedom > cdfForProbability.length)
|
||||||
|
degreesOfFreedom = cdfForProbability.length - 1;
|
||||||
|
|
||||||
|
// tDistributionQuantile(degreesOfFreedom, confidenceLevel) * sampleStandardDeviation / sqrt(numberOfSamples) * S/sqrt(numberOfSamples)
|
||||||
|
const quantile = cdfForProbability[degreesOfFreedom - 1]; // The first entry is for the one degree of freedom.
|
||||||
|
return (quantile * sampleStandardDeviation(numberOfSamples, sum, squareSum)) / Math.sqrt(numberOfSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confidenceInterval(values, probability) {
|
||||||
|
const sumValue = sum(values);
|
||||||
|
const mean = sumValue / values.length;
|
||||||
|
const delta = confidenceIntervalDelta(probability || 0.95, values.length, sumValue, squareSum(values));
|
||||||
|
return [mean - delta, mean + delta];
|
||||||
|
}
|
||||||
|
|
||||||
|
// See http://en.wikipedia.org/wiki/Student's_t-distribution#Table_of_selected_values
|
||||||
|
// This table contains one sided (a.k.a. tail) values.
|
||||||
|
var tDistributionInverseCDF = {
|
||||||
|
0.9: [
|
||||||
|
3.077684, 1.885618, 1.637744, 1.533206, 1.475884, 1.439756, 1.414924, 1.396815, 1.383029, 1.372184, 1.36343, 1.356217, 1.350171, 1.34503, 1.340606, 1.336757, 1.333379, 1.330391, 1.327728, 1.325341, 1.323188, 1.321237, 1.31946, 1.317836,
|
||||||
|
1.316345, 1.314972, 1.313703, 1.312527, 1.311434, 1.310415, 1.309464, 1.308573, 1.307737, 1.306952, 1.306212, 1.305514, 1.304854, 1.30423, 1.303639, 1.303077, 1.302543, 1.302035, 1.301552, 1.30109, 1.300649, 1.300228, 1.299825, 1.299439,
|
||||||
|
1.299069, 1.298714,
|
||||||
|
|
||||||
|
1.298373, 1.298045, 1.29773, 1.297426, 1.297134, 1.296853, 1.296581, 1.296319, 1.296066, 1.295821, 1.295585, 1.295356, 1.295134, 1.29492, 1.294712, 1.294511, 1.294315, 1.294126, 1.293942, 1.293763, 1.293589, 1.293421, 1.293256, 1.293097,
|
||||||
|
1.292941, 1.29279, 1.292643, 1.2925, 1.29236, 1.292224, 1.292091, 1.291961, 1.291835, 1.291711, 1.291591, 1.291473, 1.291358, 1.291246, 1.291136, 1.291029, 1.290924, 1.290821, 1.290721, 1.290623, 1.290527, 1.290432, 1.29034, 1.29025,
|
||||||
|
1.290161, 1.290075,
|
||||||
|
],
|
||||||
|
0.95: [
|
||||||
|
6.313752, 2.919986, 2.353363, 2.131847, 2.015048, 1.94318, 1.894579, 1.859548, 1.833113, 1.812461, 1.795885, 1.782288, 1.770933, 1.76131, 1.75305, 1.745884, 1.739607, 1.734064, 1.729133, 1.724718, 1.720743, 1.717144, 1.713872, 1.710882,
|
||||||
|
1.708141, 1.705618, 1.703288, 1.701131, 1.699127, 1.697261, 1.695519, 1.693889, 1.69236, 1.690924, 1.689572, 1.688298, 1.687094, 1.685954, 1.684875, 1.683851, 1.682878, 1.681952, 1.681071, 1.68023, 1.679427, 1.67866, 1.677927, 1.677224,
|
||||||
|
1.676551, 1.675905,
|
||||||
|
|
||||||
|
1.675285, 1.674689, 1.674116, 1.673565, 1.673034, 1.672522, 1.672029, 1.671553, 1.671093, 1.670649, 1.670219, 1.669804, 1.669402, 1.669013, 1.668636, 1.668271, 1.667916, 1.667572, 1.667239, 1.666914, 1.6666, 1.666294, 1.665996, 1.665707,
|
||||||
|
1.665425, 1.665151, 1.664885, 1.664625, 1.664371, 1.664125, 1.663884, 1.663649, 1.66342, 1.663197, 1.662978, 1.662765, 1.662557, 1.662354, 1.662155, 1.661961, 1.661771, 1.661585, 1.661404, 1.661226, 1.661052, 1.660881, 1.660715, 1.660551,
|
||||||
|
1.660391, 1.660234,
|
||||||
|
],
|
||||||
|
0.975: [
|
||||||
|
12.706205, 4.302653, 3.182446, 2.776445, 2.570582, 2.446912, 2.364624, 2.306004, 2.262157, 2.228139, 2.200985, 2.178813, 2.160369, 2.144787, 2.13145, 2.119905, 2.109816, 2.100922, 2.093024, 2.085963, 2.079614, 2.073873, 2.068658, 2.063899,
|
||||||
|
2.059539, 2.055529, 2.051831, 2.048407, 2.04523, 2.042272, 2.039513, 2.036933, 2.034515, 2.032245, 2.030108, 2.028094, 2.026192, 2.024394, 2.022691, 2.021075, 2.019541, 2.018082, 2.016692, 2.015368, 2.014103, 2.012896, 2.011741, 2.010635,
|
||||||
|
2.009575, 2.008559,
|
||||||
|
|
||||||
|
2.007584, 2.006647, 2.005746, 2.004879, 2.004045, 2.003241, 2.002465, 2.001717, 2.000995, 2.000298, 1.999624, 1.998972, 1.998341, 1.99773, 1.997138, 1.996564, 1.996008, 1.995469, 1.994945, 1.994437, 1.993943, 1.993464, 1.992997, 1.992543,
|
||||||
|
1.992102, 1.991673, 1.991254, 1.990847, 1.99045, 1.990063, 1.989686, 1.989319, 1.98896, 1.98861, 1.988268, 1.987934, 1.987608, 1.98729, 1.986979, 1.986675, 1.986377, 1.986086, 1.985802, 1.985523, 1.985251, 1.984984, 1.984723, 1.984467,
|
||||||
|
1.984217, 1.983972,
|
||||||
|
],
|
||||||
|
0.99: [
|
||||||
|
31.820516, 6.964557, 4.540703, 3.746947, 3.36493, 3.142668, 2.997952, 2.896459, 2.821438, 2.763769, 2.718079, 2.680998, 2.650309, 2.624494, 2.60248, 2.583487, 2.566934, 2.55238, 2.539483, 2.527977, 2.517648, 2.508325, 2.499867, 2.492159,
|
||||||
|
2.485107, 2.47863, 2.47266, 2.46714, 2.462021, 2.457262, 2.452824, 2.448678, 2.444794, 2.44115, 2.437723, 2.434494, 2.431447, 2.428568, 2.425841, 2.423257, 2.420803, 2.41847, 2.41625, 2.414134, 2.412116, 2.410188, 2.408345, 2.406581,
|
||||||
|
2.404892, 2.403272,
|
||||||
|
|
||||||
|
2.401718, 2.400225, 2.39879, 2.39741, 2.396081, 2.394801, 2.393568, 2.392377, 2.391229, 2.390119, 2.389047, 2.388011, 2.387008, 2.386037, 2.385097, 2.384186, 2.383302, 2.382446, 2.381615, 2.380807, 2.380024, 2.379262, 2.378522, 2.377802,
|
||||||
|
2.377102, 2.37642, 2.375757, 2.375111, 2.374482, 2.373868, 2.37327, 2.372687, 2.372119, 2.371564, 2.371022, 2.370493, 2.369977, 2.369472, 2.368979, 2.368497, 2.368026, 2.367566, 2.367115, 2.366674, 2.366243, 2.365821, 2.365407, 2.365002,
|
||||||
|
2.364606, 2.364217,
|
||||||
|
],
|
||||||
|
};
|
||||||
268
resources/suite-runner.mjs
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
import { STEP_RUNNER_LOOKUP } from "./shared/step-runner.mjs";
|
||||||
|
import { MESSAGE_TYPE } from "./shared/benchmark.mjs";
|
||||||
|
import { WarmupSuite } from "./benchmark-runner.mjs";
|
||||||
|
|
||||||
|
function delay(ms) {
|
||||||
|
if (ms > 0)
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuiteRunner {
|
||||||
|
#frame;
|
||||||
|
#page;
|
||||||
|
#params;
|
||||||
|
#suite;
|
||||||
|
#client;
|
||||||
|
#suiteResults;
|
||||||
|
#prepareTime = 0;
|
||||||
|
|
||||||
|
constructor(frame, page, params, suite, client, measuredValues) {
|
||||||
|
// FIXME: Create SuiteRunner-local measuredValues.
|
||||||
|
this.#suiteResults = measuredValues.tests[suite.name];
|
||||||
|
if (!this.#suiteResults) {
|
||||||
|
this.#suiteResults = { tests: {}, prepare: 0, total: 0 };
|
||||||
|
measuredValues.tests[suite.name] = this.#suiteResults;
|
||||||
|
}
|
||||||
|
this.#frame = frame;
|
||||||
|
this.#page = page;
|
||||||
|
this.#client = client;
|
||||||
|
this.#suite = suite;
|
||||||
|
this.#params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
get frame() {
|
||||||
|
return this.#frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
get page() {
|
||||||
|
return this.#page;
|
||||||
|
}
|
||||||
|
|
||||||
|
get params() {
|
||||||
|
return this.#params;
|
||||||
|
}
|
||||||
|
|
||||||
|
get suite() {
|
||||||
|
return this.#suite;
|
||||||
|
}
|
||||||
|
|
||||||
|
get client() {
|
||||||
|
return this.#client;
|
||||||
|
}
|
||||||
|
|
||||||
|
get suiteResults() {
|
||||||
|
return this.#suiteResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
await this._prepareSuite();
|
||||||
|
await this._runSuite();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareSuite() {
|
||||||
|
const suiteName = this.#suite.name;
|
||||||
|
const suitePrepareStartLabel = `suite-${suiteName}-prepare-start`;
|
||||||
|
const suitePrepareEndLabel = `suite-${suiteName}-prepare-end`;
|
||||||
|
|
||||||
|
performance.mark(suitePrepareStartLabel);
|
||||||
|
await this._loadFrame();
|
||||||
|
await this.#suite.prepare(this.#page);
|
||||||
|
performance.mark(suitePrepareEndLabel);
|
||||||
|
|
||||||
|
const entry = performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
|
||||||
|
this.#prepareTime = entry.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _runSuite() {
|
||||||
|
const suiteName = this.#suite.name;
|
||||||
|
const suiteStartLabel = `suite-${suiteName}-start`;
|
||||||
|
const suiteEndLabel = `suite-${suiteName}-end`;
|
||||||
|
|
||||||
|
await delay(this.#params.waitAfterSetup);
|
||||||
|
|
||||||
|
performance.mark(suiteStartLabel);
|
||||||
|
for (const step of this.#suite.tests) {
|
||||||
|
await this.#client?.willRunTest?.(this.#suite, step);
|
||||||
|
|
||||||
|
const stepRunnerType = this.#suite.type ?? this.params.useAsyncSteps ? "async" : "default";
|
||||||
|
const stepRunnerClass = STEP_RUNNER_LOOKUP[stepRunnerType];
|
||||||
|
const stepRunner = new stepRunnerClass(this.#frame, this.#page, this.#params, this.#suite, step, stepRunnerType);
|
||||||
|
let { syncTime, asyncTime } = await stepRunner.runStep();
|
||||||
|
this._recordTestResults(step, syncTime, asyncTime);
|
||||||
|
}
|
||||||
|
performance.mark(suiteEndLabel);
|
||||||
|
|
||||||
|
await delay(this.#params.waitAfterSuite);
|
||||||
|
|
||||||
|
performance.measure(`suite-${suiteName}`, suiteStartLabel, suiteEndLabel);
|
||||||
|
this._validateSuiteResults();
|
||||||
|
await this._updateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
_validateSuiteResults() {
|
||||||
|
// When the test is fast and the precision is low (for example with Firefox'
|
||||||
|
// privacy.resistFingerprinting preference), it's possible that the measured
|
||||||
|
// total duration for an entire is 0.
|
||||||
|
const suiteTotal = this.#suiteResults.total;
|
||||||
|
const suitePrepare = this.#suiteResults.prepare;
|
||||||
|
if (suiteTotal === 0)
|
||||||
|
throw new Error(`Got invalid 0-time total for suite ${this.#suite.name}: ${suiteTotal}`);
|
||||||
|
if (this.#params.measurePrepare && suitePrepare === 0)
|
||||||
|
throw new Error(`Got invalid 0-time prepare time for suite ${this.#suite.name}: ${suitePrepare}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadFrame() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const frame = this.#frame;
|
||||||
|
frame.onload = () => resolve();
|
||||||
|
frame.onerror = () => reject();
|
||||||
|
const url = new URL(this.#suite.url, document.baseURI);
|
||||||
|
for (const [key, value] of this.#params.toSearchParamsObject())
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
frame.src = url.href;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _recordTestResults(step, syncTime, asyncTime) {
|
||||||
|
// Skip reporting updates for the warmup suite.
|
||||||
|
if (this.#suite === WarmupSuite)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let total = syncTime + asyncTime;
|
||||||
|
this.#suiteResults.tests[step.name] = {
|
||||||
|
tests: { Sync: syncTime, Async: asyncTime },
|
||||||
|
total: total,
|
||||||
|
};
|
||||||
|
this.#suiteResults.prepare = this.#prepareTime;
|
||||||
|
this.#suiteResults.total += total;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateClient(suite = this.#suite) {
|
||||||
|
await this.#client?.didFinishSuite?.(suite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoteSuiteRunner extends SuiteRunner {
|
||||||
|
#appId;
|
||||||
|
#prepareTime;
|
||||||
|
|
||||||
|
get appId() {
|
||||||
|
return this.#appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
set appId(id) {
|
||||||
|
this.#appId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
this.postMessageCallbacks = new Map();
|
||||||
|
const handler = this._handlePostMessage.bind(this);
|
||||||
|
window.addEventListener("message", handler);
|
||||||
|
|
||||||
|
// FIXME: use this._suite in all SuiteRunner methods directly.
|
||||||
|
try {
|
||||||
|
await this._prepareSuite();
|
||||||
|
await this._runSuite();
|
||||||
|
} finally {
|
||||||
|
window.removeEventListener("message", handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareSuite() {
|
||||||
|
const suiteName = this.suite.name;
|
||||||
|
const suitePrepareStartLabel = `suite-${suiteName}-prepare-start`;
|
||||||
|
const suitePrepareEndLabel = `suite-${suiteName}-prepare-end`;
|
||||||
|
performance.mark(suitePrepareStartLabel);
|
||||||
|
|
||||||
|
// Wait for the app-ready message from the workload.
|
||||||
|
const appReadyPromise = this._subscribeOnce(MESSAGE_TYPE.appReady);
|
||||||
|
await this._loadFrame(this.suite);
|
||||||
|
// The suite will send out the app-ready message.
|
||||||
|
const response = await appReadyPromise;
|
||||||
|
this._initializeAppId(response.appId);
|
||||||
|
await this.suite.prepare?.(this.page);
|
||||||
|
|
||||||
|
performance.mark(suitePrepareEndLabel);
|
||||||
|
const entry = performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
|
||||||
|
this.#prepareTime = entry.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _runSuite() {
|
||||||
|
await delay(this.params.waitAfterSetup);
|
||||||
|
|
||||||
|
// Ask workload to run its own tests.
|
||||||
|
this._sendMessage(MESSAGE_TYPE.suiteStart, { name: this.suite.config?.name || "default" });
|
||||||
|
// Capture metrics from the completed tests.
|
||||||
|
const { result } = await this._subscribeOnce(MESSAGE_TYPE.suiteComplete);
|
||||||
|
|
||||||
|
await delay(this.params.waitAfterSuite);
|
||||||
|
|
||||||
|
this.suiteResults.tests = {
|
||||||
|
...this.suiteResults.tests,
|
||||||
|
...result.tests,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.suiteResults.prepare = this.#prepareTime;
|
||||||
|
this.suiteResults.total = result.total;
|
||||||
|
|
||||||
|
this._validateSuiteResults();
|
||||||
|
await this._updateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendMessage(type, payload) {
|
||||||
|
const message = {
|
||||||
|
appId: this.appId,
|
||||||
|
key: "benchmark-connector",
|
||||||
|
type: type,
|
||||||
|
payload: payload,
|
||||||
|
};
|
||||||
|
this.frame.contentWindow.postMessage(message, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePostMessage(event) {
|
||||||
|
const message = event.data;
|
||||||
|
const callback = this.postMessageCallbacks.get(message.type);
|
||||||
|
if (callback)
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
_startSubscription(type, callback) {
|
||||||
|
if (this.postMessageCallbacks.has(type))
|
||||||
|
throw new Error("Callback exists already");
|
||||||
|
|
||||||
|
this.postMessageCallbacks.set(type, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopSubscription(type) {
|
||||||
|
if (!this.postMessageCallbacks.has(type))
|
||||||
|
throw new Error("Callback does not exist");
|
||||||
|
|
||||||
|
this.postMessageCallbacks.delete(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscribeOnce(type) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this._startSubscription(type, (e) => {
|
||||||
|
const message = e.data;
|
||||||
|
if (type !== MESSAGE_TYPE.appReady && message.appId !== this.appId)
|
||||||
|
throw new Error(`Got message for invalid app: ${message.appId} instead of ${this.appId}`);
|
||||||
|
this._stopSubscription(type);
|
||||||
|
resolve(message.payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_initializeAppId(appId) {
|
||||||
|
console.assert(!this.appId, `Cannot receive ${MESSAGE_TYPE.appReady} twice`);
|
||||||
|
console.assert(appId, `${MESSAGE_TYPE.appReady} sent no appId`);
|
||||||
|
this.appId = appId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SUITE_RUNNER_LOOKUP = {
|
||||||
|
__proto__: null,
|
||||||
|
default: SuiteRunner,
|
||||||
|
async: SuiteRunner,
|
||||||
|
remote: RemoteSuiteRunner,
|
||||||
|
};
|
||||||
11
resources/suites-helper.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export function freezeSuites(suites) {
|
||||||
|
suites = suites.map((suite) => {
|
||||||
|
// FIXME: freeze tags
|
||||||
|
// suite.tags = Object.freeze(suite.tags);
|
||||||
|
suite.steps = Object.freeze(suite.steps);
|
||||||
|
// FIXME: freeze suite after fixing the benchmark-configurator.
|
||||||
|
// return Object.freeze(suite);
|
||||||
|
return suite;
|
||||||
|
});
|
||||||
|
return suites;
|
||||||
|
}
|
||||||
31
suites-experimental/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Experimental Workloads
|
||||||
|
|
||||||
|
Experimental Speedometer workloads can be used for workloads that do not
|
||||||
|
directly meet the criteria for official workloads:
|
||||||
|
|
||||||
|
- Incubation of new workloads
|
||||||
|
- Edge-cases that don't work well in the official setup
|
||||||
|
- Testing features that aren't fully shipped in all browsers
|
||||||
|
|
||||||
|
The main purpose is to have a space for low-friction contributions to
|
||||||
|
experiment with new ideas. There is no explicit requirement to promote
|
||||||
|
experimental workloads to the official selection.
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
Contributions to experimental workloads can be merged with the approval from a
|
||||||
|
single vendor.
|
||||||
|
|
||||||
|
This is the opposite from official workloads that are scored, where we require
|
||||||
|
approval from all vendors.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Stored in the `/experimental` folder
|
||||||
|
- Disabled by default and thus do not contribute to the official score
|
||||||
|
- Use the `experimental` tag in the Suites declaration
|
||||||
|
|
||||||
|
## Hosting
|
||||||
|
|
||||||
|
Experimental workloads will not be officially hosted on Speedometer releases on
|
||||||
|
<https://browserbench.org>, unlike the official workloads.
|
||||||
2
suites-experimental/javascript-wc-indexeddb/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
.DS_Store
|
||||||
|
/node_modules
|
||||||
72
suites-experimental/javascript-wc-indexeddb/README.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Speedometer 3.0: TodoMVC: Web Components IndexedDB
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
A todoMVC application implemented with native web components and indexedDB as the backing storage.
|
||||||
|
It utilizes custom elements and html templates to build reusable components.
|
||||||
|
|
||||||
|
In contrast to other workloads, this application uses an updated set of css rules and an optimized dom structure to ensure the application follows best practices in regards to accessibility.
|
||||||
|
|
||||||
|
### Benchmark steps
|
||||||
|
|
||||||
|
In contrast to other versions of the todoMVC workload, this one only shows 10 todo items at a time.
|
||||||
|
|
||||||
|
#### Add 100 items.
|
||||||
|
|
||||||
|
All the items are added to the DOM and to the database, it uses CSS to show only 10 of the items on screen.
|
||||||
|
|
||||||
|
The measured time stops when the last item has been added to the DOM, it doesn't measure the time spent to complete the database update.
|
||||||
|
|
||||||
|
#### Complete 100 items.
|
||||||
|
|
||||||
|
The benchmark runs a loop of 10 iterations. On each iteration 10 items are marked completed (in the DOM and in the database), and the "Next page" button is clicked. When moving to the next page the items in the "current page" are deleted from the DOM.
|
||||||
|
|
||||||
|
The measured time stops when the last item has been marked as completed, it doesn't measure the time spent to complete the database update.
|
||||||
|
|
||||||
|
#### Delete 100 items.
|
||||||
|
|
||||||
|
The benchmarks runs a loop of 10 iterations. On each iteration the 10 items in the current page are deleted (from the DOM and the database), and the "Previous page" button is clicked.
|
||||||
|
|
||||||
|
When moving to the previous page the previous 10 items are loaded from the database, this is included in the measured time.
|
||||||
|
|
||||||
|
## Storage Options
|
||||||
|
|
||||||
|
This application supports two different IndexedDB implementations that can be selected via URL search parameters:
|
||||||
|
|
||||||
|
- **Vanilla IndexedDB** (default): Uses the native IndexedDB API
|
||||||
|
- Access via: `http://localhost:7005/?storageType=vanilla` or `http://localhost:7005/`
|
||||||
|
- **Dexie.js**: Uses the Dexie.js wrapper library for IndexedDB
|
||||||
|
- Access via: `http://localhost:7005/?storageType=dexie`
|
||||||
|
|
||||||
|
Simply use the URL parameters to switch between implementations. The database will be reset when switching between storage types.
|
||||||
|
|
||||||
|
Navigation within the app uses simple hash-based routes like `#/active`, `#/completed`, or `#/` for all todos.
|
||||||
|
|
||||||
|
## Built steps
|
||||||
|
|
||||||
|
A simple build script copies all necessary files to a `dist` folder.
|
||||||
|
It does not rely on compilers or transpilers and serves raw html, css and js files to the user.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server.
|
||||||
|
|
||||||
|
```
|
||||||
|
* Node (min version: 18.13.0)
|
||||||
|
* NPM (min version: 8.19.3)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local preview
|
||||||
|
|
||||||
|
```
|
||||||
|
terminal:
|
||||||
|
1. npm install
|
||||||
|
2. npm run dev
|
||||||
|
|
||||||
|
browser:
|
||||||
|
1. http://localhost:7005/
|
||||||
|
```
|
||||||
31
suites-experimental/javascript-wc-indexeddb/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
|
||||||
|
<title>TodoMVC: JavaScript Web Components</title>
|
||||||
|
<link rel="stylesheet" href="styles/global.css" />
|
||||||
|
<link rel="stylesheet" href="styles/header.css" />
|
||||||
|
<link rel="stylesheet" href="styles/footer.css" />
|
||||||
|
<!-- load these first, so that they are registered by the time the app components needs them -->
|
||||||
|
<script type="module" src="src/components/todo-topbar/todo-topbar.component.js"></script>
|
||||||
|
<script type="module" src="src/components/todo-list/todo-list.component.js"></script>
|
||||||
|
<script type="module" src="src/components/todo-bottombar/todo-bottombar.component.js"></script>
|
||||||
|
<script type="module" src="src/components/todo-app/todo-app.component.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<a href="#" style="text-decoration: none"><h1 class="title">todos</h1></a>
|
||||||
|
</header>
|
||||||
|
<todo-app></todo-app>
|
||||||
|
<footer class="footer">
|
||||||
|
<p class="footer-text">Click on input field to write your todo.</p>
|
||||||
|
<p class="footer-text">At least one character is needed to be a valid entry.</p>
|
||||||
|
<p class="footer-text">Press 'enter' to add the todo.</p>
|
||||||
|
<p class="footer-text">Double-click to edit a todo</p>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="src/index.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5873
suites-experimental/javascript-wc-indexeddb/dist/libs/dexie.mjs
vendored
Normal file
39
suites-experimental/javascript-wc-indexeddb/dist/resources.txt
vendored
Normal file
|
|
@ -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
|
||||||
156
suites-experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.component.js
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
import template from "./todo-app.template.js";
|
||||||
|
import { useRouter } from "../../hooks/useRouter.js";
|
||||||
|
|
||||||
|
import globalStyles from "../../../styles/global.constructable.js";
|
||||||
|
import appStyles from "../../../styles/app.constructable.js";
|
||||||
|
import mainStyles from "../../../styles/main.constructable.js";
|
||||||
|
class TodoApp extends HTMLElement {
|
||||||
|
#isReady = false;
|
||||||
|
#numberOfItems = 0;
|
||||||
|
#numberOfCompletedItems = 0;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const node = document.importNode(template.content, true);
|
||||||
|
this.topbar = node.querySelector("todo-topbar");
|
||||||
|
this.list = node.querySelector("todo-list");
|
||||||
|
this.bottombar = node.querySelector("todo-bottombar");
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
this.htmlDirection = document.dir || "ltr";
|
||||||
|
this.setAttribute("dir", this.htmlDirection);
|
||||||
|
this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles];
|
||||||
|
this.shadow.append(node);
|
||||||
|
|
||||||
|
this.addItem = this.addItem.bind(this);
|
||||||
|
this.toggleItem = this.toggleItem.bind(this);
|
||||||
|
this.removeItem = this.removeItem.bind(this);
|
||||||
|
this.updateItem = this.updateItem.bind(this);
|
||||||
|
this.toggleItems = this.toggleItems.bind(this);
|
||||||
|
this.clearCompletedItems = this.clearCompletedItems.bind(this);
|
||||||
|
this.routeChange = this.routeChange.bind(this);
|
||||||
|
this.moveToNextPage = this.moveToNextPage.bind(this);
|
||||||
|
this.moveToPreviousPage = this.moveToPreviousPage.bind(this);
|
||||||
|
|
||||||
|
this.router = useRouter();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isReady() {
|
||||||
|
return this.#isReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstance() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(event) {
|
||||||
|
const { detail: item } = event;
|
||||||
|
this.list.addItem(item, this.#numberOfItems++);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem(event) {
|
||||||
|
if (event.detail.completed)
|
||||||
|
this.#numberOfCompletedItems++;
|
||||||
|
else
|
||||||
|
this.#numberOfCompletedItems--;
|
||||||
|
|
||||||
|
this.list.toggleItem(event.detail.itemNumber, event.detail.completed);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(event) {
|
||||||
|
if (event.detail.completed)
|
||||||
|
this.#numberOfCompletedItems--;
|
||||||
|
|
||||||
|
this.#numberOfItems--;
|
||||||
|
this.update();
|
||||||
|
this.list.removeItem(event.detail.itemNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItem(event) {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItems(event) {
|
||||||
|
this.list.toggleItems(event.detail.completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCompletedItems() {
|
||||||
|
this.list.removeCompletedItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToNextPage() {
|
||||||
|
this.list.moveToNextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToPreviousPage() {
|
||||||
|
await this.list.moveToPreviousPage();
|
||||||
|
this.bottombar.reenablePreviousPageButton();
|
||||||
|
window.dispatchEvent(new CustomEvent("previous-page-loaded", {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const totalItems = this.#numberOfItems;
|
||||||
|
const completedItems = this.#numberOfCompletedItems;
|
||||||
|
const activeItems = totalItems - completedItems;
|
||||||
|
|
||||||
|
this.list.setAttribute("total-items", totalItems);
|
||||||
|
|
||||||
|
this.topbar.setAttribute("total-items", totalItems);
|
||||||
|
this.topbar.setAttribute("active-items", activeItems);
|
||||||
|
this.topbar.setAttribute("completed-items", completedItems);
|
||||||
|
|
||||||
|
this.bottombar.setAttribute("total-items", totalItems);
|
||||||
|
this.bottombar.setAttribute("active-items", activeItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
addListeners() {
|
||||||
|
this.topbar.addEventListener("toggle-all", this.toggleItems);
|
||||||
|
this.topbar.addEventListener("add-item", this.addItem);
|
||||||
|
|
||||||
|
this.list.listNode.addEventListener("toggle-item", this.toggleItem);
|
||||||
|
this.list.listNode.addEventListener("remove-item", this.removeItem);
|
||||||
|
this.list.listNode.addEventListener("update-item", this.updateItem);
|
||||||
|
|
||||||
|
this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems);
|
||||||
|
this.bottombar.addEventListener("next-page", this.moveToNextPage);
|
||||||
|
this.bottombar.addEventListener("previous-page", this.moveToPreviousPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListeners() {
|
||||||
|
this.topbar.removeEventListener("toggle-all", this.toggleItems);
|
||||||
|
this.topbar.removeEventListener("add-item", this.addItem);
|
||||||
|
|
||||||
|
this.list.listNode.removeEventListener("toggle-item", this.toggleItem);
|
||||||
|
this.list.listNode.removeEventListener("remove-item", this.removeItem);
|
||||||
|
this.list.listNode.removeEventListener("update-item", this.updateItem);
|
||||||
|
|
||||||
|
this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems);
|
||||||
|
this.bottombar.removeEventListener("next-page", this.moveToNextPage);
|
||||||
|
this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
routeChange(route) {
|
||||||
|
const routeName = route.split("/")[1] || "all";
|
||||||
|
this.list.updateRoute(routeName);
|
||||||
|
this.bottombar.updateRoute(routeName);
|
||||||
|
this.topbar.updateRoute(routeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.update();
|
||||||
|
this.addListeners();
|
||||||
|
this.router.initRouter(this.routeChange);
|
||||||
|
this.#isReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.removeListeners();
|
||||||
|
this.#isReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("todo-app", TodoApp);
|
||||||
|
|
||||||
|
export default TodoApp;
|
||||||
14
suites-experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.template.js
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const template = document.createElement("template");
|
||||||
|
|
||||||
|
template.id = "todo-app-template";
|
||||||
|
template.innerHTML = `
|
||||||
|
<section class="app">
|
||||||
|
<todo-topbar></todo-topbar>
|
||||||
|
<main class="main">
|
||||||
|
<todo-list></todo-list>
|
||||||
|
</main>
|
||||||
|
<todo-bottombar></todo-bottombar>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default template;
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import template from "./todo-bottombar.template.js";
|
||||||
|
|
||||||
|
import globalStyles from "../../../styles/global.constructable.js";
|
||||||
|
import bottombarStyles from "../../../styles/bottombar.constructable.js";
|
||||||
|
|
||||||
|
const customStyles = new CSSStyleSheet();
|
||||||
|
customStyles.replaceSync(`
|
||||||
|
|
||||||
|
.clear-completed-button, .clear-completed-button:active,
|
||||||
|
.todo-status,
|
||||||
|
.filter-list
|
||||||
|
{
|
||||||
|
position: unset;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottombar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottombar > * {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-list {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([total-items="0"]) > .bottombar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
class TodoBottombar extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ["total-items", "active-items"];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const node = document.importNode(template.content, true);
|
||||||
|
this.element = node.querySelector(".bottombar");
|
||||||
|
this.clearCompletedButton = node.querySelector(".clear-completed-button");
|
||||||
|
this.todoStatus = node.querySelector(".todo-status");
|
||||||
|
this.filterLinks = node.querySelectorAll(".filter-link");
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
this.htmlDirection = document.dir || "ltr";
|
||||||
|
this.setAttribute("dir", this.htmlDirection);
|
||||||
|
this.shadow.adoptedStyleSheets = [globalStyles, bottombarStyles, customStyles];
|
||||||
|
this.shadow.append(node);
|
||||||
|
|
||||||
|
this.clearCompletedItems = this.clearCompletedItems.bind(this);
|
||||||
|
this.MoveToNextPage = this.MoveToNextPage.bind(this);
|
||||||
|
this.MoveToPreviousPage = this.MoveToPreviousPage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
this.todoStatus.textContent = `${this["active-items"]} ${this["active-items"] === "1" ? "item" : "items"} left!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRoute(route) {
|
||||||
|
this.filterLinks.forEach((link) => {
|
||||||
|
if (link.dataset.route === route)
|
||||||
|
link.classList.add("selected");
|
||||||
|
else
|
||||||
|
link.classList.remove("selected");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCompletedItems() {
|
||||||
|
this.dispatchEvent(new CustomEvent("clear-completed-items"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveToNextPage() {
|
||||||
|
this.dispatchEvent(new CustomEvent("next-page"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveToPreviousPage() {
|
||||||
|
this.element.querySelector(".previous-page-button").disabled = true;
|
||||||
|
this.dispatchEvent(new CustomEvent("previous-page"));
|
||||||
|
}
|
||||||
|
|
||||||
|
addListeners() {
|
||||||
|
this.clearCompletedButton.addEventListener("click", this.clearCompletedItems);
|
||||||
|
this.element.querySelector(".next-page-button").addEventListener("click", this.MoveToNextPage);
|
||||||
|
this.element.querySelector(".previous-page-button").addEventListener("click", this.MoveToPreviousPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListeners() {
|
||||||
|
this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems);
|
||||||
|
this.getElementById("next-page-button").removeEventListener("click", this.MoveToNextPage);
|
||||||
|
this.getElementById("previous-page-button").removeEventListener("click", this.MoveToPreviousPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(property, oldValue, newValue) {
|
||||||
|
if (oldValue === newValue)
|
||||||
|
return;
|
||||||
|
this[property] = newValue;
|
||||||
|
|
||||||
|
if (this.isConnected)
|
||||||
|
this.updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
reenablePreviousPageButton() {
|
||||||
|
this.element.querySelector(".previous-page-button").disabled = false;
|
||||||
|
window.dispatchEvent(new CustomEvent("previous-page-button-enabled", {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.updateDisplay();
|
||||||
|
this.addListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.removeListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("todo-bottombar", TodoBottombar);
|
||||||
|
|
||||||
|
export default TodoBottombar;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
const template = document.createElement("template");
|
||||||
|
|
||||||
|
template.id = "todo-bottombar-template";
|
||||||
|
template.innerHTML = `
|
||||||
|
<footer class="bottombar">
|
||||||
|
<div class="todo-status"><span class="todo-count">0</span> item left</div>
|
||||||
|
<ul class="filter-list">
|
||||||
|
<li class="filter-item">
|
||||||
|
<a id="filter-link-all" class="filter-link selected" href="#/" data-route="all">All</a>
|
||||||
|
</li>
|
||||||
|
<li class="filter-item">
|
||||||
|
<a id="filter-link-active" class="filter-link" href="#/active" data-route="active">Active</a>
|
||||||
|
</li>
|
||||||
|
<li class="filter-item">
|
||||||
|
<a id="filter-link-completed" class="filter-link" href="#/completed" data-route="completed">Completed</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button id="clear-completed" class="clear-completed-button">Clear completed</button>
|
||||||
|
<button id="previous-page" class="previous-page-button"> Previous </button>
|
||||||
|
<button id="next-page" class="next-page-button"> Next </button>
|
||||||
|
</footer>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default template;
|
||||||
182
suites-experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.component.js
vendored
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import template from "./todo-item.template.js";
|
||||||
|
import { useDoubleClick } from "../../hooks/useDoubleClick.js";
|
||||||
|
import { useKeyListener } from "../../hooks/useKeyListener.js";
|
||||||
|
|
||||||
|
import globalStyles from "../../../styles/global.constructable.js";
|
||||||
|
import itemStyles from "../../../styles/todo-item.constructable.js";
|
||||||
|
|
||||||
|
class TodoItem extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ["itemid", "itemtitle", "itemcompleted"];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Renamed this.id to this.itemid and this.title to this.itemtitle.
|
||||||
|
// When the component assigns to this.id or this.title, this causes the browser's implementation of the existing setters to run, which convert these property sets into internal setAttribute calls. This can have surprising consequences.
|
||||||
|
// [Issue]: https://github.com/WebKit/Speedometer/issues/313
|
||||||
|
this.itemid = "";
|
||||||
|
this.itemtitle = "Todo Item";
|
||||||
|
this.itemcompleted = "false";
|
||||||
|
this.itemIndex = null;
|
||||||
|
|
||||||
|
const node = document.importNode(template.content, true);
|
||||||
|
this.item = node.querySelector(".todo-item");
|
||||||
|
this.toggleLabel = node.querySelector(".toggle-todo-label");
|
||||||
|
this.toggleInput = node.querySelector(".toggle-todo-input");
|
||||||
|
this.todoText = node.querySelector(".todo-item-text");
|
||||||
|
this.todoButton = node.querySelector(".remove-todo-button");
|
||||||
|
this.editLabel = node.querySelector(".edit-todo-label");
|
||||||
|
this.editInput = node.querySelector(".edit-todo-input");
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
this.htmlDirection = document.dir || "ltr";
|
||||||
|
this.setAttribute("dir", this.htmlDirection);
|
||||||
|
this.shadow.adoptedStyleSheets = [globalStyles, itemStyles];
|
||||||
|
this.shadow.append(node);
|
||||||
|
|
||||||
|
this.keysListeners = [];
|
||||||
|
|
||||||
|
this.updateItem = this.updateItem.bind(this);
|
||||||
|
this.toggleItem = this.toggleItem.bind(this);
|
||||||
|
this.removeItem = this.removeItem.bind(this);
|
||||||
|
this.startEdit = this.startEdit.bind(this);
|
||||||
|
this.stopEdit = this.stopEdit.bind(this);
|
||||||
|
this.cancelEdit = this.cancelEdit.bind(this);
|
||||||
|
|
||||||
|
if (window.extraTodoItemCssToAdopt) {
|
||||||
|
let extraAdoptedStyleSheet = new CSSStyleSheet();
|
||||||
|
extraAdoptedStyleSheet.replaceSync(window.extraTodoItemCssToAdopt);
|
||||||
|
this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(...args) {
|
||||||
|
args.forEach((argument) => {
|
||||||
|
switch (argument) {
|
||||||
|
case "itemid":
|
||||||
|
if (this.itemid !== undefined)
|
||||||
|
this.item.id = `todo-item-${this.itemid}`;
|
||||||
|
break;
|
||||||
|
case "itemtitle":
|
||||||
|
if (this.itemtitle !== undefined) {
|
||||||
|
this.todoText.textContent = this.itemtitle;
|
||||||
|
this.editInput.value = this.itemtitle;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "itemcompleted":
|
||||||
|
this.toggleInput.checked = this.itemcompleted === "true";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startEdit() {
|
||||||
|
this.item.classList.add("editing");
|
||||||
|
this.editInput.value = this.itemtitle;
|
||||||
|
this.editInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEdit() {
|
||||||
|
this.item.classList.remove("editing");
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelEdit() {
|
||||||
|
this.editInput.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem() {
|
||||||
|
// The todo-list checks the "completed" attribute to filter based on route
|
||||||
|
// (therefore the completed state needs to already be updated before the check)
|
||||||
|
this.setAttribute("itemcompleted", this.toggleInput.checked);
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("toggle-item", {
|
||||||
|
detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("remove-item", {
|
||||||
|
detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItem(event) {
|
||||||
|
if (event.target.value !== this.itemtitle) {
|
||||||
|
if (!event.target.value.length)
|
||||||
|
this.removeItem();
|
||||||
|
else
|
||||||
|
this.setAttribute("itemtitle", event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cancelEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
addListeners() {
|
||||||
|
this.toggleInput.addEventListener("change", this.toggleItem);
|
||||||
|
this.todoText.addEventListener("click", useDoubleClick(this.startEdit, 500));
|
||||||
|
this.editInput.addEventListener("blur", this.stopEdit);
|
||||||
|
this.todoButton.addEventListener("click", this.removeItem);
|
||||||
|
|
||||||
|
this.keysListeners.forEach((listener) => listener.connect());
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListeners() {
|
||||||
|
this.toggleInput.removeEventListener("change", this.toggleItem);
|
||||||
|
this.todoText.removeEventListener("click", this.startEdit);
|
||||||
|
this.editInput.removeEventListener("blur", this.stopEdit);
|
||||||
|
this.todoButton.removeEventListener("click", this.removeItem);
|
||||||
|
|
||||||
|
this.keysListeners.forEach((listener) => listener.disconnect());
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(property, oldValue, newValue) {
|
||||||
|
if (oldValue === newValue)
|
||||||
|
return;
|
||||||
|
this[property] = newValue;
|
||||||
|
|
||||||
|
if (this.isConnected)
|
||||||
|
this.update(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.update("itemid", "itemtitle", "itemcompleted");
|
||||||
|
|
||||||
|
this.keysListeners.push(
|
||||||
|
useKeyListener({
|
||||||
|
target: this.editInput,
|
||||||
|
event: "keyup",
|
||||||
|
callbacks: {
|
||||||
|
["Enter"]: this.updateItem,
|
||||||
|
["Escape"]: this.cancelEdit,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useKeyListener({
|
||||||
|
target: this.todoText,
|
||||||
|
event: "keyup",
|
||||||
|
callbacks: {
|
||||||
|
[" "]: this.startEdit, // this feels weird
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.removeListeners();
|
||||||
|
this.keysListeners = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("todo-item", TodoItem);
|
||||||
|
|
||||||
|
export default TodoItem;
|
||||||
19
suites-experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.template.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
const template = document.createElement("template");
|
||||||
|
|
||||||
|
template.id = "todo-item-template";
|
||||||
|
template.innerHTML = `
|
||||||
|
<li class="todo-item">
|
||||||
|
<div class="display-todo">
|
||||||
|
<label for="toggle-todo" class="toggle-todo-label visually-hidden">Toggle Todo</label>
|
||||||
|
<input id="toggle-todo" class="toggle-todo-input" type="checkbox" />
|
||||||
|
<span class="todo-item-text truncate-singleline" tabindex="0">Placeholder Text</span>
|
||||||
|
<button class="remove-todo-button" title="Remove Todo"></button>
|
||||||
|
</div>
|
||||||
|
<div class="edit-todo-container">
|
||||||
|
<label for="edit-todo" class="edit-todo-label visually-hidden">Edit todo</label>
|
||||||
|
<input id="edit-todo" class="edit-todo-input" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default template;
|
||||||
175
suites-experimental/javascript-wc-indexeddb/dist/src/components/todo-list/todo-list.component.js
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import template from "./todo-list.template.js";
|
||||||
|
import TodoItem from "../todo-item/todo-item.component.js";
|
||||||
|
import { createStorageManager } from "../../storage/storage-factory.js";
|
||||||
|
|
||||||
|
import globalStyles from "../../../styles/global.constructable.js";
|
||||||
|
import listStyles from "../../../styles/todo-list.constructable.js";
|
||||||
|
|
||||||
|
const MAX_ON_SCREEN_ITEMS = 10;
|
||||||
|
|
||||||
|
const customListStyles = new CSSStyleSheet();
|
||||||
|
customListStyles.replaceSync(`
|
||||||
|
.todo-list[route="completed"] > [itemcompleted="false"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list[route="active"] > [itemcompleted="true"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(${MAX_ON_SCREEN_ITEMS}) ~ todo-item {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
class TodoList extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ["total-items"];
|
||||||
|
}
|
||||||
|
|
||||||
|
#route = undefined;
|
||||||
|
#firstItemIndexOnScreen = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const node = document.importNode(template.content, true);
|
||||||
|
this.listNode = node.querySelector(".todo-list");
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
this.htmlDirection = document.dir || "ltr";
|
||||||
|
this.setAttribute("dir", this.htmlDirection);
|
||||||
|
this.shadow.adoptedStyleSheets = [globalStyles, listStyles, customListStyles];
|
||||||
|
this.shadow.append(node);
|
||||||
|
this.classList.add("show-priority");
|
||||||
|
this.storageManager = createStorageManager();
|
||||||
|
|
||||||
|
if (window.extraTodoListCssToAdopt) {
|
||||||
|
let extraAdoptedStyleSheet = new CSSStyleSheet();
|
||||||
|
extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt);
|
||||||
|
this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(entry, itemIndex) {
|
||||||
|
const { id, title, completed } = entry;
|
||||||
|
const priority = 4 - (itemIndex % 5);
|
||||||
|
const element = new TodoItem();
|
||||||
|
|
||||||
|
element.setAttribute("itemid", id);
|
||||||
|
element.setAttribute("itemtitle", title);
|
||||||
|
element.setAttribute("itemcompleted", completed);
|
||||||
|
element.setAttribute("data-priority", priority);
|
||||||
|
element.itemIndex = itemIndex;
|
||||||
|
|
||||||
|
this.listNode.append(element);
|
||||||
|
|
||||||
|
this.#addItemToStorage(itemIndex, id, title, priority, completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(itemIndex) {
|
||||||
|
this.storageManager.removeTodo(itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
addItems(items) {
|
||||||
|
items.forEach((entry) => this.addItem(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCompletedItems() {
|
||||||
|
Array.from(this.listNode.children).forEach((element) => {
|
||||||
|
if (element.itemcompleted === "true")
|
||||||
|
element.removeItem();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItems(completed) {
|
||||||
|
Array.from(this.listNode.children).forEach((element) => {
|
||||||
|
if (completed && element.itemcompleted === "false")
|
||||||
|
element.toggleInput.click();
|
||||||
|
else if (!completed && element.itemcompleted === "true")
|
||||||
|
element.toggleInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem(itemNumber, completed) {
|
||||||
|
// Update the item in the IndexedDB
|
||||||
|
this.storageManager.toggleTodo(itemNumber, completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStyles() {
|
||||||
|
if (parseInt(this["total-items"]) !== 0)
|
||||||
|
this.listNode.style.display = "block";
|
||||||
|
else
|
||||||
|
this.listNode.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRoute(route) {
|
||||||
|
this.#route = route;
|
||||||
|
switch (route) {
|
||||||
|
case "completed":
|
||||||
|
this.listNode.setAttribute("route", "completed");
|
||||||
|
break;
|
||||||
|
case "active":
|
||||||
|
this.listNode.setAttribute("route", "active");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.listNode.setAttribute("route", "all");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(property, oldValue, newValue) {
|
||||||
|
if (oldValue === newValue)
|
||||||
|
return;
|
||||||
|
this[property] = newValue;
|
||||||
|
if (this.isConnected)
|
||||||
|
this.updateStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.updateStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToNextPage() {
|
||||||
|
for (let i = 0; i < MAX_ON_SCREEN_ITEMS; i++) {
|
||||||
|
const child = this.listNode.firstChild;
|
||||||
|
if (!child)
|
||||||
|
break;
|
||||||
|
child.remove();
|
||||||
|
}
|
||||||
|
this.#firstItemIndexOnScreen = this.listNode.firstChild.itemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToPreviousPage() {
|
||||||
|
const items = await this.storageManager.getTodos(this.#firstItemIndexOnScreen, MAX_ON_SCREEN_ITEMS);
|
||||||
|
const elements = items.map((item) => {
|
||||||
|
const { id, title, completed, priority } = item;
|
||||||
|
const element = new TodoItem();
|
||||||
|
element.setAttribute("itemid", id);
|
||||||
|
element.setAttribute("itemtitle", title);
|
||||||
|
element.setAttribute("itemcompleted", completed);
|
||||||
|
element.setAttribute("data-priority", priority);
|
||||||
|
element.itemIndex = item.itemNumber;
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
this.#firstItemIndexOnScreen = items[0].itemNumber;
|
||||||
|
this.listNode.replaceChildren(...elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
#addItemToStorage(itemIndex, id, title, priority, completed) {
|
||||||
|
// Create a todo object with the structure expected by IndexedDB
|
||||||
|
const todoItem = {
|
||||||
|
itemNumber: itemIndex,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
completed,
|
||||||
|
priority,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the item to IndexedDB and handle the Promise
|
||||||
|
this.storageManager.addTodo(todoItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("todo-list", TodoList);
|
||||||
|
|
||||||
|
export default TodoList;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
const template = document.createElement("template");
|
||||||
|
|
||||||
|
template.id = "todo-list-template";
|
||||||
|
template.innerHTML = `
|
||||||
|
<ul class="todo-list"></ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default template;
|
||||||
138
suites-experimental/javascript-wc-indexeddb/dist/src/components/todo-topbar/todo-topbar.component.js
vendored
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import template from "./todo-topbar.template.js";
|
||||||
|
import { useKeyListener } from "../../hooks/useKeyListener.js";
|
||||||
|
import { nanoid } from "../../utils/nanoid.js";
|
||||||
|
|
||||||
|
import globalStyles from "../../../styles/global.constructable.js";
|
||||||
|
import topbarStyles from "../../../styles/topbar.constructable.js";
|
||||||
|
|
||||||
|
const customListStyles = new CSSStyleSheet();
|
||||||
|
customListStyles.replaceSync(`
|
||||||
|
:host([total-items="0"]) .toggle-all-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
class TodoTopbar extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ["active-items", "completed-items"];
|
||||||
|
}
|
||||||
|
|
||||||
|
#route = undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const node = document.importNode(template.content, true);
|
||||||
|
this.todoInput = node.querySelector("#new-todo");
|
||||||
|
this.toggleInput = node.querySelector("#toggle-all");
|
||||||
|
this.toggleContainer = node.querySelector(".toggle-all-container");
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
this.htmlDirection = document.dir || "ltr";
|
||||||
|
this.setAttribute("dir", this.htmlDirection);
|
||||||
|
this.shadow.adoptedStyleSheets = [globalStyles, topbarStyles, customListStyles];
|
||||||
|
this.shadow.append(node);
|
||||||
|
|
||||||
|
this.keysListeners = [];
|
||||||
|
|
||||||
|
this.toggleAll = this.toggleAll.bind(this);
|
||||||
|
this.addItem = this.addItem.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAll(event) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("toggle-all", {
|
||||||
|
detail: { completed: event.target.checked },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(event) {
|
||||||
|
if (!event.target.value.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("add-item", {
|
||||||
|
detail: {
|
||||||
|
id: nanoid(),
|
||||||
|
title: event.target.value,
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
if (!parseInt(this["total-items"])) {
|
||||||
|
this.toggleContainer.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleContainer.style.display = "block";
|
||||||
|
|
||||||
|
switch (this.#route) {
|
||||||
|
case "active":
|
||||||
|
this.toggleInput.checked = false;
|
||||||
|
this.toggleInput.disabled = !parseInt(this["active-items"]);
|
||||||
|
break;
|
||||||
|
case "completed":
|
||||||
|
this.toggleInput.checked = parseInt(this["completed-items"]);
|
||||||
|
this.toggleInput.disabled = !parseInt(this["completed-items"]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.toggleInput.checked = !parseInt(this["active-items"]);
|
||||||
|
this.toggleInput.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRoute(route) {
|
||||||
|
this.#route = route;
|
||||||
|
this.updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
addListeners() {
|
||||||
|
this.toggleInput.addEventListener("change", this.toggleAll);
|
||||||
|
this.keysListeners.forEach((listener) => listener.connect());
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListeners() {
|
||||||
|
this.toggleInput.removeEventListener("change", this.toggleAll);
|
||||||
|
this.keysListeners.forEach((listener) => listener.disconnect());
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(property, oldValue, newValue) {
|
||||||
|
if (oldValue === newValue)
|
||||||
|
return;
|
||||||
|
this[property] = newValue;
|
||||||
|
|
||||||
|
if (this.isConnected)
|
||||||
|
this.updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.keysListeners.push(
|
||||||
|
useKeyListener({
|
||||||
|
target: this.todoInput,
|
||||||
|
event: "keyup",
|
||||||
|
callbacks: {
|
||||||
|
["Enter"]: this.addItem,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateDisplay();
|
||||||
|
this.addListeners();
|
||||||
|
this.todoInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.removeListeners();
|
||||||
|
this.keysListeners = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("todo-topbar", TodoTopbar);
|
||||||
|
|
||||||
|
export default TodoTopbar;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
const template = document.createElement("template");
|
||||||
|
|
||||||
|
template.id = "todo-topbar-template";
|
||||||
|
template.innerHTML = `
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="new-todo-display">
|
||||||
|
<label for="new-todo" class="visually-hidden">Enter a new todo.</label>
|
||||||
|
<input id="new-todo" class="new-todo-input" placeholder="What needs to be done?" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="toggle-all-container">
|
||||||
|
<input id="toggle-all" class="toggle-all-input" type="checkbox" />
|
||||||
|
<label for="toggle-all" class="toggle-all-label">Mark all todos as complete.</label>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default template;
|
||||||
19
suites-experimental/javascript-wc-indexeddb/dist/src/hooks/useDoubleClick.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* A simple function to normalize a double-click and a double-tab action.
|
||||||
|
* There is currently no comparable tab action to dblclick.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @param {number} delay
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useDoubleClick(fn, delay) {
|
||||||
|
let last = 0;
|
||||||
|
return function (...args) {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const difference = now - last;
|
||||||
|
if (difference < delay && difference > 0)
|
||||||
|
fn.apply(this, args);
|
||||||
|
|
||||||
|
last = now;
|
||||||
|
};
|
||||||
|
}
|
||||||
23
suites-experimental/javascript-wc-indexeddb/dist/src/hooks/useKeyListener.js
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
export function useKeyListener(props) {
|
||||||
|
const { target, event, callbacks } = props;
|
||||||
|
|
||||||
|
function handleEvent(event) {
|
||||||
|
Object.keys(callbacks).forEach((key) => {
|
||||||
|
if (event.key === key)
|
||||||
|
callbacks[key](event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
target.addEventListener(event, handleEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
target.removeEventListener(event, handleEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
};
|
||||||
|
}
|
||||||
43
suites-experimental/javascript-wc-indexeddb/dist/src/hooks/useRouter.js
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* Listens for hash change of the url and calls onChange if available.
|
||||||
|
*
|
||||||
|
* @param {Function} callback
|
||||||
|
* @returns Methods to interact with useRouter.
|
||||||
|
*/
|
||||||
|
export const useRouter = (callback) => {
|
||||||
|
let onChange = callback;
|
||||||
|
let current = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change event handler.
|
||||||
|
*/
|
||||||
|
const handleChange = () => {
|
||||||
|
current = document.location.hash;
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (onChange)
|
||||||
|
onChange(document.location.hash);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes router and adds listeners.
|
||||||
|
*
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
const initRouter = (callback) => {
|
||||||
|
onChange = callback;
|
||||||
|
window.addEventListener("hashchange", handleChange);
|
||||||
|
window.addEventListener("load", handleChange);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes listeners
|
||||||
|
*/
|
||||||
|
const disableRouter = () => {
|
||||||
|
window.removeEventListener("hashchange", handleChange);
|
||||||
|
window.removeEventListener("load", handleChange);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoute = () => current.split("/").slice(-1)[0];
|
||||||
|
|
||||||
|
return { initRouter, getRoute, disableRouter };
|
||||||
|
};
|
||||||
12
suites-experimental/javascript-wc-indexeddb/dist/src/index.mjs
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { BenchmarkConnector } from "./speedometer-utils/benchmark.mjs";
|
||||||
|
import suites, { appName, appVersion } from "./workload-test.mjs";
|
||||||
|
|
||||||
|
window.addEventListener("db-ready", () => {
|
||||||
|
/*
|
||||||
|
Paste below into dev console for manual testing:
|
||||||
|
window.addEventListener("message", (event) => console.log(event.data));
|
||||||
|
window.postMessage({ id: "todomvc-postmessage-1.0.0", key: "benchmark-connector", type: "benchmark-suite", name: "default" }, "*");
|
||||||
|
*/
|
||||||
|
const benchmarkConnector = new BenchmarkConnector(suites, appName, appVersion);
|
||||||
|
benchmarkConnector.connect();
|
||||||
|
});
|
||||||
154
suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
|
||||||
|
import { Params } from "./params.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BenchmarkStep
|
||||||
|
*
|
||||||
|
* A single test step, with a common interface to interact with.
|
||||||
|
*/
|
||||||
|
export class BenchmarkStep {
|
||||||
|
constructor(name, run, ignoreResult = false) {
|
||||||
|
this.name = name;
|
||||||
|
this.run = run;
|
||||||
|
this.ignoreResult = ignoreResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAndRecord(params, suite, test) {
|
||||||
|
const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner;
|
||||||
|
const type = params.useAsyncSteps ? "async" : "sync";
|
||||||
|
const stepRunner = new StepRunnerClass(null, null, params, suite, test, type);
|
||||||
|
const result = await stepRunner.runStep();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BenchmarkSuite
|
||||||
|
*
|
||||||
|
* A single test suite that contains one or more test steps.
|
||||||
|
*/
|
||||||
|
export class BenchmarkSuite {
|
||||||
|
constructor(name, tests) {
|
||||||
|
this.name = name;
|
||||||
|
this.tests = tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async runAndRecord(params, onProgress) {
|
||||||
|
const measuredValues = {
|
||||||
|
tests: {},
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
const suiteStartLabel = `suite-${this.name}-start`;
|
||||||
|
const suiteEndLabel = `suite-${this.name}-end`;
|
||||||
|
|
||||||
|
performance.mark(suiteStartLabel);
|
||||||
|
|
||||||
|
for (const test of this.tests) {
|
||||||
|
const { syncTime, asyncTime } = await test.runAndRecord(params, this, test);
|
||||||
|
const total = syncTime + asyncTime;
|
||||||
|
const result = {
|
||||||
|
tests: { Sync: syncTime, Async: asyncTime },
|
||||||
|
total: total,
|
||||||
|
};
|
||||||
|
if (!test.ignoreResult) {
|
||||||
|
measuredValues.tests[test.name] = result;
|
||||||
|
measuredValues.total += total;
|
||||||
|
}
|
||||||
|
onProgress?.(test.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
performance.mark(suiteEndLabel);
|
||||||
|
performance.measure(`suite-${this.name}`, suiteStartLabel, suiteEndLabel);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "suite-tests-complete",
|
||||||
|
status: "success",
|
||||||
|
result: measuredValues,
|
||||||
|
suitename: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MESSAGE_TYPE = Object.freeze({
|
||||||
|
__proto__: null,
|
||||||
|
appReady: "app-ready",
|
||||||
|
suiteStart: "suite-start",
|
||||||
|
stepComplete: "step-complete",
|
||||||
|
suiteComplete: "suite-complete",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MESSAGE_STATUS = Object.freeze({
|
||||||
|
__proto__: null,
|
||||||
|
success: "success",
|
||||||
|
error: "error",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** **********************************************************************
|
||||||
|
* BenchmarkConnector
|
||||||
|
*
|
||||||
|
* postMessage is used to communicate between app and benchmark.
|
||||||
|
* When the app is ready, an 'app-ready' message is sent to signal that the app can receive instructions.
|
||||||
|
*
|
||||||
|
* A prepare script within the apps appends window.name and window.version from the package.json file.
|
||||||
|
* The appId is build by appending name-version
|
||||||
|
* It's used as an additional safe-guard to ensure the correct app responds to a message.
|
||||||
|
*************************************************************************/
|
||||||
|
export class BenchmarkConnector {
|
||||||
|
constructor(suites, name, version) {
|
||||||
|
this.suites = suites;
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if (!name || !version)
|
||||||
|
console.warn("No name or version supplied, to create a unique appId");
|
||||||
|
|
||||||
|
this.appId = name && version ? `${name}-${version}` : -1;
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(event) {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.appId !== this.appId || message.key !== "benchmark-connector") {
|
||||||
|
console.warn("Invalid message", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case MESSAGE_TYPE.suiteStart:
|
||||||
|
const params = new Params(new URLSearchParams(window.location.search));
|
||||||
|
const { name } = message.payload;
|
||||||
|
const suite = this.suites[name];
|
||||||
|
if (!suite)
|
||||||
|
console.error(`Suite with the name of "${name}" not found!`);
|
||||||
|
const onProgress = (step) => this._sendMessage(MESSAGE_TYPE.stepComplete, { name: this.name, step });
|
||||||
|
const { result } = await suite.runAndRecord(params, onProgress);
|
||||||
|
this._sendMessage(MESSAGE_TYPE.suiteComplete, { result });
|
||||||
|
this.disconnect();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Message data type not supported: ${message.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendMessage(type, payload, status = MESSAGE_STATUS.success) {
|
||||||
|
const message = {
|
||||||
|
appId: this.appId,
|
||||||
|
key: "benchmark-connector",
|
||||||
|
type: type,
|
||||||
|
payload: payload,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
window.top.postMessage(message, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
window.addEventListener("message", this.onMessage);
|
||||||
|
this._sendMessage(MESSAGE_TYPE.appReady, { appId: this.appId });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
window.removeEventListener("message", this.onMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/helpers.mjs
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Recursively queries the DOM for an element, traversing through shadow DOMs.
|
||||||
|
*
|
||||||
|
* @param {Element|ShadowRoot} lookupStartNode The node or shadow root to start the lookup from.
|
||||||
|
* @param {string[]} path An array of CSS selectors representing the path to the target element.
|
||||||
|
* @returns {Element|null} The target element if found, otherwise null.
|
||||||
|
*/
|
||||||
|
export function recursivelyQuerySelector(lookupStartNode, path) {
|
||||||
|
lookupStartNode = lookupStartNode.shadowRoot ?? lookupStartNode;
|
||||||
|
const target = path.reduce((root, selector) => {
|
||||||
|
const node = root.querySelector(selector);
|
||||||
|
return node.shadowRoot ?? node;
|
||||||
|
}, lookupStartNode);
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single DOM element, optionally traversing through shadow DOMs to a specified path before the final selection.
|
||||||
|
*
|
||||||
|
* @param {string} selector The CSS selector for the desired element.
|
||||||
|
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
|
||||||
|
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
|
||||||
|
* @returns {Element|null} The found element, or null if not found.
|
||||||
|
*/
|
||||||
|
export function getElement(selector, path = [], lookupStartNode = document) {
|
||||||
|
const element = recursivelyQuerySelector(lookupStartNode, path).querySelector(selector);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all DOM elements matching a selector, optionally traversing through shadow DOMs to a specified path before the final selection.
|
||||||
|
*
|
||||||
|
* @param {string} selector The CSS selector for the desired elements.
|
||||||
|
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
|
||||||
|
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
|
||||||
|
* @returns {Element[]} An array of found elements.
|
||||||
|
*/
|
||||||
|
export function getAllElements(selector, path = [], lookupStartNode = document) {
|
||||||
|
const elements = Array.from(recursivelyQuerySelector(lookupStartNode, path).querySelectorAll(selector));
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forceLayout(body, layoutMode = "getBoundingRectAndElementFromPoint") {
|
||||||
|
body ??= document.body;
|
||||||
|
const rect = body.getBoundingClientRect();
|
||||||
|
switch (layoutMode) {
|
||||||
|
case "getBoundingRectAndElementFromPoint":
|
||||||
|
return document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0);
|
||||||
|
case "getBoundingClientRect":
|
||||||
|
return rect.height;
|
||||||
|
default:
|
||||||
|
throw Error(`Invalid layoutMode: ${layoutMode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
241
suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs
vendored
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
export const LAYOUT_MODES = Object.freeze(["getBoundingClientRect", "getBoundingRectAndElementFromPoint"]);
|
||||||
|
|
||||||
|
export class Params {
|
||||||
|
viewport = {
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
};
|
||||||
|
// Enable a detailed developer menu to change the current Params.
|
||||||
|
developerMode = false;
|
||||||
|
startAutomatically = false;
|
||||||
|
iterationCount = 10;
|
||||||
|
suites = [];
|
||||||
|
// A list of tags to filter suites
|
||||||
|
tags = ["default"];
|
||||||
|
// Toggle running a dummy suite once before the normal test suites.
|
||||||
|
useWarmupSuite = false;
|
||||||
|
// toggle async type vs default raf type.
|
||||||
|
useAsyncSteps = false;
|
||||||
|
// Change how a test measurement is triggered and async time is measured:
|
||||||
|
// "timer": The classic (as in Speedometer 2.x) way using setTimeout
|
||||||
|
// "raf": Using rAF callbacks, both for triggering the sync part and for measuring async time.
|
||||||
|
measurementMethod = "raf";
|
||||||
|
// Wait time after suite preparation in ms.
|
||||||
|
waitAfterSetup = 0;
|
||||||
|
// Wait time before the sync step in ms.
|
||||||
|
waitBeforeSync = 0;
|
||||||
|
// Wait after running a suite.
|
||||||
|
waitAfterSuite = 0;
|
||||||
|
// Warmup time before the sync step in ms.
|
||||||
|
warmupBeforeSync = 0;
|
||||||
|
// Seed for shuffling the execution order of suites.
|
||||||
|
// "off": do not shuffle
|
||||||
|
// "generate": generate a random seed
|
||||||
|
// <integer>: use the provided integer as a seed
|
||||||
|
shuffleSeed = "off";
|
||||||
|
// Choices: "getBoundingClientRect" or "getBoundingRectAndElementFromPoint"
|
||||||
|
layoutMode = LAYOUT_MODES[0];
|
||||||
|
// Measure more workload prepare time.
|
||||||
|
measurePrepare = false;
|
||||||
|
// External config url to override internal tests.
|
||||||
|
config = "";
|
||||||
|
|
||||||
|
constructor(searchParams = undefined, warnUnused = false) {
|
||||||
|
if (searchParams)
|
||||||
|
this._copyFromSearchParams(searchParams, warnUnused);
|
||||||
|
if (!this.developerMode) {
|
||||||
|
Object.freeze(this.viewport);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseInt(value, errorMessage) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isInteger(number) && errorMessage)
|
||||||
|
throw new Error(`Invalid ${errorMessage} param: '${value}', expected int.`);
|
||||||
|
return parseInt(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
_copyFromSearchParams(searchParams, warnUnused) {
|
||||||
|
this.viewport = this._parseViewport(searchParams);
|
||||||
|
this.startAutomatically = this._parseBooleanParam(searchParams, "startAutomatically");
|
||||||
|
this.iterationCount = this._parseIntParam(searchParams, "iterationCount", 1);
|
||||||
|
this.suites = this._parseSuites(searchParams);
|
||||||
|
this.tags = this._parseTags(searchParams);
|
||||||
|
this.developerMode = this._parseBooleanParam(searchParams, "developerMode");
|
||||||
|
this.useWarmupSuite = this._parseBooleanParam(searchParams, "useWarmupSuite");
|
||||||
|
this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps");
|
||||||
|
this.waitAfterSetup = this._parseIntParam(searchParams, "waitAfterSetup", 0);
|
||||||
|
this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0);
|
||||||
|
this.waitAfterSuite = this._parseIntParam(searchParams, "waitAfterSuite", 0);
|
||||||
|
this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0);
|
||||||
|
this.measurementMethod = this._parseEnumParam(searchParams, "measurementMethod", ["raf"]);
|
||||||
|
this.shuffleSeed = this._parseShuffleSeed(searchParams);
|
||||||
|
this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES);
|
||||||
|
this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare");
|
||||||
|
this.config = this._parseConfig(searchParams);
|
||||||
|
|
||||||
|
if (warnUnused) {
|
||||||
|
const unused = Array.from(searchParams.keys());
|
||||||
|
if (unused.length > 0)
|
||||||
|
console.error(`Got unused search params: ${unused.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseBooleanParam(searchParams, paramKey) {
|
||||||
|
if (!searchParams.has(paramKey))
|
||||||
|
return false;
|
||||||
|
searchParams.delete(paramKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseIntParam(searchParams, paramKey, minValue) {
|
||||||
|
if (!searchParams.has(paramKey))
|
||||||
|
return defaultParams[paramKey];
|
||||||
|
|
||||||
|
const parsedValue = this._parseInt(searchParams.get(paramKey), "waitBeforeSync");
|
||||||
|
if (parsedValue < minValue)
|
||||||
|
throw new Error(`Invalid ${paramKey} param: '${parsedValue}', value must be >= ${minValue}.`);
|
||||||
|
searchParams.delete(paramKey);
|
||||||
|
return parsedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseViewport(searchParams) {
|
||||||
|
if (!searchParams.has("viewport"))
|
||||||
|
return defaultParams.viewport;
|
||||||
|
const viewportParam = searchParams.get("viewport");
|
||||||
|
const [width, height] = viewportParam.split("x");
|
||||||
|
const viewport = {
|
||||||
|
width: this._parseInt(width, "viewport.width"),
|
||||||
|
height: this._parseInt(height, "viewport.height"),
|
||||||
|
};
|
||||||
|
if (this.viewport.width < 800 || this.viewport.height < 600)
|
||||||
|
throw new Error(`Invalid viewport param: ${viewportParam}`);
|
||||||
|
searchParams.delete("viewport");
|
||||||
|
return viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseSuites(searchParams) {
|
||||||
|
if (searchParams.has("suite") || searchParams.has("suites")) {
|
||||||
|
if (searchParams.has("suite") && searchParams.has("suites"))
|
||||||
|
throw new Error("Params 'suite' and 'suites' can not be used together.");
|
||||||
|
const value = searchParams.get("suite") || searchParams.get("suites");
|
||||||
|
const suites = value.split(",");
|
||||||
|
if (suites.length === 0)
|
||||||
|
throw new Error("No suites selected");
|
||||||
|
searchParams.delete("suite");
|
||||||
|
searchParams.delete("suites");
|
||||||
|
return suites;
|
||||||
|
}
|
||||||
|
return defaultParams.suites;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseTags(searchParams) {
|
||||||
|
if (!searchParams.has("tags"))
|
||||||
|
return defaultParams.tags;
|
||||||
|
if (this.suites.length)
|
||||||
|
throw new Error("'suites' and 'tags' cannot be used together.");
|
||||||
|
const tags = searchParams.get("tags").split(",");
|
||||||
|
searchParams.delete("tags");
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseEnumParam(searchParams, paramKey, enumArray) {
|
||||||
|
if (!searchParams.has(paramKey))
|
||||||
|
return defaultParams[paramKey];
|
||||||
|
const value = searchParams.get(paramKey);
|
||||||
|
if (!enumArray.includes(value))
|
||||||
|
throw new Error(`Got invalid ${paramKey}: '${value}', choices are ${enumArray}`);
|
||||||
|
searchParams.delete(paramKey);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseShuffleSeed(searchParams) {
|
||||||
|
if (!searchParams.has("shuffleSeed"))
|
||||||
|
return defaultParams.shuffleSeed;
|
||||||
|
let shuffleSeed = searchParams.get("shuffleSeed");
|
||||||
|
if (shuffleSeed !== "off") {
|
||||||
|
if (shuffleSeed === "generate") {
|
||||||
|
shuffleSeed = Math.floor((Math.random() * 1) << 16);
|
||||||
|
console.log(`Generated a random suite order seed: ${shuffleSeed}`);
|
||||||
|
} else {
|
||||||
|
shuffleSeed = parseInt(shuffleSeed);
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(shuffleSeed))
|
||||||
|
throw new Error(`Invalid shuffle seed: '${shuffleSeed}', must be either 'off', 'generate' or an integer.`);
|
||||||
|
}
|
||||||
|
searchParams.delete("shuffleSeed");
|
||||||
|
return shuffleSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseConfig(searchParams) {
|
||||||
|
const config = searchParams.get("config") ?? "";
|
||||||
|
searchParams.delete("config");
|
||||||
|
if (config && !isValidJsonUrl(config))
|
||||||
|
throw new Error("Invalid config url passed in.");
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
toCompleteSearchParamsObject() {
|
||||||
|
return this.toSearchParamsObject(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
toSearchParamsObject(filter = true) {
|
||||||
|
const rawUrlParams = { __proto__: null };
|
||||||
|
for (const [key, value] of Object.entries(this)) {
|
||||||
|
// Handle composite values separately.
|
||||||
|
if (key === "viewport" || key === "suites" || key === "tags")
|
||||||
|
continue;
|
||||||
|
// Skip over default values.
|
||||||
|
if (filter && value === defaultParams[key])
|
||||||
|
continue;
|
||||||
|
rawUrlParams[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.viewport.width !== defaultParams.viewport.width || this.viewport.height !== defaultParams.viewport.height)
|
||||||
|
rawUrlParams.viewport = `${this.viewport.width}x${this.viewport.height}`;
|
||||||
|
|
||||||
|
if (this.suites.length) {
|
||||||
|
rawUrlParams.suites = this.suites.join(",");
|
||||||
|
} else if (this.tags.length) {
|
||||||
|
if (!(this.tags.length === 1 && this.tags[0] === "default"))
|
||||||
|
rawUrlParams.tags = this.tags.join(",");
|
||||||
|
} else {
|
||||||
|
rawUrlParams.suites = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URLSearchParams(rawUrlParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
toSearchParams() {
|
||||||
|
return this.toSearchParamsObject().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidJsonUrl(url) {
|
||||||
|
if (typeof url !== "string" || url.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(url, "http://www.example.com");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParams = new Params();
|
||||||
|
|
||||||
|
export let paramsError = null;
|
||||||
|
let maybeCustomParams = defaultParams;
|
||||||
|
if (globalThis?.location?.search) {
|
||||||
|
const searchParams = new URLSearchParams(globalThis.location.search);
|
||||||
|
try {
|
||||||
|
maybeCustomParams = new Params(searchParams, true);
|
||||||
|
} catch (e) {
|
||||||
|
paramsError = e;
|
||||||
|
console.error("Invalid URL Param", e, "\nUsing defaults as fallback:", maybeCustomParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const params = maybeCustomParams;
|
||||||
109
suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs";
|
||||||
|
import { forceLayout } from "./helpers.mjs";
|
||||||
|
|
||||||
|
export class StepRunner {
|
||||||
|
#frame;
|
||||||
|
#page;
|
||||||
|
#params;
|
||||||
|
#suite;
|
||||||
|
#step;
|
||||||
|
#type;
|
||||||
|
|
||||||
|
constructor(frame, page, params, suite, step, type) {
|
||||||
|
this.#suite = suite;
|
||||||
|
this.#step = step;
|
||||||
|
this.#params = params;
|
||||||
|
this.#page = page;
|
||||||
|
this.#frame = frame;
|
||||||
|
this.#type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get page() {
|
||||||
|
return this.#page;
|
||||||
|
}
|
||||||
|
|
||||||
|
get step() {
|
||||||
|
return this.#step;
|
||||||
|
}
|
||||||
|
|
||||||
|
_runSyncStep(step, page) {
|
||||||
|
step.run(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runStep() {
|
||||||
|
// Prepare all mark labels outside the measuring loop.
|
||||||
|
const suiteName = this.#suite.name;
|
||||||
|
const stepName = this.#step.name;
|
||||||
|
const syncStartLabel = `${suiteName}.${stepName}-start`;
|
||||||
|
const syncEndLabel = `${suiteName}.${stepName}-sync-end`;
|
||||||
|
const asyncEndLabel = `${suiteName}.${stepName}-async-end`;
|
||||||
|
|
||||||
|
let syncTime;
|
||||||
|
let asyncStartTime;
|
||||||
|
let asyncTime;
|
||||||
|
|
||||||
|
const runSync = async () => {
|
||||||
|
if (this.#params.warmupBeforeSync) {
|
||||||
|
performance.mark("warmup-start");
|
||||||
|
const startTime = performance.now();
|
||||||
|
// Infinite loop for the specified ms.
|
||||||
|
while (performance.now() - startTime < this.#params.warmupBeforeSync)
|
||||||
|
continue;
|
||||||
|
performance.mark("warmup-end");
|
||||||
|
}
|
||||||
|
performance.mark(syncStartLabel);
|
||||||
|
const syncStartTime = performance.now();
|
||||||
|
|
||||||
|
if (this.#type === "async")
|
||||||
|
await this._runSyncStep(this.step, this.page);
|
||||||
|
else
|
||||||
|
this._runSyncStep(this.step, this.page);
|
||||||
|
|
||||||
|
const mark = performance.mark(syncEndLabel);
|
||||||
|
const syncEndTime = mark.startTime;
|
||||||
|
|
||||||
|
syncTime = syncEndTime - syncStartTime;
|
||||||
|
asyncStartTime = syncEndTime;
|
||||||
|
};
|
||||||
|
const measureAsync = () => {
|
||||||
|
// Some browsers don't immediately update the layout for paint.
|
||||||
|
// Force the layout here to ensure we're measuring the layout time.
|
||||||
|
//
|
||||||
|
// Note: This matches the behavior of Page.layout() in benchmark-runner.mjs.
|
||||||
|
// Since shared code cannot depend on Page, we duplicate the logic here,
|
||||||
|
// falling back to document.body for remote workloads that don't have a frame.
|
||||||
|
const body = this.#frame?.contentDocument?.body ?? document.body;
|
||||||
|
const value = forceLayout(body, this.#params.layoutMode);
|
||||||
|
body._leakedLayoutValue = value; // Prevent dead code elimination.
|
||||||
|
|
||||||
|
const asyncEndTime = performance.now();
|
||||||
|
performance.mark(asyncEndLabel);
|
||||||
|
|
||||||
|
asyncTime = asyncEndTime - asyncStartTime;
|
||||||
|
|
||||||
|
if (this.#params.warmupBeforeSync)
|
||||||
|
performance.measure("warmup", "warmup-start", "warmup-end");
|
||||||
|
performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel);
|
||||||
|
performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
|
||||||
|
const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType];
|
||||||
|
const scheduler = new schedulerClass(runSync, measureAsync, this.#params);
|
||||||
|
await scheduler.start();
|
||||||
|
return { syncTime, asyncTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsyncStepRunner extends StepRunner {
|
||||||
|
async _runSyncStep(step, page) {
|
||||||
|
await step.run(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STEP_RUNNER_LOOKUP = {
|
||||||
|
__proto__: null,
|
||||||
|
default: StepRunner,
|
||||||
|
async: AsyncStepRunner,
|
||||||
|
remote: StepRunner,
|
||||||
|
};
|
||||||
77
suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
class StepScheduler {
|
||||||
|
constructor(syncCallback, asyncCallback, params) {
|
||||||
|
this._syncCallback = syncCallback;
|
||||||
|
this._asyncCallback = asyncCallback;
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this._params.waitBeforeSync)
|
||||||
|
setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync);
|
||||||
|
else
|
||||||
|
this._scheduleCallbacks(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RAFStepScheduler extends StepScheduler {
|
||||||
|
_scheduleCallbacks(resolve) {
|
||||||
|
requestAnimationFrame(() => this._syncCallback());
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._asyncCallback();
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsyncRAFStepScheduler extends StepScheduler {
|
||||||
|
static mc = new MessageChannel();
|
||||||
|
_scheduleCallbacks(resolve) {
|
||||||
|
let gotTimer = false;
|
||||||
|
let gotMessage = false;
|
||||||
|
let gotPromise = false;
|
||||||
|
|
||||||
|
const tryTriggerAsyncCallback = () => {
|
||||||
|
if (!gotTimer || !gotMessage || !gotPromise)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._asyncCallback();
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(async () => {
|
||||||
|
await this._syncCallback();
|
||||||
|
gotPromise = true;
|
||||||
|
tryTriggerAsyncCallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
gotTimer = true;
|
||||||
|
tryTriggerAsyncCallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
AsyncRAFStepScheduler.mc.port1.addEventListener(
|
||||||
|
"message",
|
||||||
|
async function () {
|
||||||
|
await Promise.resolve();
|
||||||
|
gotMessage = true;
|
||||||
|
tryTriggerAsyncCallback();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
AsyncRAFStepScheduler.mc.port1.start();
|
||||||
|
AsyncRAFStepScheduler.mc.port2.postMessage("speedometer");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STEP_SCHEDULER_LOOKUP = {
|
||||||
|
__proto__: null,
|
||||||
|
raf: RAFStepScheduler,
|
||||||
|
async: AsyncRAFStepScheduler,
|
||||||
|
};
|
||||||
1
suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/todomvc-utils.mjs
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const numberOfItemsToAdd = 100;
|
||||||
734
suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/translations.mjs
vendored
Normal file
|
|
@ -0,0 +1,734 @@
|
||||||
|
export const todos = {
|
||||||
|
en: [
|
||||||
|
"Electronic Granite Hat",
|
||||||
|
"Bespoke Soft Table",
|
||||||
|
"Ergonomic Fresh Bike",
|
||||||
|
"Luxurious Wooden Cheese",
|
||||||
|
"Gorgeous Fresh Pizza",
|
||||||
|
"Rustic Rubber Shirt",
|
||||||
|
"Modern Rubber Soap",
|
||||||
|
"Small Bronze Ball",
|
||||||
|
"Awesome Bronze Shoes",
|
||||||
|
"Bespoke Steel Chair",
|
||||||
|
"Practical Plastic Soap",
|
||||||
|
"Incredible Granite Bacon",
|
||||||
|
"Elegant Metal Keyboard",
|
||||||
|
"Electronic Wooden Sausages",
|
||||||
|
"Tasty Wooden Gloves",
|
||||||
|
"Luxurious Metal Cheese",
|
||||||
|
"Awesome Rubber Gloves",
|
||||||
|
"Sleek Soft Car",
|
||||||
|
"Licensed Fresh Salad",
|
||||||
|
"Ergonomic Frozen Towels",
|
||||||
|
"Modern Rubber Keyboard",
|
||||||
|
"Tasty Concrete Pizza",
|
||||||
|
"Handmade Plastic Chicken",
|
||||||
|
"Luxurious Rubber Chicken",
|
||||||
|
"Practical Soft Fish",
|
||||||
|
"Ergonomic Bronze Shirt",
|
||||||
|
"Handcrafted Plastic Bacon",
|
||||||
|
"Unbranded Plastic Pants",
|
||||||
|
"Modern Wooden Sausages",
|
||||||
|
"Handmade Steel Shoes",
|
||||||
|
"Rustic Steel Bike",
|
||||||
|
"Gorgeous Frozen Salad",
|
||||||
|
"Handmade Bronze Chicken",
|
||||||
|
"Sleek Granite Bike",
|
||||||
|
"Generic Concrete Sausages",
|
||||||
|
"Incredible Plastic Tuna",
|
||||||
|
"Bespoke Fresh Cheese",
|
||||||
|
"Electronic Fresh Bacon",
|
||||||
|
"Licensed Wooden Car",
|
||||||
|
"Recycled Fresh Fish",
|
||||||
|
"Incredible Fresh Shoes",
|
||||||
|
"Practical Soft Chips",
|
||||||
|
"Small Soft Chicken",
|
||||||
|
"Intelligent Fresh Mouse",
|
||||||
|
"Modern Metal Mouse",
|
||||||
|
"Tasty Granite Gloves",
|
||||||
|
"Awesome Rubber Bike",
|
||||||
|
"Small Steel Shirt",
|
||||||
|
"Refined Concrete Computer",
|
||||||
|
"Sleek Frozen Shirt",
|
||||||
|
"Intelligent Concrete Shoes",
|
||||||
|
"Handmade Rubber Car",
|
||||||
|
"Sleek Rubber Towels",
|
||||||
|
"Unbranded Concrete Hat",
|
||||||
|
"Incredible Plastic Fish",
|
||||||
|
"Practical Soft Gloves",
|
||||||
|
"Organic Stone Pizza",
|
||||||
|
"Generic Wooden Keyboard",
|
||||||
|
"Recycled Wooden Chips",
|
||||||
|
"Incredible Rubber Chips",
|
||||||
|
"Ergonomic Granite Shirt",
|
||||||
|
"Tasty Frozen Keyboard",
|
||||||
|
"Gorgeous Steel Soap",
|
||||||
|
"Luxurious Plastic Chair",
|
||||||
|
"Elegant Frozen Bike",
|
||||||
|
"Recycled Steel Chair",
|
||||||
|
"Modern Bronze Sausages",
|
||||||
|
"Elegant Wooden Cheese",
|
||||||
|
"Small Plastic Sausages",
|
||||||
|
"Luxurious Frozen Shoes",
|
||||||
|
"Sleek Plastic Sausages",
|
||||||
|
"Handcrafted Fresh Sausages",
|
||||||
|
"Incredible Soft Chair",
|
||||||
|
"Recycled Wooden Soap",
|
||||||
|
"Soft Rubber Duck",
|
||||||
|
"Licensed Concrete Tuna",
|
||||||
|
"Luxurious Granite Pants",
|
||||||
|
"Refined Rubber Keyboard",
|
||||||
|
"Handcrafted Plastic Computer",
|
||||||
|
"Practical Steel Salad",
|
||||||
|
"Incredible Soft Bacon",
|
||||||
|
"Practical Metal Fish",
|
||||||
|
"Elegant Rubber Shirt",
|
||||||
|
"Handcrafted Rubber Table",
|
||||||
|
"Gorgeous Wooden Table",
|
||||||
|
"Fantastic Steel Sausages",
|
||||||
|
"Small Soft Keyboard",
|
||||||
|
"Generic Steel Ball",
|
||||||
|
"Electronic Frozen Hat",
|
||||||
|
"Gorgeous Fresh Chair",
|
||||||
|
"Sleek Soft Sausages",
|
||||||
|
"Gorgeous Wooden Towels",
|
||||||
|
"Bespoke Granite Pizza",
|
||||||
|
"Generic Metal Salad",
|
||||||
|
"Handmade Rubber Cheese",
|
||||||
|
"Fantastic Steel Chair",
|
||||||
|
"Handcrafted Frozen Computer",
|
||||||
|
"Rustic Rubber Mouse",
|
||||||
|
"Sleek Granite Pizza",
|
||||||
|
"Gorgeous Plastic Keyboard",
|
||||||
|
],
|
||||||
|
"zh-cn": [
|
||||||
|
"电子花岗岩帽子",
|
||||||
|
"定制软桌",
|
||||||
|
"符合人体工程学新鲜自行车",
|
||||||
|
"豪华的木制奶酪",
|
||||||
|
"华丽的新鲜披萨",
|
||||||
|
"质朴的橡胶衬衫",
|
||||||
|
"现代橡胶肥皂",
|
||||||
|
"小青铜球",
|
||||||
|
"很棒的青铜鞋",
|
||||||
|
"定制钢椅",
|
||||||
|
"实用的塑料肥皂",
|
||||||
|
"令人难以置信的花岗岩培根",
|
||||||
|
"优雅的金属键盘",
|
||||||
|
"电子木香肠",
|
||||||
|
"美味的木制手套",
|
||||||
|
"豪华的金属奶酪",
|
||||||
|
"很棒的橡胶手套",
|
||||||
|
"光滑的柔软汽车",
|
||||||
|
"有执照的新鲜沙拉",
|
||||||
|
"人体工程学的冷冻毛巾",
|
||||||
|
"现代橡胶键盘",
|
||||||
|
"美味的混凝土披萨",
|
||||||
|
"手工塑料鸡",
|
||||||
|
"豪华橡胶鸡",
|
||||||
|
"实用的软鱼",
|
||||||
|
"人体工程学的青铜衬衫",
|
||||||
|
"手工制作的塑料培根",
|
||||||
|
"未品牌的塑料裤",
|
||||||
|
"现代木香肠",
|
||||||
|
"手工钢鞋",
|
||||||
|
"质朴的钢自行车",
|
||||||
|
"华丽的冷冻沙拉",
|
||||||
|
"手工铜鸡",
|
||||||
|
"光滑的花岗岩自行车",
|
||||||
|
"通用混凝土香肠",
|
||||||
|
"令人难以置信的塑料金枪鱼",
|
||||||
|
"定制新鲜奶酪",
|
||||||
|
"电子新鲜培根",
|
||||||
|
"有执照的木车",
|
||||||
|
"回收的新鲜鱼",
|
||||||
|
"令人难以置信的新鲜鞋",
|
||||||
|
"实用的软芯片",
|
||||||
|
"小鸡肉",
|
||||||
|
"智能新鲜鼠标",
|
||||||
|
"现代金属鼠标",
|
||||||
|
"美味的花岗岩手套",
|
||||||
|
"很棒的橡胶自行车",
|
||||||
|
"小钢衬衫",
|
||||||
|
"精制混凝土计算机",
|
||||||
|
"光滑的冷冻衬衫",
|
||||||
|
"智能混凝土鞋",
|
||||||
|
"手工橡胶车",
|
||||||
|
"光滑的橡胶毛巾",
|
||||||
|
"未品牌的混凝土帽子",
|
||||||
|
"令人难以置信的塑料鱼",
|
||||||
|
"实用的软手套",
|
||||||
|
"有机石比萨",
|
||||||
|
"通用木制键盘",
|
||||||
|
"再生木芯片",
|
||||||
|
"令人难以置信的橡胶芯片",
|
||||||
|
"人体工程学花岗岩衬衫",
|
||||||
|
"美味的冷冻键盘",
|
||||||
|
"华丽的钢肥皂",
|
||||||
|
"豪华的塑料椅",
|
||||||
|
"优雅的冷冻自行车",
|
||||||
|
"再生钢椅",
|
||||||
|
"现代铜香肠",
|
||||||
|
"优雅的木制奶酪",
|
||||||
|
"小塑料香肠",
|
||||||
|
"豪华的冷冻鞋",
|
||||||
|
"光滑的塑料香肠",
|
||||||
|
"手工制作的新鲜香肠",
|
||||||
|
"令人难以置信的柔软椅子",
|
||||||
|
"回收的木制肥皂",
|
||||||
|
"软橡皮鸭",
|
||||||
|
"许可的混凝土金枪鱼",
|
||||||
|
"豪华的花岗岩裤",
|
||||||
|
"精制橡胶键盘",
|
||||||
|
"手工制作的塑料计算机",
|
||||||
|
"实用的钢沙拉",
|
||||||
|
"令人难以置信的软培根",
|
||||||
|
"实用的金属鱼",
|
||||||
|
"优雅的橡胶衬衫",
|
||||||
|
"手工橡胶桌",
|
||||||
|
"华丽的木桌",
|
||||||
|
"奇妙的钢香肠",
|
||||||
|
"小软键盘",
|
||||||
|
"通用钢球",
|
||||||
|
"电子冷冻帽子",
|
||||||
|
"华丽的新鲜椅子",
|
||||||
|
"光滑的软香肠",
|
||||||
|
"华丽的木制毛巾",
|
||||||
|
"定制的花岗岩披萨",
|
||||||
|
"通用金属沙拉",
|
||||||
|
"手工橡胶奶酪",
|
||||||
|
"奇妙的钢椅",
|
||||||
|
"手工制作的冷冻计算机",
|
||||||
|
"质朴的橡胶老鼠",
|
||||||
|
"光滑的花岗岩披萨",
|
||||||
|
"华丽的塑料键盘",
|
||||||
|
],
|
||||||
|
ja: [
|
||||||
|
"電子花崗岩の帽子",
|
||||||
|
"オーダーメイドのソフトテーブル",
|
||||||
|
"人間工学に基づいた新鮮な自転車",
|
||||||
|
"豪華な木製チーズ",
|
||||||
|
"ゴージャスな新鮮なピザ",
|
||||||
|
"素朴なラバーシャツ",
|
||||||
|
"モダンゴム石鹸",
|
||||||
|
"小さなブロンズボール",
|
||||||
|
"素晴らしいブロンズシューズ",
|
||||||
|
"オーダーメイドのスチールチェア",
|
||||||
|
"実用的なプラスチック石鹸",
|
||||||
|
"信じられないほどの花崗岩のベーコン",
|
||||||
|
"エレガントなメタルキーボード",
|
||||||
|
"電子木製ソーセージ",
|
||||||
|
"おいしい木製手袋",
|
||||||
|
"豪華なメタルチーズ",
|
||||||
|
"素晴らしいラバーグローブ",
|
||||||
|
"洗練されたソフトカー",
|
||||||
|
"ライセンスされた新鮮なサラダ",
|
||||||
|
"人間工学に基づいた冷凍タオル",
|
||||||
|
"モダンなゴムキーボード",
|
||||||
|
"おいしいコンクリートピザ",
|
||||||
|
"手作りのプラスチックチキン",
|
||||||
|
"豪華なゴム鶏",
|
||||||
|
"実用的な柔らかい魚",
|
||||||
|
"人間工学に基づいたブロンズシャツ",
|
||||||
|
"手作りのプラスチックベーコン",
|
||||||
|
"ブランドのないプラスチックパンツ",
|
||||||
|
"モダンな木製ソーセージ",
|
||||||
|
"手作りのスチールシューズ",
|
||||||
|
"素朴なスチールバイク",
|
||||||
|
"ゴージャスな冷凍サラダ",
|
||||||
|
"手作りのブロンズチキン",
|
||||||
|
"洗練された花崗岩の自転車",
|
||||||
|
"一般的なコンクリートソーセージ",
|
||||||
|
"信じられないほどのプラスチックマグロ",
|
||||||
|
"オーダーメイドのフレッシュチーズ",
|
||||||
|
"電子新鮮なベーコン",
|
||||||
|
"ライセンスされた木製車",
|
||||||
|
"新鮮な魚をリサイクルしました",
|
||||||
|
"信じられないほどの新鮮な靴",
|
||||||
|
"実用的なソフトチップ",
|
||||||
|
"小さな柔らかい鶏",
|
||||||
|
"インテリジェントな新鮮なマウス",
|
||||||
|
"現代の金属マウス",
|
||||||
|
"おいしい花崗岩の手袋",
|
||||||
|
"素晴らしいラバーバイク",
|
||||||
|
"小さなスチールシャツ",
|
||||||
|
"洗練されたコンクリートコンピューター",
|
||||||
|
"洗練された冷凍シャツ",
|
||||||
|
"インテリジェントコンクリートシューズ",
|
||||||
|
"手作りのラバーカー",
|
||||||
|
"洗練されたゴムタオル",
|
||||||
|
"ブランドのないコンクリートの帽子",
|
||||||
|
"信じられないほどのプラスチックの魚",
|
||||||
|
"実用的なソフトグローブ",
|
||||||
|
"オーガニックストーンピザ",
|
||||||
|
"一般的な木製キーボード",
|
||||||
|
"リサイクルされた木製チップ",
|
||||||
|
"信じられないほどのゴムチップ",
|
||||||
|
"人間工学に基づいた花崗岩のシャツ",
|
||||||
|
"おいしい冷凍キーボード",
|
||||||
|
"ゴージャスなスチールソープ",
|
||||||
|
"豪華なプラスチック製の椅子",
|
||||||
|
"エレガントな冷凍自転車",
|
||||||
|
"リサイクルスチールチェア",
|
||||||
|
"モダンブロンズソーセージ",
|
||||||
|
"エレガントな木製チーズ",
|
||||||
|
"小さなプラスチックソーセージ",
|
||||||
|
"豪華な冷凍靴",
|
||||||
|
"なめらかなプラスチックソーセージ",
|
||||||
|
"手作りの新鮮なソーセージ",
|
||||||
|
"信じられないほどのソフトチェア",
|
||||||
|
"リサイクルされた木製石鹸",
|
||||||
|
"柔らかいラバーアヒル",
|
||||||
|
"ライセンスされたコンクリートマグロ",
|
||||||
|
"豪華な花崗岩のズボン",
|
||||||
|
"洗練されたゴムキーボード",
|
||||||
|
"手作りのプラスチックコンピューター",
|
||||||
|
"実用的なスチールサラダ",
|
||||||
|
"信じられないほどのソフトベーコン",
|
||||||
|
"実用的な金属魚",
|
||||||
|
"エレガントなラバーシャツ",
|
||||||
|
"手作りのゴムテーブル",
|
||||||
|
"ゴージャスな木製のテーブル",
|
||||||
|
"素晴らしいスチールソーセージ",
|
||||||
|
"小さなソフトキーボード",
|
||||||
|
"ジェネリックスチールボール",
|
||||||
|
"電子冷凍帽子",
|
||||||
|
"ゴージャスな新鮮な椅子",
|
||||||
|
"洗練された柔らかいソーセージ",
|
||||||
|
"ゴージャスな木製タオル",
|
||||||
|
"オーダーメイドの花崗岩のピザ",
|
||||||
|
"ジェネリックメタルサラダ",
|
||||||
|
"手作りのゴムチーズ",
|
||||||
|
"素晴らしいスチールチェア",
|
||||||
|
"手作りの冷凍コンピューター",
|
||||||
|
"素朴なゴムマウス",
|
||||||
|
"洗練された花崗岩のピザ",
|
||||||
|
"ゴージャスなプラスチックキーボード",
|
||||||
|
],
|
||||||
|
es: [
|
||||||
|
"Sombrero de granito electrónico",
|
||||||
|
"Mesa suave a medida",
|
||||||
|
"Bicicleta ergonómica fresca",
|
||||||
|
"Lujoso queso de madera",
|
||||||
|
"Hermosa pizza fresca",
|
||||||
|
"Camisa de goma rústica",
|
||||||
|
"Jabón de goma moderno",
|
||||||
|
"Bola de bronce pequeña",
|
||||||
|
"Zapatos de bronce impresionantes",
|
||||||
|
"Silla de acero a medida",
|
||||||
|
"Jabón de plástico práctico",
|
||||||
|
"Tocino de granito increíble",
|
||||||
|
"Teclado de metal elegante",
|
||||||
|
"Salchichas de madera electrónica",
|
||||||
|
"Sabrosos guantes de madera",
|
||||||
|
"Lujoso queso de metal",
|
||||||
|
"Guantes de goma impresionantes",
|
||||||
|
"Coche suave y elegante",
|
||||||
|
"Ensalada fresca con licencia",
|
||||||
|
"Toallas congeladas ergonómicas",
|
||||||
|
"Teclado de goma moderno",
|
||||||
|
"Pizza de hormigón sabrosa",
|
||||||
|
"Pollo de plástico hecho a mano",
|
||||||
|
"Pollo de goma lujoso",
|
||||||
|
"Pescado suave práctico",
|
||||||
|
"Camisa de bronce ergonómica",
|
||||||
|
"Tocino de plástico artesanal",
|
||||||
|
"Pantalones de plástico sin marca",
|
||||||
|
"Salchichas de madera modernas",
|
||||||
|
"Zapatos de acero hechos a mano",
|
||||||
|
"Bicicleta de acero rústica",
|
||||||
|
"Hermosa ensalada congelada",
|
||||||
|
"Pollo bronce hecho a mano",
|
||||||
|
"Bicicleta de granito elegante",
|
||||||
|
"Salchichas de concreto genérico",
|
||||||
|
"Atún de plástico increíble",
|
||||||
|
"Queso fresco a medida",
|
||||||
|
"Tocino fresco electrónico",
|
||||||
|
"Coche de madera con licencia",
|
||||||
|
"Pescado reciclado",
|
||||||
|
"Increíbles zapatos frescos",
|
||||||
|
"Chips suaves prácticos",
|
||||||
|
"Pollo pequeño y suave",
|
||||||
|
"Ratón fresco inteligente",
|
||||||
|
"Ratón de metal moderno",
|
||||||
|
"Sabrosos guantes de granito",
|
||||||
|
"Impresionante bicicleta de goma",
|
||||||
|
"Camisa de acero pequeña",
|
||||||
|
"Ordenador de concreto refinada",
|
||||||
|
"Camisa elegante congelada",
|
||||||
|
"Zapatos de concreto inteligentes",
|
||||||
|
"Coche de goma hecho a mano",
|
||||||
|
"Toallas de goma elegantes",
|
||||||
|
"Sombrero de hormigón sin marca",
|
||||||
|
"Pescado de plástico increíble",
|
||||||
|
"Guantes suaves prácticos",
|
||||||
|
"Pizza de piedra orgánica",
|
||||||
|
"Teclado de madera genérico",
|
||||||
|
"Chips de madera reciclados",
|
||||||
|
"Increíbles chips de goma",
|
||||||
|
"Camisa de granito ergonómico",
|
||||||
|
"Sabroso teclado congelado",
|
||||||
|
"Hermoso jabón de acero",
|
||||||
|
"Lujosa silla de plástico",
|
||||||
|
"Bicicleta congelada elegante",
|
||||||
|
"Silla de acero reciclada",
|
||||||
|
"Salchichas de bronce modernas",
|
||||||
|
"Elegante queso de madera",
|
||||||
|
"Pequeñas salchichas de plástico",
|
||||||
|
"Zapatos congelados lujosos",
|
||||||
|
"Salchichas de plástico elegantes",
|
||||||
|
"Salchichas frescas artesanales",
|
||||||
|
"Increíble silla suave",
|
||||||
|
"Jabón de madera reciclado",
|
||||||
|
"Pato de goma suave",
|
||||||
|
"Atún de hormigón con licencia",
|
||||||
|
"Pantalones de granito lujosos",
|
||||||
|
"Teclado de goma refinado",
|
||||||
|
"Computadora de plástico hecha a mano",
|
||||||
|
"Ensalada de acero práctica",
|
||||||
|
"Bocino suave increíble",
|
||||||
|
"Pescado de metal práctico",
|
||||||
|
"Elegante camisa de goma",
|
||||||
|
"Mesa de goma artesanal",
|
||||||
|
"Hermosa mesa de madera",
|
||||||
|
"Fantásticas salchichas de acero",
|
||||||
|
"Pequeño teclado suave",
|
||||||
|
"Bola de acero genérica",
|
||||||
|
"Sombrero de congelado",
|
||||||
|
"Hermosa silla fresca",
|
||||||
|
"Salchichas suaves y elegantes",
|
||||||
|
"Hermosas toallas de madera",
|
||||||
|
"Pizza de granito a medida",
|
||||||
|
"Ensalada de metal genérica",
|
||||||
|
"Queso de goma hecho a mano",
|
||||||
|
"Fantástica silla de acero",
|
||||||
|
"Computadora congelada hecha a mano",
|
||||||
|
"Ratón de goma rústica",
|
||||||
|
"Pizza de granito elegante",
|
||||||
|
"Hermoso teclado de plástico",
|
||||||
|
],
|
||||||
|
de: [
|
||||||
|
"Elektronischer Granithut",
|
||||||
|
"Maßgeschneiderter weicher Tisch",
|
||||||
|
"Ergonomisches frisches Fahrrad",
|
||||||
|
"Luxuriöser Holzkäse",
|
||||||
|
"Wunderschöne frische Pizza",
|
||||||
|
"Rustikales Gummihemd",
|
||||||
|
"Moderne Gummiseife",
|
||||||
|
"Kleine Bronzekugel",
|
||||||
|
"Tolle Bronzeschuhe",
|
||||||
|
"Maßgeschneiderter Stahlstuhl",
|
||||||
|
"Praktische Plastikseife",
|
||||||
|
"Unglaublicher Granitspeck",
|
||||||
|
"Elegante Metall -Tastatur",
|
||||||
|
"Elektronische Holzwurst",
|
||||||
|
"Leckere Holzhandschuhe",
|
||||||
|
"Luxuriöser Metallkäse",
|
||||||
|
"Super Gummihandschuhe",
|
||||||
|
"Schlankes weiches Auto",
|
||||||
|
"Lizenzierter frischer Salat",
|
||||||
|
"Ergonomische gefrorene Handtücher",
|
||||||
|
"Moderne Gummi Tastatur",
|
||||||
|
"Leckere Betonpizza",
|
||||||
|
"Handgefertigtes Plastikhuhn",
|
||||||
|
"Luxuriöses Gummihähnchen",
|
||||||
|
"Praktischer weicher Fisch",
|
||||||
|
"Ergonomisches Bronzehemd",
|
||||||
|
"Handgefertigter Plastikspeck",
|
||||||
|
"Plastikhosen ohne Marken",
|
||||||
|
"Moderne Holz Würste",
|
||||||
|
"Handgefertigte Stahlschuhe",
|
||||||
|
"Rustikales Stahlrad",
|
||||||
|
"Wunderschöner gefrorener Salat",
|
||||||
|
"Handgefertigtes Bronze Hühnchen",
|
||||||
|
"Schlankes Granitrad",
|
||||||
|
"Generische Beton Würste",
|
||||||
|
"Unglaublicher Plastik Thunfisch",
|
||||||
|
"Maßgeschneiderter frischer Käse",
|
||||||
|
"Elektronischer frischer Speck",
|
||||||
|
"Lizenziertes Holzauto",
|
||||||
|
"Recycelter frischer Fisch",
|
||||||
|
"Unglaubliche frische Schuhe",
|
||||||
|
"Praktische weiche Chips",
|
||||||
|
"Kleines weiches Huhn",
|
||||||
|
"Intelligente frische Maus",
|
||||||
|
"Moderne Metallmaus",
|
||||||
|
"Leckere Granithandschuhe",
|
||||||
|
"Super Gummi Fahrrad",
|
||||||
|
"Kleines Stahlhemd",
|
||||||
|
"Raffinierter Betoncomputer",
|
||||||
|
"Schlankes gefrorenes Hemd",
|
||||||
|
"Intelligente Betonschuhe",
|
||||||
|
"Handgefertigtes Gummiauto",
|
||||||
|
"Schlanke Gummi Handtücher",
|
||||||
|
"Betonhut ohne Markenzeichen",
|
||||||
|
"Unglaublicher Plastikfisch",
|
||||||
|
"Praktische weiche Handschuhe",
|
||||||
|
"Bio Steinpizza",
|
||||||
|
"Generische hölzerne Tastatur",
|
||||||
|
"Recycelte Holzchips",
|
||||||
|
"Unglaubliche Gummischchips",
|
||||||
|
"Ergonomisches Granithemd",
|
||||||
|
"Leckere gefrorene Tastatur",
|
||||||
|
"Wunderschöne Stahlseife",
|
||||||
|
"Luxuriöser Plastikstuhl",
|
||||||
|
"Elegantes gefrorenes Fahrrad",
|
||||||
|
"Recycelter Stahlstuhl",
|
||||||
|
"Moderne Bronze Würste",
|
||||||
|
"Eleganter Holzkäse",
|
||||||
|
"Kleine Plastikwurst",
|
||||||
|
"Luxuriöse gefrorene Schuhe",
|
||||||
|
"Schlanke Plastikwurst",
|
||||||
|
"Handgefertigte frische Würste",
|
||||||
|
"Unglaublicher weicher Stuhl",
|
||||||
|
"Recycelte Holzseife",
|
||||||
|
"Weiche Gummi Ente",
|
||||||
|
"Lizenzierter Beton Thunfisch",
|
||||||
|
"Luxuriöse Granithosen",
|
||||||
|
"Raffinierte Gummi Tastatur",
|
||||||
|
"Handgefertigter Kunststoffcomputer",
|
||||||
|
"Praktischer Stahlsalat",
|
||||||
|
"Unglaublicher weicher Speck",
|
||||||
|
"Praktische Metallfische",
|
||||||
|
"Elegantes Gummihemd",
|
||||||
|
"Handgefertigter Gummi Tisch",
|
||||||
|
"Wunderschöner Holztisch",
|
||||||
|
"Fantastische Stahlwurst",
|
||||||
|
"Kleine weiche Tastatur",
|
||||||
|
"Generischer Stahlkugel",
|
||||||
|
"Elektronischer Gefrorenhut",
|
||||||
|
"Wunderschöner frischer Stuhl",
|
||||||
|
"Schlanke weiche Würste",
|
||||||
|
"Wunderschöne Holztücher",
|
||||||
|
"Maßgeschneiderte Granitpizza",
|
||||||
|
"Generischer Metallsalat",
|
||||||
|
"Handgefertigter Gummi Käse",
|
||||||
|
"Fantastischer Stahlstuhl",
|
||||||
|
"Handgefertigter gefrorener Computer",
|
||||||
|
"Rustikale Gummi Maus",
|
||||||
|
"Schlanke Granitpizza",
|
||||||
|
"Wunderschöne Plastiktastatur",
|
||||||
|
],
|
||||||
|
ru: [
|
||||||
|
"Электронная гранитная шляпа",
|
||||||
|
"Беспокойный мягкий стол",
|
||||||
|
"Эргономичный свежий велосипед",
|
||||||
|
"Роскошный деревянный сыр",
|
||||||
|
"Великолепная свежая пицца",
|
||||||
|
"Деревенская резиновая рубашка",
|
||||||
|
"Современное резиновое мыло",
|
||||||
|
"Маленький бронзовый мяч",
|
||||||
|
"Потрясающие бронзовые туфли",
|
||||||
|
"Стальное кресло на заказ",
|
||||||
|
"Практическое пластиковое мыло",
|
||||||
|
"Невероятный гранитный бекон",
|
||||||
|
"Элегантная металлическая клавиатура",
|
||||||
|
"Электронные деревянные колбасы",
|
||||||
|
"Вкусные деревянные перчатки",
|
||||||
|
"Роскошный металлический сыр",
|
||||||
|
"Потрясающие резиновые перчатки",
|
||||||
|
"Гладкая мягкая машина",
|
||||||
|
"Лицензированный свежий салат",
|
||||||
|
"Эргономичные замороженные полотенца",
|
||||||
|
"Современная резиновая клавиатура",
|
||||||
|
"Вкусная бетонная пицца",
|
||||||
|
"Пластиковая курица ручной работы",
|
||||||
|
"Роскошная резиновая курица",
|
||||||
|
"Практическая мягкая рыба",
|
||||||
|
"Эргономичная бронзовая рубашка",
|
||||||
|
"Пластиковый бекон из ручной работы",
|
||||||
|
"Непревзойденные пластиковые брюки",
|
||||||
|
"Современные деревянные колбасы",
|
||||||
|
"Стальные туфли ручной работы",
|
||||||
|
"Деревенский стальный велосипед",
|
||||||
|
"Великолепный замороженный салат",
|
||||||
|
"Бронзовая курица ручной работы",
|
||||||
|
"Гладкий гранитный велосипед",
|
||||||
|
"Общие бетонные колбасы",
|
||||||
|
"Невероятный пластиковый тунец",
|
||||||
|
"Созданный свежий сыр",
|
||||||
|
"Электронный свежий бекон",
|
||||||
|
"Лицензированный деревянный автомобиль",
|
||||||
|
"Переработанная свежая рыба",
|
||||||
|
"Невероятные свежие туфли",
|
||||||
|
"Практические мягкие чипсы",
|
||||||
|
"Маленькая мягкая курица",
|
||||||
|
"Интеллектуальная свежая мышь",
|
||||||
|
"Современная металлическая мышь",
|
||||||
|
"Вкусные гранитные перчатки",
|
||||||
|
"Потрясающий резиновый велосипед",
|
||||||
|
"Маленькая стальная рубашка",
|
||||||
|
"Изысканный бетонный компьютер",
|
||||||
|
"Гладкая замороженная рубашка",
|
||||||
|
"Интеллектуальные бетонные туфли",
|
||||||
|
"Резиновый автомобиль ручной работы",
|
||||||
|
"Гладкие резиновые полотенца",
|
||||||
|
"Непревзойденная бетонная шляпа",
|
||||||
|
"Невероятная пластиковая рыба",
|
||||||
|
"Практические мягкие перчатки",
|
||||||
|
"Органическая камня пицца",
|
||||||
|
"Общая деревянная клавиатура",
|
||||||
|
"Переработанные деревянные чипсы",
|
||||||
|
"Невероятные резиновые чипсы",
|
||||||
|
"Эргономичная гранитная рубашка",
|
||||||
|
"Вкусная замороженная клавиатура",
|
||||||
|
"Великолепное стальное мыло",
|
||||||
|
"Роскошное пластиковое кресло",
|
||||||
|
"Элегантный замороженный велосипед",
|
||||||
|
"Переработанное стальное кресло",
|
||||||
|
"Современные бронзовые колбасы",
|
||||||
|
"Элегантный деревянный сыр",
|
||||||
|
"Маленькие пластиковые колбаски",
|
||||||
|
"Роскошные замороженные туфли",
|
||||||
|
"Гладкие пластиковые колбаски",
|
||||||
|
"Свежие колбаски ручной работы",
|
||||||
|
"Невероятный мягкий стул",
|
||||||
|
"Переработанное деревянное мыло",
|
||||||
|
"Мягкая резиновая утка",
|
||||||
|
"Лицензированный бетонный тунец",
|
||||||
|
"Роскошные гранитные штаны",
|
||||||
|
"Рафинированная резиновая клавиатура",
|
||||||
|
"Пластиковый компьютер ручной работы",
|
||||||
|
"Практический стальной салат",
|
||||||
|
"Невероятный мягкий бекон",
|
||||||
|
"Практическая металлическая рыба",
|
||||||
|
"Элегантная резиновая рубашка",
|
||||||
|
"Ручной резиновый стол",
|
||||||
|
"Великолепный деревянный стол",
|
||||||
|
"Фантастические стальные колбаски",
|
||||||
|
"Небольшая мягкая клавиатура",
|
||||||
|
"Общий стальной шар",
|
||||||
|
"Электронная замороженная шляпа",
|
||||||
|
"Великолепный свежий стул",
|
||||||
|
"Гладкие мягкие колбаски",
|
||||||
|
"Великолепные деревянные полотенца",
|
||||||
|
"Сделанная на заказ гранитная пицца",
|
||||||
|
"Общий металлический салат",
|
||||||
|
"Резиновый сыр ручной работы",
|
||||||
|
"Фантастическое стальное кресло",
|
||||||
|
"Замороженный компьютер ручной работы",
|
||||||
|
"Деревенская резиновая мышь",
|
||||||
|
"Главная гранитная пицца",
|
||||||
|
"Великолепная пластиковая клавиатура",
|
||||||
|
],
|
||||||
|
emoji: [
|
||||||
|
"Electronic Granite Hat 👆🏻",
|
||||||
|
"Bespoke Soft 🍷 Table",
|
||||||
|
"Ergonomic Fresh Bike 😚😚",
|
||||||
|
"Luxurious 🍉 Wooden Cheese 🍮",
|
||||||
|
"Gorgeous Fresh Pizza ⛔",
|
||||||
|
"Rustic 💪🏽 Rubber Shirt",
|
||||||
|
"Modern Rubber 🍀 Soap",
|
||||||
|
"👍 Small Bronze Ball 👍",
|
||||||
|
"Awesome Bronze Shoes 😎",
|
||||||
|
"Bespoke 👈🏽 Steel Chair",
|
||||||
|
"Practical Plastic 💋 💋 Soap",
|
||||||
|
"🙌🏻 Incredible Granite Bacon",
|
||||||
|
"🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃",
|
||||||
|
"Electronic Wooden Sausages 🌷",
|
||||||
|
"Tasty 🍺 Wooden Gloves",
|
||||||
|
"🏖️ Luxurious Metal Cheese",
|
||||||
|
"Awesome Rubber 😉 Gloves",
|
||||||
|
"Sleek Soft Car 💁🏻♂️",
|
||||||
|
"Licensed 👏👏👏 Fresh Salad",
|
||||||
|
"Ergonomic Frozen Towels 🐇",
|
||||||
|
"🖐🏻 Modern Rubber Keyboard",
|
||||||
|
"Tasty Concrete Pizza ✨✨",
|
||||||
|
"Handmade 😘 Plastic 😘 Chicken 😘",
|
||||||
|
"🏁 Luxurious Rubber Chicken 🏁",
|
||||||
|
"Practical Soft Fish 🤍",
|
||||||
|
"Ergonomic Bronze Shirt 😍",
|
||||||
|
"😸😸 Handcrafted 🐻 Plastic Bacon",
|
||||||
|
"Unbranded 🐭 Plastic Pants",
|
||||||
|
"🤘 Modern 🤘 Wooden 🤘 Sausages",
|
||||||
|
"Handmade Steel Shoes 👍",
|
||||||
|
"Rustic 🧁🧁 Steel Bike",
|
||||||
|
"Gorgeous Frozen Salad 👩💻",
|
||||||
|
"Handmade Bronze Chicken 😮😸",
|
||||||
|
"Sleek 🍐 Granite Bike",
|
||||||
|
" ❌ ❌ Generic Concrete Sausages",
|
||||||
|
"Incredible 🍉 Plastic Tuna",
|
||||||
|
"Bespoke Fresh Cheese 😘",
|
||||||
|
"💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡",
|
||||||
|
"Licensed 🍐 Wooden 🍅 Car ",
|
||||||
|
"Recycled Fresh Fish 🤡",
|
||||||
|
"📞 Incredible Fresh Shoes",
|
||||||
|
"Practical 🐻🐻 Soft Chips",
|
||||||
|
"Small 💝 Soft Chicken",
|
||||||
|
" 💝 Intelligent Fresh Mouse 💝",
|
||||||
|
"Modern Metal 🧵 Mouse",
|
||||||
|
"🦈 Tasty Granite Gloves",
|
||||||
|
"Awesome Rubber Bike 😡😡😡😡",
|
||||||
|
"🚮 Small Steel Shirt 🚮",
|
||||||
|
"Refined 🌲 Concrete Computer",
|
||||||
|
"Sleek Frozen Shirt 👨🦰",
|
||||||
|
"Intelligent Concrete ➗➗➗ Shoes",
|
||||||
|
"🏅Handmade Rubber Car",
|
||||||
|
"Sleek 👨🏼🌾 Rubber 👨🏼🌾 Towels",
|
||||||
|
"Unbranded Concrete Hat🎇",
|
||||||
|
"🌀🌀Incredible Plastic Fish",
|
||||||
|
"Practical Soft Gloves🌶️ 🌶️",
|
||||||
|
"Organic 🍞 Stone 🔽 Pizza 🥴",
|
||||||
|
"Generic Wooden Keyboard 💙",
|
||||||
|
"Recycled 🔴 Wooden Chips",
|
||||||
|
"Incredible Rubber Chips 🍹",
|
||||||
|
"🌵 Ergonomic Granite Shirt",
|
||||||
|
"Tasty Frozen 🦄 Keyboard",
|
||||||
|
"🍣 Gorgeous Steel 🥯 Soap",
|
||||||
|
"Luxurious Plastic Chair 🧑🦰",
|
||||||
|
"Elegant Frozen 🧑🦰 Bike",
|
||||||
|
"Recycled 🟠🟠 Steel Chair",
|
||||||
|
"⭐⭐ Modern ⭐ Bronze ⭐ Sausages",
|
||||||
|
"Elegant Wooden Cheese🤘",
|
||||||
|
"Small 🎎 Plastic 🛩️ Sausages",
|
||||||
|
"*️⃣*️⃣*️⃣*️⃣ Luxurious Frozen Shoes",
|
||||||
|
"Sleek Plastic Sausages 🚩",
|
||||||
|
"Handcrafted Fresh 💮 Sausages",
|
||||||
|
"Incredible 🤢🤢 Soft Chair",
|
||||||
|
"🇬🇪 Recycled 🇲🇺 Wooden Soap",
|
||||||
|
"Soft 🦌 Rubber Duck 🐥",
|
||||||
|
"Licensed Concrete Tuna 👎👎",
|
||||||
|
"Luxurious Granite 💝 Pants",
|
||||||
|
"Refined Rubber Keyboard 💝",
|
||||||
|
"👌🏻👌🏻 Handcrafted Plastic Computer",
|
||||||
|
"Practical Steel 🐪 Salad",
|
||||||
|
"Incredible Soft Bacon 🌺",
|
||||||
|
"Practical Metal 🥊 Fish",
|
||||||
|
"Elegant 👩🏾❤️💋👨🏽👩🏾❤️💋👨🏽 Rubber Shirt",
|
||||||
|
"🛺 Handcrafted Rubber Table",
|
||||||
|
"Gorgeous 🦙 Wooden Table 🦙",
|
||||||
|
"🍉 Fantastic Steel Sausages",
|
||||||
|
"Small Soft Keyboard👟",
|
||||||
|
"Generic 🦙🦙 Steel Ball",
|
||||||
|
"Electronic Frozen Hat ✌🏾✌🏾✌🏾✌🏾✌🏾",
|
||||||
|
"Gorgeous 🍏 Fresh Chair",
|
||||||
|
"Sleek Soft 💧 Sausages",
|
||||||
|
"Gorgeous Wooden Towels 🍿",
|
||||||
|
"Bespoke 🌅 Granite Pizza",
|
||||||
|
"Generic Metal Salad 🎗️",
|
||||||
|
"✨ Handmade ✨ Rubber ✨ Cheese ✨",
|
||||||
|
"Fantastic 🐥 🌺 🤷🏾 Steel Chair",
|
||||||
|
"Handcrafted Frozen Computer 🛡️ 🧸 🐓",
|
||||||
|
"🐹 Rustic Rubber Mouse",
|
||||||
|
"💠 Sleek Granite Pizza 💠",
|
||||||
|
"Gorgeous 🧝🏻♂️ Plastic Keyboard",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTodoText = {
|
||||||
|
en: "Something to do",
|
||||||
|
"zh-cn": "做某事",
|
||||||
|
ja: "何かをする必要がある",
|
||||||
|
es: "Algo que hacer",
|
||||||
|
de: "Etwas zu tun",
|
||||||
|
ru: "Кое-что сделать",
|
||||||
|
emoji: "Something to do 😊",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultLanguage = "en";
|
||||||
|
|
||||||
|
export function getTodoText(lang = "en", index) {
|
||||||
|
const todosSelection = todos[lang];
|
||||||
|
const currentIndex = index % todosSelection.length;
|
||||||
|
return todosSelection[currentIndex];
|
||||||
|
}
|
||||||
72
suites-experimental/javascript-wc-indexeddb/dist/src/storage/base-storage-manager.js
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { numberOfItemsToAdd, promisesEventsNames } from "../workload-test.mjs";
|
||||||
|
|
||||||
|
// Base class for storage managers that provides common functionality
|
||||||
|
// for tracking pending operations and dispatching events.
|
||||||
|
class BaseStorageManager {
|
||||||
|
constructor() {
|
||||||
|
this.dbName = "todoDB";
|
||||||
|
this.storeName = "todos";
|
||||||
|
this.db = null;
|
||||||
|
this.finishedAdditions = 0;
|
||||||
|
this.finishedToggles = 0;
|
||||||
|
this.finishedDeletions = 0;
|
||||||
|
this.initDB().then(() => {
|
||||||
|
this._dispatchReadyEvent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureDbConnection() {
|
||||||
|
if (!this.db)
|
||||||
|
throw new Error("Database connection is not established");
|
||||||
|
}
|
||||||
|
|
||||||
|
// When runner in Speedometer, additions, completions and removals are
|
||||||
|
// triggered synchonously in a tight loop, increasing the pending counters.
|
||||||
|
// The completion events are dispatched only when all pending operations
|
||||||
|
// of that type are complete.
|
||||||
|
|
||||||
|
_handleAddComplete() {
|
||||||
|
if (++this.finishedAdditions === numberOfItemsToAdd)
|
||||||
|
window.dispatchEvent(new CustomEvent(promisesEventsNames.add, {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleToggleComplete() {
|
||||||
|
if (++this.finishedToggles === numberOfItemsToAdd)
|
||||||
|
window.dispatchEvent(new CustomEvent(promisesEventsNames.toggle, {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleRemoveComplete() {
|
||||||
|
if (++this.finishedDeletions === numberOfItemsToAdd) {
|
||||||
|
this.db.close();
|
||||||
|
window.dispatchEvent(new CustomEvent(promisesEventsNames.delete, {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dispatchReadyEvent() {
|
||||||
|
window.dispatchEvent(new CustomEvent("db-ready", {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstract methods that must be implemented by subclasses
|
||||||
|
|
||||||
|
async initDB() {
|
||||||
|
throw new Error("initDB method must be implemented by subclass");
|
||||||
|
}
|
||||||
|
|
||||||
|
addTodo(todo) {
|
||||||
|
throw new Error("addTodo method must be implemented by subclass");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTodos(upperItemNumber, count) {
|
||||||
|
throw new Error("getTodos method must be implemented by subclass");
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTodo(itemNumber, completed) {
|
||||||
|
throw new Error("toggleTodo method must be implemented by subclass");
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTodo(itemNumber) {
|
||||||
|
throw new Error("removeTodo method must be implemented by subclass");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseStorageManager;
|
||||||
77
suites-experimental/javascript-wc-indexeddb/dist/src/storage/dexieDB-manager.js
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import Dexie from "../../libs/dexie.mjs";
|
||||||
|
import BaseStorageManager from "./base-storage-manager.js";
|
||||||
|
|
||||||
|
class DexieDBManager extends BaseStorageManager {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initDB() {
|
||||||
|
// Delete the existing database first for clean state
|
||||||
|
await Dexie.delete(this.dbName);
|
||||||
|
|
||||||
|
this.db = new Dexie(this.dbName);
|
||||||
|
|
||||||
|
this.db.version(1).stores({
|
||||||
|
todos: "itemNumber, id, title, completed, priority",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.db.open();
|
||||||
|
|
||||||
|
return this.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTodo(todo) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
this.db.todos
|
||||||
|
.add(todo)
|
||||||
|
.then(() => {
|
||||||
|
// When running in Speedometer, the event will be dispatched only once
|
||||||
|
// because all the additions are done in a tight loop.
|
||||||
|
this._handleAddComplete();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTodos(upperItemNumber, count) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
// Get items with itemNumber less than upperItemNumber
|
||||||
|
// Use reverse to get highest first, then limit, then reverse result back to ascending
|
||||||
|
const items = await this.db.todos.where("itemNumber").below(upperItemNumber).reverse().limit(count).toArray();
|
||||||
|
|
||||||
|
// Reverse to get ascending order (lowest itemNumber first) to match IndexedDB implementation
|
||||||
|
return items.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTodo(itemNumber, completed) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
this.db.todos
|
||||||
|
.update(itemNumber, { completed: completed })
|
||||||
|
.then(() => {
|
||||||
|
this._handleToggleComplete();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTodo(itemNumber) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
this.db.todos
|
||||||
|
.delete(itemNumber)
|
||||||
|
.then(() => {
|
||||||
|
this._handleRemoveComplete();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DexieDBManager;
|
||||||
165
suites-experimental/javascript-wc-indexeddb/dist/src/storage/indexedDB-manager.js
vendored
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import BaseStorageManager from "./base-storage-manager.js";
|
||||||
|
|
||||||
|
class IndexedDBManager extends BaseStorageManager {
|
||||||
|
initDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Delete the existing database first for clean state
|
||||||
|
const deleteRequest = indexedDB.deleteDatabase(this.dbName);
|
||||||
|
|
||||||
|
deleteRequest.onerror = (event) => {
|
||||||
|
reject(event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRequest.onsuccess = () => {
|
||||||
|
this.openDatabase(resolve, reject);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRequest.onblocked = () => {
|
||||||
|
reject(new Error("Database deletion blocked - please close other tabs using this database"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openDatabase(resolve, reject) {
|
||||||
|
const request = indexedDB.open(this.dbName, 1);
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject(event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
this.db = event.target.result;
|
||||||
|
resolve(this.db);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
|
||||||
|
const store = db.createObjectStore(this.storeName, { keyPath: "itemNumber" });
|
||||||
|
store.createIndex("id", "id", { unique: true });
|
||||||
|
store.createIndex("title", "title", { unique: false });
|
||||||
|
store.createIndex("completed", "completed", { unique: false });
|
||||||
|
store.createIndex("priority", "priority", { unique: false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addTodo(todo) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
// When running in Speedometer, the event will be dispatched only once
|
||||||
|
// because all the additions are done in a tight loop.
|
||||||
|
this._handleAddComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.onerror = (event) => {
|
||||||
|
console.error("Transaction error:", event.target.error);
|
||||||
|
throw event.target.error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
store.add(todo);
|
||||||
|
transaction.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTodos(upperItemNumber, count) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(this.storeName, "readonly");
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
|
||||||
|
// Use IDBKeyRange to get items with itemNumber less than upperItemNumber
|
||||||
|
const range = IDBKeyRange.upperBound(upperItemNumber, true); // true = exclusive bound
|
||||||
|
|
||||||
|
// Open a cursor to iterate through records in descending order
|
||||||
|
const request = store.openCursor(range, "prev");
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
let itemsProcessed = 0;
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
|
||||||
|
// Check if we have a valid cursor and haven't reached our count limit
|
||||||
|
if (cursor && itemsProcessed < count) {
|
||||||
|
items.push(cursor.value);
|
||||||
|
itemsProcessed++;
|
||||||
|
cursor.continue(); // Move to next item
|
||||||
|
} else {
|
||||||
|
// We're done - sort items by itemNumber in ascending order
|
||||||
|
// for proper display order (oldest to newest)
|
||||||
|
items.sort((a, b) => a.itemNumber - b.itemNumber);
|
||||||
|
transaction.commit();
|
||||||
|
resolve(items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.onerror = (event) => {
|
||||||
|
reject(event.target.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTodo(itemNumber, completed) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
|
||||||
|
const getRequest = store.get(itemNumber);
|
||||||
|
|
||||||
|
getRequest.onsuccess = (event) => {
|
||||||
|
const todoItem = getRequest.result;
|
||||||
|
|
||||||
|
if (!todoItem)
|
||||||
|
throw new Error(`Todo item with itemNumber '${itemNumber}' not found`);
|
||||||
|
|
||||||
|
todoItem.completed = completed;
|
||||||
|
const updateRequest = store.put(todoItem);
|
||||||
|
|
||||||
|
updateRequest.onerror = (event) => {
|
||||||
|
throw event.target.error;
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.commit();
|
||||||
|
};
|
||||||
|
|
||||||
|
getRequest.onerror = (event) => {
|
||||||
|
throw event.target.error;
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
this._handleToggleComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle transaction errors
|
||||||
|
transaction.onerror = (event) => {
|
||||||
|
throw event.target.error;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTodo(itemNumber) {
|
||||||
|
this._ensureDbConnection();
|
||||||
|
|
||||||
|
// Access the todo item directly by its itemNumber (keyPath)
|
||||||
|
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
this._handleRemoveComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.onerror = (event) => {
|
||||||
|
throw event.target.error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete the todo item directly using its primary key (itemNumber)
|
||||||
|
store.delete(itemNumber);
|
||||||
|
transaction.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexedDBManager;
|
||||||
24
suites-experimental/javascript-wc-indexeddb/dist/src/storage/storage-factory.js
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import IndexedDBManager from "./indexedDB-manager.js";
|
||||||
|
import DexieDBManager from "./dexieDB-manager.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function that returns the appropriate storage manager based on URL search parameters
|
||||||
|
* @returns {IndexedDBManager|DexieDBManager} The storage manager instance
|
||||||
|
*/
|
||||||
|
export function createStorageManager() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
let storageType = params.get("storageType");
|
||||||
|
if (storageType && storageType !== "vanilla" && storageType !== "dexie")
|
||||||
|
throw new Error(`Invalid storage type specified in URL parameter: ${storageType}`);
|
||||||
|
|
||||||
|
storageType = storageType || "vanilla";
|
||||||
|
|
||||||
|
if (storageType === "dexie") {
|
||||||
|
console.log("Using Dexie.js storage manager");
|
||||||
|
return new DexieDBManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to vanilla IndexedDB
|
||||||
|
console.log("Using vanilla IndexedDB storage manager");
|
||||||
|
return new IndexedDBManager();
|
||||||
|
}
|
||||||
41
suites-experimental/javascript-wc-indexeddb/dist/src/utils/nanoid.js
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* Borrowed from https://github.com/ai/nanoid/blob/3.0.2/non-secure/index.js
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright 2017 Andrey Sitnik <andrey@sitnik.ru>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||||
|
|
||||||
|
// This alphabet uses `A-Za-z0-9_-` symbols.
|
||||||
|
// The order of characters is optimized for better gzip and brotli compression.
|
||||||
|
// References to the same file (works both for gzip and brotli):
|
||||||
|
// `'use`, `andom`, and `rict'`
|
||||||
|
// References to the brotli default dictionary:
|
||||||
|
// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf`
|
||||||
|
let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
||||||
|
|
||||||
|
export function nanoid(size = 21) {
|
||||||
|
let id = "";
|
||||||
|
// A compact alternative for `for (var i = 0; i < step; i++)`.
|
||||||
|
let i = size;
|
||||||
|
while (i--) {
|
||||||
|
// `| 0` is more compact and faster than `Math.floor()`.
|
||||||
|
id += urlAlphabet[(Math.random() * 64) | 0];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
99
suites-experimental/javascript-wc-indexeddb/dist/src/workload-test.mjs
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { BenchmarkStep, BenchmarkSuite } from "./speedometer-utils/benchmark.mjs";
|
||||||
|
import { getTodoText, defaultLanguage } from "./speedometer-utils/translations.mjs";
|
||||||
|
import { numberOfItemsToAdd } from "./speedometer-utils/todomvc-utils.mjs";
|
||||||
|
|
||||||
|
export const appName = "todomvc-indexeddb";
|
||||||
|
export const appVersion = "1.0.0";
|
||||||
|
export { numberOfItemsToAdd };
|
||||||
|
|
||||||
|
export const promisesEventsNames = {
|
||||||
|
add: "db-add-completed",
|
||||||
|
toggle: "db-toggle-completed",
|
||||||
|
delete: "db-delete-completed",
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPromise = new Promise((resolve) => {
|
||||||
|
window.addEventListener(promisesEventsNames.add, () => resolve());
|
||||||
|
});
|
||||||
|
const togglePromise = new Promise((resolve) => {
|
||||||
|
window.addEventListener(promisesEventsNames.toggle, () => resolve());
|
||||||
|
});
|
||||||
|
const deletePromise = new Promise((resolve) => {
|
||||||
|
window.addEventListener(promisesEventsNames.delete, () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
const suites = {
|
||||||
|
default: new BenchmarkSuite("indexeddb", [
|
||||||
|
new BenchmarkStep(`Adding${numberOfItemsToAdd}Items`, async () => {
|
||||||
|
const input = document.querySelector("todo-app").shadowRoot.querySelector("todo-topbar").shadowRoot.querySelector(".new-todo-input");
|
||||||
|
for (let i = 0; i < numberOfItemsToAdd; i++) {
|
||||||
|
input.value = getTodoText(defaultLanguage, i);
|
||||||
|
input.dispatchEvent(new Event("input"));
|
||||||
|
input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" }));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new BenchmarkStep(
|
||||||
|
"FinishAddingItemsToDB",
|
||||||
|
async () => {
|
||||||
|
await addPromise;
|
||||||
|
},
|
||||||
|
/* ignoreResult = */ true
|
||||||
|
),
|
||||||
|
new BenchmarkStep("CompletingAllItems", async () => {
|
||||||
|
const numberOfItemsPerIteration = 10;
|
||||||
|
const numberOfIterations = 10;
|
||||||
|
for (let j = 0; j < numberOfIterations; j++) {
|
||||||
|
const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list");
|
||||||
|
const items = todoList.shadowRoot.querySelectorAll("todo-item");
|
||||||
|
for (let i = 0; i < numberOfItemsPerIteration; i++) {
|
||||||
|
const item = items[i].shadowRoot.querySelector(".toggle-todo-input");
|
||||||
|
item.click();
|
||||||
|
}
|
||||||
|
if (j < 9) {
|
||||||
|
const nextPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".next-page-button");
|
||||||
|
nextPageButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new BenchmarkStep(
|
||||||
|
"FinishModifyingItemsInDB",
|
||||||
|
async () => {
|
||||||
|
await togglePromise;
|
||||||
|
},
|
||||||
|
/* ignoreResult = */ true
|
||||||
|
),
|
||||||
|
new BenchmarkStep("DeletingAllItems", async () => {
|
||||||
|
const numberOfItemsPerIteration = 10;
|
||||||
|
const numberOfIterations = 10;
|
||||||
|
function iterationFinishedListener() {
|
||||||
|
iterationFinishedListener.promiseResolve();
|
||||||
|
}
|
||||||
|
window.addEventListener("previous-page-loaded", iterationFinishedListener);
|
||||||
|
for (let j = 0; j < numberOfIterations; j++) {
|
||||||
|
const iterationFinishedPromise = new Promise((resolve) => {
|
||||||
|
iterationFinishedListener.promiseResolve = resolve;
|
||||||
|
});
|
||||||
|
const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list");
|
||||||
|
const items = todoList.shadowRoot.querySelectorAll("todo-item");
|
||||||
|
for (let i = numberOfItemsPerIteration - 1; i >= 0; i--) {
|
||||||
|
const item = items[i].shadowRoot.querySelector(".remove-todo-button");
|
||||||
|
item.click();
|
||||||
|
}
|
||||||
|
if (j < 9) {
|
||||||
|
const previousPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".previous-page-button");
|
||||||
|
previousPageButton.click();
|
||||||
|
await iterationFinishedPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new BenchmarkStep(
|
||||||
|
"FinishDeletingItemsFromDB",
|
||||||
|
async () => {
|
||||||
|
await deletePromise;
|
||||||
|
},
|
||||||
|
/* ignoreResult = */ true
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default suites;
|
||||||
15
suites-experimental/javascript-wc-indexeddb/dist/styles/app.constructable.js
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(`:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
min-height: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
background: #fff;
|
||||||
|
margin: 24px 16px 40px 16px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
export default sheet;
|
||||||
158
suites-experimental/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js
vendored
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(`:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottombar {
|
||||||
|
padding: 10px 0;
|
||||||
|
height: 41px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottombar::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
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-status {
|
||||||
|
text-align: left;
|
||||||
|
padding: 3px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 26px;
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-count {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-link {
|
||||||
|
color: inherit;
|
||||||
|
margin: 3px;
|
||||||
|
padding: 0 7px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-link:hover {
|
||||||
|
border-color: #db7676;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-link.selected {
|
||||||
|
border-color: #ce4646;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-completed-button,
|
||||||
|
.clear-completed-button:active {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 26px;
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-completed-button:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rtl support */
|
||||||
|
html[dir="rtl"] .todo-status,
|
||||||
|
:host([dir="rtl"]) .todo-status {
|
||||||
|
right: 12px;
|
||||||
|
left: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .clear-completed-button,
|
||||||
|
:host([dir="rtl"]) .clear-completed-button {
|
||||||
|
left: 12px;
|
||||||
|
right: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.bottombar {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-status {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
left: unset;
|
||||||
|
right: unset;
|
||||||
|
top: unset;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
left: unset;
|
||||||
|
right: unset;
|
||||||
|
top: unset;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-completed-button,
|
||||||
|
.clear-completed-button:active {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
left: unset;
|
||||||
|
right: unset;
|
||||||
|
top: unset;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .todo-status,
|
||||||
|
:host([dir="rtl"]) .todo-status {
|
||||||
|
right: unset;
|
||||||
|
left: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .clear-completed-button,
|
||||||
|
:host([dir="rtl"]) .clear-completed-button {
|
||||||
|
left: unset;
|
||||||
|
right: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
export default sheet;
|
||||||
26
suites-experimental/javascript-wc-indexeddb/dist/styles/footer.css
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin: 65px auto 0;
|
||||||
|
color: #4d4d4d;
|
||||||
|
font-size: 11px;
|
||||||
|
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
86
suites-experimental/javascript-wc-indexeddb/dist/styles/global.constructable.js
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(`*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.4em;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #111;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 582px;
|
||||||
|
margin: 0 auto;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
box-shadow: inset 0 0 2px 2px #cf7d7d !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:placeholder-shown {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncate-singleline {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
export default sheet;
|
||||||
88
suites-experimental/javascript-wc-indexeddb/dist/styles/global.css
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
/** defaults */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.4em;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #111;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 582px;
|
||||||
|
margin: 0 auto;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
box-shadow: inset 0 0 2px 2px #cf7d7d !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** resets */
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:placeholder-shown {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* utility classes */
|
||||||
|
|
||||||
|
/* used for things that should be hidden in the ui,
|
||||||
|
but useful for people who use screen readers */
|
||||||
|
.visually-hidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncate-singleline {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
21
suites-experimental/javascript-wc-indexeddb/dist/styles/header.css
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-top: 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 80px;
|
||||||
|
line-height: 80px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 200;
|
||||||
|
text-align: center;
|
||||||
|
color: #b83f45;
|
||||||
|
-webkit-text-rendering: optimizeLegibility;
|
||||||
|
-moz-text-rendering: optimizeLegibility;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
11
suites-experimental/javascript-wc-indexeddb/dist/styles/main.constructable.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(`:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
export default sheet;
|
||||||
147
suites-experimental/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js
vendored
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(`:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:last-child) > .todo-item {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
border-bottom: 1px solid #ededed;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item.editing {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-todo-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item.editing .edit-todo-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-todo-input {
|
||||||
|
padding: 0 16px 0 60px;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.4em;
|
||||||
|
background: rgba(0, 0, 0, 0.003);
|
||||||
|
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||||
|
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%20%20style%3D%22opacity%3A%200.2%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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-todo {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item.editing .display-todo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-todo-input {
|
||||||
|
text-align: center;
|
||||||
|
width: 40px;
|
||||||
|
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 3px;
|
||||||
|
margin: auto 0;
|
||||||
|
border: none; appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item-text {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
padding: 0 60px;
|
||||||
|
display: block;
|
||||||
|
line-height: 60px;
|
||||||
|
transition: color 0.4s;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #484848;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-todo-input:checked + .todo-item-text {
|
||||||
|
color: #949494;
|
||||||
|
text-decoration: line-through;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-todo-button {
|
||||||
|
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;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-todo-button:hover,
|
||||||
|
.remove-todo-button:focus {
|
||||||
|
color: #c18585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-todo-button::after {
|
||||||
|
content: "×";
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item:hover .remove-todo-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||||
|
.toggle-todo-input {
|
||||||
|
background: none;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.remove-todo-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .toggle-todo-input,
|
||||||
|
:host([dir="rtl"]) .toggle-todo-input {
|
||||||
|
right: 3px;
|
||||||
|
left: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .todo-item-text,
|
||||||
|
:host([dir="rtl"]) .todo-item-text {
|
||||||
|
background-position: center right 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .remove-todo-button,
|
||||||
|
:host([dir="rtl"]) .remove-todo-button {
|
||||||
|
left: 10px;
|
||||||
|
right: unset;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
export default sheet;
|
||||||
15
suites-experimental/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(`:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: block;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
export default sheet;
|
||||||
90
suites-experimental/javascript-wc-indexeddb/dist/styles/topbar.constructable.js
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(`:host {
|
||||||
|
display: block;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-todo-input {
|
||||||
|
padding: 0 32px 0 60px;
|
||||||
|
width: 100%;
|
||||||
|
height: 68px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.4em;
|
||||||
|
background: rgba(0, 0, 0, 0.003);
|
||||||
|
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-todo-input::placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all-container {
|
||||||
|
width: 45px;
|
||||||
|
height: 68px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all-input {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
font-size: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 11.5px;
|
||||||
|
left: 0;
|
||||||
|
border: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 45px;
|
||||||
|
height: 68px;
|
||||||
|
font-size: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all-label::before {
|
||||||
|
content: "❯";
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 22px;
|
||||||
|
color: #949494;
|
||||||
|
padding: 10px 27px 10px 27px;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all-input:checked + .toggle-all-label::before {
|
||||||
|
color: #484848;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||||
|
.toggle-all-input {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .new-todo-input,
|
||||||
|
:host([dir="rtl"]) .new-todo-input {
|
||||||
|
padding: 0 60px 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .toggle-all-container,
|
||||||
|
:host([dir="rtl"]) .toggle-all-container {
|
||||||
|
right: 0;
|
||||||
|
left: unset;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
export default sheet;
|
||||||
31
suites-experimental/javascript-wc-indexeddb/index.html
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
|
||||||
|
<title>TodoMVC: JavaScript Web Components</title>
|
||||||
|
<link rel="stylesheet" href="node_modules/todomvc-css/dist/global.css" />
|
||||||
|
<link rel="stylesheet" href="node_modules/todomvc-css/dist/header.css" />
|
||||||
|
<link rel="stylesheet" href="node_modules/todomvc-css/dist/footer.css" />
|
||||||
|
<!-- load these first, so that they are registered by the time the app components needs them -->
|
||||||
|
<script type="module" src="src/components/todo-topbar/todo-topbar.component.js"></script>
|
||||||
|
<script type="module" src="src/components/todo-list/todo-list.component.js"></script>
|
||||||
|
<script type="module" src="src/components/todo-bottombar/todo-bottombar.component.js"></script>
|
||||||
|
<script type="module" src="src/components/todo-app/todo-app.component.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<a href="#" style="text-decoration: none"><h1 class="title">todos</h1></a>
|
||||||
|
</header>
|
||||||
|
<todo-app></todo-app>
|
||||||
|
<footer class="footer">
|
||||||
|
<p class="footer-text">Click on input field to write your todo.</p>
|
||||||
|
<p class="footer-text">At least one character is needed to be a valid entry.</p>
|
||||||
|
<p class="footer-text">Press 'enter' to add the todo.</p>
|
||||||
|
<p class="footer-text">Double-click to edit a todo</p>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="src/index.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
840
suites-experimental/javascript-wc-indexeddb/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,840 @@
|
||||||
|
{
|
||||||
|
"name": "todomvc-javascript-web-components-indexeddb",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "todomvc-javascript-web-components-indexeddb",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"dexie": "^4.2.1",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"speedometer-utils": "../../resources/shared",
|
||||||
|
"todomvc-css": "file:../../suites/todomvc/todomvc-css"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0",
|
||||||
|
"npm": ">=8.19.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../../resources/shared": {
|
||||||
|
"extraneous": true
|
||||||
|
},
|
||||||
|
"../../../shared": {
|
||||||
|
"extraneous": true
|
||||||
|
},
|
||||||
|
"../../resources/shared": {
|
||||||
|
"name": "speedometer-utils",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"../../resources/todomvc/todomvc-css": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-babel": "^6.0.3",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.0",
|
||||||
|
"@rollup/plugin-html": "^1.0.2",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
|
"@rollup/plugin-terser": "^0.4.3",
|
||||||
|
"@rollup/pluginutils": "^5.0.2",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"globby": "^13.2.0",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"rollup": "^3.23.0",
|
||||||
|
"rollup-plugin-cleaner": "^1.0.0",
|
||||||
|
"rollup-plugin-copy-merge": "^1.0.2",
|
||||||
|
"rollup-plugin-import-css": "^3.2.1",
|
||||||
|
"strip-comments": "^2.0.1",
|
||||||
|
"stylelint": "^15.6.2",
|
||||||
|
"stylelint-config-standard": "^33.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.13.0",
|
||||||
|
"npm": ">=8.19.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../suites/todomvc/todomvc-css": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-babel": "^6.0.3",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.0",
|
||||||
|
"@rollup/plugin-html": "^1.0.2",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
|
"@rollup/plugin-terser": "^0.4.3",
|
||||||
|
"@rollup/pluginutils": "^5.0.2",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"globby": "^13.2.0",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"rollup": "^3.23.0",
|
||||||
|
"rollup-plugin-cleaner": "^1.0.0",
|
||||||
|
"rollup-plugin-copy-merge": "^1.0.2",
|
||||||
|
"rollup-plugin-import-css": "^3.2.1",
|
||||||
|
"strip-comments": "^2.0.1",
|
||||||
|
"stylelint": "^15.6.2",
|
||||||
|
"stylelint-config-standard": "^33.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.13.0",
|
||||||
|
"npm": ">=8.19.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../todomvc-css": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-babel": "^6.0.3",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.0",
|
||||||
|
"@rollup/plugin-html": "^1.0.2",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
|
"@rollup/plugin-terser": "^0.4.3",
|
||||||
|
"@rollup/pluginutils": "^5.0.2",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"globby": "^13.2.0",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"rollup": "^3.23.0",
|
||||||
|
"rollup-plugin-cleaner": "^1.0.0",
|
||||||
|
"rollup-plugin-copy-merge": "^1.0.2",
|
||||||
|
"rollup-plugin-import-css": "^3.2.1",
|
||||||
|
"strip-comments": "^2.0.1",
|
||||||
|
"stylelint": "^15.6.2",
|
||||||
|
"stylelint-config-standard": "^33.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.13.0",
|
||||||
|
"npm": ">=8.19.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "2.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/basic-auth": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.1",
|
||||||
|
"get-intrinsic": "^1.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"node_modules/corser": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dexie": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||||
|
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.1",
|
||||||
|
"has": "^1.0.3",
|
||||||
|
"has-proto": "^1.0.1",
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/he": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||||
|
"bin": {
|
||||||
|
"he": "bin/he"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^4.0.0",
|
||||||
|
"follow-redirects": "^1.0.0",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-server": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-auth": "^2.0.1",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"corser": "^2.0.1",
|
||||||
|
"he": "^1.2.0",
|
||||||
|
"html-encoding-sniffer": "^3.0.0",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"mime": "^1.6.0",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"opener": "^1.5.1",
|
||||||
|
"portfinder": "^1.0.28",
|
||||||
|
"secure-compare": "3.0.1",
|
||||||
|
"union": "~0.5.0",
|
||||||
|
"url-join": "^4.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"http-server": "bin/http-server"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
|
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "bin/cmd.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||||
|
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/opener": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||||
|
"bin": {
|
||||||
|
"opener": "bin/opener-bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/portfinder": {
|
||||||
|
"version": "1.0.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
|
||||||
|
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^2.6.4",
|
||||||
|
"debug": "^3.2.7",
|
||||||
|
"mkdirp": "^0.5.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
|
||||||
|
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"node_modules/secure-compare": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw=="
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"get-intrinsic": "^1.0.2",
|
||||||
|
"object-inspect": "^1.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/speedometer-utils": {
|
||||||
|
"resolved": "../../resources/shared",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/todomvc-css": {
|
||||||
|
"resolved": "../../suites/todomvc/todomvc-css",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/union": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
||||||
|
"dependencies": {
|
||||||
|
"qs": "^6.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/url-join": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"async": {
|
||||||
|
"version": "2.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.17.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"basic-auth": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"call-bind": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||||
|
"requires": {
|
||||||
|
"function-bind": "^1.1.1",
|
||||||
|
"get-intrinsic": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"corser": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ=="
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dexie": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="
|
||||||
|
},
|
||||||
|
"eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
|
"follow-redirects": {
|
||||||
|
"version": "1.15.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||||
|
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
|
||||||
|
},
|
||||||
|
"function-bind": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||||
|
},
|
||||||
|
"get-intrinsic": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||||
|
"requires": {
|
||||||
|
"function-bind": "^1.1.1",
|
||||||
|
"has": "^1.0.3",
|
||||||
|
"has-proto": "^1.0.1",
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"has": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
|
"requires": {
|
||||||
|
"function-bind": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||||
|
},
|
||||||
|
"has-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
|
||||||
|
},
|
||||||
|
"has-symbols": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||||
|
},
|
||||||
|
"he": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||||
|
},
|
||||||
|
"html-encoding-sniffer": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||||
|
"requires": {
|
||||||
|
"whatwg-encoding": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http-proxy": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||||
|
"requires": {
|
||||||
|
"eventemitter3": "^4.0.0",
|
||||||
|
"follow-redirects": "^1.0.0",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http-server": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
|
||||||
|
"requires": {
|
||||||
|
"basic-auth": "^2.0.1",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"corser": "^2.0.1",
|
||||||
|
"he": "^1.2.0",
|
||||||
|
"html-encoding-sniffer": "^3.0.0",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"mime": "^1.6.0",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"opener": "^1.5.1",
|
||||||
|
"portfinder": "^1.0.28",
|
||||||
|
"secure-compare": "3.0.1",
|
||||||
|
"union": "~0.5.0",
|
||||||
|
"url-join": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||||
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
||||||
|
},
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
|
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||||
|
"requires": {
|
||||||
|
"minimist": "^1.2.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
|
},
|
||||||
|
"object-inspect": {
|
||||||
|
"version": "1.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||||
|
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="
|
||||||
|
},
|
||||||
|
"opener": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="
|
||||||
|
},
|
||||||
|
"portfinder": {
|
||||||
|
"version": "1.0.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
|
||||||
|
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
|
||||||
|
"requires": {
|
||||||
|
"async": "^2.6.4",
|
||||||
|
"debug": "^3.2.7",
|
||||||
|
"mkdirp": "^0.5.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "6.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
|
||||||
|
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
|
||||||
|
"requires": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
|
"safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"secure-compare": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw=="
|
||||||
|
},
|
||||||
|
"side-channel": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||||
|
"requires": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"get-intrinsic": "^1.0.2",
|
||||||
|
"object-inspect": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speedometer-utils": {
|
||||||
|
"version": "file:../../resources/shared"
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"todomvc-css": {
|
||||||
|
"version": "file:../../suites/todomvc/todomvc-css",
|
||||||
|
"requires": {
|
||||||
|
"@rollup/plugin-babel": "^6.0.3",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.0",
|
||||||
|
"@rollup/plugin-html": "^1.0.2",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
|
"@rollup/plugin-terser": "^0.4.3",
|
||||||
|
"@rollup/pluginutils": "^5.0.2",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"globby": "^13.2.0",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"rollup": "^3.23.0",
|
||||||
|
"rollup-plugin-cleaner": "^1.0.0",
|
||||||
|
"rollup-plugin-copy-merge": "^1.0.2",
|
||||||
|
"rollup-plugin-import-css": "^3.2.1",
|
||||||
|
"strip-comments": "^2.0.1",
|
||||||
|
"stylelint": "^15.6.2",
|
||||||
|
"stylelint-config-standard": "^33.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"union": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
||||||
|
"requires": {
|
||||||
|
"qs": "^6.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url-join": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
|
||||||
|
},
|
||||||
|
"whatwg-encoding": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||||
|
"requires": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
suites-experimental/javascript-wc-indexeddb/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "todomvc-javascript-web-components-indexeddb",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TodoMVC app written with JavaScript using web components.",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0",
|
||||||
|
"npm": ">=8.19.3"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "http-server ./ -p 7005 -c-1 --cors -o",
|
||||||
|
"build": "node scripts/build.mjs",
|
||||||
|
"serve": "http-server ./dist -p 7006 -c-1 --cors -o"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"dexie": "^4.2.1",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"todomvc-css": "file:../../suites/todomvc/todomvc-css",
|
||||||
|
"speedometer-utils": "../../resources/shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
170
suites-experimental/javascript-wc-indexeddb/scripts/build.mjs
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import path from "path";
|
||||||
|
import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { dirname } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createDirectory
|
||||||
|
*
|
||||||
|
* Removes and recreates a directory.
|
||||||
|
*
|
||||||
|
* @param {string} directory Directory name.
|
||||||
|
*/
|
||||||
|
async function createDirectory(directory) {
|
||||||
|
await fs.rm(directory, { recursive: true, force: true });
|
||||||
|
await fs.mkdir(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* copyDirectory
|
||||||
|
*
|
||||||
|
* Copies a source folder to a destination folder.
|
||||||
|
*
|
||||||
|
* @param {string} src Source directory.
|
||||||
|
* @param {string} dest Destination directory.
|
||||||
|
*/
|
||||||
|
async function copyDirectory(src, dest) {
|
||||||
|
await fs.cp(src, dest, { recursive: true }, (err) => {
|
||||||
|
if (err)
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* copyFile
|
||||||
|
*
|
||||||
|
* Copies a file from a source to a destination.
|
||||||
|
*
|
||||||
|
* @param {string} src Source file.
|
||||||
|
* @param {string} dest Destination file.
|
||||||
|
*/
|
||||||
|
async function copyFile(src, dest) {
|
||||||
|
await fs.mkdir(dirname(dest), { recursive: true });
|
||||||
|
await fs.copyFile(src, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* copyFiles
|
||||||
|
*
|
||||||
|
* Copies multiple files from a source to a destination.
|
||||||
|
*
|
||||||
|
* @param {string[]} files Array of files to copy.
|
||||||
|
*/
|
||||||
|
async function copyFiles(files) {
|
||||||
|
for (const file of files)
|
||||||
|
await copyFile(file.src, file.dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updateImportsInFile
|
||||||
|
*
|
||||||
|
* Reads a file and replaces a source path with a destination path.
|
||||||
|
*
|
||||||
|
* @param {Object} config - Config to update imports
|
||||||
|
* @param {string} config.src - The source path.
|
||||||
|
* @param {string} config.dest - The destination path.
|
||||||
|
* @param {string} config.file - File to read from.
|
||||||
|
*/
|
||||||
|
async function updateImportsInFile({ file, src, dest }) {
|
||||||
|
let contents = await fs.readFile(file, "utf8");
|
||||||
|
contents = contents.replaceAll(src, dest);
|
||||||
|
await fs.writeFile(file, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updateImports
|
||||||
|
*
|
||||||
|
* Updates imports in multiple files.
|
||||||
|
*
|
||||||
|
* @param {Object} config - Config to update imports
|
||||||
|
* @param {string} config.src - The source path.
|
||||||
|
* @param {string} config.dest - The destination path.
|
||||||
|
* @param {string} config.file - Files to read from.
|
||||||
|
*/
|
||||||
|
async function updateImports({ files, src, dest }) {
|
||||||
|
for (const file of files)
|
||||||
|
await updateImportsInFile({ file, src, dest });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToMove = [
|
||||||
|
{ src: "index.html", dest: "./dist/index.html" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/global.css", dest: "./dist/styles/global.css" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/header.css", dest: "./dist/styles/header.css" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/footer.css", dest: "./dist/styles/footer.css" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/global.constructable.js", dest: "./dist/styles/global.constructable.js" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/app.constructable.js", dest: "./dist/styles/app.constructable.js" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/topbar.constructable.js", dest: "./dist/styles/topbar.constructable.js" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/main.constructable.js", dest: "./dist/styles/main.constructable.js" },
|
||||||
|
{ src: "node_modules/todomvc-css/dist/bottombar.constructable.js", dest: "./dist/styles/bottombar.constructable.js" },
|
||||||
|
{ 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/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" },
|
||||||
|
{ src: "node_modules/speedometer-utils/helpers.mjs", dest: "./dist/src/speedometer-utils/helpers.mjs" },
|
||||||
|
{ src: "node_modules/speedometer-utils/translations.mjs", dest: "./dist/src/speedometer-utils/translations.mjs" },
|
||||||
|
{ src: "node_modules/speedometer-utils/todomvc-utils.mjs", dest: "./dist/src/speedometer-utils/todomvc-utils.mjs" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const importsToRename = [
|
||||||
|
{
|
||||||
|
src: "node_modules/todomvc-css/dist/",
|
||||||
|
dest: "styles/",
|
||||||
|
files: ["./dist/index.html"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "../../../node_modules/todomvc-css/dist/",
|
||||||
|
dest: "../../../styles/",
|
||||||
|
files: [
|
||||||
|
"./dist/src/components/todo-app/todo-app.component.js",
|
||||||
|
"./dist/src/components/todo-bottombar/todo-bottombar.component.js",
|
||||||
|
"./dist/src/components/todo-item/todo-item.component.js",
|
||||||
|
"./dist/src/components/todo-list/todo-list.component.js",
|
||||||
|
"./dist/src/components/todo-topbar/todo-topbar.component.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "../../../node_modules/dexie/dist/modern/dexie.mjs",
|
||||||
|
dest: "../../libs/dexie.mjs",
|
||||||
|
files: ["./dist/src/storage/dexieDB-manager.js"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/src/",
|
||||||
|
dest: "./",
|
||||||
|
files: ["./dist/src/index.mjs"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/node_modules/speedometer-utils/",
|
||||||
|
dest: "./speedometer-utils/",
|
||||||
|
files: ["./dist/src/index.mjs", "./dist/src/workload-test.mjs"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/node_modules/speedometer-utils/",
|
||||||
|
dest: "./",
|
||||||
|
files: ["./dist/src/speedometer-utils/benchmark.mjs"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const build = async () => {
|
||||||
|
// create dist folder
|
||||||
|
await createDirectory("./dist");
|
||||||
|
|
||||||
|
// copy src folder
|
||||||
|
await copyDirectory("./src", "./dist/src");
|
||||||
|
|
||||||
|
// copy files to Move
|
||||||
|
await copyFiles(filesToMove);
|
||||||
|
|
||||||
|
// rename imports files
|
||||||
|
for (const entry of importsToRename) {
|
||||||
|
const { files, src, dest } = entry;
|
||||||
|
await updateImports({ files, src, dest });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Done with building!");
|
||||||
|
};
|
||||||
|
|
||||||
|
await build();
|
||||||
|
await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
import template from "./todo-app.template.js";
|
||||||
|
import { useRouter } from "../../hooks/useRouter.js";
|
||||||
|
|
||||||
|
import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js";
|
||||||
|
import appStyles from "../../../node_modules/todomvc-css/dist/app.constructable.js";
|
||||||
|
import mainStyles from "../../../node_modules/todomvc-css/dist/main.constructable.js";
|
||||||
|
class TodoApp extends HTMLElement {
|
||||||
|
#isReady = false;
|
||||||
|
#numberOfItems = 0;
|
||||||
|
#numberOfCompletedItems = 0;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const node = document.importNode(template.content, true);
|
||||||
|
this.topbar = node.querySelector("todo-topbar");
|
||||||
|
this.list = node.querySelector("todo-list");
|
||||||
|
this.bottombar = node.querySelector("todo-bottombar");
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
this.htmlDirection = document.dir || "ltr";
|
||||||
|
this.setAttribute("dir", this.htmlDirection);
|
||||||
|
this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles];
|
||||||
|
this.shadow.append(node);
|
||||||
|
|
||||||
|
this.addItem = this.addItem.bind(this);
|
||||||
|
this.toggleItem = this.toggleItem.bind(this);
|
||||||
|
this.removeItem = this.removeItem.bind(this);
|
||||||
|
this.updateItem = this.updateItem.bind(this);
|
||||||
|
this.toggleItems = this.toggleItems.bind(this);
|
||||||
|
this.clearCompletedItems = this.clearCompletedItems.bind(this);
|
||||||
|
this.routeChange = this.routeChange.bind(this);
|
||||||
|
this.moveToNextPage = this.moveToNextPage.bind(this);
|
||||||
|
this.moveToPreviousPage = this.moveToPreviousPage.bind(this);
|
||||||
|
|
||||||
|
this.router = useRouter();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isReady() {
|
||||||
|
return this.#isReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstance() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(event) {
|
||||||
|
const { detail: item } = event;
|
||||||
|
this.list.addItem(item, this.#numberOfItems++);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem(event) {
|
||||||
|
if (event.detail.completed)
|
||||||
|
this.#numberOfCompletedItems++;
|
||||||
|
else
|
||||||
|
this.#numberOfCompletedItems--;
|
||||||
|
|
||||||
|
this.list.toggleItem(event.detail.itemNumber, event.detail.completed);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(event) {
|
||||||
|
if (event.detail.completed)
|
||||||
|
this.#numberOfCompletedItems--;
|
||||||
|
|
||||||
|
this.#numberOfItems--;
|
||||||
|
this.update();
|
||||||
|
this.list.removeItem(event.detail.itemNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItem(event) {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItems(event) {
|
||||||
|
this.list.toggleItems(event.detail.completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCompletedItems() {
|
||||||
|
this.list.removeCompletedItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToNextPage() {
|
||||||
|
this.list.moveToNextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToPreviousPage() {
|
||||||
|
await this.list.moveToPreviousPage();
|
||||||
|
this.bottombar.reenablePreviousPageButton();
|
||||||
|
window.dispatchEvent(new CustomEvent("previous-page-loaded", {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const totalItems = this.#numberOfItems;
|
||||||
|
const completedItems = this.#numberOfCompletedItems;
|
||||||
|
const activeItems = totalItems - completedItems;
|
||||||
|
|
||||||
|
this.list.setAttribute("total-items", totalItems);
|
||||||
|
|
||||||
|
this.topbar.setAttribute("total-items", totalItems);
|
||||||
|
this.topbar.setAttribute("active-items", activeItems);
|
||||||
|
this.topbar.setAttribute("completed-items", completedItems);
|
||||||
|
|
||||||
|
this.bottombar.setAttribute("total-items", totalItems);
|
||||||
|
this.bottombar.setAttribute("active-items", activeItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
addListeners() {
|
||||||
|
this.topbar.addEventListener("toggle-all", this.toggleItems);
|
||||||
|
this.topbar.addEventListener("add-item", this.addItem);
|
||||||
|
|
||||||
|
this.list.listNode.addEventListener("toggle-item", this.toggleItem);
|
||||||
|
this.list.listNode.addEventListener("remove-item", this.removeItem);
|
||||||
|
this.list.listNode.addEventListener("update-item", this.updateItem);
|
||||||
|
|
||||||
|
this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems);
|
||||||
|
this.bottombar.addEventListener("next-page", this.moveToNextPage);
|
||||||
|
this.bottombar.addEventListener("previous-page", this.moveToPreviousPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListeners() {
|
||||||
|
this.topbar.removeEventListener("toggle-all", this.toggleItems);
|
||||||
|
this.topbar.removeEventListener("add-item", this.addItem);
|
||||||
|
|
||||||
|
this.list.listNode.removeEventListener("toggle-item", this.toggleItem);
|
||||||
|
this.list.listNode.removeEventListener("remove-item", this.removeItem);
|
||||||
|
this.list.listNode.removeEventListener("update-item", this.updateItem);
|
||||||
|
|
||||||
|
this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems);
|
||||||
|
this.bottombar.removeEventListener("next-page", this.moveToNextPage);
|
||||||
|
this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
routeChange(route) {
|
||||||
|
const routeName = route.split("/")[1] || "all";
|
||||||
|
this.list.updateRoute(routeName);
|
||||||
|
this.bottombar.updateRoute(routeName);
|
||||||
|
this.topbar.updateRoute(routeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.update();
|
||||||
|
this.addListeners();
|
||||||
|
this.router.initRouter(this.routeChange);
|
||||||
|
this.#isReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.removeListeners();
|
||||||
|
this.#isReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("todo-app", TodoApp);
|
||||||
|
|
||||||
|
export default TodoApp;
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
const template = document.createElement("template");
|
||||||
|
|
||||||
|
template.id = "todo-app-template";
|
||||||
|
template.innerHTML = `
|
||||||
|
<section class="app">
|
||||||
|
<todo-topbar></todo-topbar>
|
||||||
|
<main class="main">
|
||||||
|
<todo-list></todo-list>
|
||||||
|
</main>
|
||||||
|
<todo-bottombar></todo-bottombar>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default template;
|
||||||