Compare commits
273 commits
1.0
...
3.1-custom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292e6720d1 | ||
|
|
f40db1000a | ||
|
|
115ae74716 | ||
|
|
859bc0b019 | ||
|
|
e49c1bf40e | ||
|
|
e9710090ba | ||
|
|
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
|
||||
|
||||
/resources/todomvc/architecture-examples/*
|
||||
!/resources/todomvc/architecture-examples/angular
|
||||
!/resources/todomvc/architecture-examples/angular-complex
|
||||
!/resources/todomvc/architecture-examples/backbone
|
||||
!/resources/todomvc/architecture-examples/backbone-complex
|
||||
!/resources/todomvc/architecture-examples/lit
|
||||
/resources/todomvc/architecture-examples/lit/.wireit
|
||||
/resources/todomvc/architecture-examples/lit/dist/index.js
|
||||
!/resources/todomvc/architecture-examples/lit-complex
|
||||
!/resources/todomvc/architecture-examples/preact
|
||||
!/resources/todomvc/architecture-examples/preact-complex
|
||||
!/resources/todomvc/architecture-examples/react
|
||||
!/resources/todomvc/architecture-examples/react-complex
|
||||
!/resources/todomvc/architecture-examples/react-redux
|
||||
!/resources/todomvc/architecture-examples/react-redux-complex
|
||||
!/resources/todomvc/architecture-examples/svelte
|
||||
!/resources/todomvc/architecture-examples/svelte-complex
|
||||
!/resources/todomvc/architecture-examples/vue
|
||||
!/resources/todomvc/architecture-examples/vue-complex
|
||||
|
||||
/resources/todomvc/architecture-examples/react/public/window/*
|
||||
|
||||
/resources/todomvc/vanilla-examples/*
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||
|
||||
!/resources/todomvc/big-dom-generator
|
||||
|
||||
!/resources/editors
|
||||
/resources/editors/assets
|
||||
|
||||
!/resources/newssite
|
||||
!/resources/newssite/news-next
|
||||
!/resources/newssite/news-nuxt
|
||||
!/resources/newssite/news-site-css
|
||||
|
||||
/resources/perf.webkit.org
|
||||
|
||||
/resources/react-stockcharts
|
||||
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
|
||||
}
|
||||
}
|
||||
59
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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@v3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.13.0
|
||||
- name: Install
|
||||
run: |
|
||||
npm install
|
||||
- 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
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [chrome, firefox, safari]
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.13.0
|
||||
- name: Install
|
||||
run: |
|
||||
npm install
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "Running in $BROWSER"
|
||||
npm run test:${{ matrix.browser }}
|
||||
33
.gitignore
vendored
|
|
@ -2,3 +2,36 @@
|
|||
*.pyc
|
||||
.DS_Store
|
||||
.directory
|
||||
/node_modules
|
||||
|
||||
.vscode
|
||||
.angular
|
||||
|
||||
/resources/todomvc/architecture-examples/angular/node_modules/
|
||||
/resources/todomvc/architecture-examples/angular-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/backbone/node_modules/
|
||||
/resources/todomvc/architecture-examples/backbone-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/jquery/node_modules/
|
||||
/resources/todomvc/architecture-examples/jquery-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/preact/node_modules/
|
||||
/resources/todomvc/architecture-examples/preact-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/react/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-redux/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-redux-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/svelte/node_modules/
|
||||
/resources/todomvc/architecture-examples/svelte-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/vue/node_modules/
|
||||
/resources/todomvc/architecture-examples/vue-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/lit-complex/node_modules/
|
||||
|
||||
/resources/todomvc/vanilla-examples/javascript-es5/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es5-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-webpack/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-web-components-complex/node_modules/
|
||||
|
||||
/resources/charts/node_modules/
|
||||
/resources/todomvc/big-dom-generator/node_modules/
|
||||
|
|
|
|||
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict = true
|
||||
50
.prettierignore
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/node_modules
|
||||
**/dist
|
||||
package-lock.json
|
||||
**/.angular
|
||||
|
||||
/resources/todomvc/architecture-examples/*
|
||||
!/resources/todomvc/architecture-examples/angular
|
||||
!/resources/todomvc/architecture-examples/angular-complex
|
||||
!/resources/todomvc/architecture-examples/backbone
|
||||
!/resources/todomvc/architecture-examples/backbone-complex
|
||||
!/resources/todomvc/architecture-examples/lit
|
||||
/resources/todomvc/architecture-examples/lit/dist/index.js
|
||||
/resources/todomvc/architecture-examples/lit/.wireit
|
||||
!/resources/todomvc/architecture-examples/lit-complex
|
||||
!/resources/todomvc/architecture-examples/preact
|
||||
!/resources/todomvc/architecture-examples/preact-complex
|
||||
!/resources/todomvc/architecture-examples/react
|
||||
!/resources/todomvc/architecture-examples/react-complex
|
||||
!/resources/todomvc/architecture-examples/react-redux
|
||||
!/resources/todomvc/architecture-examples/react-redux-complex
|
||||
!/resources/todomvc/architecture-examples/svelte
|
||||
!/resources/todomvc/architecture-examples/svelte-complex
|
||||
!/resources/todomvc/architecture-examples/vue
|
||||
!/resources/todomvc/architecture-examples/vue-complex
|
||||
|
||||
/resources/todomvc/architecture-examples/react/public/window/*
|
||||
|
||||
/resources/todomvc/vanilla-examples/*
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||
|
||||
!/resources/todomvc/big-dom-generator
|
||||
|
||||
!/resources/editors
|
||||
/resources/editors/assets
|
||||
|
||||
!/resources/newssite
|
||||
!/resources/newssite/news-next
|
||||
!/resources/newssite/news-nuxt
|
||||
!/resources/newssite/news-site-css
|
||||
|
||||
/resources/perf.webkit.org
|
||||
|
||||
/resources/react-stockcharts
|
||||
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"printWidth": 250,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
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
|
||||
npm run dev
|
||||
```
|
||||
2. Open your browser of choice and navigate to [http://127.0.0.1:7000](http://127.0.0.1:7000)
|
||||
|
||||
## 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.
|
||||
40
InteractiveRunner.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=850" />
|
||||
<title>Speedometer 3.1 Interactive Runner</title>
|
||||
<script src="resources/interactive.mjs" type="module"></script>
|
||||
<link rel="icon" href="resources/favicon.png" />
|
||||
<style>
|
||||
iframe {
|
||||
border: 1px solid black;
|
||||
}
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ol ol {
|
||||
margin-left: 2em;
|
||||
list-style-position: outside;
|
||||
}
|
||||
.running {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ran {
|
||||
color: grey;
|
||||
}
|
||||
nav {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
height: 600px;
|
||||
}
|
||||
nav > ol {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
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
|
||||
modification, are permitted provided that the following conditions
|
||||
|
|
|
|||
16
README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# 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.
|
||||
29
Testing.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# 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
|
||||
|
||||
To run this 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 3.1 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">3.1</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">About Speedometer 3</h1>
|
||||
<div class="section-content">
|
||||
<p>Speedometer 3 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.1/resources/todomvc/vanilla-examples/javascript-es5" target="_blank">TodoMVC-JavaScript-ES5</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/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.1/resources/todomvc/vanilla-examples/javascript-web-components" target="_blank">TodoMVC-WebComponents</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/react-complex" target="_blank">TodoMVC-React-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/react-redux" target="_blank">TodoMVC-React-Redux</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/backbone" target="_blank">TodoMVC-Backbone</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/angular-complex" target="_blank">TodoMVC-Angular-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/vue" target="_blank">TodoMVC-Vue</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/jquery" target="_blank">TodoMVC-jQuery</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/preact-complex" target="_blank">TodoMVC-Preact-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/todomvc/architecture-examples/svelte-complex" target="_blank">TodoMVC-Svelte-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/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.1/resources/editors" target="_blank">Editor-CodeMirror</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/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.1/resources/charts" target="_blank">Charts-observable-plot</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/charts" target="_blank">Charts-chartjs</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/resources/react-stockcharts" target="_blank">React-Stockcharts-SVG</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/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.1/resources/newssite/news-next" target="_blank">NewsSite-Next</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.1/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">
|
||||
<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
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
113
index.html
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<!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 3.1</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">3.1</div>
|
||||
</a>
|
||||
<div class="content">
|
||||
<p>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">Start Test</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a href="about.html">About Speedometer</a>
|
||||
<a href="instructions.html">Test Instructions</a>
|
||||
</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">3.1</div>
|
||||
</a>
|
||||
<div id="testContainer"></div>
|
||||
<div id="leakedIframes"></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">3.1</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" 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">3.1</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">Detailed Results</h1>
|
||||
<div class="section-content all-metric-results">
|
||||
<div class="aggregated-metric-result">
|
||||
<h2>Aggregate Metric</h2>
|
||||
<div id="geomean-chart"></div>
|
||||
<h2>Test Metrics Overview</h2>
|
||||
<div id="toplevel-chart"></div>
|
||||
<h2>Tests</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 3.1 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">3.1</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">Speedometer 3 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">
|
||||
<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>
|
||||
11509
package-lock.json
generated
Normal file
52
package.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "speedometer",
|
||||
"version": "3.0.0-alpha",
|
||||
"description": "An open source repository for the Speedometer benchmark.",
|
||||
"engines": {
|
||||
"node": ">=18.13.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": "http-server ./ -p 7000 -c-1 --cors",
|
||||
"server": "http-server ./ -p 7000 --cors",
|
||||
"lint:check": "eslint **/*.{js,mjs,jsx,ts,tsx}",
|
||||
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix",
|
||||
"pretty:check": "prettier --check ./",
|
||||
"pretty:fix": "prettier --write ./",
|
||||
"format": "npm run pretty:fix && npm run lint:fix",
|
||||
"test:chrome": "node tests/run.mjs --browser chrome",
|
||||
"test:firefox": "node tests/run.mjs --browser firefox",
|
||||
"test:safari": "node tests/run.mjs --browser safari",
|
||||
"test:edge": "node tests/run.mjs --browser edge"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
"@next/eslint-plugin-next": "^13.4.5",
|
||||
"@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",
|
||||
"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",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.3",
|
||||
"selenium-webdriver": "^4.8.0",
|
||||
"sinon": "^15.0.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
895
resources/benchmark-runner.mjs
Normal file
|
|
@ -0,0 +1,895 @@
|
|||
import { Metric } from "./metric.mjs";
|
||||
import { params } from "./params.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;
|
||||
// Downstream-only. this._lookupStartNode is used by the React-Window workload to run the
|
||||
// querySelector* methods on a child window.
|
||||
this._lookupStartNode = null;
|
||||
}
|
||||
|
||||
layout() {
|
||||
const body = this._frame.contentDocument.body.getBoundingClientRect();
|
||||
this.layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
|
||||
}
|
||||
|
||||
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._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._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]());
|
||||
}
|
||||
|
||||
_wrapElement(element) {
|
||||
return new PageElement(element);
|
||||
}
|
||||
|
||||
// Downstream only.
|
||||
async targetChildWindow() {
|
||||
return new Promise((resolve) => {
|
||||
const resolveIfReady = () => {
|
||||
// Expects the workload to set a global childWindow object.
|
||||
const maybeChildWindow = this._frame.contentWindow.globalThis.childWindow;
|
||||
let callback = resolveIfReady;
|
||||
if (maybeChildWindow) {
|
||||
this._lookupStartNode = maybeChildWindow.document;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
window.requestAnimationFrame(callback);
|
||||
};
|
||||
resolveIfReady();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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]());
|
||||
}
|
||||
|
||||
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",
|
||||
disabled: false,
|
||||
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);
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
class TestInvoker {
|
||||
constructor(syncCallback, asyncCallback, reportCallback) {
|
||||
this._syncCallback = syncCallback;
|
||||
this._asyncCallback = asyncCallback;
|
||||
this._reportCallback = reportCallback;
|
||||
}
|
||||
}
|
||||
|
||||
class TimerTestInvoker extends TestInvoker {
|
||||
start() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this._syncCallback();
|
||||
setTimeout(() => {
|
||||
this._asyncCallback();
|
||||
requestAnimationFrame(async () => {
|
||||
await this._reportCallback();
|
||||
resolve();
|
||||
});
|
||||
}, 0);
|
||||
}, params.waitBeforeSync);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RAFTestInvoker extends TestInvoker {
|
||||
start() {
|
||||
return new Promise((resolve) => {
|
||||
if (params.waitBeforeSync)
|
||||
setTimeout(() => this._scheduleCallbacks(resolve), params.waitBeforeSync);
|
||||
else
|
||||
this._scheduleCallbacks(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
_scheduleCallbacks(resolve) {
|
||||
requestAnimationFrame(() => this._syncCallback());
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
this._asyncCallback();
|
||||
setTimeout(async () => {
|
||||
await this._reportCallback();
|
||||
resolve();
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
this._currentIteration = 0;
|
||||
if (params.shuffleSeed !== "off")
|
||||
this._suiteOrderRandomNumberGenerator = seededHashRandomNumberGenerator(params.shuffleSeed);
|
||||
this._wakeLock = new WakeLock();
|
||||
this._maybeForceGC = this._initForceGC();
|
||||
this._useSignPost = !!globalThis.__signpostStart;
|
||||
}
|
||||
|
||||
markStart(groupLabel, startLabel) {
|
||||
performance.mark(startLabel);
|
||||
if (this._useSignPost)
|
||||
globalThis.__signpostStart(groupLabel);
|
||||
}
|
||||
|
||||
markEnd(groupLabel, startLabel, endLabel) {
|
||||
performance.mark(endLabel);
|
||||
if (this._useSignPost)
|
||||
globalThis.__signpostStop(groupLabel);
|
||||
performance.measure(groupLabel, startLabel, endLabel);
|
||||
}
|
||||
|
||||
_initForceGC() {
|
||||
if (!params.suiteForceGC)
|
||||
return () => {};
|
||||
try {
|
||||
let gc_extension = eval("gc");
|
||||
return () => gc_extension({ type: "major", execution: "async" });
|
||||
} catch (e) {
|
||||
console.error("suiteForceGC was enabled but --expose-gc was not set", e);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
get currentIteration() {
|
||||
return this._currentIteration;
|
||||
}
|
||||
|
||||
async runMultipleIterations(iterationCount) {
|
||||
this._iterationCount = iterationCount;
|
||||
this._currentIteration = 0;
|
||||
if (this._client?.willStartFirstIteration)
|
||||
await this._client.willStartFirstIteration( this._iterationCount);
|
||||
|
||||
try {
|
||||
await this._runMultipleIterations();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (this._client?.handleError) {
|
||||
await this._client.handleError(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._client?.didFinishLastIteration)
|
||||
await this._client.didFinishLastIteration(this._metrics);
|
||||
}
|
||||
|
||||
async _runMultipleIterations() {
|
||||
const iterationStartLabel = "iteration-start";
|
||||
const iterationEndLabel = "iteration-end";
|
||||
for (; this._currentIteration < this._iterationCount; this._currentIteration++) {
|
||||
const iterationLabel = `iteration-${this._currentIteration}`;
|
||||
this.markStart(iterationLabel, iterationStartLabel);
|
||||
await this._runAllSuites();
|
||||
this.markEnd(iterationLabel, iterationStartLabel, iterationEndLabel);
|
||||
}
|
||||
}
|
||||
|
||||
_removeFrame() {
|
||||
if (this._frame) {
|
||||
if (params.leakyIframes) {
|
||||
document.getElementById("leakedIframes").append(this._frame);
|
||||
} else {
|
||||
// Downstream only. The React-Window workload sets a global childWindow object.
|
||||
if (this._frame.contentWindow.globalThis.childWindow)
|
||||
this._frame.contentWindow.globalThis.childWindow.close();
|
||||
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";
|
||||
if (params.embedded) {
|
||||
style.left = "0px";
|
||||
style.top = "0px";
|
||||
} else {
|
||||
style.left = "50%";
|
||||
style.top = "50%";
|
||||
style.transform = "translate(-50%, -50%)";
|
||||
}
|
||||
|
||||
if (this._client?.willAddTestFrame)
|
||||
await this._client.willAddTestFrame(frame);
|
||||
|
||||
document.body.insertBefore(frame, document.body.firstChild);
|
||||
this._frame = frame;
|
||||
return frame;
|
||||
}
|
||||
|
||||
async _runAllSuites() {
|
||||
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
|
||||
await this._wakeLock.request();
|
||||
|
||||
const prepareLabel = "runner-prepare";
|
||||
const prepareStartLabel = "runner-prepare-start";
|
||||
const prepareEndLabel = "runner-prepare-end";
|
||||
|
||||
this.markStart(prepareLabel, prepareStartLabel);
|
||||
this._removeFrame();
|
||||
await this._appendFrame();
|
||||
this._page = new Page(this._frame);
|
||||
|
||||
let suites = [...this._suites];
|
||||
if (this._suiteOrderRandomNumberGenerator) {
|
||||
// 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++) {
|
||||
let j = i + (this._suiteOrderRandomNumberGenerator() % (suites.length - i));
|
||||
let tmp = suites[i];
|
||||
suites[i] = suites[j];
|
||||
suites[j] = tmp;
|
||||
}
|
||||
}
|
||||
this.markEnd(prepareLabel, prepareStartLabel, prepareEndLabel);
|
||||
|
||||
try {
|
||||
for (const suite of suites) {
|
||||
if (!suite.disabled)
|
||||
await this._runSuite(suite);
|
||||
}
|
||||
|
||||
} finally {
|
||||
await this._finishRunAllSuites();
|
||||
}
|
||||
}
|
||||
|
||||
async _finishRunAllSuites() {
|
||||
const finalizeLabel = "runner-finalize";
|
||||
const finalizeStartLabel = "runner-finalize-start";
|
||||
const finalizeEndLabel = "runner-finalize-end";
|
||||
|
||||
this.markStart(finalizeLabel, finalizeStartLabel);
|
||||
// Remove frame to clear the view for displaying the results.
|
||||
this._removeFrame();
|
||||
await this._finalize();
|
||||
this.markEnd(finalizeLabel, finalizeStartLabel, finalizeEndLabel);
|
||||
await this._wakeLock.release();
|
||||
}
|
||||
|
||||
async _runSuite(suite) {
|
||||
this._prepareMetrics(suite);
|
||||
const suitePrepareLabel = `suite-${suite.name}-prepare`;
|
||||
const suitePrepareStartLabel = `suite-${suite.name}-prepare-start`;
|
||||
const suitePrepareEndLabel = `suite-${suite.name}-prepare-end`;
|
||||
const suiteLabel = `suite-${suite.name}`;
|
||||
const suiteStartLabel = `suite-${suite.name}-start`;
|
||||
const suiteEndLabel = `suite-${suite.name}-end`;
|
||||
|
||||
await this._maybeForceGC();
|
||||
|
||||
this.markStart(suitePrepareLabel, suitePrepareStartLabel);
|
||||
const prepareStartTime = performance.now();
|
||||
await this._prepareSuite(suite);
|
||||
const prepareEndTime = performance.now();
|
||||
this.markEnd(suitePrepareLabel, suitePrepareStartLabel, suitePrepareEndLabel);
|
||||
|
||||
await this._maybeForceGC();
|
||||
|
||||
if (params.measurePrepare) {
|
||||
const prepareTime = prepareEndTime - prepareStartTime;
|
||||
this._recordPrepareMetric(suite, prepareTime);
|
||||
}
|
||||
|
||||
if (params.waitAfterSetup > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, params.waitAfterSetup));
|
||||
}
|
||||
|
||||
this.markStart(suiteLabel, suiteStartLabel);
|
||||
for (const test of suite.tests)
|
||||
await this._runTestAndRecordResults(suite, test);
|
||||
this.markEnd(suiteLabel, suiteStartLabel, suiteEndLabel);
|
||||
|
||||
if (params.waitAfterSuite > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, params.waitAfterSuite));
|
||||
}
|
||||
}
|
||||
|
||||
_prepareMetrics(suite) {
|
||||
this._measuredValues.tests[suite.name] = {
|
||||
tests: {},
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async _prepareSuite(suite) {
|
||||
return new Promise((resolve) => {
|
||||
const frame = this._page._frame;
|
||||
frame.onload = async () => {
|
||||
await suite.prepare(this._page);
|
||||
resolve();
|
||||
};
|
||||
frame.src = `resources/${suite.url}`;
|
||||
});
|
||||
}
|
||||
|
||||
_recordPrepareMetric(suite, prepareTime) {
|
||||
const suiteResults = this._measuredValues.tests[suite.name];
|
||||
suiteResults.tests.Prepare = prepareTime;
|
||||
suiteResults.total += prepareTime;
|
||||
}
|
||||
|
||||
async _runTestAndRecordResults(suite, test) {
|
||||
/* eslint-disable-next-line no-async-promise-executor */
|
||||
if (this._client?.willRunTest)
|
||||
await this._client.willRunTest(suite, test);
|
||||
|
||||
// Prepare all mark labels outside the measuring loop.
|
||||
const syncLabel = `${suite.name}.${test.name}`;
|
||||
const syncStartLabel = `${suite.name}.${test.name}-start`;
|
||||
const syncEndLabel = `${suite.name}.${test.name}-sync-end`;
|
||||
const asyncLabel = `${suite.name}.${test.name}-async`;
|
||||
const asyncStartLabel = `${suite.name}.${test.name}-async-start`;
|
||||
const asyncEndLabel = `${suite.name}.${test.name}-async-end`;
|
||||
|
||||
let syncTime;
|
||||
let asyncStartTime;
|
||||
let asyncTime;
|
||||
const runSync = () => {
|
||||
if (params.warmupBeforeSync) {
|
||||
this.markStart("warmup", "warmup-start");
|
||||
const startTime = performance.now();
|
||||
// Infinite loop for the specified ms.
|
||||
while (performance.now() - startTime < params.warmupBeforeSync)
|
||||
continue;
|
||||
this.markEnd("warmup", "warmup-start", "warmup-end");
|
||||
}
|
||||
this.markStart(syncLabel, syncStartLabel);
|
||||
const syncStartTime = performance.now();
|
||||
test.run(this._page);
|
||||
const syncEndTime = performance.now();
|
||||
this.markEnd(syncLabel, syncStartLabel, syncEndLabel);
|
||||
|
||||
syncTime = syncEndTime - syncStartTime;
|
||||
|
||||
this.markStart(asyncLabel, asyncStartLabel);
|
||||
asyncStartTime = performance.now();
|
||||
};
|
||||
const measureAsync = () => {
|
||||
// Some browsers don't immediately update the layout for paint.
|
||||
// Force the layout here to ensure we're measuring the layout time.
|
||||
const height = this._frame.contentDocument.body.getBoundingClientRect().height;
|
||||
const asyncEndTime = performance.now();
|
||||
asyncTime = asyncEndTime - asyncStartTime;
|
||||
this._frame.contentWindow._unusedHeightValue = height; // Prevent dead code elimination.
|
||||
this.markEnd(asyncLabel, asyncStartLabel, asyncEndLabel);
|
||||
};
|
||||
const report = () => this._recordTestResults(suite, test, syncTime, asyncTime);
|
||||
const invokerClass = params.measurementMethod === "raf" ? RAFTestInvoker : TimerTestInvoker;
|
||||
const invoker = new invokerClass(runSync, measureAsync, report);
|
||||
|
||||
return invoker.start();
|
||||
}
|
||||
|
||||
async _recordTestResults(suite, test, syncTime, asyncTime) {
|
||||
// Skip reporting updates for the warmup suite.
|
||||
if (suite.name === "Warmup")
|
||||
return;
|
||||
|
||||
const suiteResults = this._measuredValues.tests[suite.name];
|
||||
const total = syncTime + asyncTime;
|
||||
suiteResults.tests[test.name] = {
|
||||
tests: {
|
||||
Sync: syncTime,
|
||||
Async: asyncTime,
|
||||
},
|
||||
total: total
|
||||
};
|
||||
suiteResults.total += total;
|
||||
|
||||
if (this._client?.didRunTest)
|
||||
await this._client.didRunTest(suite, test);
|
||||
}
|
||||
|
||||
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 iterationTotalMetric = (i) => {
|
||||
if (i >= params.iterationCount)
|
||||
throw new Error(`Requested iteration=${i} does not exist.`);
|
||||
return getMetric(`Iteration-${i}-Total`);
|
||||
};
|
||||
const aggregates = Object.create(null);
|
||||
|
||||
const collectSubMetrics = (prefix, items, parent) => {
|
||||
for (let name in items) {
|
||||
const results = items[name];
|
||||
const metric = getMetric(prefix + name);
|
||||
const value = results.total ?? results;
|
||||
metric.add(value);
|
||||
if (metric.parent !== parent)
|
||||
parent.addChild(metric);
|
||||
if (results.tests) {
|
||||
collectSubMetrics(`${metric.name}${Metric.separator}`, results.tests, metric);
|
||||
} else {
|
||||
let aggregate = aggregates[name];
|
||||
if (!aggregate)
|
||||
aggregate = aggregates[name] = new Metric(name);
|
||||
aggregate.add(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
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++)
|
||||
iterationTotalMetric(i).description = `Test totals for iteration ${i}`;
|
||||
getMetric("Geomean", "ms").description = "Geomean of test totals";
|
||||
getMetric("Score", "score").description = "Scaled inverse of the Geomean";
|
||||
}
|
||||
|
||||
const geomean = getMetric("Geomean");
|
||||
const iterationTotal = iterationTotalMetric(geomean.length);
|
||||
for (const results of Object.values(iterationResults))
|
||||
iterationTotal.add(results.total);
|
||||
for (let aggregate of Object.values(aggregates)) {
|
||||
aggregate.computeAggregatedMetrics();
|
||||
getMetric(aggregate.name).add(aggregate.geomean);
|
||||
}
|
||||
iterationTotal.computeAggregatedMetrics();
|
||||
geomean.add(iterationTotal.geomean);
|
||||
getMetric("Score").add(geomeanToScore(iterationTotal.geomean));
|
||||
|
||||
for (const metric of Object.values(this._metrics))
|
||||
metric.computeAggregatedMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
class PingBackDone {}
|
||||
const DONE = new PingBackDone();
|
||||
|
||||
class PostMessageRunner extends BenchmarkRunner {
|
||||
_postMessageHandlers = { __proto__: null };
|
||||
|
||||
constructor(suites, client) {
|
||||
super(suites, client);
|
||||
globalThis.addEventListener("message", this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
onMessage(event) {
|
||||
const { cmd, data = undefined } = event.data;
|
||||
const resolver = this._postMessageHandlers[cmd];
|
||||
if (!resolver)
|
||||
throw Error(`Unknown onMessage: ${cmd}`);
|
||||
if (resolver === DONE)
|
||||
throw Error(`Cannot evaluate ping back resolver twice: ${cmd}`);
|
||||
this._postMessageHandlers[cmd] = DONE;
|
||||
resolver(data);
|
||||
}
|
||||
|
||||
async pingBackMessage(name) {
|
||||
if (this._postMessageHandlers[name] !== DONE)
|
||||
throw Error(`Unknown ping back message: ${name}`);
|
||||
return new Promise(resolver => {
|
||||
this._postMessageHandlers[name] = resolver;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SubdomainBenchmarkRunner extends PostMessageRunner {
|
||||
|
||||
_finishPromise;
|
||||
_postMessageHandlers = {
|
||||
__proto__: null,
|
||||
prepareDone: DONE,
|
||||
recordPrepareMetric: DONE,
|
||||
recordTestResults: DONE,
|
||||
};
|
||||
|
||||
// Assume all workloads report the metrics back via postMessage.
|
||||
async _runSuite(suite) {
|
||||
this._prepareMetrics(suite);
|
||||
await this._prepareSuite(suite);
|
||||
if (params.measurePrepare) {
|
||||
const { prepareTime } = await this.pingBackMessage("recordPrepareMetric");
|
||||
this._recordPrepareMetric(suite, prepareTime);
|
||||
}
|
||||
for (const test of suite.tests)
|
||||
await this._runTestAndRecordResults(suite, test);
|
||||
}
|
||||
|
||||
async _runTestAndRecordResults(suite, test) {
|
||||
this.postMessage("runTestAndRecordResults");
|
||||
const { syncTime, asyncTime } = await this.pingBackMessage("recordTestResults");
|
||||
await this._recordTestResults(suite, test, syncTime, asyncTime);
|
||||
}
|
||||
|
||||
postMessage(cmd, data) {
|
||||
this._frame.contentWindow.postMessage({ cmd, data }, "*");
|
||||
}
|
||||
|
||||
async _prepareSuite(suite) {
|
||||
await new Promise((resolve) => {
|
||||
const frame = this._page._frame;
|
||||
frame.onload = async () => resolve();
|
||||
const url = new URL(window.location.href);
|
||||
const customParams = params.copy();
|
||||
customParams.embedded = true;
|
||||
customParams.suites = [suite.name];
|
||||
customParams.tags = [];
|
||||
customParams.startAutomatically = true;
|
||||
customParams.developerMode = false;
|
||||
customParams.iterationCount = 1;
|
||||
// TODO: handle local test URLs better
|
||||
if (this._isLocalUrl(url)) {
|
||||
url.port = parseInt(url.port) + this._currentIteration;
|
||||
} else {
|
||||
// Input: foo-bar.custom.domain.com, foo-bar-0.custom.domain.com
|
||||
// Output: foo-bar-1.custom.domain.com, foo-bar-1.custom.domain.com
|
||||
const hostParts = url.hostname.split(".");
|
||||
const domainWithoutIndex = hostParts[0].match(DOMAIN_WITHOUT_INDEX_RE).groups.nonIndexed;
|
||||
hostParts[0] = `${domainWithoutIndex}-${this.currentIteration}`;
|
||||
url.host = hostParts.join(".");
|
||||
}
|
||||
url.search = customParams.toSearchParams();
|
||||
frame.src = url.toString();
|
||||
});
|
||||
await this.pingBackMessage("prepareDone");
|
||||
this.postMessage("startSuite");
|
||||
}
|
||||
|
||||
_isLocalUrl(url) {
|
||||
return url.hostname === "127.0.0.1" || url.hostname === "localhost";
|
||||
}
|
||||
}
|
||||
|
||||
const DOMAIN_WITHOUT_INDEX_RE = /(?<nonIndexed>([^-]+(-[^0-9])?)+)(-[0-9]+)?/;
|
||||
|
||||
export class EmbeddedBenchmarkRunner extends PostMessageRunner {
|
||||
|
||||
_postMessageHandlers = {
|
||||
__proto__: null,
|
||||
startSuite: DONE,
|
||||
runTestAndRecordResults: DONE,
|
||||
};
|
||||
|
||||
constructor(suites, client) {
|
||||
super(suites, client);
|
||||
if (globalThis === globalThis.parent) {
|
||||
const msg = "Cannot use embedded runner in toplevel iframe";
|
||||
alert(msg);
|
||||
throw Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
postMessage(cmd, data) {
|
||||
globalThis.parent.postMessage({ cmd, data }, "*");
|
||||
}
|
||||
|
||||
async _prepareSuite(suite) {
|
||||
await super._prepareSuite(suite);
|
||||
this.postMessage("prepareDone", { prepareTime: 0 });
|
||||
await this.pingBackMessage("startSuite");
|
||||
}
|
||||
|
||||
async _runTestAndRecordResults(suite, test) {
|
||||
await this.pingBackMessage("runTestAndRecordResults");
|
||||
return super._runTestAndRecordResults(suite, test);
|
||||
}
|
||||
|
||||
_recordPrepareMetric(suite, prepareTime) {
|
||||
super._recordPrepareMetric(suite, prepareTime);
|
||||
this.postMessage("recordPrepareMetric", { prepareTime });
|
||||
}
|
||||
|
||||
async _recordTestResults(suite, test, syncTime, asyncTime) {
|
||||
this.postMessage("recordTestResults", { syncTime, asyncTime });
|
||||
}
|
||||
}
|
||||
78
resources/charts/README.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Chart benchmarks
|
||||
|
||||
## How to run locally
|
||||
|
||||
This project uses `npm` to manage dependencies and run scripts. It has been
|
||||
tested with node v18 but should work with other versions of node.
|
||||
|
||||
This uses [vite](https://vitejs.dev/) as builder but this should be fairly
|
||||
transparent.
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
### Run the development server
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Build the app
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Preview the production build
|
||||
|
||||
```
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## Included benchmarks
|
||||
|
||||
All the benchmarks are included in iframes in the index page. But it may be more
|
||||
convenient to open these benchmarks with their specific files.
|
||||
|
||||
### [Observable Plot](https://github.com/observablehq/plot)
|
||||
|
||||
You can load this benchmark with the `/observable-plot.html` page, for example
|
||||
http://localhost:5173/observable-plot.html if you run it locally or
|
||||
http://localhost:7000/resources/charts/dist/observable-plot.html in the
|
||||
context of the speedometer.
|
||||
|
||||
Observable Plot is D3-based and outputs SVG.
|
||||
|
||||
When run in development mode, the page will automatically execute the 3
|
||||
included graphs:
|
||||
|
||||
- a stacked bar graph
|
||||
- a grouped bar graph
|
||||
- a graph using dots
|
||||
|
||||
In production mode nothing executes by default, the user needs to push the
|
||||
buttons to run any code. That's how the benchmark exercizes this code.
|
||||
|
||||
To build these graphs, we use datasets representing flight information in the
|
||||
US. You can consult them in the [datasets directory](./datasets).
|
||||
|
||||
### [ChartJS](https://github.com/chartjs/Chart.js)
|
||||
|
||||
You can load this benchmark with the `/chartjs.html` page, for example
|
||||
http://localhost:5173/chartjs.html if you run it locally or
|
||||
http://localhost:7000/resources/charts/dist/chartjs.html in the
|
||||
context of speedometer.
|
||||
|
||||
ChartJS is canvas-based.
|
||||
|
||||
When run in development mode, the page will automatically execute the scatter
|
||||
graph.
|
||||
|
||||
In production mode nothing executes by default, the user needs to push the
|
||||
buttons to run any code. That's how the benchmark exercizes this code.
|
||||
|
||||
To build these graphs, we use datasets representing flight information in the
|
||||
US. You can consult them in the [datasets directory](./datasets).
|
||||
33
resources/charts/chartjs.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Chart.js</title>
|
||||
<style type="text/css" media="all">
|
||||
label {
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
|
||||
<button id="open-tooltip" type="button">Open tooltips</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
|
||||
</div>
|
||||
<canvas id="chart"></canvas>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
<script type="module" src="/chartjs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
174
resources/charts/chartjs.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import Chart from "chart.js/auto";
|
||||
import { csvParse } from "d3-dsv";
|
||||
|
||||
// These 2 imports use some vite machinery to directly import these files as
|
||||
// strings. Then they are constants and this reduces the load on the GC.
|
||||
import airportsString from "./datasets/airports.csv?raw";
|
||||
import flightsString from "./datasets/flights-airports.csv?raw";
|
||||
|
||||
/*
|
||||
* From https://www.omnicalculator.com/other/latitude-longitude-distance:
|
||||
* d = 2R × sin⁻¹(√[sin²((θ₂ - θ₁)/2) + cosθ₁ × cosθ₂ × sin²((φ₂ - φ₁)/2)])
|
||||
*
|
||||
* where:
|
||||
* θ₁, φ₁ – First point latitude and longitude coordinates;
|
||||
* θ₂, φ₂ – Second point latitude and longitude coordinates;
|
||||
* R – Earth's radius (R = 6371 km); and
|
||||
* d – Distance between them along Earth's surface.
|
||||
*/
|
||||
const R = 6371;
|
||||
function computeDistance(coords1, coords2) {
|
||||
const long1 = toRadian(coords1.longitude);
|
||||
const lat1 = toRadian(coords1.latitude);
|
||||
const long2 = toRadian(coords2.longitude);
|
||||
const lat2 = toRadian(coords2.latitude);
|
||||
const longSquareSin = Math.sin((long2 - long1) / 2) ** 2;
|
||||
const latSquareSin = Math.sin((lat2 - lat1) / 2) ** 2;
|
||||
const d = 2 * R * Math.asin(Math.sqrt(latSquareSin + Math.cos(lat1) * Math.cos(lat2) * longSquareSin));
|
||||
return d;
|
||||
}
|
||||
|
||||
const RAD = Math.PI / 180;
|
||||
function toRadian(deg) {
|
||||
return deg * RAD;
|
||||
}
|
||||
|
||||
let preparedData = null;
|
||||
function prepare() {
|
||||
/**
|
||||
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
|
||||
* airports: Array<AirportInformation>
|
||||
* flights: Array<{ origin, destination, count }>
|
||||
*/
|
||||
const airports = csvParse(airportsString);
|
||||
const flights = csvParse(flightsString);
|
||||
const airportMap = new Map(airports.map((airport) => [airport.iata, airport]));
|
||||
|
||||
for (const flight of flights) {
|
||||
const origAirport = airportMap.get(flight.origin);
|
||||
const destAirport = airportMap.get(flight.destination);
|
||||
flight.distance = computeDistance(origAirport, destAirport);
|
||||
}
|
||||
|
||||
preparedData = { flights, airportMap };
|
||||
}
|
||||
|
||||
const ROOT = document.getElementById("chart");
|
||||
const opaqueCheckBox = document.getElementById("opaque-color");
|
||||
|
||||
let currentChart = null;
|
||||
function drawScattered(data) {
|
||||
if (!preparedData)
|
||||
throw new Error("Please prepare the data first.");
|
||||
|
||||
reset();
|
||||
|
||||
currentChart = new Chart(ROOT, {
|
||||
type: "scatter",
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
data: preparedData.flights,
|
||||
backgroundColor: opaqueCheckBox.checked ? "rgb(0, 125, 255)" : "rgba(0, 125, 255, .20)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
parsing: {
|
||||
xAxisKey: "distance",
|
||||
yAxisKey: "count",
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Number of flights for a distance",
|
||||
position: "bottom",
|
||||
},
|
||||
tooltip: {
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: (item) => {
|
||||
const orig = preparedData.airportMap.get(item.raw.origin);
|
||||
const dest = preparedData.airportMap.get(item.raw.destination);
|
||||
const result = `✈ ${orig.name} (${orig.iata}) → ${dest.name} (${dest.iata}): ${Math.round(item.raw.distance)} km`;
|
||||
return result;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
text: "distance →",
|
||||
align: "end",
|
||||
display: true,
|
||||
},
|
||||
ticks: {
|
||||
format: {
|
||||
style: "unit",
|
||||
unit: "kilometer",
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "logarithmic",
|
||||
title: {
|
||||
text: "# of flights →",
|
||||
align: "end",
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openTooltip() {
|
||||
if (!currentChart)
|
||||
throw new Error("No chart is present, please draw a chart first");
|
||||
|
||||
const renderedDataset = currentChart.getDatasetMeta(0);
|
||||
const node = currentChart.canvas;
|
||||
const rect = node.getBoundingClientRect();
|
||||
|
||||
// Index 2426 is carefully chosen to display a lot of lines (which depends
|
||||
// on the zoom level).
|
||||
const point = renderedDataset.data[2426];
|
||||
const event = new MouseEvent("mousemove", {
|
||||
clientX: rect.left + point.x,
|
||||
clientY: rect.top + point.y,
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
});
|
||||
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (currentChart) {
|
||||
currentChart.destroy();
|
||||
currentChart = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllTheThings() {
|
||||
[
|
||||
// Force prettier to use a multiline formatting
|
||||
"prepare",
|
||||
"add-scatter-chart-button",
|
||||
"open-tooltip",
|
||||
].forEach((id) => document.getElementById(id).click());
|
||||
}
|
||||
|
||||
document.getElementById("prepare").addEventListener("click", prepare);
|
||||
document.getElementById("add-scatter-chart-button").addEventListener("click", drawScattered);
|
||||
document.getElementById("open-tooltip").addEventListener("click", openTooltip);
|
||||
document.getElementById("reset").addEventListener("click", reset);
|
||||
document.getElementById("run-all").addEventListener("click", runAllTheThings);
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
runAllTheThings();
|
||||
4
resources/charts/datasets/README
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Origin of the datasets:
|
||||
|
||||
airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/airports.csv
|
||||
flights-airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/flights-airport.csv
|
||||
3377
resources/charts/datasets/airports.csv
Normal file
5367
resources/charts/datasets/flights-airports.csv
Normal file
33
resources/charts/developer.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
|
||||
<iframe src="./observable-plot.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>
|
||||
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
|
||||
</summary>
|
||||
<iframe src="./chartjs.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
14248
resources/charts/dist/assets/chartjs-7fd89fd7.js
vendored
Normal file
1
resources/charts/dist/assets/chartjs-7fd89fd7.js.map
vendored
Normal file
1
resources/charts/dist/assets/developer-4ed993c7.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"developer-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
3517
resources/charts/dist/assets/flights-airports-9a9e6422.js
vendored
Normal file
1
resources/charts/dist/assets/flights-airports-9a9e6422.js.map
vendored
Normal file
1
resources/charts/dist/assets/index-4ed993c7.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"index-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
12316
resources/charts/dist/assets/plot-562fbdb6.js
vendored
Normal file
1
resources/charts/dist/assets/plot-562fbdb6.js.map
vendored
Normal file
35
resources/charts/dist/chartjs.html
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Chart.js</title>
|
||||
<style type="text/css" media="all">
|
||||
label {
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="./assets/chartjs-7fd89fd7.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
|
||||
<button id="open-tooltip" type="button">Open tooltips</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
|
||||
</div>
|
||||
<canvas id="chart"></canvas>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
33
resources/charts/dist/developer.html
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
|
||||
<iframe src="./observable-plot.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>
|
||||
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
|
||||
</summary>
|
||||
<iframe src="./chartjs.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
15
resources/charts/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Charting workloads</h1>
|
||||
<ul>
|
||||
<li><a href="./observable-plot.html">Observable Plot</a></li>
|
||||
<li><a href="./chartjs.html">Chart.js</a></li>
|
||||
</ul>
|
||||
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
|
||||
</body>
|
||||
</html>
|
||||
37
resources/charts/dist/observable-plot.html
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Observable Plot</title>
|
||||
<style>
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-content: center;
|
||||
gap: 3px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
#airport-group-size {
|
||||
min-width: 30px;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="./assets/plot-562fbdb6.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
|
||||
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label>
|
||||
Number of airports to keep in each group:
|
||||
<span id="airport-group-size"></span>
|
||||
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="chart"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
15
resources/charts/index.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Charting workloads</h1>
|
||||
<ul>
|
||||
<li><a href="./observable-plot.html">Observable Plot</a></li>
|
||||
<li><a href="./chartjs.html">Chart.js</a></li>
|
||||
</ul>
|
||||
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
|
||||
</body>
|
||||
</html>
|
||||
35
resources/charts/observable-plot.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Observable Plot</title>
|
||||
<style>
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-content: center;
|
||||
gap: 3px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
#airport-group-size {
|
||||
min-width: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
|
||||
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label>
|
||||
Number of airports to keep in each group:
|
||||
<span id="airport-group-size"></span>
|
||||
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="chart"></div>
|
||||
<script type="module" src="/observable-plot.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
233
resources/charts/observable-plot.js
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import * as Plot from "@observablehq/plot";
|
||||
import { csvParse } from "d3-dsv";
|
||||
import * as d3Array from "d3-array";
|
||||
import { format as d3Format } from "d3-format";
|
||||
|
||||
// These 2 imports use some vite machinery to directly import these files as
|
||||
// strings. Then they are constants and this reduces the load on the GC.
|
||||
import airportsString from "./datasets/airports.csv?raw";
|
||||
import flightsString from "./datasets/flights-airports.csv?raw";
|
||||
|
||||
const ROOT = document.getElementById("chart");
|
||||
const DEFAULT_OPTIONS = {
|
||||
// This width is really a max-width: the plot adjusts using the available
|
||||
// width as well.
|
||||
width: 2000,
|
||||
// The height is especially used for the ratio.
|
||||
height: 1000,
|
||||
};
|
||||
|
||||
let preparedData;
|
||||
function prepare() {
|
||||
/**
|
||||
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
|
||||
* airports: Array<AirportInformation>
|
||||
* flights: Array<{ origin, destination, count }>
|
||||
*/
|
||||
const { airports, flights } = parseAirportsInformation();
|
||||
/**
|
||||
* flightsByAirport: Map<iata: string, { origin: number, destination: number, total: number}>
|
||||
*/
|
||||
const flightsByAirport = groupFlightsByAirports(flights);
|
||||
/**
|
||||
* byAirport: Map<iata: string, AirportInformation>
|
||||
*/
|
||||
const byAirport = d3Array.index(airports, (d) => d.iata);
|
||||
|
||||
/* Array<[state, AirportInformation[]]> */
|
||||
const airportsGroupedByStateArray = d3Array.groups(airports, (d) => d.state);
|
||||
|
||||
/* DescSortedArray<[{ state: string, total: number, mostUsedAirportsInState: AirportInformation[] }]> */
|
||||
const stateInformationSortedArray = airportsGroupedByStateArray
|
||||
.map(([state, airportsInState]) => {
|
||||
const totalFlightsInState = d3Array.sum(airportsInState, ({ iata }) => flightsByAirport.get(iata)?.total);
|
||||
const sorted = d3Array.sort(airportsInState, ({ iata }) => -flightsByAirport.get(iata)?.total);
|
||||
const mostUsedAirportsInState = sorted.slice(0, airportCountPerGroup());
|
||||
return {
|
||||
state,
|
||||
total: totalFlightsInState,
|
||||
mostUsedAirports: mostUsedAirportsInState,
|
||||
};
|
||||
})
|
||||
.sort((stateA, stateB) => stateB.total - stateA.total);
|
||||
|
||||
/* Array<state: string> */
|
||||
const stateSortedArray = stateInformationSortedArray.map(({ state }) => state);
|
||||
|
||||
// Flatten the information in preparedData.stateInformationSortedArray, so that we
|
||||
// have one item == one airport information.
|
||||
/* Array<{state, iata, name, city, index, origin, destination, total}> */
|
||||
const plotData = stateInformationSortedArray.flatMap(({ mostUsedAirports, total, state }) => {
|
||||
const enrichedMostUsedAirports = mostUsedAirports.map(({ iata, name, city }, index) => ({
|
||||
state,
|
||||
iata,
|
||||
name,
|
||||
city,
|
||||
index, // This will be used to have consistent colors.
|
||||
...flightsByAirport.get(iata),
|
||||
}));
|
||||
const otherTotal = total - d3Array.sum(mostUsedAirports, ({ iata }) => flightsByAirport.get(iata)?.total);
|
||||
|
||||
if (otherTotal > 0) {
|
||||
enrichedMostUsedAirports.push({
|
||||
state,
|
||||
iata: "Other",
|
||||
total: otherTotal,
|
||||
index: enrichedMostUsedAirports.length,
|
||||
});
|
||||
}
|
||||
return enrichedMostUsedAirports;
|
||||
});
|
||||
|
||||
preparedData = {
|
||||
airports,
|
||||
flights,
|
||||
flightsByAirport,
|
||||
byAirport,
|
||||
stateInformationSortedArray,
|
||||
stateSortedArray,
|
||||
plotData,
|
||||
};
|
||||
}
|
||||
|
||||
function parseAirportsInformation() {
|
||||
return {
|
||||
airports: csvParse(airportsString),
|
||||
flights: csvParse(flightsString),
|
||||
};
|
||||
}
|
||||
|
||||
function groupFlightsByAirports(flights) {
|
||||
const flightsByAirport = new Map();
|
||||
|
||||
for (const { origin, destination, count } of flights) {
|
||||
const infoForOriginAirport = flightsByAirport.get(origin) ?? { origin: 0, destination: 0, total: 0 };
|
||||
const intCount = Number(count);
|
||||
if (Number.isNaN(intCount)) {
|
||||
console.error(`Couldn't convert ${count} to number.`);
|
||||
continue;
|
||||
}
|
||||
infoForOriginAirport.origin += intCount;
|
||||
infoForOriginAirport.total += intCount;
|
||||
flightsByAirport.set(origin, infoForOriginAirport);
|
||||
const infoForDestinationAirport = flightsByAirport.get(destination) ?? { origin: 0, destination: 0, total: 0 };
|
||||
infoForDestinationAirport.destination += intCount;
|
||||
infoForDestinationAirport.total += intCount;
|
||||
flightsByAirport.set(destination, infoForDestinationAirport);
|
||||
}
|
||||
|
||||
return flightsByAirport;
|
||||
}
|
||||
|
||||
function isReady() {
|
||||
return !!preparedData;
|
||||
}
|
||||
|
||||
function addStackedBars() {
|
||||
if (!isReady())
|
||||
throw new Error("Please prepare the data first.");
|
||||
|
||||
const options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
color: { type: "categorical" },
|
||||
x: {
|
||||
domain: preparedData.stateSortedArray,
|
||||
},
|
||||
y: { grid: true, tickFormat: "~s" },
|
||||
marks: [
|
||||
Plot.barY(preparedData.plotData, {
|
||||
x: "state",
|
||||
y: "total",
|
||||
fill: "index",
|
||||
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(d.total)} flights`,
|
||||
}),
|
||||
Plot.text(preparedData.stateInformationSortedArray, { x: "state", y: "total", text: (d) => d3Format(".2~s")(d.total), dy: -10 }),
|
||||
Plot.ruleY([0]),
|
||||
],
|
||||
};
|
||||
ROOT.append(Plot.plot(options));
|
||||
}
|
||||
|
||||
function addDottedBars() {
|
||||
if (!isReady())
|
||||
throw new Error("Please prepare the data first.");
|
||||
|
||||
const data = [...preparedData.flightsByAirport]
|
||||
.flatMap(([iata, { origin, destination }]) => {
|
||||
const airportInformation = preparedData.byAirport.get(iata);
|
||||
return [
|
||||
{ ...airportInformation, value: -origin },
|
||||
{ ...airportInformation, value: destination },
|
||||
];
|
||||
})
|
||||
.filter((d) => d.value);
|
||||
|
||||
const options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
color: { type: "threshold", domain: [0] },
|
||||
x: {
|
||||
domain: preparedData.stateSortedArray,
|
||||
},
|
||||
y: {
|
||||
grid: true,
|
||||
label: "← outward Number of flights inward →",
|
||||
labelAnchor: "center",
|
||||
tickFormat: (v) => d3Format("~s")(Math.abs(v)),
|
||||
type: "pow",
|
||||
exponent: 0.2,
|
||||
},
|
||||
marks: [
|
||||
Plot.dot(data, {
|
||||
x: "state",
|
||||
y: "value",
|
||||
r: 4,
|
||||
stroke: "value",
|
||||
strokeWidth: 3,
|
||||
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(Math.abs(d.value))} ${d.value > 0 ? "inward" : "outward"} flights`,
|
||||
}),
|
||||
Plot.ruleY([0]),
|
||||
],
|
||||
};
|
||||
|
||||
ROOT.append(Plot.plot(options));
|
||||
}
|
||||
|
||||
function reset() {
|
||||
ROOT.textContent = "";
|
||||
}
|
||||
|
||||
async function runAllTheThings() {
|
||||
reset();
|
||||
[
|
||||
// Force prettier to use a multiline formatting
|
||||
"prepare",
|
||||
"add-stacked-chart-button",
|
||||
"add-dotted-chart-button",
|
||||
].forEach((id) => document.getElementById(id).click());
|
||||
}
|
||||
|
||||
// This is the number of airports we keep for each state in the stacked bar
|
||||
// graph. One additional group will be added, that will sum all airports that
|
||||
// haven't been kept. It's retrieved from the input directly.
|
||||
function airportCountPerGroup() {
|
||||
return document.querySelector("#airport-group-size-input").value;
|
||||
}
|
||||
|
||||
function onGroupSizeInputChange() {
|
||||
document.querySelector("#airport-group-size").textContent = airportCountPerGroup();
|
||||
if (import.meta.env.DEV) {
|
||||
// In dev mode, redraw everything
|
||||
runAllTheThings();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("prepare").addEventListener("click", prepare);
|
||||
document.getElementById("add-stacked-chart-button").addEventListener("click", addStackedBars);
|
||||
document.getElementById("add-dotted-chart-button").addEventListener("click", addDottedBars);
|
||||
document.getElementById("reset").addEventListener("click", reset);
|
||||
document.getElementById("run-all").addEventListener("click", runAllTheThings);
|
||||
document.getElementById("airport-group-size-input").addEventListener("input", onGroupSizeInputChange);
|
||||
onGroupSizeInputChange();
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
runAllTheThings();
|
||||
1022
resources/charts/package-lock.json
generated
Normal file
19
resources/charts/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "charts-benchmark",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@observablehq/plot": "^0.6.4",
|
||||
"chart.js": "^4.2.1",
|
||||
"d3-dsv": "^3.0.1"
|
||||
}
|
||||
}
|
||||
18
resources/charts/vite.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
base: "./", // Since this will be loaded from the project root
|
||||
build: {
|
||||
modulePreload: { polyfill: false },
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "index.html"),
|
||||
developer: resolve(__dirname, "developer.html"),
|
||||
plot: resolve(__dirname, "observable-plot.html"),
|
||||
chartjs: resolve(__dirname, "chartjs.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
384
resources/developer-mode.mjs
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
import { Suites, Tags } from "./tests.mjs";
|
||||
import { params, defaultParams } from "./params.mjs";
|
||||
|
||||
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(createUIForMeasurementMethod());
|
||||
settings.append(createUIForWarmupSuite());
|
||||
settings.append(createUIForWarmupBeforeSync());
|
||||
settings.append(createUIForWaitAfterSetup());
|
||||
settings.append(createUIForSyncStepDelay());
|
||||
settings.append(createUIForWaitAfterSuite());
|
||||
settings.append(createUIForLeakyIframes());
|
||||
settings.append(createUIForMeasurePrepare());
|
||||
settings.append(createUIForDomainPerIteration());
|
||||
settings.append(createUIForForceGC());
|
||||
|
||||
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 createUIForMeasurementMethod() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "measurement-method";
|
||||
check.checked = params.measurementMethod === "raf";
|
||||
|
||||
check.onchange = () => {
|
||||
params.measurementMethod = check.checked ? "raf" : "timer";
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", span("rAF timing"));
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForLeakyIframes() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "leaky-iframes-checkbox";
|
||||
check.checked = !!params.leakyIframes;
|
||||
|
||||
check.onchange = () => {
|
||||
params.leakyIframes = check.checked;
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", span("Leak IFrames"));
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForWarmupSuite() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "warmup-suite";
|
||||
check.checked = !!params.useWarmupSuite;
|
||||
|
||||
check.onchange = () => {
|
||||
params.useWarmupSuite = check.checked;
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", span("Use Warmup Suite"));
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForMeasurePrepare() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "measure-prepare";
|
||||
check.checked = params.measurePrepare;
|
||||
|
||||
check.onchange = () => {
|
||||
params.measurePrepare = check.checked;
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", "Measure Prepare");
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForDomainPerIteration() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "domain-per-iteration";
|
||||
check.checked = params.domainPerIteration;
|
||||
|
||||
check.onchange = () => {
|
||||
params.domainPerIteration = check.checked;
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", "Use Subdomain-Runner");
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForForceGC() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "suite-force-gc";
|
||||
check.checked = params.suiteForceGC;
|
||||
|
||||
check.onchange = () => {
|
||||
params.suiteForceGC = check.checked;
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", "Force GC before every Suite");
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForIterationCount() {
|
||||
const { range, label } = createTimeRangeUI("Iterations: ", params.iterationCount, "#", 1, 200);
|
||||
range.onchange = () => {
|
||||
params.iterationCount = parseInt(range.value);
|
||||
updateURL();
|
||||
};
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForWarmupBeforeSync() {
|
||||
const { range, label } = createTimeRangeUI("Warmup time: ", params.warmupBeforeSync);
|
||||
range.onchange = () => {
|
||||
params.warmupBeforeSync = parseInt(range.value);
|
||||
updateURL();
|
||||
};
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForSyncStepDelay() {
|
||||
const { range, label } = createTimeRangeUI("Sync step delay: ", params.waitBeforeSync);
|
||||
range.onchange = () => {
|
||||
params.waitBeforeSync = parseInt(range.value);
|
||||
updateURL();
|
||||
};
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForWaitAfterSetup() {
|
||||
const { range, label } = createTimeRangeUI("Post setup delay: ", params.waitAfterSetup);
|
||||
range.onchange = () => {
|
||||
params.waitAfterSetup = parseInt(range.value);
|
||||
updateURL();
|
||||
};
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForWaitAfterSuite() {
|
||||
const { range, label } = createTimeRangeUI("Post suite delay: ", params.waitAfterSuite);
|
||||
range.onchange = () => {
|
||||
params.waitAfterSuite = parseInt(range.value);
|
||||
updateURL();
|
||||
};
|
||||
return label;
|
||||
}
|
||||
|
||||
function createTimeRangeUI(labelText, initialValue, unit = "ms", min = 0, max = 1000) {
|
||||
const range = document.createElement("input");
|
||||
range.type = "range";
|
||||
range.min = min;
|
||||
range.max = max;
|
||||
range.value = initialValue;
|
||||
|
||||
const rangeValueAndUnit = document.createElement("span");
|
||||
rangeValueAndUnit.className = "range-label-data";
|
||||
|
||||
const rangeValue = document.createElement("span");
|
||||
rangeValue.textContent = initialValue;
|
||||
rangeValueAndUnit.append(rangeValue, " ", unit);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.append(span(labelText), range, rangeValueAndUnit);
|
||||
|
||||
range.oninput = () => {
|
||||
rangeValue.textContent = range.value;
|
||||
};
|
||||
|
||||
return { range, label };
|
||||
}
|
||||
|
||||
function createUIForSuites() {
|
||||
const control = document.createElement("nav");
|
||||
control.className = "suites";
|
||||
const ol = document.createElement("ol");
|
||||
const checkboxes = [];
|
||||
const setSuiteEnabled = (suiteIndex, enabled) => {
|
||||
Suites[suiteIndex].disabled = !enabled;
|
||||
checkboxes[suiteIndex].checked = enabled;
|
||||
};
|
||||
|
||||
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.disabled;
|
||||
checkbox.onchange = () => {
|
||||
suite.disabled = !checkbox.checked;
|
||||
updateURL();
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ol.appendChild(li);
|
||||
}
|
||||
control.appendChild(ol);
|
||||
let buttons = control.appendChild(document.createElement("div"));
|
||||
buttons.className = "button-bar";
|
||||
|
||||
let button = document.createElement("button");
|
||||
button.textContent = "Select all";
|
||||
button.onclick = () => {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
|
||||
setSuiteEnabled(suiteIndex, true);
|
||||
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Unselect all";
|
||||
button.onclick = () => {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
|
||||
setSuiteEnabled(suiteIndex, false);
|
||||
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
|
||||
let i = 0;
|
||||
const kTagsPerLine = 3;
|
||||
for (const tag of Tags) {
|
||||
if (tag === "all")
|
||||
continue;
|
||||
if (!(i % kTagsPerLine)) {
|
||||
buttons = control.appendChild(document.createElement("div"));
|
||||
buttons.className = "button-bar";
|
||||
}
|
||||
i++;
|
||||
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);
|
||||
}
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
function createUIForRun() {
|
||||
let button = document.createElement("button");
|
||||
button.textContent = "Start Test";
|
||||
button.onclick = (event) => {
|
||||
globalThis.benchmarkClient.start();
|
||||
};
|
||||
let buttons = document.createElement("div");
|
||||
buttons.className = "button-bar";
|
||||
buttons.appendChild(button);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// 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.disabled);
|
||||
|
||||
if (!selectedSuites.length) {
|
||||
url.searchParams.delete("tags");
|
||||
url.searchParams.delete("suites");
|
||||
url.searchParams.delete("suite");
|
||||
} else {
|
||||
url.searchParams.delete("tags");
|
||||
url.searchParams.delete("suite");
|
||||
// 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.disabled)
|
||||
suite.tags.forEach((tag) => commonTags.delete(tag));
|
||||
else
|
||||
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
|
||||
}
|
||||
if (commonTags.size) {
|
||||
const tags = [...commonTags][0];
|
||||
if (tags === "default")
|
||||
url.searchParams.delete("tags");
|
||||
else
|
||||
url.searchParams.set("tags", tags);
|
||||
url.searchParams.delete("suites");
|
||||
} else {
|
||||
url.searchParams.delete("tags");
|
||||
url.searchParams.set("suites", selectedSuites.map((suite) => suite.name).join(","));
|
||||
}
|
||||
}
|
||||
|
||||
if (params.measurementMethod !== "raf")
|
||||
url.searchParams.set("measurementMethod", "timer");
|
||||
else
|
||||
url.searchParams.delete("measurementMethod");
|
||||
|
||||
const boolParamKeys = ["iterationCount", "useWarmupSuite", "warmupBeforeSync", "waitAfterSetup", "waitBeforeSync", "waitAfterSuite", "domainPerIteration", "suiteForceGC", "leakyIframes"];
|
||||
for (const paramKey of boolParamKeys) {
|
||||
if (params[paramKey] !== defaultParams[paramKey])
|
||||
url.searchParams.set(paramKey, params[paramKey]);
|
||||
else
|
||||
url.searchParams.delete(paramKey);
|
||||
}
|
||||
|
||||
if (params.measurePrepare !== defaultParams.measurePrepare)
|
||||
url.searchParams.set("measurePrepare", params.measurePrepare);
|
||||
else
|
||||
url.searchParams.delete("measurePrepare");
|
||||
|
||||
// Only push state if changed
|
||||
url.search = decodeURIComponent(url.search);
|
||||
if (url.href !== window.location.href)
|
||||
window.history.pushState({}, "", url);
|
||||
}
|
||||
26
resources/editors/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
27
resources/editors/README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## Description
|
||||
|
||||
Lots of people edit text content in the browser. Lots of that content, like WYSIWYG content or code, is too rich or complex to represent well with a `<textarea>`. Sites typically rely on advanced editor libraries for this, and we should make sure browsers perform well at common patterns used by them.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
## What are we testing
|
||||
|
||||
- Basic DOM and editing
|
||||
- Virtualization (DOM content changing during scroll)
|
||||
- Basic flex layout with SVG icons
|
||||
|
||||
## How are we testing
|
||||
|
||||
The test simulates a real-world user flow by loading a number of popular editor libraries. After the initial load is complete, the following steps are timed:
|
||||
|
||||
- Setting to a fairly large value
|
||||
- "Formatting" the text - in code editors this means turning on syntax highlighting, and in WYSISWYG this means bolding all of the contents
|
||||
- Scrolling to the bottom of the editor
|
||||
|
||||
## Developer Documentation
|
||||
|
||||
The app was created with `npm create vite@latest editors`, and can be previewed with `npm run dev`. In order to update the files run in the harness you have to use `npm run build` which will recreate the `dist/` directory.
|
||||
|
||||
The built test can be loaded within the harness using i.e. http://localhost:7000/?suite=Editor-CodeMirror&startAutomatically=true, or the static versions at http://localhost:7000/resources/editors/dist/.
|
||||
2
resources/editors/assets/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
longtext.html is "Du côté de chez Swann" by Marcel Proust, downloaded from https://www.gutenberg.org/ebooks/2650
|
||||
longscript.js has been downloaded from https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js
|
||||
29869
resources/editors/assets/longscript.js
Normal file
1560
resources/editors/assets/longtext.html
Normal file
206
resources/editors/codemirror.html
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>CodeMirror Test</title>
|
||||
<script>
|
||||
window.requestIdleCallback = undefined;
|
||||
window.cancelIdleCallback = undefined;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
<script type="module">
|
||||
import { code as shorttext } from "./shorttext.js";
|
||||
import { code as longtext } from "./longtext.js";
|
||||
import editor from "./codemirror.js";
|
||||
|
||||
let editorContainer = document.querySelector("#editor");
|
||||
let editorInstance = null;
|
||||
|
||||
let buttons = {
|
||||
create: document.querySelector("#create"),
|
||||
highlight: document.querySelector("#highlight"),
|
||||
unhighlight: document.querySelector("#unhighlight"),
|
||||
long: document.querySelector("#long"),
|
||||
short: document.querySelector("#short"),
|
||||
scroll: document.querySelector("#scroll"),
|
||||
layout: document.querySelector("#layout"),
|
||||
};
|
||||
buttons.scroll.addEventListener("click", scroll);
|
||||
buttons.highlight.addEventListener("click", highlight);
|
||||
buttons.unhighlight.addEventListener("click", unhighlight);
|
||||
buttons.long.addEventListener("click", long);
|
||||
buttons.short.addEventListener("click", short);
|
||||
buttons.layout.addEventListener("click", layout);
|
||||
|
||||
buttons.create.addEventListener("click", (e) => {
|
||||
if (!editorInstance) {
|
||||
editorInstance = editor(editorContainer);
|
||||
editorInstance.ready.then(() => {
|
||||
buttons.unhighlight.classList.add("active", "true");
|
||||
buttons.create.setAttribute("disabled", "true");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function layout() {
|
||||
// Todo - is this necessary with the runner?
|
||||
const body = document.body.getBoundingClientRect();
|
||||
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
|
||||
}
|
||||
|
||||
function highlight() {
|
||||
buttons.unhighlight.classList.toggle("active", false);
|
||||
buttons.highlight.classList.toggle("active", true);
|
||||
editorInstance.format(true);
|
||||
}
|
||||
function unhighlight() {
|
||||
buttons.unhighlight.classList.toggle("active", true);
|
||||
buttons.highlight.classList.toggle("active", false);
|
||||
editorInstance.format(false);
|
||||
}
|
||||
function long() {
|
||||
buttons.short.classList.toggle("active", false);
|
||||
buttons.long.classList.toggle("active", true);
|
||||
editorInstance.setValue(longtext);
|
||||
}
|
||||
function short() {
|
||||
buttons.short.classList.toggle("active", true);
|
||||
buttons.long.classList.toggle("active", false);
|
||||
editorInstance.setValue(shorttext);
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
let isTop = editorInstance.getScrollTop() == 0;
|
||||
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
47
resources/editors/codemirror.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// https://codemirror.net/examples/bundle/
|
||||
import { EditorView, basicSetup } from "codemirror";
|
||||
import { StateEffect } from "@codemirror/state";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
|
||||
let lang = javascript();
|
||||
let extensions = [basicSetup, EditorView.lineWrapping];
|
||||
|
||||
export default function (element, value) {
|
||||
let view = new EditorView({
|
||||
extensions,
|
||||
parent: element,
|
||||
doc: value,
|
||||
wordWrapColumn: 80,
|
||||
});
|
||||
|
||||
return {
|
||||
editor: view,
|
||||
// Anything before this promise resolves will happen before timing starts
|
||||
ready: Promise.resolve(),
|
||||
getScrollHeight() {
|
||||
return element.scrollHeight;
|
||||
},
|
||||
getScrollTop() {
|
||||
return element.scrollTop;
|
||||
},
|
||||
setScrollTop(value) {
|
||||
element.scrollTop = value;
|
||||
},
|
||||
setValue: (value) =>
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: value },
|
||||
}),
|
||||
format(on) {
|
||||
// https://codemirror.net/examples/config/
|
||||
// https://discuss.codemirror.net/t/cm6-dynamically-switching-syntax-theme-w-reconfigure/2858/6
|
||||
if (on && extensions.length === 2)
|
||||
extensions.push(lang);
|
||||
else if (!on && extensions.length === 3)
|
||||
extensions.pop();
|
||||
|
||||
view.dispatch({
|
||||
effects: StateEffect.reconfigure.of(extensions),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
21908
resources/editors/dist/assets/codemirror-521de7ab.js
vendored
Normal file
1
resources/editors/dist/assets/codemirror-521de7ab.js.map
vendored
Normal file
66
resources/editors/dist/assets/index-2feebe42.css
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
:root {
|
||||
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
#controls button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#controls button.active {
|
||||
color: blue;
|
||||
}
|
||||
129
resources/editors/dist/assets/index.es-02a92ebc.js
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
const style = "";
|
||||
const code$1 = `
|
||||
function x() {
|
||||
console.log("Hello world!");
|
||||
}
|
||||
`;
|
||||
const text = `
|
||||
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
|
||||
`;
|
||||
var base = {
|
||||
8: "Backspace",
|
||||
9: "Tab",
|
||||
10: "Enter",
|
||||
12: "NumLock",
|
||||
13: "Enter",
|
||||
16: "Shift",
|
||||
17: "Control",
|
||||
18: "Alt",
|
||||
20: "CapsLock",
|
||||
27: "Escape",
|
||||
32: " ",
|
||||
33: "PageUp",
|
||||
34: "PageDown",
|
||||
35: "End",
|
||||
36: "Home",
|
||||
37: "ArrowLeft",
|
||||
38: "ArrowUp",
|
||||
39: "ArrowRight",
|
||||
40: "ArrowDown",
|
||||
44: "PrintScreen",
|
||||
45: "Insert",
|
||||
46: "Delete",
|
||||
59: ";",
|
||||
61: "=",
|
||||
91: "Meta",
|
||||
92: "Meta",
|
||||
106: "*",
|
||||
107: "+",
|
||||
108: ",",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
144: "NumLock",
|
||||
145: "ScrollLock",
|
||||
160: "Shift",
|
||||
161: "Shift",
|
||||
162: "Control",
|
||||
163: "Control",
|
||||
164: "Alt",
|
||||
165: "Alt",
|
||||
173: "-",
|
||||
186: ";",
|
||||
187: "=",
|
||||
188: ",",
|
||||
189: "-",
|
||||
190: ".",
|
||||
191: "/",
|
||||
192: "`",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'"
|
||||
};
|
||||
var shift = {
|
||||
48: ")",
|
||||
49: "!",
|
||||
50: "@",
|
||||
51: "#",
|
||||
52: "$",
|
||||
53: "%",
|
||||
54: "^",
|
||||
55: "&",
|
||||
56: "*",
|
||||
57: "(",
|
||||
59: ":",
|
||||
61: "+",
|
||||
173: "_",
|
||||
186: ":",
|
||||
187: "+",
|
||||
188: "<",
|
||||
189: "_",
|
||||
190: ">",
|
||||
191: "?",
|
||||
192: "~",
|
||||
219: "{",
|
||||
220: "|",
|
||||
221: "}",
|
||||
222: '"'
|
||||
};
|
||||
var chrome = typeof navigator != "undefined" && /Chrome\/(\d+)/.exec(navigator.userAgent);
|
||||
var mac = typeof navigator != "undefined" && /Mac/.test(navigator.platform);
|
||||
var ie = typeof navigator != "undefined" && /MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);
|
||||
var brokenModifierNames = mac || chrome && +chrome[1] < 57;
|
||||
for (var i = 0; i < 10; i++)
|
||||
base[48 + i] = base[96 + i] = String(i);
|
||||
for (var i = 1; i <= 24; i++)
|
||||
base[i + 111] = "F" + i;
|
||||
for (var i = 65; i <= 90; i++) {
|
||||
base[i] = String.fromCharCode(i + 32);
|
||||
shift[i] = String.fromCharCode(i);
|
||||
}
|
||||
for (var code in base)
|
||||
if (!shift.hasOwnProperty(code))
|
||||
shift[code] = base[code];
|
||||
function keyName(event) {
|
||||
var ignoreKey = brokenModifierNames && (event.ctrlKey || event.altKey || event.metaKey) || ie && event.shiftKey && event.key && event.key.length == 1 || event.key == "Unidentified";
|
||||
var name = !ignoreKey && event.key || (event.shiftKey ? shift : base)[event.keyCode] || event.key || "Unidentified";
|
||||
if (name == "Esc")
|
||||
name = "Escape";
|
||||
if (name == "Del")
|
||||
name = "Delete";
|
||||
if (name == "Left")
|
||||
name = "ArrowLeft";
|
||||
if (name == "Up")
|
||||
name = "ArrowUp";
|
||||
if (name == "Right")
|
||||
name = "ArrowRight";
|
||||
if (name == "Down")
|
||||
name = "ArrowDown";
|
||||
return name;
|
||||
}
|
||||
export {
|
||||
base as b,
|
||||
code$1 as c,
|
||||
keyName as k,
|
||||
shift as s,
|
||||
text as t
|
||||
};
|
||||
//# sourceMappingURL=index.es-02a92ebc.js.map
|
||||
1
resources/editors/dist/assets/index.es-02a92ebc.js.map
vendored
Normal file
1
resources/editors/dist/assets/main-4ed993c7.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"main-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
16358
resources/editors/dist/assets/tiptap-95a40ba8.js
vendored
Normal file
1
resources/editors/dist/assets/tiptap-95a40ba8.js.map
vendored
Normal file
144
resources/editors/dist/codemirror.html
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>CodeMirror Test</title>
|
||||
<script>
|
||||
window.requestIdleCallback = undefined;
|
||||
window.cancelIdleCallback = undefined;
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/codemirror-521de7ab.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
|
||||
<link rel="stylesheet" href="./assets/index-2feebe42.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
32
resources/editors/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Code Editors</strong></summary>
|
||||
<iframe src="./codemirror.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary><strong>WYSIWYG Editors</strong></summary>
|
||||
<iframe src="./tiptap.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
134
resources/editors/dist/tiptap.html
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>TipTap Test</title>
|
||||
<script type="module" crossorigin src="./assets/tiptap-95a40ba8.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
|
||||
<link rel="stylesheet" href="./assets/index-2feebe42.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
resources/editors/dist/vite.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
32
resources/editors/index.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Code Editors</strong></summary>
|
||||
<iframe src="./codemirror.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary><strong>WYSIWYG Editors</strong></summary>
|
||||
<iframe src="./tiptap.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2
resources/editors/longtext.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as code } from "./assets/longscript.js?raw";
|
||||
export { default as text } from "./assets/longtext.html?raw";
|
||||
2615
resources/editors/package-lock.json
generated
Normal file
18
resources/editors/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "editors",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.4",
|
||||
"@tiptap/core": "^2.0.0-beta.217",
|
||||
"@tiptap/starter-kit": "^2.0.0-beta.217",
|
||||
"codemirror": "^6.0.1",
|
||||
"vite": "^4.1.0"
|
||||
}
|
||||
}
|
||||
1
resources/editors/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
resources/editors/screenshot.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
9
resources/editors/shorttext.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const code = `
|
||||
function x() {
|
||||
console.log("Hello world!");
|
||||
}
|
||||
`;
|
||||
|
||||
export const text = `
|
||||
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
|
||||
`;
|
||||
66
resources/editors/style.css
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
:root {
|
||||
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
#controls button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#controls button.active {
|
||||
color: blue;
|
||||
}
|
||||
196
resources/editors/tiptap.html
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>TipTap Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<script type="module">
|
||||
import { text as shorttext } from "./shorttext.js";
|
||||
import { text as longtext } from "./longtext.js";
|
||||
import editor from "./tiptap.js";
|
||||
|
||||
let editorContainer = document.querySelector("#editor");
|
||||
let editorInstance = null;
|
||||
|
||||
let buttons = {
|
||||
create: document.querySelector("#create"),
|
||||
highlight: document.querySelector("#highlight"),
|
||||
unhighlight: document.querySelector("#unhighlight"),
|
||||
long: document.querySelector("#long"),
|
||||
short: document.querySelector("#short"),
|
||||
scroll: document.querySelector("#scroll"),
|
||||
layout: document.querySelector("#layout"),
|
||||
};
|
||||
buttons.scroll.addEventListener("click", scroll);
|
||||
buttons.highlight.addEventListener("click", highlight);
|
||||
buttons.unhighlight.addEventListener("click", unhighlight);
|
||||
buttons.long.addEventListener("click", long);
|
||||
buttons.short.addEventListener("click", short);
|
||||
buttons.layout.addEventListener("click", layout);
|
||||
|
||||
buttons.create.addEventListener("click", (e) => {
|
||||
if (!editorInstance) {
|
||||
editorInstance = editor(editorContainer);
|
||||
editorInstance.ready.then(() => {
|
||||
buttons.unhighlight.classList.add("active", "true");
|
||||
buttons.create.setAttribute("disabled", "true");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function layout() {
|
||||
// Todo - is this necessary with the runner?
|
||||
const body = document.body.getBoundingClientRect();
|
||||
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
|
||||
}
|
||||
|
||||
function highlight() {
|
||||
buttons.unhighlight.classList.toggle("active", false);
|
||||
buttons.highlight.classList.toggle("active", true);
|
||||
editorInstance.format(true);
|
||||
}
|
||||
function unhighlight() {
|
||||
buttons.unhighlight.classList.toggle("active", true);
|
||||
buttons.highlight.classList.toggle("active", false);
|
||||
editorInstance.format(false);
|
||||
}
|
||||
function long() {
|
||||
buttons.short.classList.toggle("active", false);
|
||||
buttons.long.classList.toggle("active", true);
|
||||
editorInstance.setValue(longtext);
|
||||
}
|
||||
function short() {
|
||||
buttons.short.classList.toggle("active", true);
|
||||
buttons.long.classList.toggle("active", false);
|
||||
editorInstance.setValue(shorttext);
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
let isTop = editorInstance.getScrollTop() == 0;
|
||||
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
42
resources/editors/tiptap.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// https://tiptap.dev/examples/default
|
||||
import { Editor } from "@tiptap/core";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
|
||||
export default function (element, value) {
|
||||
let editor = new Editor({
|
||||
element,
|
||||
extensions: [StarterKit],
|
||||
content: value,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
editor,
|
||||
// Anything before this promise resolves will happen before timing starts
|
||||
ready: Promise.resolve(),
|
||||
getScrollHeight() {
|
||||
return element.scrollHeight;
|
||||
},
|
||||
getScrollTop() {
|
||||
return element.scrollTop;
|
||||
},
|
||||
setScrollTop(value) {
|
||||
element.scrollTop = value;
|
||||
},
|
||||
setValue(value) {
|
||||
// Recommendation is to call focus before most commands
|
||||
// https://tiptap.dev/api/commands#chain-commands
|
||||
editor.chain().focus().setContent(value).setTextSelection(0).run();
|
||||
element.scrollTop = 0;
|
||||
},
|
||||
format(on) {
|
||||
if (on)
|
||||
editor.chain().focus().selectAll().setBold().setTextSelection(0).run();
|
||||
else
|
||||
editor.chain().focus().selectAll().unsetBold().setTextSelection(0).run();
|
||||
},
|
||||
};
|
||||
}
|
||||
17
resources/editors/vite.config.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
base: "./", // Since this will be loaded from the project root
|
||||
build: {
|
||||
modulePreload: { polyfill: false },
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
codemirror: resolve(__dirname, "codemirror.html"),
|
||||
main: resolve(__dirname, "index.html"),
|
||||
tiptap: resolve(__dirname, "tiptap.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
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 |
213
resources/interactive.mjs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { BenchmarkRunner } from "./benchmark-runner.mjs";
|
||||
import { params } from "./params.mjs";
|
||||
import { Suites } from "./tests.mjs";
|
||||
|
||||
class InteractiveBenchmarkRunner extends BenchmarkRunner {
|
||||
_stepPromise = undefined;
|
||||
_stepPromiseResolve = undefined;
|
||||
_isRunning = false;
|
||||
_isStepping = false;
|
||||
|
||||
constructor(suites, iterationCount) {
|
||||
super(suites);
|
||||
this._client = this._createClient();
|
||||
if (!Number.isInteger(iterationCount) || iterationCount <= 0)
|
||||
throw Error("iterationCount must be a positive integer.");
|
||||
this._iterationCount = iterationCount;
|
||||
}
|
||||
|
||||
_createClient() {
|
||||
return {
|
||||
willStartFirstIteration: this._start.bind(this),
|
||||
willRunTest: this._testStart.bind(this),
|
||||
didRunTest: this._testDone.bind(this),
|
||||
didRunSuites: this._iterationDone.bind(this),
|
||||
didFinishLastIteration: this._done.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
_start() {
|
||||
if (this._isRunning)
|
||||
throw Error("Runner was not stopped before starting;");
|
||||
this._isRunning = true;
|
||||
if (this._isStepping)
|
||||
this._stepPromise = this._newStepPromise();
|
||||
}
|
||||
|
||||
_step() {
|
||||
if (!this._stepPromise) {
|
||||
// Allow switching to stepping mid-run.
|
||||
this._stepPromise = this._newStepPromise();
|
||||
} else {
|
||||
const resolve = this._stepPromiseResolve;
|
||||
this._stepPromise = this._newStepPromise();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
_newStepPromise() {
|
||||
return new Promise((resolve) => {
|
||||
this._stepPromiseResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_testStart(suite, test) {
|
||||
test.anchor.classList.add("running");
|
||||
}
|
||||
|
||||
async _testDone(suite, test) {
|
||||
const classList = test.anchor.classList;
|
||||
classList.remove("running");
|
||||
classList.add("ran");
|
||||
if (this._isStepping)
|
||||
await this._stepPromise;
|
||||
}
|
||||
|
||||
_iterationDone(measuredValues) {
|
||||
let results = "";
|
||||
for (const suiteName in measuredValues.tests) {
|
||||
let suiteResults = measuredValues.tests[suiteName];
|
||||
for (const testName in suiteResults.tests) {
|
||||
let testResults = suiteResults.tests[testName];
|
||||
for (const subtestName in testResults.tests)
|
||||
results += `${suiteName} : ${testName} : ${subtestName}: ${testResults.tests[subtestName]} ms\n`;
|
||||
}
|
||||
results += `${suiteName} : ${suiteResults.total} ms\n`;
|
||||
}
|
||||
results += `Arithmetic Mean : ${measuredValues.mean} ms\n`;
|
||||
results += `Geometric Mean : ${measuredValues.geomean} ms\n`;
|
||||
results += `Total : ${measuredValues.total} ms\n`;
|
||||
results += `Score : ${measuredValues.score} rpm\n`;
|
||||
|
||||
if (!results)
|
||||
return;
|
||||
|
||||
const pre = document.createElement("pre");
|
||||
document.body.appendChild(pre);
|
||||
pre.textContent = results;
|
||||
}
|
||||
|
||||
_done() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
runStep() {
|
||||
this._isStepping = true;
|
||||
if (!this._isRunning)
|
||||
this.runMultipleIterations(this._iterationCount);
|
||||
else
|
||||
this._step();
|
||||
}
|
||||
|
||||
runSuites() {
|
||||
if (this._isRunning) {
|
||||
if (this._isStepping) {
|
||||
// Switch to continuous running only if we've been stepping.
|
||||
this._isStepping = false;
|
||||
this._step();
|
||||
}
|
||||
} else {
|
||||
this._isStepping = false;
|
||||
this.runMultipleIterations(this._iterationCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose Suites/BenchmarkRunner for backwards compatibility
|
||||
globalThis.BenchmarkRunner = InteractiveBenchmarkRunner;
|
||||
|
||||
function formatTestName(suiteName, testName) {
|
||||
return suiteName + (testName ? `/${testName}` : "");
|
||||
}
|
||||
|
||||
function createUIForSuites(suites, onStep, onRunSuites) {
|
||||
const control = document.createElement("nav");
|
||||
const ol = document.createElement("ol");
|
||||
const checkboxes = [];
|
||||
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||
const suite = suites[suiteIndex];
|
||||
const li = document.createElement("li");
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.id = suite.name;
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = !suite.disabled;
|
||||
checkbox.onchange = () => {
|
||||
suite.disabled = !checkbox.checked;
|
||||
};
|
||||
checkbox.onchange();
|
||||
checkboxes.push(checkbox);
|
||||
|
||||
li.appendChild(checkbox);
|
||||
var label = document.createElement("label");
|
||||
label.appendChild(document.createTextNode(formatTestName(suite.name)));
|
||||
li.appendChild(label);
|
||||
label.htmlFor = checkbox.id;
|
||||
|
||||
const testList = document.createElement("ol");
|
||||
for (let testIndex = 0; testIndex < suite.tests.length; testIndex++) {
|
||||
const testItem = document.createElement("li");
|
||||
const test = suite.tests[testIndex];
|
||||
const anchor = document.createElement("a");
|
||||
anchor.id = `${suite.name}-${test.name}`;
|
||||
test.anchor = anchor;
|
||||
anchor.appendChild(document.createTextNode(formatTestName(suite.name, test.name)));
|
||||
testItem.appendChild(anchor);
|
||||
testList.appendChild(testItem);
|
||||
}
|
||||
li.appendChild(testList);
|
||||
|
||||
ol.appendChild(li);
|
||||
}
|
||||
|
||||
control.appendChild(ol);
|
||||
|
||||
let button = document.createElement("button");
|
||||
button.textContent = "Step";
|
||||
button.onclick = onStep;
|
||||
control.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Run";
|
||||
button.id = "runSuites";
|
||||
button.onclick = onRunSuites;
|
||||
control.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Select all";
|
||||
button.onclick = () => {
|
||||
for (var suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||
suites[suiteIndex].disabled = false;
|
||||
checkboxes[suiteIndex].checked = true;
|
||||
}
|
||||
};
|
||||
control.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Unselect all";
|
||||
button.onclick = () => {
|
||||
for (var suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||
suites[suiteIndex].disabled = true;
|
||||
checkboxes[suiteIndex].checked = false;
|
||||
}
|
||||
};
|
||||
control.appendChild(button);
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
function startTest() {
|
||||
if (params.suites.length > 0 || params.tags.length > 0)
|
||||
Suites.enable(params.suites, params.tags);
|
||||
|
||||
const interactiveRunner = new window.BenchmarkRunner(Suites, params.iterationCount);
|
||||
if (!(interactiveRunner instanceof InteractiveBenchmarkRunner))
|
||||
throw Error("window.BenchmarkRunner must be a subclass of InteractiveBenchmarkRunner");
|
||||
|
||||
// Don't call step while step is already executing.
|
||||
document.body.appendChild(createUIForSuites(Suites, interactiveRunner.runStep.bind(interactiveRunner), interactiveRunner.runSuites.bind(interactiveRunner)));
|
||||
|
||||
if (params.startAutomatically)
|
||||
document.getElementById("runSuites").click();
|
||||
}
|
||||
|
||||
window.addEventListener("load", startTest);
|
||||
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 |
856
resources/main.css
Normal file
|
|
@ -0,0 +1,856 @@
|
|||
:root {
|
||||
--viewport-width: 800px;
|
||||
--viewport-height: 600px;
|
||||
--foreground: rgb(235, 235, 235);
|
||||
--foreground-alpha: rgba(235, 235, 235, 0.2);
|
||||
--inactive-color: rgb(128, 128, 128);
|
||||
--background: rgb(46, 51, 55);
|
||||
--running-background: #f5f5f5;
|
||||
--highlight: rgb(232, 79, 79);
|
||||
--text-width: 650px;
|
||||
--metrics-line-height: 25px;
|
||||
--scrollbar-width: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--background);
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-track,
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: var(--background);
|
||||
}
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--inactive-color);
|
||||
border-radius: 8px;
|
||||
width: var(--scrollbar-width);
|
||||
height: var(--scrollbar-width);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--foreground);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
thead {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
h1,
|
||||
button,
|
||||
.button {
|
||||
font-family: "Futura-Medium", Futura, "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Menlo, Monaco, monospace;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid var(--foreground);
|
||||
width: 50%;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
main {
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
left: -70px;
|
||||
top: 155px;
|
||||
width: 75px;
|
||||
height: 406px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.version {
|
||||
transform: rotate(-90deg);
|
||||
font-size: 65px;
|
||||
font-family: Futura-Medium, Futura, Roboto, "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
font-style: italic;
|
||||
letter-spacing: -0.05em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -105px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 30px;
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
color: var(--foreground);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 10px 0 6px 0;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 21px;
|
||||
}
|
||||
|
||||
li + ul > li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.compact li {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
li + ul {
|
||||
margin-bottom: 21px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section#home > .buttons {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.buttons button,
|
||||
.buttons .button {
|
||||
appearance: none;
|
||||
border: 3px solid var(--foreground);
|
||||
border-radius: 10px;
|
||||
min-width: 200px;
|
||||
padding: 5px 0px;
|
||||
margin: 0 20px;
|
||||
font-size: 25px;
|
||||
color: var(--foreground);
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.buttons button:active {
|
||||
background-color: var(--foreground);
|
||||
color: var(--background);
|
||||
border-color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
.buttons button:focus {
|
||||
outline: none;
|
||||
border-color: var(--highlight);
|
||||
}
|
||||
|
||||
.developer-mode {
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
background: #602525;
|
||||
border: 3px solid rgba(255, 255, 255, 0.5);
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.developer-mode summary {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
margin: -1rem;
|
||||
}
|
||||
|
||||
.developer-mode-content {
|
||||
flex: 1;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
margin-right: calc(0px - var(--scrollbar-width));
|
||||
padding-right: var(--scrollbar-width);
|
||||
}
|
||||
|
||||
.developer-mode-content ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1em 0 0.5em 0;
|
||||
}
|
||||
.developer-mode-content .button-bar {
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.developer-mode-content .settings label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.developer-mode-content .settings label * {
|
||||
flex: 1;
|
||||
}
|
||||
.developer-mode-content .settings input[type="checkbox"] {
|
||||
flex: 0;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.developer-mode-content .settings .range-label-data {
|
||||
flex: 0;
|
||||
min-width: 5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.developer-mode-content li + li {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.developer-mode-content button {
|
||||
background: white;
|
||||
flex: auto;
|
||||
padding: 4px;
|
||||
appearance: button;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.developer-mode-content .tag {
|
||||
border-radius: 100vh;
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.developer-mode-content hr {
|
||||
width: initial;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
section {
|
||||
display: none;
|
||||
--padding-width: 15px;
|
||||
--border-width: 6px;
|
||||
position: absolute;
|
||||
width: var(--viewport-width);
|
||||
height: var(--viewport-height);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: calc(var(--viewport-height) / -2 - var(--padding-width) - var(--border-width));
|
||||
margin-left: calc(var(--viewport-width) / -2 - var(--padding-width) - var(--border-width));
|
||||
padding: var(--padding-width);
|
||||
border: var(--border-width) solid var(--foreground);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
section.visible,
|
||||
:root[data-visible-section="home"] #home,
|
||||
:root[data-visible-section="running"] #running,
|
||||
:root[data-visible-section="summary"] #summary,
|
||||
:root[data-visible-section="details"] #details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
section > p {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
#testContainer {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
width: var(--viewport-width);
|
||||
height: var(--viewport-height);
|
||||
}
|
||||
|
||||
#leakedIframes {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section#home p {
|
||||
margin: 0 auto;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section#home .content {
|
||||
margin-top: 160px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.show-about {
|
||||
margin-top: 100px;
|
||||
font-size: 16px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#screen-size-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress {
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
height: 6px;
|
||||
border-left: 6px solid var(--background);
|
||||
border-right: 6px solid var(--background);
|
||||
}
|
||||
|
||||
#progress-completed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background-color: var(--inactive-color);
|
||||
}
|
||||
|
||||
#progress-completed::-webkit-progress-value {
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
#progress-completed::-moz-progress-bar {
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
#progress .iteration-marker {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
#info {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
height: 12px;
|
||||
color: var(--inactive-color);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
#info-label {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
}
|
||||
#info-progress {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
iframe.test-runner {
|
||||
background: var(--running-background);
|
||||
}
|
||||
|
||||
section#summary > #result-number,
|
||||
section#summary > #confidence-number {
|
||||
font-family: "Futura-CondensedMedium", Futura, "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
section#summary > #result-number {
|
||||
text-align: center;
|
||||
font-size: 145px;
|
||||
line-height: 145px;
|
||||
}
|
||||
|
||||
section#summary > #confidence-number {
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
line-height: 36px;
|
||||
color: var(--inactive-color);
|
||||
}
|
||||
|
||||
section#summary.invalid h1,
|
||||
section#summary.invalid .gauge,
|
||||
section#summary.invalid hr {
|
||||
opacity: 0.2;
|
||||
}
|
||||
section#summary.invalid #result-number {
|
||||
color: var(--highlight);
|
||||
}
|
||||
section#summary.invalid .buttons {
|
||||
display: none;
|
||||
}
|
||||
#invalid-score-text {
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
section#summary.invalid #invalid-score-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
section#details {
|
||||
--viewport-height: max(600px, 90vh);
|
||||
--viewport-width: max(800px, 80vw);
|
||||
}
|
||||
|
||||
section#details .results-table {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
section#details .export-buttons {
|
||||
justify-content: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
section#details .export-buttons button,
|
||||
section#details .export-buttons .button {
|
||||
padding: 5px 10px;
|
||||
min-width: auto;
|
||||
font-size: 14px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean {
|
||||
clear: both;
|
||||
padding-top: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean > label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
section#details button.show-about {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
section#details h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section#about .section-content > *,
|
||||
section#instructions .section-content > * {
|
||||
width: var(--text-width);
|
||||
padding-left: calc((var(--viewport-width) - var(--text-width)) / 2);
|
||||
}
|
||||
|
||||
:root[data-visible-section="details"] section#details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section#details .all-metric-results {
|
||||
flex: auto;
|
||||
overflow-y: auto;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean {
|
||||
clear: both;
|
||||
padding-top: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean > label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
section#details h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section#details .metric {
|
||||
margin: 0px 0 10px 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
section#details .metric dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.submetrics h3 {
|
||||
font-size: medium;
|
||||
}
|
||||
.metrics-table thead .prefix {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metrics-table thead th {
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
border-bottom: 1px var(--foreground) solid;
|
||||
padding: 0 4px 3px 0px;
|
||||
}
|
||||
.metrics-table thead th + th {
|
||||
padding: 0 4px 3px 4px;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table .number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: inline;
|
||||
padding-left: 10px;
|
||||
vertical-align: top;
|
||||
margin-top: 0;
|
||||
line-height: var(--metrics-line-height);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.chart-legend td {
|
||||
padding: 0 5px 0 0;
|
||||
max-width: calc(var(--viewport-width) * 0.22);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
section#details h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section#about h1,
|
||||
section#instructions h1 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0px;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
section#about .note {
|
||||
color: var(--inactive-color);
|
||||
}
|
||||
|
||||
.section-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"content"
|
||||
"footer";
|
||||
}
|
||||
|
||||
.section-header {
|
||||
grid-area: header;
|
||||
}
|
||||
.section-content {
|
||||
grid-area: content;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.section-footer {
|
||||
grid-area: footer;
|
||||
}
|
||||
|
||||
.gauge {
|
||||
position: relative;
|
||||
width: 738px;
|
||||
height: 78px;
|
||||
background-image: -webkit-image-set(url("gauge@2x.png") 2x, url("gauge.png") 1x);
|
||||
background-image: image-set(url("gauge@2x.png") 2x, url("gauge.png") 1x);
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.gauge > .window {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 33px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gauge > .window > .needle {
|
||||
position: absolute;
|
||||
left: 363px;
|
||||
bottom: -88px;
|
||||
width: 4px;
|
||||
height: 400px;
|
||||
background-color: rgb(247, 148, 29);
|
||||
transform: rotate(-70deg);
|
||||
transform-origin: 2px 400px;
|
||||
}
|
||||
|
||||
.all-metric-results .submetrics {
|
||||
display: none;
|
||||
margin-left: 35px;
|
||||
}
|
||||
.all-metric-results .submetrics.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.metric-chart {
|
||||
display: grid;
|
||||
grid-template-columns: calc((var(--viewport-width) - 120px) / 2) 1fr;
|
||||
}
|
||||
|
||||
.metric-chart-relative {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.relative-charts .metric-chart-absolute {
|
||||
display: none;
|
||||
}
|
||||
.relative-charts .metric-chart-relative {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.details-toggle {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-left: -4px;
|
||||
}
|
||||
.details-toggle input {
|
||||
display: inline-block;
|
||||
appearance: none;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
.details-toggle input:after {
|
||||
content: "▶";
|
||||
color: var(--foreground);
|
||||
display: inline-block;
|
||||
transform-origin: 50%;
|
||||
position: relative;
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
.details-toggle input:checked:after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Charts (lib/charts.js) */
|
||||
.bar-chart text,
|
||||
.scatter-plot text {
|
||||
font-size: 11px;
|
||||
fill: var(--foreground);
|
||||
dominant-baseline: hanging;
|
||||
}
|
||||
.bar-chart .axis,
|
||||
.scatter-plot .axis {
|
||||
stroke: #aaa;
|
||||
}
|
||||
.bar-chart .minMax {
|
||||
stroke: var(--foreground);
|
||||
stroke-width: 1;
|
||||
stroke-opacity: 0.5;
|
||||
stroke-dasharray: 2;
|
||||
}
|
||||
.bar-chart .mean {
|
||||
stroke: var(--foreground);
|
||||
stroke-width: 2;
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
.bar-chart .mean:hover {
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
.bar-chart g.bar {
|
||||
fill: #8ad;
|
||||
}
|
||||
.bar-chart g.bar.large text {
|
||||
display: none;
|
||||
}
|
||||
.bar-chart g.bar:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.bar-chart g.bar text {
|
||||
fill: #ddd;
|
||||
text-anchor: middle;
|
||||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.bar-chart g.bar:hover text {
|
||||
display: block;
|
||||
fill: #fff;
|
||||
text-shadow: 0 0 5px #000;
|
||||
}
|
||||
.bar-chart .label {
|
||||
text-anchor: end;
|
||||
dominant-baseline: middle;
|
||||
}
|
||||
|
||||
.scatter-plot .marker {
|
||||
stroke-width: 0px;
|
||||
opacity: 0.8;
|
||||
fill: currentColor;
|
||||
cursor: crosshair;
|
||||
}
|
||||
.scatter-plot .marker:hover {
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 2px currentColor);
|
||||
}
|
||||
.scatter-plot .percentile {
|
||||
fill: currentColor;
|
||||
fill-opacity: 0.3;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
.scatter-plot .percentile line {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.scatter-plot .percentile:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Chart colors */
|
||||
.chart .blue {
|
||||
color: #5b9aff;
|
||||
stroke: #5b9aff;
|
||||
}
|
||||
|
||||
.chart .blue-light {
|
||||
color: #5bd6ff;
|
||||
stroke: #5bd6ff;
|
||||
}
|
||||
|
||||
.chart .green-light {
|
||||
color: #73d147;
|
||||
stroke: #73d147;
|
||||
}
|
||||
|
||||
.chart .green {
|
||||
color: #94bc4b;
|
||||
stroke: #94bc4b;
|
||||
}
|
||||
|
||||
.chart .yellow {
|
||||
color: #ded300;
|
||||
stroke: #ded300;
|
||||
}
|
||||
|
||||
.chart .orange {
|
||||
color: #fe8f06;
|
||||
stroke: #fe8f06;
|
||||
}
|
||||
|
||||
.chart .red {
|
||||
color: #de4040;
|
||||
stroke: #de4040;
|
||||
}
|
||||
|
||||
.chart .magenta {
|
||||
color: #cd1e90;
|
||||
stroke: #cd1e90;
|
||||
}
|
||||
|
||||
.chart .violet {
|
||||
color: #a41ecd;
|
||||
stroke: #a41ecd;
|
||||
}
|
||||
|
||||
.chart .purple {
|
||||
color: #7b3eff;
|
||||
stroke: #7b3eff;
|
||||
}
|
||||
|
||||
.chart .blue-dark {
|
||||
color: #2d4ef7;
|
||||
stroke: #2d4ef7;
|
||||
}
|
||||
|
||||
.chart .green-dark {
|
||||
color: #349a58;
|
||||
stroke: #349a58;
|
||||
}
|
||||
|
||||
.chart .ochre {
|
||||
color: #cc9900;
|
||||
stroke: #cc9900;
|
||||
}
|
||||
|
||||
.chart .rust {
|
||||
color: #cc5200;
|
||||
stroke: #cc5200;
|
||||
}
|
||||
|
||||
.chart .white {
|
||||
color: #fff;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
|
||||
.embedded section {
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.embedded #logo,
|
||||
.embedded #progress,
|
||||
.embedded #info,
|
||||
.embedded #info-progress,
|
||||
.embedded #info-label {
|
||||
display: none !important;
|
||||
}
|
||||
415
resources/main.mjs
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
import { BenchmarkRunner, SubdomainBenchmarkRunner, EmbeddedBenchmarkRunner } from "./benchmark-runner.mjs";
|
||||
import * as Statistics from "./statistics.mjs";
|
||||
import { Suites } from "./tests.mjs";
|
||||
import { renderMetricView } from "./metric-ui.mjs";
|
||||
import { params } from "./params.mjs";
|
||||
import { createDeveloperModeContainer } from "./developer-mode.mjs";
|
||||
|
||||
// FIXME(camillobruni): Add base class
|
||||
class MainBenchmarkClient {
|
||||
developerMode = false;
|
||||
stepCount = null;
|
||||
suitesCount = null;
|
||||
_measuredValuesList = [];
|
||||
_finishedTestCount = 0;
|
||||
_totalSubtestsCount = 0;
|
||||
_progressCompleted = null;
|
||||
_isRunning = false;
|
||||
_hasResults = false;
|
||||
_developerModeContainer = null;
|
||||
_metrics = Object.create(null);
|
||||
_runner = null;
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("DOMContentLoaded", () => this.prepareUI());
|
||||
this._showSection(window.location.hash);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this._startBenchmark())
|
||||
this._showSection("#running");
|
||||
}
|
||||
|
||||
_startBenchmark() {
|
||||
if (this._isRunning)
|
||||
return false;
|
||||
|
||||
if (Suites.every((suite) => suite.disabled)) {
|
||||
const message = `No suites selected - "${params.suites}" does not exist.`;
|
||||
alert(message);
|
||||
console.error(
|
||||
message,
|
||||
params.suites,
|
||||
"\nValid values:",
|
||||
Suites.map((each) => each.name)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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._isRunning = true;
|
||||
|
||||
const enabledSuites = Suites.filter((suite) => !suite.disabled);
|
||||
this._totalSubtestsCount = enabledSuites.reduce((testsCount, suite) => {
|
||||
return testsCount + suite.tests.length;
|
||||
}, 0);
|
||||
this.stepCount = params.iterationCount * this._totalSubtestsCount;
|
||||
this._progressCompleted.max = this.stepCount;
|
||||
this.suitesCount = enabledSuites.length;
|
||||
if (params.embedded)
|
||||
this._runner = new EmbeddedBenchmarkRunner(Suites, this);
|
||||
else if (params.domainPerIteration)
|
||||
this._runner = new SubdomainBenchmarkRunner(Suites, this);
|
||||
else
|
||||
this._runner = new BenchmarkRunner(Suites, this);
|
||||
this._runner.runMultipleIterations(params.iterationCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
get metrics() {
|
||||
return this._metrics;
|
||||
}
|
||||
|
||||
willAddTestFrame(frame) {
|
||||
}
|
||||
|
||||
willRunTest(suite, test) {
|
||||
document.getElementById("info-label").textContent = suite.name;
|
||||
document.getElementById("info-progress").textContent = `${this._finishedTestCount} / ${this.stepCount}`;
|
||||
}
|
||||
|
||||
didRunTest() {
|
||||
this._finishedTestCount++;
|
||||
this._progressCompleted.value = this._finishedTestCount;
|
||||
}
|
||||
|
||||
didRunSuites(measuredValues) {
|
||||
this._measuredValuesList.push(measuredValues);
|
||||
}
|
||||
|
||||
willStartFirstIteration() {
|
||||
this._measuredValuesList = [];
|
||||
this._finishedTestCount = params.currentIteration * this._totalSubtestsCount;
|
||||
}
|
||||
|
||||
didFinishLastIteration(metrics) {
|
||||
console.assert(this._isRunning);
|
||||
this._isRunning = false;
|
||||
this._hasResults = true;
|
||||
this._metrics = metrics;
|
||||
// Don't display the results if the runnier is embedded.
|
||||
if (params.embedded)
|
||||
return;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
console.assert(this._isRunning);
|
||||
this._isRunning = false;
|
||||
this._hasResults = true;
|
||||
this._metrics = Object.create(null);
|
||||
this._populateInvalidScore();
|
||||
this.showResultsSummary();
|
||||
}
|
||||
|
||||
_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() {
|
||||
document.getElementById("summary").className = "invalid";
|
||||
document.getElementById("result-number").textContent = "Error";
|
||||
document.getElementById("confidence-number").textContent = "";
|
||||
}
|
||||
|
||||
_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) {
|
||||
const trackHeight = 24;
|
||||
document.documentElement.style.setProperty("--metrics-line-height", `${trackHeight}px`);
|
||||
const plotWidth = (params.viewport.width - 120) / 2;
|
||||
const singleToplevelMetrics = Object.values(metrics).filter((each) => each && each.unit === "ms" && !each.parent && !each.children.length);
|
||||
document.getElementById("toplevel-chart").innerHTML = renderMetricView({
|
||||
metrics: singleToplevelMetrics,
|
||||
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`);
|
||||
}
|
||||
|
||||
prepareUI() {
|
||||
if (params.embedded)
|
||||
document.body.className = "embedded";
|
||||
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);
|
||||
});
|
||||
|
||||
if (params.suites.length > 0 || params.tags.length > 0)
|
||||
Suites.enable(params.suites, params.tags);
|
||||
|
||||
if (params.developerMode) {
|
||||
this._developerModeContainer = createDeveloperModeContainer(Suites);
|
||||
document.body.append(this._developerModeContainer);
|
||||
}
|
||||
|
||||
if (params.startAutomatically)
|
||||
this.start();
|
||||
}
|
||||
|
||||
_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._hasResults) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
resources/metric.mjs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import * as Statistics from "./statistics.mjs";
|
||||
|
||||
export const MILLISECONDS_PER_MINUTE = 60 * 1000;
|
||||
|
||||
export class Metric {
|
||||
static separator = "/";
|
||||
|
||||
constructor(name, unit = "ms", values = []) {
|
||||
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 = 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, {
|
||||
children: {
|
||||
writable: true,
|
||||
value: [],
|
||||
},
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
resources/newssite/news-next/.eslintrc.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "plugin:@next/next/recommended",
|
||||
"settings": {
|
||||
"next": {
|
||||
"rootDir": "resources/newssite/news-next/"
|
||||
}
|
||||
}
|
||||
}
|
||||
38
resources/newssite/news-next/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
/dist/cache
|
||||
/dist/server
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
38
resources/newssite/news-next/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# The Daily Broadcast
|
||||
|
||||
> **_NOTE:_** This is not a typical use-case for Next.js and we encourage developers to follow the [official documentation](https://vercel.com/docs) for recommended usage of the framework.
|
||||
|
||||
This app is a news-site built with [Next.js](https://nextjs.org/). It utilizes the [News Site Template](https://github.com/flashdesignory/news-site-template) as the basis for styling and functionality.
|
||||
Since Speedometer expects static files for all apps included, this project's build step uses [static html export](https://nextjs.org/docs/pages/building-your-application/deploying/static-exports).
|
||||
<br>With this implementation, some features of Next.js are not available and therefore omitted to ensure compatibility with Speedometer.
|
||||
|
||||
## Local Development
|
||||
|
||||
Start the local dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Deployment of changes
|
||||
|
||||
To ensure Next.js lint rules are followed, run:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
To create new build files, run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Add, commit and push changes to the working branch.
|
||||
|
||||
## Test steps
|
||||
|
||||
The Speedometer test consists of navigating between the different pages of the news site.
|
||||
It includes interactions with the navigation drop-down menu to ensure state changes happen in between the page navigations.
|
||||
1
resources/newssite/news-next/dist/404.html
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/pages/_error-54de1933a164a1ff.js" defer=""></script><script src="./_next/static/5v6xiLKzLrBzD4hPqXwOj/_buildManifest.js" defer=""></script><script src="./_next/static/5v6xiLKzLrBzD4hPqXwOj/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"5v6xiLKzLrBzD4hPqXwOj","assetPrefix":".","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
|
||||
1
resources/newssite/news-next/dist/_next/static/5v6xiLKzLrBzD4hPqXwOj/_buildManifest.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/743-fd706aeabb7828e3.js","static/css/2cf5163b53bb0adb.css","static/chunks/pages/index-61e648f02ae686f6.js"],"/_error":["static/chunks/pages/_error-54de1933a164a1ff.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|
||||
1
resources/newssite/news-next/dist/_next/static/5v6xiLKzLrBzD4hPqXwOj/_ssgManifest.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
self.__SSG_MANIFEST=new Set,self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB();
|
||||
32
resources/newssite/news-next/dist/_next/static/chunks/743-fd706aeabb7828e3.js
vendored
Normal file
33
resources/newssite/news-next/dist/_next/static/chunks/framework-2c79e2a64abdb08b.js
vendored
Normal file
1
resources/newssite/news-next/dist/_next/static/chunks/main-2ba37e62325cc71b.js
vendored
Normal file
1
resources/newssite/news-next/dist/_next/static/chunks/pages/_app-77983e68be50f72a.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{1118:function(n,e,i){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return i(8375)}])},8375:function(n,e,i){"use strict";i.r(e);var t=i(5893);i(6026),i(858),i(6669),i(3454),i(9917);var u=i(7294),o=i(4298),a=i.n(o);e.default=function(n){let{Component:e,pageProps:i}=n,[o,r]=(0,u.useState)(!1);return(0,u.useEffect)(()=>r(!0),[]),o?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(a(),{id:"raf-mock",children:"// This hack allows to capture the work normally happening in a rAF. We\n// may be able to remove it if the runner improves.\nwindow.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);\nwindow.cancelAnimationFrame = window.clearTimeout;\n// Disable requestIdleCallback until WebKit / Safari supports it.\nwindow.requestIdleCallback = undefined;\nwindow.cancelIdleCallback = undefined;"}),(0,t.jsx)(e,{...i})]}):null}},6669:function(){},858:function(){},3454:function(){},9917:function(){},6026:function(){},4298:function(n,e,i){n.exports=i(5442)}},function(n){var e=function(e){return n(n.s=e)};n.O(0,[774,179],function(){return e(1118),e(6885)}),_N_E=n.O()}]);
|
||||
1
resources/newssite/news-next/dist/_next/static/chunks/pages/_error-54de1933a164a1ff.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(3499)}])}},function(n){n.O(0,[774,888,179],function(){return n(n.s=1981)}),_N_E=n.O()}]);
|
||||
1
resources/newssite/news-next/dist/_next/static/chunks/pages/index-61e648f02ae686f6.js
vendored
Normal file
1
resources/newssite/news-next/dist/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js
vendored
Normal file
1
resources/newssite/news-next/dist/_next/static/chunks/webpack-e50e9853db18b759.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
!function(){"use strict";var e,n,r,t,o={},u={};function i(e){var n=u[e];if(void 0!==n)return n.exports;var r=u[e]={exports:{}},t=!0;try{o[e](r,r.exports,i),t=!1}finally{t&&delete u[e]}return r.exports}i.m=o,e=[],i.O=function(n,r,t,o){if(r){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[r,t,o];return}for(var f=1/0,u=0;u<e.length;u++){for(var r=e[u][0],t=e[u][1],o=e[u][2],l=!0,c=0;c<r.length;c++)f>=o&&Object.keys(i.O).every(function(e){return i.O[e](r[c])})?r.splice(c--,1):(l=!1,o<f&&(f=o));if(l){e.splice(u--,1);var a=t();void 0!==a&&(n=a)}}return n},i.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(n,{a:n}),n},i.d=function(e,n){for(var r in n)i.o(n,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},i.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.p=".//_next/",n={272:0},i.O.j=function(e){return 0===n[e]},r=function(e,r){var t,o,u=r[0],f=r[1],l=r[2],c=0;if(u.some(function(e){return 0!==n[e]})){for(t in f)i.o(f,t)&&(i.m[t]=f[t]);if(l)var a=l(i)}for(e&&e(r);c<u.length;c++)o=u[c],i.o(n,o)&&n[o]&&n[o][0](),n[o]=0;return i.O(a)},(t=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))}();
|
||||