Compare commits

...

373 commits

Author SHA1 Message Date
Camillo Bruni
86710fec5e
Fix DeveloperMenu z-index (#578)
The button unification PR accidentally raised the button bar above the developer menu.
Adjusting the z-index fixes this.
2026-07-23 17:38:29 +02:00
Camillo Bruni
8682ae07c4
Improve MainBenchmarkClient with state and better error handling (#573)
- Add explicit BENCHMARK_STATE as preparation for ServiceWorker-based preloading
- Use new state to early-abort runs if there are toplevel errors
- Add explicit top-level error and unhandledrejection handlers to surfce errors better
2026-07-22 11:27:47 +02:00
Camillo Bruni
b4b6b1d5dc
Fix total metric in summary view (#576)
In a previous refactoring we accidentally broke the summary page.
- Accumulate overall total proprely
- Fix broken destructuring for total and prepare time
2026-07-22 11:27:22 +02:00
Camillo Bruni
c831667c22
Fix primary button size (#575)
A previous PR caused some accidental CSS rule mismatch which caused the main start button to be small.
2026-07-21 17:34:29 +02:00
Camillo Bruni
6713c52fbd
Add --cache option for npm run dev server (#574)
- --cache without args uses cache-control 3600 header
- --cache=DURATION for explcit cache duration
- use --cache for test server to speed up runs a bit
2026-07-21 17:33:15 +02:00
Camillo Bruni
663dc26cb8
Use optional chaining to call BenchmarkClient hooks (#572)
This way we can avoid a few duplicate property loads in the runner.
2026-07-21 15:49:07 +02:00
Camillo Bruni
e258bb1661
Update landing page buttons (#571)
Use the same button style for home, about, instructions and details page.
2026-07-21 15:48:32 +02:00
Camillo Bruni
9c07200b65
Add more unittests and package.json validation (#570)
- Add package validation (and fix versions identifiers in package.json files)
- Add metrics unittests
- Add basic translations data unittest
2026-07-16 15:06:34 +02:00
Camillo Bruni
779ca6ffb7
Add workload link to developer menu (#569)
This makes it easy to navigate to the raw workload for debugging and investigation.
2026-07-16 13:16:06 +02:00
Camillo Bruni
8344a61b3e
Replace report callback with async await (#566)
We have asyncyfied almost the complete runner by now and no longer need the report callback.
This makes it a bit easer to follow the code path.

Drive-by-fix:
- Remove needless constructor
- Add more tests to cover the schedulers
2026-07-16 11:26:59 +02:00
Camillo Bruni
59832dd01d
Non-hashed builds and prepare bump node version to 24 (#562)
- Disable content hashing across Next.js, Nuxt, Vite, Vue CLI, and Angular CLI  configurations to ensure deterministic build outputs with static filenames (e.g.  main.js  instead of  main.d511.js ).
 - React enforces content-hashing thus we maintain status quo there
 - Bump minimum Node.js version to  >=24.0.0  across all  package.json  files so we can build again on the github CI which requires >= 22
 - Update package-lock.json by running "npm run install" on all workloads
2026-07-15 15:27:03 +02:00
Camillo Bruni
ed40d3fcaf
Reduce newssite image asset count (#565)
Reduce the number of pre-generated images in newssite next and nuxt from 343 to 20 each.
The high file count causes delays when using the service workers.

Reduce image count
- Udpate news site ressources to use the limited image set
- Add build script to update ressources.txt for news-nuxt / next
2026-07-15 13:58:06 +02:00
Camillo Bruni
86b99d4a1f
Bind dev server to IPv4 127.0.0.1 address (#564)
Using "localhost' can cause some issues on certain OS / browser combination where we end up binding to the IPv6 address instead of IPv4 and the browser uses the other version for resolving "localhost". Forcing IPv4 for the test server seems to resolve this.
2026-07-15 11:02:27 +02:00
Camillo Bruni
a867a41832
Add complexity param (#430)
Depends on params.mjs changes in #469

Work in progress proposal for adding a variable complexity param.

- complexity === 1.0: workloads run in default configuration
- complexity > 1.0: workloads run proportionally slower
- complexity < 1.0: workloads run proportionally faster

Background:
We're occasionally seeing issues with gc timing / optimisations heuristics with the current workloads. If we can dynamically change the duration / complexity of workloads it's a bit easier to assess how good these heuristics are and how much we're potentially overfitting.

Current Limitations:

DeveloperMode UI cannot reload tests.mjs causing stale BenchmarkTestStep titles
Only TodoMVC are currently scaling the workload size (other workloads are depend on whether we're fine with the general idea here)
2026-07-14 17:03:35 +02:00
Camillo Bruni
80d4e409f9
Add support for running tests in nodejs (#559)
By default we run all tests with webdriver in full browsers.
However, for project validation (which requires more file access) and basic unittests we can rely on node.
2026-07-14 16:59:05 +02:00
Camillo Bruni
8698c7839f
Implement Params.prototype.isDefault() helper (#555)
The isDefault getter is a bit more explict than checking for equalirty against defaultParams.
2026-07-14 16:58:38 +02:00
Camillo Bruni
64bbc821c8
Harden url parsing in perf.webkit.org workload (#553)
Since we forward the top-level url params to the workloads we need to ensure robust url parsing.
- handle empty params e.g. ?&
- handle empty bool params e.g. ?developerMode
2026-07-14 16:15:38 +02:00
Camillo Bruni
9b4716cbe9
Support async and remote workloads (#526)
- Ensure that the AsyncRAFTestInvoker actually passed out the measured results
- Add MESSAGE_TYPE, MESSAGE_STATUS and BENCHMARK_SUITE_TYPE enums and avoid some magic constants
- Use _sendMessage helpers in RemoteSuiteRunner and BenchmarkConnector
- Freeze TEST_RUNNER_LOOKUP
- Rebuild workloads the depen on resources/shared
2026-07-14 16:09:21 +02:00
Camillo Bruni
20c02284b7
Add workload resources.txt (#554)
For supporting service workers in PR #540 we need an explicit list of resources for each workload so we can preload them.
This adds a common resources/shared/generate-resources.mjs helper that is used in all build scripts to generate a list of all files in the respective dist/ output folder.
2026-07-14 15:09:20 +02:00
Camillo Bruni
d580beea58
Format and lint script fixes (#560)
- Ignore all node_modules folders
- fix "npm run lint:check"
2026-07-13 22:26:13 +02:00
Camillo Bruni
9ea7e71ac2
Open Update .gitignore file (#556)
- globally exclude node_modules folder
- ignore typical agent folders
- exclude build-dev from all suites by default
2026-07-08 09:53:41 +02:00
Camillo Bruni
4332004a86
Add more delay params (#547)
For locall investigation we found that having these additional delay params was very helpful:

- waitAfterSetup: Post setup delay (before running the steps)
- waitAfterSuite: Post suite delay (after running all steps)
2026-07-07 11:23:04 +02:00
Camillo Bruni
56ae0346e4
Move suites to dedicated folder (#549)
Instead of storing suites partially in resources/ and experimental, let's use a more consistent
naming scheme that makes it more clear where which code lives

- "resources/" runner and shared helpers
- "suites/" contains the default suites
- "suites-experimental/" contains the experimental suites
2026-07-06 10:05:02 +02:00
Camillo Bruni
a6d7f0ae29
Use full URL parsing and search params for frame.src (#552)
The string-based concateation was rather brittle and can cause subtle bugs.

- Fix issue where hash and url params were not merged properly
- Add warnUnused argument to Params to only warn at top-level
2026-07-01 14:31:09 +02:00
Camillo Bruni
f866d2b077
Improve workload stepping in developerMode (#550)
- Use the temp currentSteppingResolver varialbe in places we modiify the
  _steppingResolver to avoid acessing the oudated version
- Add an explicti "Finished" step so we can inspect the final state of a workload 
   instead of jumping directly to the setup of the next one
2026-06-18 10:49:30 +02:00
Camillo Bruni
001a3926e6
Add --retry support for end2end tests (#545)
Run the the end2end tests with an additionaly retry run to mitigatate flaky browsers with selenium.
This can be further configured with a new --retry flag.
2026-06-16 16:42:31 +02:00
Camillo Bruni
58dc3ffb4c
Rename suites config files to suites.mjs (#548)
This is more consistent with the rest of the codebase.
2026-06-16 15:47:43 +02:00
Camillo Bruni
6c8c0be8f1
Better test error logging (#527)
- Update selenium-webdriver to latest version
- When available use webdriver bidi to forward console logging to the test script
2026-06-16 15:41:24 +02:00
Camillo Bruni
abe0def4f3
Convert build.js to mjs modules (#541)
Let's use a bit more modern code and try to avoid node modules in favour of mjs modules.
2026-06-15 12:04:34 +02:00
Camillo Bruni
64656e22f3
Implement partial npm run format (#546)
- "npm run format" only formats changed files compared to upstream
- "npm run format:all" formats all files (existing behavior)
2026-06-15 12:00:17 +02:00
Camillo Bruni
6e175efc3c
Rename TestInvoker to StepScheduler (#538)
Based on top of #537 this renames TestInvoker to StepScheduler which is a bit more descriptive.
2026-06-11 17:20:35 +02:00
Camillo Bruni
017f8a2b6e
Consistent naming: use step instead of test (#537)
We use step and test interchangeably which is rathe confusing.
This PR changes the runner code to use step eclusively.
2026-06-11 16:47:09 +02:00
Camillo Bruni
a1f4c0b3cd
Convert webpack configs to mjs (#542)
Migrate webpack configs from node modules to standard mjs modules.
2026-06-11 16:46:29 +02:00
Camillo Bruni
b274b98c33
Update developer menu layout (#543)
- Use gridlayout to avoid fixd settings panel height and better adjust to window height
- Indent settings without chekcbox
- Better style select elements
- Use consistent spacing around checkbox in settings and suite selection
2026-06-11 16:45:59 +02:00
Camillo Bruni
3175f4851a
List developerMode with npm run dev (#544)
This adds a little bit of convenience when developping locally.

Server started:
- http://localhost:8080
- http://localhost:8080?developerMode
2026-06-11 16:43:30 +02:00
Issack John
0f34e5752f
Experimental Workload: Responsive Design (#453)
* Add responsive design benchmark
2026-06-01 16:28:42 -07:00
Issack John
39469ce904
Add TodoMVC with indexedDB storage (#531)
---------

Co-authored-by: Luis Pardo <lpardosixtos@microsoft.com>
2026-05-07 14:32:42 -07:00
Martin Kustermann
6cd6735748
Update TodoMVC-Jaspr-* benchmarks with newer Jaspr & Dart SDK (#530)
This adopts some API changes in Jaspr framework (e.g. build methods no longer returning iterables but a single component, ...) and uses newer Dart2Wasm compiler
2026-01-07 10:46:26 +01:00
Camillo Bruni
e2e2538900
Split off ExperimentalSuites (#519)
- Add experimental/tests.mjs which exports ExperimentalSuites
- Add "TodoMVC-LocalStorage" and "NewsSite-PostMessage" to ExperimentalSuites
- ExperimentalSuites is merged into the default Suites
- Add unit tests for Suites and ExperimentalSuites to ensure all tags are set correctly
2025-11-04 18:40:11 +01:00
lpardosixtosMs
5744c55d13
Remove mockup storage and inline styles from web componenets todoMVC (#521)
Currently most versions of TodoMVC use a mockup storage in the form of an array. This causes expensive array operations to be overrepresented in the benchmark (like splices in the delete100items step). Similarly inline style changes are used to manage the state of the list. This PR:

1. Removes the lists of todo entries at the app and list level and keep them only as children of the todolist element.
1. Remove inline styling of elements display, use css attribute selectors instead.
2025-11-04 09:39:50 -08:00
Thorsten Kober
d90b6fde07
Cleanup/shared (#513) 2025-10-08 12:20:44 -04:00
Thorsten Kober
06449bdc34
Enable Speedometer to use an external config.json file (#515) 2025-08-20 10:28:35 -04:00
Martin Kustermann
87f9ed88c8
Add TodoMVC for dart2js & dart2wasm (#502)
Add TodoMVC written in Dart using Jaspr DOM framework

This adds a TodoMVC app written in Dart using the Jaspr DOM framework.
The app is compiled in two variants: dart2js and dart2wasm.

See experimental/todomvc-dart-jaspr/README.md for more details.
2025-07-30 12:35:56 +02:00
Thorsten Kober
f1eddfe695
PostMessage Cleanup - TodoMVC example (experimental) (#509) 2025-07-15 10:52:03 -04:00
Camillo Bruni
deba93c88d
Add default tag (#520)
- Add "default" tag for all default-scored workloads (as an explicit distinction from "experimental")
- Replace .disabled with .enabled to simplify a bit more code

Drive-by-fix:
- Rename _validateSuiteRessultsStub to _validateSuiteResultsStub
2025-07-09 17:07:02 +02:00
Camillo Bruni
c126fed439
Add experimental/README.md (#518)
Explain the purpose of experimental workloads and the simplified review process.
2025-07-09 09:44:09 +02:00
Camillo Bruni
ba41f91e48
Customize Force Layout (#510)
Currently we have two different modes in which we force layout

- getBoundingClientRect used by default in the benchmark runner and tests
- elementFromPoint used in the remote workloads

This PR adds a new layoutMode param to switch between the two modes.
2025-06-19 13:46:37 +02:00
Camillo Bruni
25eb32e898
Improve developer menu and non-standard params table styling (#512)
- Use tags-style for step-button
- Increase start / step button icon
- Use red highlight color for non-standard params
- Use explicit font-size for developerMenu button for more consistent rendering across browsers
2025-05-28 10:06:47 +02:00
Camillo Bruni
0746a31b76
Add measurePrepare param (#440)
Collect the suite prepare time as separate metric if param.measurePrepare is set.
2025-05-27 16:28:45 +02:00
Camillo Bruni
bb9e3e1ba6
Use Params.prototype.toSearchParams() in developer-mode.mjs (#511)
- Remove duplicated code and directly use Params.prototype.toSearchParams().
- Now the Params class is the centralized place to implement the serialization logic.
- Support empty suites selection 
- Use a single "default" tag as default value for tags
2025-05-05 15:39:51 +02:00
Julien Wajsberg
dd661c033a
Update package-lock.json after package.json was changed (#508) 2025-04-17 09:16:46 +02:00
Camillo Bruni
1fa5234087
Use "next" as version on main consistently (#497)
- Use "next" for the main branch version identifier consistently in all sections.
- Remove version number from about and instruction page title
2025-04-16 18:44:59 +02:00
Camillo Bruni
cf76cb5106
Update URL when clicking on tags in the developerMenu (#507) 2025-04-14 17:24:09 +02:00
Camillo Bruni
c156335c1a
Fix non-standard suites and tags with developerMode (#506)
- Fixes issue #489 by properly updating the params from the developer menu
- Keep params and Suites selection in sync
- Refactor updateParamsSuitesAndTags into separate method
2025-04-14 17:09:55 +02:00
Camillo Bruni
5e7cec81a1
Improve Params.toSearchParamsObject (#505)
- Add better viewport, suite and tags handling
- Add basic Params unittests
2025-04-14 17:02:52 +02:00
Camillo Bruni
a5ee467351
Use local-web-server package for all servers (#473)
- Merge the test, dev and npm run server commands to use local-web-server
- Send COEP headers by default
- Run npm ci to regenerate package-lock and sort package.json entries
2025-04-09 17:10:36 +02:00
mattwoodrow
c760d160ca
Perf-Dashboard.Render sync phase sets location.hash, but the actual DOM (#488)
modifications happen asynchronously in the onhashchange() callback.

This results in nothing interesting being measured during the sync phase, and
the resulting rendering update (during the async phase) not including the
change.

Change the test to manually and synchronously trigger the onhashchange()
callback, and change the page content to detect and omit duplicate events.

Co-authored-by: Matt Woodrow <m_woodrow@apple.com>
2025-02-28 23:31:13 -08:00
Camillo Bruni
f4763c3f86
Add icons to developer mode selection buttons (#493)
Add icons to better visually distinguish the suite selection buttons.
2025-02-27 18:05:23 +01:00
Camillo Bruni
763c517938
Update github workloads (#478)
* Use latest action versions
* Bump node version to current stable v22
* Use caches for node modules to slightly speed up setup
* Mild formatting was applied
2025-02-27 17:59:21 +01:00
Camillo Bruni
91b7adeaed
Score math: Add legend for variables (#490)
* Add more detailed label description
* Add custom spacing for legend
2025-02-27 17:57:29 +01:00
Camillo Bruni
fa1560af92
Use WakeLock (#491)
* Add WakeLock wrapper class around the web-api to keep the screen unlocked during benchmark runs
* Use WakeLock.aquire() and WakeLock.release() in the benchmark runner
2025-02-27 17:56:48 +01:00
Camillo Bruni
275043eba4
Use "next" as version ID on the main branch (#492)
Distinguish the main-branch version from the release version numbers by using "next" as version id.
2025-02-27 17:55:26 +01:00
Thorsten Kober
cdc058a1dd
Release/3.1 - merge 428 (add id to input) (#484) 2025-02-25 12:35:39 -05:00
Thorsten Kober
68d8f57ddf
News Site Next: simplify message popup integration (#471) 2025-01-29 10:18:04 -05:00
Thorsten Kober
a3b0e71d97
News site Next: run build command (#474) 2025-01-23 12:19:03 -05:00
Julien Wajsberg
7d7504f9bd
Add a netlify configuration file (#472) 2025-01-22 14:40:54 +01:00
Camillo Bruni
0c3f0fb132
Add end2end tests (#470)
- Move unittests to run-unittests.mjs
- Move common test driver and server setup to separate helper.mjs module
- Add "SpeedometerReady" and "SpeedometerDone" events for easier testing
- Add simple end2end tests that invoke the benchmark via the html page
2025-01-22 12:56:07 +01:00
Thorsten Kober
9f85f8e3bd
News Site Nuxt - Article Ids (#448) 2025-01-21 11:54:13 -05:00
Thorsten Kober
2ff56c9d76
News Site Next - Article Ids (#447) 2025-01-21 11:51:56 -05:00
Thorsten Kober
d979723ef6
Async Runner with Message Channel (#458) 2025-01-15 14:30:42 -05:00
Camillo Bruni
d6b5ffea95
Display non-standard params on summary page (#469)
Make sure we better highlight non default params since they affect the default score.

- Display non-default params table in the summary view
- Simplify defaultParams creation code a bit
- Rebuild news-next since params.mjs changed
2025-01-09 17:08:47 +01:00
Camillo Bruni
4dfa8faeea
Add Measurement and Scoring details to README.md (#467)
Document in more detail how scoring and workload measurement works:
- Add example timeline to explain which parts contribute to the score
- Add basic formulas on how the test steps are combined into a final score
2025-01-08 11:26:46 +01:00
Camillo Bruni
faf9fff140
Avoid measurement dead-spot between sync and async phase (#464)
We could potentially miss a gc between the performance.mark calls for syncEndTime and asyncStartTime.

This PR changes the code to reuse the sync end times and marker for the async start.
2025-01-06 16:43:46 +01:00
Camillo Bruni
a169b1d56a
Auto run unittests in tests/index.html (#468)
Instead of triggering the tests from the outside, we can always directly start the mocha runner in tests/index.html.
The selenium driver now syncs on either globalThis.testResults being there or the test-complete event.

This way we should be able to reliably run the tests both via selenium and the browser.
2025-01-06 15:23:54 +01:00
Ryosuke Niwa
d87b307974
Delete the timer based measurement since we've switched over to (#466)
requestAnimationFrame based measurement for 3.0 and found no issues.
2024-12-20 10:14:58 -08:00
Thorsten Kober
f7c3a7014a
Remote Runner / PostMessge api (#456) 2024-12-17 17:27:08 -05:00
Ryosuke Niwa
0dc0b1c50e
Make the progress bar track indivisual suites, not indivisual steps. (#465)
This is needed to allow for remote workloads.
2024-12-17 13:37:04 -08:00
Thorsten Kober
e2e3c496f5
Fix/selenium (#461) 2024-12-09 08:05:08 -05:00
Thorsten Kober
67bc21f1f4
Runner Cleanup: SuiteRunner & TestRunner classes (#452) 2024-11-15 13:42:38 -05:00
Camillo Bruni
e70778f1a8
Add more flexible SUITE_RUNNER_LOOKUP (#438)
Add SUITE_RUNNER_LOOKUP to dynamically look up the suite runner class
2024-11-13 13:30:47 +01:00
Thorsten Kober
a64fa26876
split test-invoker (#451) 2024-11-12 20:08:11 -05:00
Thorsten Kober
e2afcb6e4e
News Site Nuxt - Images (#450) 2024-11-11 20:26:51 -05:00
Thorsten Kober
66b5324d02
News Site Next - Images (#449) 2024-11-11 20:26:29 -05:00
Thorsten Kober
94f0d81686
Test Runner cleanup - for our internal tests (#446) 2024-11-06 08:24:54 -05:00
Camillo Bruni
4f87c768c4
Add Params parse helper methods (#444)
- Move parsing to separate helper methods to main the _copyFromSearchParams method more readable.
- Add Params.prototype._parseXXX helper methods
2024-11-04 19:55:53 +01:00
Camillo Bruni
1552769e5c
Add local results variable to SuiteRunner (#445)
We can migrate some of the measuredValue logic directly to the SuiteRunner and simplify the code a bit.
Additionally this will allow us to generate more flexible metrics in the future.
2024-11-04 16:25:04 +01:00
Camillo Bruni
04cfe65d7d
Refactor developer-mode UI code (#443)
- Move default onchange handler to createTimeRangeUI
- Add updateCallback to createCheckboxUI
2024-11-04 15:20:25 +01:00
Camillo Bruni
617c7310fa
SuiteRunner: use suite instance variables (#437)
* use suite directly
* fix tests
2024-10-29 14:27:39 +01:00
Camillo Bruni
4719e3f8bd
Use TEST_INVOKER_LOOKUP (#439)
Add generic lookup dict for the test-invoker class.
This makes future extensions and local hacking easier.
2024-10-23 09:58:30 +02:00
Thorsten Kober
3ce6105a3a
Cleanup to prepare for postMessage / remote runner (#435) 2024-10-21 11:54:42 -04:00
Camillo Bruni
2387b9fe89
Split off SuiteRunner class (#433)
Move Suite-related code to a separate SuiteRunner class.
This happens in preparation for async and remote suites where it will be cleaner to handle the code paths independently instead of adding more functionality to the BenchmarkRunner class.
2024-10-21 17:16:06 +02:00
Camillo Bruni
bb4a4ad1c1
Improve developerMode styling (#436)
- Maintain the same margin to the "Developer Mode" text when closed and open
- Remove display flex that had no effect
- Make sure the run settings are always visible
- Scroll only the suites part
- Move the select all / unselect all buttons to the top for better usability
2024-10-17 16:06:24 +02:00
Camillo Bruni
2a4d041656
Remove interactive runner (#432)
- To simplify future runner refactorings we remove the interactive runner.
- Stepping functionality is added to the developer-menu and integrated directly into the default MainBenchmarkClient.
2024-10-11 08:26:53 -04:00
Camillo Bruni
c2e8d4f6bc
Improve developerMode updateURL (#431)
- Simplify the tag/suite/suites selection code by always deleting all searchParams first
- Don't use tags if there is only one selected suite (previously selecting "React-Stockcharts-SVG" currently sets the "svg" tag which is a bit unexpected)
- Compare against defaultParams for "measurementMethod" using the existing method for bool values
2024-10-10 13:30:57 -04:00
Thorsten Kober
400a5b87a0
TodoMVC - React - add id to input (#428) 2024-10-02 07:20:55 -04:00
Camillo Bruni
0dcab0317e
Use more helper runner helper methods (#421)
Refactor the runner and add more helper methods to be more flexible for upcoming experimental PRs.
2024-09-30 21:25:56 +02:00
Matthieu Dubet
ea14d27e03
Change default port to 8900 (#429)
Port 7000 is occupied by default on macOS.
This also makes it simpler how to customize it
(use env var instead of hardcoded in the package.json)

Fix https://github.com/WebKit/Speedometer/issues/325
2024-09-10 13:09:29 -07:00
Thorsten Kober
3150f41173
Add emojis to a todomvc app (#417) 2024-08-07 15:05:18 -04:00
Thorsten Kober
5a19851e54
Add local storage to todoMVC-es5 (#414) 2024-08-07 14:52:34 -04:00
Camillo Bruni
a999ad1024
Don't display invalid results (#406)
This partially fixes #399 by not display non-finite scores which are caused by 0-measured suite results.

- For non-positive or non-finite scores, the following summary page is displayed. Note that the detailed page is still accessible since you might find some useful information there.
- Additionally, a console.error is generated for each zero-sum suite
2024-07-25 09:03:30 +02:00
Konrad Kügler
e6e8a4f969
Remove obsolete eslint ignore (#410)
The offending `new Promise(async ...)` code was already removed in 19184dc694 but this linting comment remained.
2024-07-24 09:56:03 -07:00
Camillo Bruni
33dec4f328
Deconfuse Geomean Metric and geomean aggregate value (#408)
The metric's "geomean" aggregate value can be easily confused with the top-level Geomean metric (issue #407).

- Don't serialize "geomean" in the JSON data
- Add descriptions to Score, Geomean, and Iteration metrics that are serialized in the JSON data
- Make the detail view titles more descriptive
2024-05-30 17:37:45 +02:00
Camillo Bruni
3ae822bcff
Manually install Firefox on macos-latest image (#411)
macos-latest does not include Firefox yet.
Manually running brew install --cask firefox on the firefox test job solves the problem.
2024-05-29 14:47:40 +02:00
Thorsten Kober
13cc928140
[fix] News Site Nuxt: rename 'className' -> 'class' (#400) 2024-03-22 12:21:29 -04:00
Zev Eisenberg
cbf8311a76
Remove duplicated word. (#402) 2024-03-15 11:23:18 -07:00
Morgan Roderick
bd9ab77462
chore: upgrade sinon to latest (#398)
See https://github.com/sinonjs/sinon/blob/main/CHANGES.md
2024-03-12 10:55:19 +01:00
Issack John
8d67f28d02
Fix Cross-platform issue with npm script (#397)
* Make commands work cross-env

* make npm format work as well.
2024-03-07 01:41:02 -08:00
Markus Stange
3d2a28e2dc
Avoid :has selector in CSS. (#395)
This changes how section visibility is controlled.
All sections are still `display:none` by default.
Rather than becoming `display:block` when they match `:target`,
they now become `display:block` if their ID matches the value
of the root element's `data-visible-section` attribute - we hardcode
the four section names "home", "running", "summary" and "details".

This lets us default to the home section just by setting
`<html data-visible-section="home">` in index.html, and we
can avoid the `:has` selector.

Now Speedometer can run in older browsers which don't support `:has`.
2024-03-06 11:02:43 -08:00
lpardosixtosMs
406042a18d
Update license dates, add Microsoft (#393) 2024-03-05 12:09:16 -08:00
Julien Wajsberg
7efb0ac57b
Run npm audit fix on charts and editors (#390)
* Run npm audit fix on charts

* Run npm run build on charts

* Run npm audit fix on editors

* Run npm run build on editors
2024-03-05 15:30:34 +01:00
Julien Wajsberg
309437377e
Enable source maps in charts and editors (#391)
* Enable sourcemaps in editors

* Enable sourcemaps in charts
2024-03-04 12:34:43 -08:00
lpardosixtosMs
88c5323a49
Update Testing.md to include Microsoft Edge (#392) 2024-03-04 10:40:16 -08:00
Thorsten Kober
58e2e4e92b
Revert "Todomvc-vue: source-map creation (#386)" (#388)
This reverts commit 7dc43d27a5.
2024-03-01 20:19:10 -05:00
Thorsten Kober
7dc43d27a5
Todomvc-vue: source-map creation (#386) 2024-03-01 19:19:56 -05:00
Thorsten Kober
ad061acd81
run format command from root (#387)
Co-authored-by: Thorsten Kober <thorstenk@google.com>
2024-03-01 16:53:11 -05:00
Ryosuke Niwa
5654961621
Update about.html to link to each test's GitHub directories instead of directly linking to README.md files. (#384) 2024-02-29 17:56:23 -08:00
Issack John
377c6c7610
Fix Hidden dropdown menus (#376)
closes #362
2024-02-29 09:38:20 -08:00
Brian Grinstead
3ca4aed3a8
Remove experimental note from README.md (#361) 2024-02-29 09:33:37 -08:00
Camillo Bruni
c901052f2d
Add warmup and step delay to developer UI (#323)
Drive-by-fix:
- Move the more frequently used iteration count to the top
- Move warmupSuite and warmup-delay closer together
2024-02-29 10:46:39 +01:00
lpardosixtosMs
899c11317d
Enable local testing in Edge (#382) 2024-02-28 10:41:23 -08:00
Issack John
82e0a46283
Make about.html text reflect currently enabled workloads (#381)
* make text reflect currently enabled workloads
2024-02-27 09:48:24 -08:00
Camillo Bruni
f6f67e0237
Update General Instructions (#377)
Update general instructions on how to run Speedometer 3 with generic measurement advice.
2024-02-27 10:50:10 +01:00
Thorsten Kober
0c3af704a5
Rebuild workloads (#375) 2024-02-20 11:13:07 -05:00
Julien Wajsberg
ca9d23c18f
Make the github action linter step fail in the case of issues (#374)
* Make the github action linter step fail in the case of issues

* Run the formatter on the project
2024-02-17 12:26:43 +01:00
Ryosuke Niwa
3d782244a5
Add the source map for Angular TodoMVC build. (#332) 2024-02-15 11:54:42 -05:00
Brian Grinstead
2640affe64
Update about.html (#372)
Fixes #344
2024-02-09 15:47:05 -08:00
Julien Wajsberg
0fd1528419
jQuery workloads: use :nth-child() in a querySelector call in each iteration (#367)
This PR fixes the issue #365: jQuery test only ever marks the first todo item as completed

The bug was caused by jQuery replacing the whole todo list whenever each todo item is toggled.

Fixed it by running querySelector with :nth-child in each iteration.

Co-authored-by: Ryosuke Niwa <rniwa@webkit.org>
2024-02-08 10:59:05 +01:00
Camillo Bruni
9837ccc768
Add separate about.html and instructions.html (#359)
Move the about section and the newly created instruction section into a standalone
html file to simplify the in-page navigation for the runner.
2024-02-02 10:53:06 +01:00
Thorsten Kober
7b1b81ee13
fix clear-completed (#364) 2024-01-31 12:57:07 -05:00
Issack John
285e32dacb
Npm audit fix (#360)
* run npm audit fix in affected packages
* rebuild all touched dists
2024-01-30 14:28:55 -08:00
Camillo Bruni
31ba222f5b
Remove test-again button (#358) 2024-01-25 15:00:46 +01:00
Camillo Bruni
7529eedcc5
Adjust dot size to iterationCount (#356)
Gradually adjust scatter plot dot sizes from 5px down to 2px for iterationCount > 20.
2024-01-25 13:38:02 +01:00
Camillo Bruni
638a9681cf
Update document title (#354)
Use a "data-title" attribute for each section to update document.title.
2024-01-25 11:21:55 +01:00
Camillo Bruni
b0a465d60c
Fix navigation recursion (#357)
Conditionally update window.location.hash for #home to avoid navigation recursion.
2024-01-25 11:17:48 +01:00
Camillo Bruni
3d93f179b6
Fix navigation for Firefox (#353)
Refreshing the page with results and a #summary hash breaks on Firefox.
Explicitly setting the #home has a workaround cleanly shows the main page again.
2024-01-25 10:21:12 +01:00
Julien Wajsberg
7c0931ee8c Run format on a previously unformatted file 2024-01-23 14:07:58 +01:00
Julien Wajsberg
7ae836e55f Configure the next eslint plugin so that there's no false positive error when running eslint 2024-01-23 14:07:58 +01:00
Julien Wajsberg
c0269db7d7 Add root: true to the top level eslint configuration file 2024-01-23 14:07:58 +01:00
Julien Wajsberg
6ad43c3f5c Makes tsx files checked by the formatters. 2024-01-23 14:07:58 +01:00
Julien Wajsberg
0f449aa702 Remove tentative from the prettier and eslint ignore lists 2024-01-23 14:07:58 +01:00
Camillo Bruni
4b451d8388
Allow empty hash for cleaner default URLs on the home screen (#349)
This makes sure we don't need the #home hash for the default page URL.
2024-01-17 10:33:43 +01:00
Issack John
64a5e7c0ba
Fix Complex vs. non-complex DOM subtests order (#343)
* interleave the stand-alone and the complex dom tests
2024-01-11 21:28:33 -08:00
Ryosuke Niwa
2aa819faed
Always instantiate Float32Array in the cost array of findOptimalSegmentationInternal. (#342) 2024-01-11 15:50:12 -08:00
Issack John
91aaaacb42
Regenerate package-lock.json (#339)
* update package-locks
2023-12-21 11:06:20 -08:00
Issack John
4173e4d8a9
Enable complex DOM benchmarks execution in official speedometer run (#271) 2023-12-21 10:28:08 -08:00
Issack John
a268ec4391
Fixes #335: Prevent complex DOM workloads from hitting a specific WebKit pathology (#338)
* Utility function to create css variants for big-dom.css
* update build scripts
* update dists
2023-12-21 10:12:49 -08:00
Julien Wajsberg
2414364b18
Keep the parameter 'tags' as an array in the body of the function (#337)
Previously, we were transforming it into a set. This was giving problems
in a later condition, so that no message/debugInfo were defined, and the
user would get an alert with the message "undefined".

Now the set stays local to the code block where it's used, so that the
parameter variable "tags" is an array for the whole function, for more
consistency with the "names" variable.
2023-11-29 19:17:06 +01:00
Camillo Bruni
208e1abee2
Avoid setTimeout in waitOnElement (#321)
waitOnElement uses a 50ms timeout, which was historically motivated by FlightJS that used requirejs which had a hard-coded 50ms pause.

We've observed fragile CPU scaling behavior with this approach in Speedometer 2.1. V3.0 is less affected by this since the perf-dashboard is the only workload the regularly needs another cycle to wait (all other workloads are ready after onload has fired).

Using rAF lowers the timeout limit and is more realistic in required CPU ramp-up performance.
2023-11-20 10:56:59 +01:00
Camillo Bruni
2ee210ca20
Add "Start Test" developerMode button (#333) 2023-11-14 20:48:00 +01:00
lpardosixtosMs
abea7b89b9
[Complex-Dom] Combine visibility: hidden and display: none for side TreeView hidden elements. (#327) 2023-11-01 07:50:03 -07:00
Thorsten Kober
c19468aa56
TodoMVC web component: change css rule, run build on complex (#326) 2023-10-18 18:50:45 -04:00
Thorsten Kober
32c7c23a7d
Todomvc svelte cleanup (#324) 2023-10-10 10:48:08 -04:00
Camillo Bruni
b77b3dbf2b
Fix developerMode layout (#322)
- Fix scroll content overflow
- Remove li spacing in the developer menu
2023-10-05 12:42:40 +02:00
Thorsten Kober
44c809305d
todomvc web-components, change title, id & completed attributes on item component (#314) 2023-10-02 16:17:23 -04:00
Issack John
4316b5fb14
Follow up for complex DOM versions (#315)
* Check prereqs in the build script and handle them
* add code comments
* Update build scripts & package.jsons
2023-09-29 11:45:58 -07:00
Thorsten Kober
81c835f6b7
TodoMVC Angular Bug Fixes (#312) 2023-09-25 12:10:37 -04:00
lpardosixtosMs
8fd080fb67
[Complex-DOM] Fix generator node count (#308)
* Fix count
* Rewrite generator
* Wrap the generator in a function
* combine folderwrapper and treeitem
* add class for type of item
* Update dist folders

---------

Co-authored-by: Julien Wajsberg <felash@gmail.com>
Co-authored-by: Issack John <issack.john@outlook.com>
2023-09-21 12:36:40 -07:00
Camillo Bruni
b10452595e
Set proper unit for "Score" Metric (#311) 2023-09-12 22:33:43 +02:00
lpardosixtosMs
0672c38f23
Add complex DOM workloads to tests.mjs disabled by default (#310) 2023-09-08 09:43:58 -07:00
Thorsten Kober
790eed91c9
reverse lit/web components deletion steps (#309) 2023-09-07 09:09:49 -04:00
lpardosixtosMs
679cb52fdf
Add Web Components and LIT versions of Complex-DOM TodoMVC (#285)
This PR does not enable any test in the runner.

Co-authored-by: issack john <issackjohn@microsoft.com>
2023-09-06 09:23:41 -07:00
lpardosixtosMs
5748e44342
Delete extra line (#307) 2023-09-01 19:30:28 +02:00
lpardosixtosMs
c306d4237d
run npm format (#306) 2023-09-01 18:59:35 +02:00
lpardosixtosMs
095abdfc44
Delete duplicated React Complex DOM README (#305) 2023-08-30 10:44:25 -07:00
lpardosixtosMs
a03f1367ea
Add Complex DOM READMEs (#301)
Add READMEs for the big-dom-generator and each complex DOM variation.

---------

Co-authored-by: Issack John <issack.john@outlook.com>
Co-authored-by: Sulekha Kulkarni <Sulekha.Kulkarni@microsoft.com>
2023-08-29 19:02:21 -07:00
lpardosixtosMs
f6c5098d6e
Remove "targeted", "li-<index>", and "view-<index>" class names from todoMVC (#300)
The "targeted", "li-", and "view-" class names were added for the complex DOM generated CSS, the handcrafted CSS that replaces it does not use them anymore.
2023-08-23 10:09:38 -07:00
Thorsten Kober
f841f81faa
Speedometer About format & readme links (#294) 2023-08-22 10:08:29 -07:00
Camillo Bruni
a608e98396
Remove broken benchmark-runner.mjs file (#297)
The file is currently unused and was webkit-CI specific.
2023-08-21 12:12:59 +02:00
Camillo Bruni
fb9b966b37
Add metrics getter (#298)
This should simplify extracting metrics from the default page.
2023-08-21 12:08:13 +02:00
lpardosixtosMs
b3ec8c748b
Fix :last-child selector in Web Components and LIT (#296) 2023-08-18 11:12:25 -07:00
Doug Thayer
d86dad330f
Add param for shuffling suite order between runs (#292)
* Add param for shuffling suite order between runs

* pr feedback

* move into runner-prepare and log seed
2023-08-18 08:32:22 -07:00
lpardosixtosMs
5107c739c1
Replace generated complex CSS with handcrafted CSS (#291)
* Replaced generated css with complex handcrafted CSS.



---------

Co-authored-by: Luis Fernando Pardo Sixtos <lpardosixtos@microsoft.com>
Co-authored-by: lpardosixtosMs <94007365+lpardosixtosMs@users.noreply.github.com>

* Move reused logic into buildComplex Util function
* Update build scripts

---------

Co-authored-by: Issack John <issack.john@outlook.com>
2023-08-15 09:59:45 -07:00
Brian Grinstead
b6abd32982
Update about text (#293) 2023-08-09 12:09:56 -07:00
Ryosuke Niwa
0fef685dcb
Simplify the score calculation by replacing 60 / 50 by 1. (#289) 2023-07-26 11:58:01 -07:00
Ryosuke Niwa
26531fc7f1
Fix the developer menu to set measurement method only when it's not RAF-based. (#290) 2023-07-24 13:08:22 -07:00
Issack John
64798bc754
[big-dom-generator] Remove non-inclusive language (#288)
* Remove non inclusive language
* Rebuild vue-complex
2023-07-19 13:45:18 -07:00
lpardosixtosMs
5491573d0e
Rebuild react-redux-complex on top of sanitized standalone (#286) 2023-07-17 16:47:45 -07:00
Thorsten Kober
8f6982e71a
Add Sanitizing script to react-redux and vue to remove non-inclusive words in dist folders (#284) 2023-07-13 10:04:17 -07:00
Issack John
cba2b0d2e7
Complex DOM versions of todoMVCs (#264)
This PR introduces complex DOM versions of the todoMVCs.
---------

Co-authored-by: lpardosixtosMs <94007365+lpardosixtosMs@users.noreply.github.com>
Co-authored-by: Luis Fernando Pardo Sixtos <lpardosixtos@microsoft.com>

* use path.resolve to compute paths from __dirname
* Refactor package names + PR comments (#31)
* update build scripts
* more verbose build done message
---------

Co-authored-by: lpardosixtosMs <94007365+lpardosixtosMs@users.noreply.github.com>
Co-authored-by: Luis Fernando Pardo Sixtos <lpardosixtos@microsoft.com>
2023-07-13 09:50:56 -07:00
Thorsten Kober
36712deb57
Remove es6 todomvc (#283)
Since the es6-webpack workload doesn't transpile down to any earlier versions of EcmaScript, testing two todomvc workloads with similar logic is redundant.
2023-07-13 16:58:09 +02:00
Thorsten Kober
f02d7e4b64
News Site CSS Cleanup (#275) 2023-07-12 13:57:50 -07:00
Thorsten Kober
61ad820a5e
use translations (#279) 2023-07-12 09:58:28 -07:00
Ryosuke Niwa
d834dcbb07
Fix a typo in Metric class' regular expression. (#282) 2023-07-12 09:14:54 -07:00
Camillo Bruni
a9c96c3bd4
add npm run server to run caching server (#281)
`npm run server` will now run a local server with caching files.
This is more realistic for local benchmarking, while `npm run dev` keeps on the non-caching behavior which is nicer for developing the benchmark.
2023-07-11 23:36:34 +02:00
Ryosuke Niwa
e11801e3b5
Fix the developer menu's measurement method toggle. (#278) 2023-07-11 13:19:11 -07:00
Ryosuke Niwa
e2a033d54b
Remove superfluous conosle.log in the perf dashboard workload. (#277) 2023-07-11 13:18:42 -07:00
Thorsten Kober
91f54a9c6d
add translations for the text used in the tests of the todmvc apps (#259) 2023-07-11 06:20:04 -07:00
Issack John
95af4c4956
bug fix (#276) 2023-07-10 13:35:42 -07:00
Thorsten Kober
e8624b1096
News site - add translation data (#272) 2023-07-07 14:30:21 -07:00
Peter Burns
f6793b0bd5
Generate lit/dist/index.html (#270)
Fixes https://github.com/WebKit/Speedometer/issues/269
2023-07-05 12:13:11 -07:00
Issack John
815817b5e0
Remove extra semicolon from react-redux todomvc (#268) 2023-07-03 12:05:45 -07:00
Ryosuke Niwa
b44d98255f
Change the default measurement method to rAF-based approach (#262) 2023-06-30 12:23:06 -07:00
Thorsten Kober
151d73d535
[OPTIONAL] --NUXT--Newssite - updates to support different languages / html direction in the future (#265) 2023-06-30 09:13:57 -07:00
Thorsten Kober
8046654389
TodoMVC Web Component - use local todomvc-css package (#263) 2023-06-29 14:14:21 -07:00
Thorsten Kober
9025eb1fdc
[OPTIONAL] - Newssite - updates to support different languages / html direction in the future (#248) 2023-06-29 07:11:39 -07:00
Issack John
ad36a2c483
Refactor Complex DOM CSS generator (#246)
Refactored the CSS generator to use JSDOM and html markups.
2023-06-28 19:00:31 -07:00
Peter Burns
9e692e4074
Add a Lit TodoMVC app (#251) 2023-06-28 14:24:18 -07:00
Thorsten Kober
68c56ff662
[OPTIONAL] - updated todomvc styles (#254) 2023-06-28 14:03:11 -07:00
Ryosuke Niwa
1c4d9c7d10
Move Perf Dashboard suite out of tentative directory. (#244) 2023-06-28 12:52:47 -07:00
Ryosuke Niwa
0f6443e065
Avoid using generators in TimeSeries data model. (#256)
This avoids pathological perf hit in Firefox.
2023-06-28 11:53:10 -07:00
Thorsten Kober
8491149024
use external text (#260) 2023-06-28 11:19:16 -07:00
Julien Wajsberg
1f48e366d0
Run the format script after javascript web components landed (#261) 2023-06-28 09:47:44 -07:00
Julien Wajsberg
bd4dc60b24
Move stockcharts out of tentative (#231)
* Move stockcharts out of tentative

* Add some explanations about how to change react-stockcharts locally
2023-06-28 17:15:51 +02:00
Julien Wajsberg
27c0b2cddf
Move the charts workload out of the tentative directory (#252) 2023-06-28 17:15:28 +02:00
Thorsten Kober
2281e13426
[OPTIONAL]: TodoMVC built with Web Components (#249) 2023-06-28 07:30:08 -07:00
Thorsten Kober
18c6ed85d9
Use more varied set of todo text rather than Something to do ${i} (#258) 2023-06-27 15:28:25 -07:00
Ryosuke Niwa
00cac75b70
Move the initial glyph cache cost out of the measured part of the perf dashboard workload. (#255) 2023-06-23 14:16:24 -07:00
lpardosixtosMs
039f2e854f
Add Complex DOM required changes to standalone TodoMVC apps (#239) 2023-06-23 09:41:03 -07:00
Julien Wajsberg
b4f675ef5f
[charts] Group substeps to make them more stable (#220) 2023-06-23 14:52:35 +02:00
Thorsten Kober
03f74f8b4c
Adjusting Page.querySelector / Page.querySelectorAll (#253) 2023-06-22 14:49:39 -07:00
Julien Wajsberg
73fda50cb2
Group the Create step into the next step (#223) 2023-06-20 21:50:30 +02:00
Julien Wajsberg
e3b8e93ec4
Rebuild the editors code after #241 landed (#245) 2023-06-20 11:19:06 +02:00
Thorsten Kober
c7eef15502
[Optional] : News site Nuxt - use hash router (#238) 2023-06-19 20:52:14 -07:00
Camillo Bruni
42a7b3b5d6
Use progress element (#233) 2023-06-19 17:23:13 +02:00
Ryosuke Niwa
d50264b547
Undefine requestIdleCallback in the Next test for consistency. (#243)
We can undo this change once Safari / WebKit adds the support for requestIdleCallback.
2023-06-18 14:14:28 -07:00
Ryosuke Niwa
4e13390704
Undefine requestIdleCallback in the code mirror test for consistency. (#241)
We can undo this change once Safari / WebKit adds the support for requestIdleCallback.
2023-06-18 14:14:10 -07:00
Ryosuke Niwa
0b89992a1a
Undefine requestIdleCallback in the Nuxt test for consistency. (#242)
We can undo this change once Safari / WebKit adds the support for requestIdleCallback.
2023-06-17 12:17:36 -07:00
Julien Wajsberg
054b6342c6
Move the editor workload out of tentative (#214)
Co-authored-by: Brian Grinstead <briangrinstead@gmail.com>
2023-06-14 11:22:32 -07:00
Thorsten Kober
7efcc89499
move newssites out of tentative (#229) 2023-06-14 11:10:10 -07:00
Thorsten Kober
3b012c27f0
Newssite nuxt format (#235) 2023-06-13 15:01:08 -07:00
Thorsten Kober
387720d1a7
Newssite next.js format (#236) 2023-06-13 14:49:52 -07:00
Camillo Bruni
0da3406f1b
Developer Mode: Support filtering by tags (#217)
- Add Tags filtering buttons
- Update url with tag if a single tag can be used to represent the current selection
- Make sure auto-added tags are first in Tags and suite.tags
2023-06-13 22:19:15 +02:00
Camillo Bruni
a5d8934b12
Reduce version number letter spacing (#234)
- Lower version number spacing a bit
- Drive-by-fix: Add Roboto as slightly better alternative font.
2023-06-13 01:17:04 +02:00
Thorsten Kober
392d54feaa
Newssite css: cleanup (#232) 2023-06-12 10:01:46 -07:00
Camillo Bruni
e295eb8596
Display iteration marker in progress bar (#228) 2023-06-09 13:41:10 +02:00
Thorsten Kober
b8b72cda46
run format (#230) 2023-06-08 16:28:35 -07:00
Ryosuke Niwa
38d5a701c0
Add the missing tags to perf dashboard workload. (#222) 2023-06-08 19:25:52 -04:00
Camillo Bruni
df082e68c5
Auto-add empty tags (#226) 2023-06-08 23:30:05 +02:00
Camillo Bruni
1cac2f1166
Show version number in all sections #227 2023-06-08 23:29:45 +02:00
Daniel Roe
951f57b106
Set base url dynamically for Nuxt site (#225) 2023-06-08 13:52:48 -07:00
Ryosuke Niwa
d19454c8e8
Add WebKit's performance dashboard as a workload. (#156)
It's a website written in ES2015+ with web components.
The workload emulates openinig the charts page, selecting data points,
then selecting a data range.
2023-06-08 14:30:33 -04:00
Thorsten Kober
7465b1a9a8
add tags to nuxt (#218) 2023-06-08 07:25:57 -07:00
Thorsten Kober
28fbc651da
News Site - Nuxt version (#203) 2023-06-08 07:11:25 -07:00
Camillo Bruni
32a381fcab
Add Suite tags for complex filtering (#215)
Mostly self-explanatory:
- Each suite has a set of tags
  - "all" applies to all suites
  - "default" applies to suites that are enabled by default (and not tentative)
  - "tentative" applies to suites that are in the tentative directory
- `&tags=` can be used for filtering Suites by tag
2023-06-08 10:42:42 +02:00
Camillo Bruni
a0c756901d
Developer Mode UI Improvements (#216)
- Enlarge clickable targets (title and list items)
- Make buttons fill space horizontally
- Match border radius with other runner buttons
2023-06-08 10:22:32 +02:00
Thorsten Kober
c0d0626159
News site next updates (#206) 2023-06-07 18:37:09 -07:00
Julien Wajsberg
8d13cdf385
Reduce the size of the long text loaded in TipTap to reduce its running time (#208) 2023-06-07 11:40:08 +02:00
Thorsten Kober
0d8bf1226d
use nanoid for preact / react todomvc (#211) 2023-06-06 16:38:33 -07:00
Julien Wajsberg
282e2b1df7
Reduce the react stockcharts steps (PR #200) 2023-06-06 18:03:09 +02:00
Camillo Bruni
0940a7700c
Show version number (#213) 2023-06-06 16:41:46 +02:00
Camillo Bruni
c46e191e52
Fix params parsing (#212) 2023-06-05 18:02:25 -07:00
Camillo Bruni
3a678028a5
Add param.waitBeforeSync (#202)
- Add waitBeforeSync to set a timeout before running the sync step
- Change warmupBeforeSync to use 0 as default value
- Add more params comments
- Add common TestInvoker base class
2023-06-06 00:32:05 +02:00
Camillo Bruni
ec1326cf1a
Freeze params again in non-developerMode (#209)
This prevents accidental modification in normal runs.

Drive-by-fix:
- Fix comment and run `npm run format`
2023-06-06 00:26:12 +02:00
Camillo Bruni
91d378683e
Add stricter _parseInt helper to Params (#210)
- `parseInt` is too permissive and easily parses partial values like `100warmupBeforeSync=10`.
  Using `Number` and `Number.isInteger` instead to give stricter warnings.
- Add more descriptive error messages
- Use fewer default fallbacks when explicitly passing along params
2023-06-06 00:16:42 +02:00
Julien Wajsberg
39e9a52436
Replace innerHTML with DOMParser in the javascript-es5 implementation (#159) 2023-06-05 18:07:45 +02:00
Julien Wajsberg
b8a24fdbb0
[codemirror] Mock requestAnimationFrame to setTimeout so that we don't lose any work in the async time (#205) 2023-06-01 21:14:52 -07:00
Julien Wajsberg
e1957e3919
Set the font-size and text-size-adjust to alleviate differences between browsers (#198) 2023-05-31 10:05:48 -07:00
Ryosuke Niwa
ec51e3188b
Add warmupBeforeSync parameter, which triggers speciefied milliseconds of infinite loop before each sync step. (#196)
This will allow us to evaluate the impact of using rAF vs. timer with regards to CPU throttling.
2023-05-31 11:53:53 -04:00
Julien Wajsberg
4f9ffb5e3b
Stockcharts: Mock requestAnimationFrame to setTimeout so that the runner can catch asynchronous work (#176)
Fixes #97
2023-05-31 08:52:19 -07:00
Julien Wajsberg
e2530dbed3
Add a workload with Chart.js (#149) 2023-05-31 08:51:36 -07:00
Camillo Bruni
7dc36290eb
npm run format (#191)
* run formatting again
* make linter happy
* adding newlines
2023-05-30 22:32:17 +02:00
Camillo Bruni
07a4618a83
Better copy-support for detailed results table (#192)
Make sure we can easily copy the whole detailed results page as formatted text.

- Only toggle charts when clicking on charts
- Don't copy chart legend bullet
- Don't copy chart axis labels

Drive-by-fix:
- Put abbreviation ellipsis for chart legends at the beginning so we don't hide the more important metric suffix
2023-05-30 09:04:08 +02:00
Camillo Bruni
43a7448ca3
Add more markers (#190)
Adding more markers to simplify checking if work is measured:

- Add iteration-X for each runner iteration
- Add runner-prepare and runner-finalize to annotate time it takes for adding/removing the iframe
- Use suite-X-prepare
2023-05-30 09:02:49 +02:00
Brian Grinstead
232cadb2ab
Developer Mode UI improvements (#194)
* update range label on input, not change

* enable ctrl/cmd+click on both the input and the label for suite selection in devmode
2023-05-25 21:00:38 -07:00
Ryosuke Niwa
1bc9ece1f5
Adjust the multiplier to convert geomean so that scores are less than 50 in even the fastest browser/device. (#189) 2023-05-25 16:27:15 -07:00
Brian Grinstead
170710fdad
Additional developerMode controls (#188) 2023-05-25 09:22:05 -07:00
Julien Wajsberg
e0c9ed813e
[editor] Use longer text in the benchmarks (#186) 2023-05-24 16:31:13 -07:00
Thorsten Kober
dca334b044
test path (#187) 2023-05-24 12:19:52 -07:00
Ryosuke Niwa
a876f0e419
Add requestAnimationFrame-based measurement method. (#173)
This PR adds a new query string parameter requestAnimationFrame, which takes
the value of either "timer" or "raf". "timer" is the existing and default
measurement method. When it's "raf", we use two consecutive requestAnimationFrame
first of which runs the sync step and second of which schedules a 0s timer to
signify the end of async step.
2023-05-24 11:50:11 -07:00
Julien Wajsberg
34c169c724
Rename "big" and "small" to "long" and "short" (#184) 2023-05-24 11:33:04 -07:00
Ryosuke Niwa
19184dc694
Refactor test callbacks into TestInvoker class. (#164)
This class abstracts away two nested setTimeout calls and requestAnimationFrame.
2023-05-24 11:16:03 -07:00
Julien Wajsberg
36a320c98e
Remove Stockcharts-Canvas steps (#181)
Fixes #97
2023-05-24 10:53:23 -07:00
Julien Wajsberg
82f7c6e2b8
Add a meta viewport tag so that the viewport ias always the same size on all mobile browsers (#183)
Fixes #130
2023-05-24 10:52:02 -07:00
Thorsten Kober
7c94f49293
use home link (#182) 2023-05-24 10:51:30 -07:00
lpardosixtosMs
acbb9b442b
Tentative Workload: Embed TodoMVC-React in a big DOM with complex CSS. (#150) 2023-05-24 10:50:04 -07:00
Thorsten Kober
dbc708200f
newssite - potential fix for test (#180) 2023-05-24 00:48:13 -07:00
Thorsten Kober
e52236bfc5
adjust test (#178) 2023-05-23 18:39:40 -07:00
Thorsten Kober
372bd7a018
News Site - Navigation workload (#167) 2023-05-23 15:03:08 -07:00
Camillo Bruni
e2e1fcd94b
Add detailed JSON downloads (#146)
For the upcoming comparison view we will need the separate data points which are not available in the existing JSON format.
- Add `Download classic JSON` button
- `Download JSON` and `Copy JSON` now provide a serialized version of the new metrics
- Make sure there are not loops in the metrics structure to JSON.stringify them directly
- Rename "Total" to  "Geomean"
2023-05-23 19:57:15 +02:00
Camillo Bruni
cfa5eccb3b
Format all code (#177) 2023-05-23 19:56:01 +02:00
Brian Grinstead
d8e35c4921
Add a basic developer mode ui on the main runner (#166)
Fixes #158
2023-05-19 17:08:40 -07:00
Thorsten Kober
5cf8184de4
[TodoMVC]: replace crypto.randomUUID (#168) 2023-05-19 16:40:57 -07:00
Thorsten Kober
774b018832
position iframe (#170) 2023-05-19 16:29:25 -07:00
Ryosuke Niwa
2193fda65d
Make confidenceIntervalDelta work with more than 100 degrees of freedom. (#171)
Fixes https://github.com/WebKit/Speedometer/issues/138
This allows the test runner to support an iteration count higher than 100.
2023-05-19 16:25:31 -07:00
Thorsten Kober
40136381fb
add missing map (#169) 2023-05-19 14:18:59 -07:00
Ryosuke Niwa
822906a474
Remove base element from angular example. (#161)
This allows the benchmark to be ran on a subdirectory.
2023-05-19 09:44:30 -07:00
Julien Wajsberg
f196015fa7
Run the build scripts for all vanilla examples (#160) 2023-05-19 09:36:14 -07:00
Julien Wajsberg
898398f07f
Make the number of airports per group configurable and use it in tests (#155)
* Make the number of airports per group configurable

* Add test steps to run drawing the graph with more items in each state
2023-05-18 11:12:23 -07:00
Camillo Bruni
41bdda72e8
Use metrics for CSV (#137)
- Enforce better default order of metrics
- Introduce Metric.separator to be used by metrics-ui and CSV generation
- Use Geomean instead of Total
2023-05-17 01:05:58 +02:00
Camillo Bruni
daa720b67a
Add optional warmup test suite (#124)
We've noticed that we get lazy compilation events for test-runner methods.
By doing running a warmup suite first, we can avoid this addition noise during measurements.

- Add Warmup suite
- Add params.useWarmupSuite option
- Add better performance.measure annotation for all suites
2023-05-16 15:25:11 +02:00
Camillo Bruni
ac59c3fb1e
Use more performance.mark for suites (#154)
- Use more performance.mark for whole suites.
- Pre-build the mark strings upfront
2023-05-15 23:42:27 +02:00
Julien Wajsberg
373580bec9
Add a new charting workload using observable plot (#144)
* Add observable plot

* Add a grouped bar chart

* Add dot chart

* Add a reset button and minor refactoring

* Regenerate

* Cache mapped state for states

* Remove one intermediary array

* Comments tweaking

* Use vite machinery to inject the csv files as string in the production build

* Simplify the test

* Comment the Grouped Chart example

* Review comments: add more comments, extract some values to constants, and various other small things

* Add a README file

* Moved the developer page to developer.html and add a new index

* Remove the graph comments better, remove the unused parameter in the call to parseAirportsInformation and extract the number of airports to keep to a constant

* Remove addGroupedBars and related code

---------

Co-authored-by: Brian Grinstead <briangrinstead@gmail.com>
2023-05-15 13:27:53 -07:00
Thorsten Kober
0c64eb67a9
change title casing for vue todomvc app (#152) 2023-05-12 15:22:16 -07:00
Thorsten Kober
e046a17583
Todomvc angular update / cleanup (#142) 2023-05-12 09:19:16 -07:00
Brian Grinstead
238ffeed42
reduce flashing by setting a light background to better match the content of the tests (#151) 2023-05-11 15:20:16 -07:00
Thorsten Kober
e1e156febb
Todomvc svelte 02 - Optional (#145) 2023-05-02 08:13:43 -07:00
Ryosuke Niwa
19ae86221b
Don't specify the base URL as an absolute path. (#148)
This allows the benchmark to be ran inside another directory in a local HTTP server.
2023-04-20 16:51:30 -07:00
Camillo Bruni
08c4983721
Add Metric.separator (#147)
Use / as new metric separator.
2023-04-19 21:50:17 +02:00
Thorsten Kober
10a3acfb62
TodoMVC preact readme update (#143) 2023-04-18 08:20:54 -07:00
Thorsten Kober
75cddea7ce
Todomvc vue 04 - use component for item (#139) 2023-04-12 11:31:00 -07:00
Thorsten Kober
248bfec3c4
Todomvc svelte 01 (#135) 2023-04-11 16:42:36 -07:00
Thorsten Kober
24b447eeb3
Todomvc angular 01 (#129) 2023-04-11 11:02:11 -07:00
Thorsten Kober
03ea073d46
Todomvc vue 03 - Cleanup (#134) 2023-04-11 10:56:21 -07:00
Thorsten Kober
05b6f7a08d
Todomvc vue Step 01 (#114) 2023-04-07 10:22:11 -07:00
Thorsten Kober
ab819fa534
Todomvc jquery update version (#125) 2023-04-07 10:20:42 -07:00
Brian Grinstead
652fe1bc86
Remove TodoMVC-Ember (#128) 2023-04-05 13:06:35 -07:00
Thorsten Kober
9a4cf52cd6
cleanup (#126) 2023-04-03 18:43:03 -07:00
Thorsten Kober
e9565fbb1f
Ember - Step 2 - format from root / cleanup (#119) 2023-03-30 18:13:18 +02:00
Camillo Bruni
5e6adbfe19
Improve graphs (#123)
- Crop negative values in standard view (no functional change over the current version)
- Show 0-line in normalized view
- Format normalized value tooltip better
- Use camelCase for more variables
2023-03-29 09:25:34 +02:00
Brian Grinstead
bb2ea5f183
Add CSV export (#122) 2023-03-28 15:40:42 -07:00
Brian Grinstead
c1ed921716
make details view fill available space (#121) 2023-03-27 09:02:39 -07:00
Thorsten Kober
1d46644e9b
rename todomvc tests (#118) 2023-03-23 16:24:18 -07:00
Thorsten Kober
612f748595
Todomvc styles - consistent versions (#115) 2023-03-23 14:11:20 -07:00
Thorsten Kober
619b96f901
Todomvc ember step 01 (#117) 2023-03-23 11:19:55 -07:00
Thorsten Kober
bd88f95079
TodoMVC - run format from root (#113) 2023-03-23 08:41:23 -07:00
Thorsten Kober
0744a3eb3a
Todomvc preact 02 (#108) 2023-03-16 15:50:42 -07:00
Thorsten Kober
d75589873f
Todomvc react update (#98) 2023-03-16 15:21:22 -07:00
Thorsten Kober
1afa78682d
Todomvc react redux update (#99) 2023-03-16 15:08:34 -07:00
Camillo Bruni
2e4baeefc4
adding favicon (#111)
Using the the "o" from the speedometer logo.
2023-03-16 20:50:14 +01:00
Camillo Bruni
90562b145f
Use "Enter" instead of "ENTER" for runner KeyboardEvent (#110) 2023-03-16 19:10:12 +01:00
Camillo Bruni
6d04874961
fixing iteration (#109)
- Don't calculate standard deviation for single values
- Center single data points on scatter plots to fix track alignment

This resolves #107
2023-03-16 16:29:24 +01:00
Brian Grinstead
6c31329c83
Emit start-suite and end-suite performance marks outside of the timed region (#106) 2023-03-15 16:52:38 -07:00
Thorsten Kober
1933672b81
Todomvc preact - Phase 1 (#104) 2023-03-15 08:27:53 -07:00
Thorsten Kober
09210bc05e
Todomvc backbone update (#101) 2023-03-14 10:29:04 -07:00
Camillo Bruni
6523f66497
Use Metrics Visualisations from GrandPrix (#42)
- Use scatter-plots for each metrics and submetrcs
- Show tables for each metric
- Use more CSS color variables
- Update scrollbar to match existing color scheme
2023-03-13 14:43:47 +01:00
Camillo Bruni
359605012f
Allow custom section sizes (#92)
This PR makes each runner section independent to allow custom sizes.
This is in preparation for PR #42 to allow more space for the details view.

- Move border layout from `main` to each `section`
- Copy logo into every `section`
- Use grid-layout for header/content/footer in about and details view
2023-03-13 11:18:32 +01:00
Brian Grinstead
f16557016d
Two new tentative editor workloads (#81) 2023-03-10 13:22:44 -08:00
Thorsten Kober
354d5039c4
adjust eslint (#95) 2023-03-10 11:15:46 -08:00
Thorsten Kober
2c78996286
Todomvc es6 webpack updates (#85) 2023-03-09 08:20:13 -08:00
Camillo Bruni
a20855e4a8
Fixing test (#93)
I've accidentally broken the tests by refactoring runner methods.
2023-03-09 17:16:09 +01:00
Thorsten Kober
52d094dd0e
Todomvc es6 updates (#84) 2023-03-09 07:22:32 -08:00
Camillo Bruni
ce1b63420f
Use window.location.hash for navigation (#91)
- Remove history state 
- Prevent accidentally restarting benchmark in `startBenchmark()`
- Add "home" link to about page
- Add explicit logic in `_showSection(...)` for selecting the allowed views
- Use a-href that point to the shown section
- Use CSS rules to show/hide sections (in preparation for making the details full-width)

Visible change: All urls now have a explicit hash (default is `#home`)
2023-03-09 09:40:19 +01:00
Camillo Bruni
ddd75e2206
Improve runner (#90)
- Pre-allocate all mark-label strings to avoid potential gc's during sensitive measurements
- Move code to new separate `_recordTestResults` method in preparation for pr #28
2023-03-09 09:21:51 +01:00
Brian Grinstead
999f4f676b
Initial dev mode param implementation (#89) 2023-03-08 20:33:42 -08:00
Thorsten Kober
33b5e9ae76
Todomvc es5 updates (#82) 2023-03-07 14:58:05 -08:00
Thorsten Kober
6f683630ef
use consistent names (#87) 2023-03-07 13:57:27 -08:00
Camillo Bruni
a068b53a7e
Fix score view (#86)
- Use params directly in MainBenchmarkClient
- Remove params.unit
- Update summary only with score
- Update details view with ms value
- Fix gauge background on chrome by also adding `-webkit-image-set(..)`
2023-03-07 22:03:09 +01:00
Brian Grinstead
24b094d1cc
Use performance.measure api instead of manually tracking with performance.now() (#79) 2023-02-27 12:53:55 -08:00
Thorsten Kober
d8bea6b8d4
remove labs (#80) 2023-02-23 13:32:10 -08:00
Thorsten Kober
bbec60788f
update readme for todoMVC apps (#78) 2023-02-21 15:45:19 -08:00
Camillo Bruni
e884d518b6
Fix the last broken test using _writeMark (#77) 2023-02-21 20:39:28 +01:00
Camillo Bruni
0644776001
Expose frozen globalThis.Suites (#72) 2023-02-21 20:10:16 +01:00
Camillo Bruni
c64ffc8eaa
Remove _writeMark test (#76)
The method has been removed since we're now using performance.now directly.
2023-02-21 20:06:57 +01:00
Camillo Bruni
661426329a
Use params everywhere (#69)
- Add support for filtering multiple suites
- Support startAutomatically in the default runner
- Fix displaying detailed results with unit=ms
- More error messages on invalid url-params
- Add Suites.enable function instead of two separate implementations per runner
2023-02-21 20:05:28 +01:00
Camillo Bruni
cb6cc0652f
Update titles to "Speedometer 3.0" (#75) 2023-02-21 19:56:47 +01:00
Camillo Bruni
d309edfe9d
Minor runner improvements (#74)
- Directly use performance.now and performance.mark
- Reduce a bit of overhead
- Use separate variables for sync / async time measurements to make the code more clear
- Optimize _appendFrame() code by using local variables
2023-02-21 19:52:51 +01:00
Camillo Bruni
207416957e
Add --highlight color CSS variable (#73) 2023-02-21 19:45:00 +01:00
Camillo Bruni
f4eb1ebaa5
Use 2x logo and gauge images (#70)
Make sure we load the hi-res background images in all cases:
- Use srcset for logo
- Use image-set for gauge CSS
2023-02-19 20:48:03 +01:00
Thorsten Kober
bf927da6ba
Remove wait from selenium test (#67) 2023-02-13 19:36:18 -08:00
Thorsten Kober
2fdc20cc2e
Adding prefer-template rule to Eslint (#66)
* run format

* eslint prefer-template

* re-format spacing

* fix string concat

* pr feedback

* rebase main
2023-02-13 11:54:51 -08:00
Camillo Bruni
f20363a3c4
Add buttons to download and copy JSON results (#65)
- Add "Download JSON" button to details view
- Add "Copy JSON" button to details view
- Example JSON result file name: "speedometer_3-2023-02-13T09_18_32.318Z.json"
- Update CSS formatting to make details button all the same width
- Add "title" to results buttons (both summary and details)
2023-02-13 20:44:44 +01:00
Camillo Bruni
1b06e553f9
Use GrandPrix Metric model (#63)
- Use more explicit Metric objects with parent and child-metrics
- Update statistics.mjs to use more ES6 features
2023-02-13 09:44:04 +01:00
Camillo Bruni
7722c8ab06
Prepare layout for more complex details view (#62)
- Use buttons for "about" links
- Make sure about text is readable on all view sizes (adding optional scrollbar)
- Add button-row to allow for more complex buttons in the upcoming details view (see PR "Use Metrics classes from GrandPrix" #42 )
- Add `--foreground-color` and `--text-width` CSS variables
- Hide screen-size warning on startup to avoid flickering
- Fix main view centering by using more detailed calc(...)
2023-02-11 13:35:30 +01:00
Brian Grinstead
0b592ff361
Update LICENSE (#64) 2023-02-10 20:09:27 -08:00
Camillo Bruni
6fcb6e6359
Make progress labels more readable (#60)
- Move current suite label to the left
- Move suite progress to the right
2023-02-09 10:44:10 +01:00
Camillo Bruni
76f4bb1e86
Use Score instead of Runs / Minute (#61)
RPM has always been a bit a confusing term in Speedometer2 as it was never real rpms, rather a scaled value.
To avoid future confusion it is renamed to the more neutral term "Score".
2023-02-09 09:42:13 +01:00
Thorsten Kober
4c22ba45f6
Add Eslint & Prettier (#57) 2023-02-08 13:53:53 -08:00
Camillo Bruni
480c9cee0f
Add command line options to tests/run.mjs (#58)
- Add basic command line options
- Improve error messages in case $BROWSER env variables isn't set
2023-02-07 22:08:31 +01:00
Camillo Bruni
00db41cea3
Add customisable viewport size (#41)
- Introduce params.mjs module that encodes all benchmarking parameters
- Change CSS to rely on --viewport-width and --viewport-height variables for easier resizing
- Update code and tests to rely on the newly added Params object
2023-02-07 22:07:02 +01:00
Thorsten Kober
42b6dd8a8d
Dev setup (#55) 2023-01-30 14:52:46 -08:00
Brian Grinstead
dbff607c76
cast iterationCount from url to number in interactive runner. Fixes #46 (#48) 2023-01-30 09:42:32 -08:00
Brian Grinstead
9566af6fe6
Run mocha test in CI using selenium (#45) 2023-01-26 14:56:57 -08:00
Brian Grinstead
69d5d5bf75
Remove the following tests, as decided in issue #3: (#49)
- EmberJS-Debug
- AngularJS (1)
- Inferno
- Elm
- Flight
2023-01-26 13:37:14 -08:00
Daniel Holbert
c0a1bc139b
Fix typo in CSS property name, in InteractiveRunner.html (#47)
InteractiveRunner.html has a CSS rule which includes a "list-position" property, which is not actually the name of a property.

The intent was surely to use "list-style-position".  Let's just correct it to that.
2023-01-26 13:01:20 -08:00
Houssein Djirdeh
adc3cca860
Adds unit tests for BenchmarkRunner using Mocha (#40) 2023-01-26 09:06:59 -08:00
Doug Thayer
2550aebedb
Unbreak react-stockcharts tests (#44)
I blundered my style fixups and unthinkingly pushed them up without
proper testing.
2023-01-25 22:15:05 -08:00
Doug Thayer
ed6143a8cc
Add react-stockcharts benchmarks (#11)
* Add react-stockcharts tests

* fixup React-Stockcharts for new runner

* Add back React-Stockcharts-SVG

* Added react-stockcharts under tentative directory

* Fix code style of react-stockcharts tests

* Fix up loop index variables
2023-01-25 12:26:17 -08:00
Camillo Bruni
ef8d58c988
Fix interactive runner (#31)
* Make the client callbacks async
* Move the interactive runner into a separate interactive.mjs module
* Update code to use URLSearchParams instead of custom parsing
2023-01-25 13:10:55 +01:00
Camillo Bruni
dce4dfeaa0
Add more Page methods and PageElement (#26)
Migrate all test code to go through Page or PageElement.

- Page defines a fixed interface to interact with a test frame
- PageElement defines a fixed interface to interact with Elements from an iframe

Cleanups:
- Move app initialization and custom code to the specific index.html files
- Update `event.initEvent(...)` code to use the recommended `Event` constructor
- Firefox still requires `initEvent` for the submit events
2022-11-30 19:24:46 +01:00
Camillo Bruni
714b5b4cb2
move methods (#29)
Sort BenchmarRunner run methods in chronological order.
2022-11-23 22:51:52 +01:00
Camillo Bruni
731c9e871d
Async BenchmarkRunner and removing BenchmarkState (#27)
Replace the continuation-based test running with an async-await loop over all suites and tests.
This also removes the necessity of the BenchmarkState class.
2022-11-23 21:07:54 +01:00
Camillo Bruni
e1125d91bc
Add Page object (#23)
Goal: Add a fixed interface to interact with test frames and their DOM nodes.
This will allow us to run more complex scenarios in the future without sacrificing flexibility or performance for the existing tests.

- Add `Page` object
- Refactor tests to interact with the frame only through the Page object
2022-11-22 13:59:43 +01:00
Brian Grinstead
52998e962d
Add development notice (#25) 2022-11-03 16:24:07 -07:00
Camillo Bruni
b840b30f7a
Remove flight-js mailclient (#22)
Remove the flightjs-mailclient app that was not included in the main Suites list.
2022-10-31 18:05:57 +01:00
Camillo Bruni
8787833334
Convert to ES6 Modules (#19)
- Rename files from .js to .mjs (not strictly necessary but it makes clear that we use modules)
- Pull in missing statistics.js file from https://github.com/WebKit/WebKit/blob/main/PerformanceTests/resources/statistics.js
- Restructure UI init code to be all in main.mjs
- Moved free-floating UI code to MainBenchmarkClient
- Added `_isRunning` instance variable to MainBenchmarkClient
- Fix CSS to address removed links
- Use `<button>` instead of links for better keyboard navigation in all browsers
2022-10-31 10:28:25 +01:00
Ryosuke Niwa
1e6af3de5f
Fix indentation after class conversion. (#18) 2022-09-19 10:54:38 -07:00
Camillo Bruni
281b94e7ae
Use ES6 Classes (#17) 2022-09-16 16:05:22 -07:00
Camillo Bruni
694d89c47e
Basic ES6 conversions: let/const and arrow functions (#16)
This is the first, format and function preserving PR to start the ES6.

- Convert var to let/const
- Use arrow functions where possible
2022-09-16 09:53:24 +02:00
Ryosuke Niwa
600ccf0e64
Delete SimplePromise and use regular native Promise. (#14)
Also deleted the unused _waitAndWarmUp.
2022-08-26 13:04:25 -07:00
Brian Grinstead
9bae5f5ef1
Add readme with goals for the benchmark (#13) 2022-08-23 20:37:48 -07:00
Ryosuke Niwa
3ceae4f0e9
Add Speedometer Governance Policy (#10) 2022-07-28 20:09:12 -07:00
Ryosuke Niwa
6b63846bad
Reviewed by Addy Osmani and Jonathan Bedard. (#2)
Updated the copyright.

Apple started contribution in 2013: http://commits.webkit.org/r156073.
Google contributed in 2017: https://commits.webkit.org/r216706 through https://commits.webkit.org/r222534.

* LICENSE
2022-06-29 11:40:19 -07:00
Ryosuke Niwa
a1e842164b
Add Speedometer 2.1 content. (#1)
Reviewed by Yusuke Suzuki, Addy Osmani, and Julian Descottes.

* InteractiveRunner.html: Added.
* index.html Added.
* resources: Added.
2022-06-06 08:50:03 -07:00
1835 changed files with 528192 additions and 1 deletions

50
.eslintignore Normal file
View file

@ -0,0 +1,50 @@
**/node_modules
**/dist
package-lock.json
**/.angular
/suites/todomvc/architecture-examples/*
!/suites/todomvc/architecture-examples/angular
!/suites/todomvc/architecture-examples/angular-complex
!/suites/todomvc/architecture-examples/backbone
!/suites/todomvc/architecture-examples/backbone-complex
!/suites/todomvc/architecture-examples/lit
/suites/todomvc/architecture-examples/lit/.wireit
/suites/todomvc/architecture-examples/lit/dist/index.js
!/suites/todomvc/architecture-examples/lit-complex
!/suites/todomvc/architecture-examples/preact
!/suites/todomvc/architecture-examples/preact-complex
!/suites/todomvc/architecture-examples/react
!/suites/todomvc/architecture-examples/react-complex
!/suites/todomvc/architecture-examples/react-redux
!/suites/todomvc/architecture-examples/react-redux-complex
!/suites/todomvc/architecture-examples/svelte
!/suites/todomvc/architecture-examples/svelte-complex
!/suites/todomvc/architecture-examples/vue
!/suites/todomvc/architecture-examples/vue-complex
/suites/todomvc/vanilla-examples/*
!/suites/todomvc/vanilla-examples/javascript-es5
!/suites/todomvc/vanilla-examples/javascript-es5-complex
!/suites/todomvc/vanilla-examples/javascript-es6
!/suites/todomvc/vanilla-examples/javascript-es6-complex
!/suites/todomvc/vanilla-examples/javascript-es6-webpack
!/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex
!/suites/todomvc/vanilla-examples/javascript-web-components
!/suites/todomvc/vanilla-examples/javascript-web-components-complex/
!/suites/todomvc/big-dom-generator
!/suites/editors
/suites/editors/assets
!/suites/newssite
!/suites/newssite/news-next
!/suites/newssite/news-nuxt
!/suites/newssite/news-site-css
/suites/perf.webkit.org
/suites/react-stockcharts
/suites-experimental/responsive-design/build-dev/

101
.eslintrc Normal file
View file

@ -0,0 +1,101 @@
/*
* Copyright (C) 2014 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
{
"root": true,
"extends": ["eslint:recommended", "plugin:@typescript-eslint/base"],
"env": {
"browser": true,
"node": true,
"worker": true,
"es2022": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module"
},
"globals": {
// unit test specific
"sinon": true,
"expect": true,
// injected dependencies
"testRunner": true,
"startBenchmark": true,
"PerfTestRunner": true,
"deltas": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
"block-spacing": [2, "always"],
"brace-style": 2,
"comma-dangle": [2, "only-multiline"],
"comma-spacing": [2, { "before": false, "after": true }],
"comma-style": [2, "last"],
"consistent-return": 2,
"curly": ["error", "multi-or-nest", "consistent"],
"dot-notation": 0,
"eol-last": 2,
"eqeqeq": 2,
"indent": ["error", 4, { "SwitchCase": 1 }],
"func-call-spacing": [2, "never"],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
"keyword-spacing": [2, { "before": true, "after": true }],
"new-cap": 0,
"new-parens": 0,
"no-console": 0,
"no-constant-condition": 0,
"no-extra-bind": 2,
"no-extra-parens": [2, "all", { "nestedBinaryExpressions": false }],
"no-extra-semi": 2,
"no-global-assign": 2,
"no-inner-declarations": 0,
"no-multi-spaces": 2,
"no-multiple-empty-lines": ["error", { "max": 1 }],
"no-new": 0,
"no-proto": 0,
"no-redeclare": 0,
"no-return-assign": 2,
"no-return-await": 2,
"no-self-compare": 2,
"no-shadow": 0,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-underscore-dangle": 0,
"no-unused-expressions": 2,
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
"no-use-before-define": 0,
"nonblock-statement-body-position": ["error", "below"],
"object-curly-spacing": ["error", "always"],
"operator-linebreak": ["error", "before"],
"prefer-template": 2,
"quotes": [2, "double", { "avoidEscape": true }],
"semi": 2,
"semi-spacing": [2, { "before": false, "after": true }],
"space-infix-ops": 2,
"spaced-comment": [2, "always"],
"strict": 0,
"valid-typeof": 2
}
}

97
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,97 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
linters:
name: Linters
runs-on: macos-latest
steps:
- name: Checkout Branch
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: "npm"
- name: Install Node Packages
run: npm ci
- name: Run package.json linter
run: npm run lint:pkg
- name: Run linters
run: npm run format
- name: Check if anything changed
run: |
git_status="`LC_ALL=C git status --porcelain --ignore-submodules -unormal 2>&1`"
if [ -n "$git_status" ]; then
printf "Some file(s) changed as the result of formatting, this means that you need to run the formatter on your patch.\n"
printf "Here is what changed:\n"
printf -- "$git_status\n\n"
printf "And here is the diff:\n"
git diff -U8
exit 1
fi
test-node:
name: Node Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Branch
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: "npm"
- name: Install Node Packages
run: npm ci
- name: Run Node Unit Tests
run: npm test
build:
name: Build
runs-on: macos-latest
strategy:
matrix:
browser: [chrome, firefox, safari]
steps:
- name: Install Firefox
if: ${{ matrix.browser == 'firefox' }}
run: brew install --cask firefox
- name: Checkout Branch
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: "npm"
- name: Install Node Packages
run: npm ci
- name: Run Unit Tests
run: |
echo "Running in $BROWSER"
npm run test:${{ matrix.browser }}
- name: Run end2end Tests
run: |
echo "Running in $BROWSER"
npm run test-e2e:${{ matrix.browser }}

9
.gitignore vendored
View file

@ -2,3 +2,12 @@
*.pyc
.DS_Store
.directory
node_modules/
.agents
.gemini
.vscode
.angular
/suites-experimental/**/build-dev/

View file

@ -0,0 +1,3 @@
{
"extends": "npm-package-json-lint-config-default"
}

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict = true

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
24

50
.prettierignore Normal file
View file

@ -0,0 +1,50 @@
**/node_modules
**/dist
package-lock.json
**/.angular
/suites/todomvc/architecture-examples/*
!/suites/todomvc/architecture-examples/angular
!/suites/todomvc/architecture-examples/angular-complex
!/suites/todomvc/architecture-examples/backbone
!/suites/todomvc/architecture-examples/backbone-complex
!/suites/todomvc/architecture-examples/lit
/suites/todomvc/architecture-examples/lit/dist/index.js
/suites/todomvc/architecture-examples/lit/.wireit
!/suites/todomvc/architecture-examples/lit-complex
!/suites/todomvc/architecture-examples/preact
!/suites/todomvc/architecture-examples/preact-complex
!/suites/todomvc/architecture-examples/react
!/suites/todomvc/architecture-examples/react-complex
!/suites/todomvc/architecture-examples/react-redux
!/suites/todomvc/architecture-examples/react-redux-complex
!/suites/todomvc/architecture-examples/svelte
!/suites/todomvc/architecture-examples/svelte-complex
!/suites/todomvc/architecture-examples/vue
!/suites/todomvc/architecture-examples/vue-complex
/suites/todomvc/vanilla-examples/*
!/suites/todomvc/vanilla-examples/javascript-es5
!/suites/todomvc/vanilla-examples/javascript-es5-complex
!/suites/todomvc/vanilla-examples/javascript-es6
!/suites/todomvc/vanilla-examples/javascript-es6-complex
!/suites/todomvc/vanilla-examples/javascript-es6-webpack
!/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex
!/suites/todomvc/vanilla-examples/javascript-web-components
!/suites/todomvc/vanilla-examples/javascript-web-components-complex/
!/suites/todomvc/big-dom-generator
!/suites/editors
/suites/editors/assets
!/suites/newssite
!/suites/newssite/news-next
!/suites/newssite/news-nuxt
!/suites/newssite/news-site-css
/suites/perf.webkit.org
/suites/react-stockcharts
/suites-experimental/responsive-design/build-dev/
/suites-experimental/responsive-design/**/*.generated.css

13
.prettierrc Normal file
View file

@ -0,0 +1,13 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"printWidth": 250,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "es5",
"useTabs": false,
"plugins": ["prettier-plugin-tailwindcss"]
}

34
Development.md Normal file
View file

@ -0,0 +1,34 @@
# Development Environment
## Prerequisite
Node is required for local development.
We recommend using a Node version manager ([nvm](https://github.com/nvm-sh/nvm) for example) to ensure a stable development environment is used.
Below are the required node / npm versions:
```sh
"node": ">18.13.0",
"npm": ">8.19.3"
```
## Installation
In your working directory, open terminal and paste the following commands:
```sh
git clone https://github.com/WebKit/Speedometer.git
cd Speedometer
npm install
```
## Run Development Server
1. In your terminal run:
```sh
PORT=8900 npm run dev # Passing the env var PORT is optional
```
2. Open your browser of choice and navigate to [http://127.0.0.1:8900](http://127.0.0.1:8900)
## Local Server
Speedometer uses [http-server](https://github.com/http-party/http-server), which is a static HTTP server. Meaning it does not provide hot-reloading. By default, the dev script disables caching and local changes can be viewed by simply refreshing your browser window. Additional options of the http-server can be found [here](https://github.com/http-party/http-server#available-options).

34
Governance.md Normal file
View 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.

View file

@ -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

38
README.md Normal file
View file

@ -0,0 +1,38 @@
# What is Speedometer?
Speedometer is a benchmark for web browsers that measures Web application responsiveness
by timing simulated user interactions on various workloads. Our primary goal is to make it
reflect the real-world Web as much as possible. When a browser improves its score on the
benchmark, actual users should benefit. In order to achieve this, it should:
- Test end-to-end user journeys instead of testing specific features in a tight loop. Each
test should exercise the full set of what’s needed from the engine in order for a user to
accomplish a task.
- Evolve over time, adapting to the present Web on a regular basis. This should be informed
by current usage data, and by consensus about features which are important for engines to
optimize to provide a consistent experience for users and site authors.
- Be accessible to the public and useful to browser engineers. It should run in every modern
browser by visiting a normal web page. It should run relatively quickly, while providing
enough test coverage to be reflective of the real-world Web.
# Score and Test Measurement
Each test can contain several steps who contributed to the test duration. Note that the prepare step of a test is _unmeasured_ and thus does not contribute to the score. All following test
steps are **measured** and summed up in the test step time.
<img src="./resources/measurement-timeline.svg" />
Each step consists of the following phases:
<dl>
<dt>Sync:</dt>
<dd>Time spent in synchronous JS execution.</dd>
<dt>Async:</dt>
<dd>Time spent in asynchronous JS like Promise or setTimeout tasks scheduled by the sync-step.</dd>
</dl>
## Score
The final benchmark score is calculated based of the inverse of [geomean](https://en.wikipedia.org/wiki/Geometric_mean) of all tests. We average the score over multiple iterations with the [arithmetic mean](https://en.wikipedia.org/wiki/Arithmetic_mean). By using the geomean to combine the test durations we maintain the invariant that relative improvements are favoured equally amongst all tests, even though they can have vastly different durations.
<img src="./resources/score.svg" />

39
Testing.md Normal file
View file

@ -0,0 +1,39 @@
# Testing
Speedometer uses [Selenium](https://www.selenium.dev/) for testing of the application itself.
Tests are located in the `/tests` folder.
[Sinon](https://sinonjs.org/): Standalone test spies, stubs and mocks for JavaScript.
[Mocha](https://mochajs.org/): Testing framework.
## Local Testing
### Local Node.js Unit Testing
You can run the unit tests directly in Node.js using:
```bash
npm run test:node
```
### In-Browser Testing
To run the in-browser tests locally, you'll need the browsers installed along with the corresponding driver:
- [chromedriver](https://chromedriver.chromium.org/getting-started)
- [geckodriver](https://github.com/mozilla/geckodriver/releases)
- [safaridriver](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari)
- [msedgedriver](https://developer.microsoft.com/microsoft-edge/tools/webdriver).
Once installed you can run the following scripts:
```bash
npm run test:chrome
npm run test:firefox
npm run test:safari
npm run test:edge
```
## Automated Testing
Currently Speedometer's tests run automatically, when pushing to the main branch or when opening a pr.

105
about.html Normal file
View file

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer About</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
</head>
<body>
<main>
<section id="about" class="visible" data-title="About">
<a href="./" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version next">next</div>
</a>
<div class="section-grid">
<h1 class="section-header">About Speedometer</h1>
<div class="section-content">
<p>Speedometer is a benchmark for web browsers that measures Web application responsiveness by timing simulated user interactions on various workloads.</p>
<p>
It's developed as an open source project, with decisions being made under a <a href="https://github.com/WebKit/Speedometer/blob/main/Governance.md">multistakeholder governance model</a> between the three widely distributed
web browser engine projects.
</p>
<p>
The following high level user journeys are implemented in the current version. Each of these journeys has one or more workloads which test important aspects of it — for example commonly used patterns, frameworks, or
technologies.
</p>
<ul>
<li>Working with a todo list</li>
<ul>
<li>Measures the time to add, complete, and remove 100 todo items in a basic list.</li>
<li>Each example implements the same todo application (TodoMVC) using different techniques and frameworks.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es5" target="_blank">TodoMVC-JavaScript-ES5</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex" target="_blank">TodoMVC-JavaScript-ES6-Webpack-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-web-components" target="_blank">TodoMVC-WebComponents</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-complex" target="_blank">TodoMVC-React-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-redux" target="_blank">TodoMVC-React-Redux</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/backbone" target="_blank">TodoMVC-Backbone</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/angular-complex" target="_blank">TodoMVC-Angular-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/vue" target="_blank">TodoMVC-Vue</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/jquery" target="_blank">TodoMVC-jQuery</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/preact-complex" target="_blank">TodoMVC-Preact-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/svelte-complex" target="_blank">TodoMVC-Svelte-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/lit-complex" target="_blank">TodoMVC-Lit-Complex-DOM</a>
</li>
</ul>
<li>Editing rich text</li>
<ul>
<li>Loading and styling text inside WYSIWYG and code editors.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-CodeMirror</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-TipTap</a>
</li>
</ul>
<li>Rendering charts</li>
<ul>
<li>Loading and interacting with SVG and canvas charts.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-observable-plot</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-chartjs</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/react-stockcharts" target="_blank">React-Stockcharts-SVG</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/perf.webkit.org" target="_blank">Perf-Dashboard</a>
</li>
</ul>
<li>Reading a news site</li>
<ul>
<li>Navigating across pages and interacting with a typical looking news site.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-next" target="_blank">NewsSite-Next</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-nuxt" target="_blank">NewsSite-Nuxt</a>
</li>
</ul>
</ul>
<p class="note"><strong>Notes about methodology</strong></p>
<ul>
<li>Although user-driven actions like mouse movements and keyboard input cannot be fully emulated in JavaScript, Speedometer does its best to faithfully replay a typical workload within the demo applications.</li>
<li>To make the run time long enough to measure with the limited precision, we synchronously execute a large number of the operations, such as adding one hundred to-do items.</li>
<li>
Modern browser engines execute some work asynchronously as an optimization strategy to reduce the run time of synchronous operations. While returning control back to JavaScript execution as soon as possible is worth
pursuing, the run time cost of such an asynchronous work should still be taken into a holistic measurement of web application performance. In addition, some JavaScript frameworks call into DOM APIs asynchronously as an
optimization technique. Speedometer approximates the run time of this asynchronous work in the UI thread with a zero-second timer that is scheduled immediately after each execution of synchronous operations.
</li>
<li>Speedometer does not attempt to measure concurrent asynchronous work (e.g. in Web Workers).</li>
<li>Speedometer should not be used as a way to compare the performance of different JavaScript frameworks.</li>
<li>
The goal of all workloads is to represent a scenario that could be found on the Web. Although all workloads strive to use patterns that are commonly used, some implementation details are Speedometer specific and should
not be used as a guideline on how to implement and deploy a standalone app. For example, due to constraints within the test harness, workloads must not depend on a server infrastructure to function properly and are
built as static files ahead of time.
</li>
</ul>
</div>
<div class="buttons section-footer fixed-footer">
<div class="button-row">
<a class="button" href="./" title="Show main section.">Home</a>
<a class="button" href="instructions.html" title="Show test instructions.">Test Instructions</a>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

10
babel.config.json Normal file
View file

@ -0,0 +1,10 @@
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}

131
index.html Normal file
View file

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html data-visible-section="home">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer Next</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
<script src="resources/main.mjs" type="module"></script>
</head>
<body>
<main>
<section id="home">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version next">next</div>
</a>
<div class="section-grid">
<div class="section-content content">
<p class="top-row">Speedometer is a browser benchmark that measures the responsiveness of Web applications. It uses demo web applications to simulate user actions such as adding to-do items.</p>
<p id="screen-size-warning">
<strong>
Your browser window is too small. For most accurate results, please make the view port size at least <span id="min-screen-width">850px</span> by <span id="min-screen-height">650px</span>.<br />
It's currently <span id="screen-size"></span>.
</strong>
</p>
</div>
<div class="buttons">
<div class="button-row">
<button class="start-tests-button primary-button">Start Test</button>
</div>
</div>
<div class="buttons section-footer fixed-footer">
<div class="button-row">
<a class="button" href="about.html">About Speedometer</a>
<a class="button" href="instructions.html">Test Instructions</a>
</div>
</div>
</div>
</section>
<section id="running">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version next">next</div>
</a>
<div id="testContainer"></div>
<div id="progress">
<progress aria-label="Progress" id="progress-completed"></progress>
</div>
<div id="info">
<div id="info-label"></div>
<div id="info-progress"></div>
</div>
</section>
<section id="summary" data-title="Results Summary" class="valid">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version next">next</div>
</a>
<h1>Score</h1>
<div class="gauge">
<div class="window"><div class="needle"></div></div>
</div>
<hr />
<div id="result-number"></div>
<div id="confidence-number"></div>
<div id="invalid-score-text">
One or more subtests produced no duration.<br />
Please check your <a href="./instructions.html" target="_blank">browser settings</a> and re-run the benchmark.<br />
</div>
<div class="buttons">
<div class="button-row">
<a class="button primary-button" href="#details" id="show-details" title="Show detailed results data.">Details</a>
</div>
</div>
</section>
<section id="details" data-title="Results Details">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version next">next</div>
</a>
<div class="section-grid">
<h1 class="section-header">Detailed Results</h1>
<div class="section-content all-metric-results">
<div class="non-standard-params">
<h2>Non-standard Parameters</h2>
<p>
Speedometer ran with non-standard parameters.<br />
The results are likely not comparable to default runs.
</p>
<table id="non-standard-params-table">
<thead>
<tr>
<th>Param</th>
<th>Value</th>
<th>Default</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="aggregated-metric-result">
<h2>Aggregate Metric</h2>
<div id="aggregate-chart"></div>
<h2>Test Metrics Overview</h2>
<div id="tests-chart"></div>
</div>
<br />
<h2>Test Metrics Details</h2>
<div id="metrics-results"></div>
</div>
<div class="buttons section-footer">
<div class="button-row">
<a class="button" href="#summary" title="Go back to the simplified summary view.">Summary</a>
</div>
<div class="button-row export-buttons">
<button id="copy-full-json" title="Copy full result metrics as json string.">Copy JSON</button>
<a class="button" id="download-full-json" title="Download full result metrics as json string.">Download JSON</a>
<a class="button" id="download-classic-json" title="Download backwards-compatible result metrics as json string.">Download Classic JSON</a>
<a class="button" id="download-csv" title="Download all result metrics as CSV string.">Download CSV</a>
<button id="copy-csv" title="Copy all result metrics as CSV string.">Copy CSV</button>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

42
instructions.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer Test Instructions</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
</head>
<body>
<main>
<section id="instructions" class="visible" data-title="Test Instructions">
<a href="./" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version next">next</div>
</a>
<div class="section-grid">
<h1 class="section-header">Speedometer General Instructions</h1>
<div class="section-content">
<p>To get reasonably accurate results your device and browser need to be focused on the benchmark. Here are some basic instructions to allow that:</p>
<ul class="compact">
<li>Ensure you are running the latest stable browser version.</li>
<li>Prefer using a separate clean browser profile — extensions and non-default browser settings can have a large impact on the score.</li>
<li>Don't run any programs in the background.</li>
<li>Restart the browser and close all other tabs and windows.</li>
<li>Keep the Speedometer page focused and do not interact with the device while running the benchmark.</li>
<li>Keep your battery-powered device plugged into a charger.</li>
<li>Some devices might need time to cool down between consecutive runs.</li>
</ul>
</div>
<div class="buttons section-footer fixed-footer">
<div class="button-row">
<a class="button" href="./" title="Show main section.">Home</a>
<a class="button" href="about.html" title="Show about section.">About</a>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

9
netlify.toml Normal file
View file

@ -0,0 +1,9 @@
[[headers]]
for = "/*"
[headers.values]
cross-origin-embedder-policy = "require-corp"
cross-origin-opener-policy = "same-origin"
X-Frame-Options = "SAMEORIGIN"
x-content-type-options = "nosniff"
[build.environment]
NODE_VERSION = "24"

10205
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

68
package.json Normal file
View file

@ -0,0 +1,68 @@
{
"name": "speedometer",
"version": "4.0.0-alpha",
"description": "An open source repository for the Speedometer benchmark.",
"engines": {
"node": ">=24.0.0",
"npm": ">=8.19.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WebKit/Speedometer.git"
},
"bugs": {
"url": "https://github.com/WebKit/Speedometer/issues"
},
"license": "SEE LICENSE IN LICENSE",
"scripts": {
"dev": "node tests/server.mjs",
"lint:check": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\"",
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix",
"lint:pkg": "npmPkgJsonLint \"**/package.json\" --ignore-pattern \"**/node_modules/**\"",
"pretty:check": "prettier --check ./",
"pretty:fix": "prettier --write ./",
"format": "node tests/format.mjs",
"format:all": "node tests/format.mjs --all",
"test:chrome": "node tests/run-unittests.mjs --browser chrome",
"test:firefox": "node tests/run-unittests.mjs --browser firefox",
"test:safari": "node tests/run-unittests.mjs --browser safari",
"test:edge": "node tests/run-unittests.mjs --browser edge",
"test-e2e:chrome": "node tests/run-end2end.mjs --browser chrome",
"test-e2e:firefox": "node tests/run-end2end.mjs --browser firefox",
"test-e2e:safari": "node tests/run-end2end.mjs --browser safari",
"test-e2e:edge": "node tests/run-end2end.mjs --browser edge",
"test": "npm run test:node",
"test:node": "mocha --require ./tests/setup-node.mjs \"tests/unittests/*.mjs\""
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/eslint-parser": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@next/eslint-plugin-next": "^13.4.5",
"@nuxt/eslint-config": "^0.1.1",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^6.1.3",
"es-main": "^1.3.0",
"eslint": "^8.38.0",
"eslint-plugin-ember": "^11.4.8",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-vue": "^9.10.0",
"expect.js": "^0.3.1",
"lws": "^4.2.0",
"lws-cors": "^4.2.1",
"lws-index": "^3.1.1",
"lws-log": "^3.0.0",
"lws-static": "^3.1.1",
"mocha": "^10.2.0",
"normalize-package-data": "^8.0.0",
"npm-package-json-lint": "^10.4.1",
"npm-package-json-lint-config-default": "^9.0.1",
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.4.1",
"selenium-webdriver": "^4.44.0",
"sinon": "^17.0.1",
"typescript": "~5.1.0"
}
}

View file

@ -0,0 +1,163 @@
// example url for local testing:
// http://localhost:8080/?developerMode=true&config=http://localhost:8080/resources/config.json
// since the json doesn't contain a default suite, dismiss warning popups and select from the developerMenu
import { DefaultSuites } from "../suites/default-suites.mjs";
import { ExperimentalSuites } from "../suites-experimental/suites.mjs";
import { params } from "./shared/params.mjs";
const DEFAULT_TAGS = ["all", "default", "experimental"];
const DISALLOWED_DOMAINS = ["browserbench.org"];
export class BenchmarkConfigurator {
#tags = new Set(DEFAULT_TAGS);
#suites = [];
get tags() {
return this.#tags;
}
get suites() {
return this.#suites;
}
/**
* Checks if a given string is a valid URL, supporting both absolute and relative paths.
*
* This function attempts to construct a URL object. For relative paths, it uses
* a dummy base URL to allow the URL constructor to parse them successfully.
*
* @param {string} url The URL string to validate.
* @returns {boolean} True if the URL is valid (absolute or relative), false otherwise.
*/
_isValidUrl(url) {
if (typeof url !== "string" || url.length === 0)
return false;
try {
new URL(url, "http://www.example.com");
return true;
} catch (error) {
return false;
}
}
/**
* Reports an error by showing an alert and logging a message to the console.
* @private
* @param {string} message The error message to display.
* @param {object} [debugInfo] An optional object containing additional debug information.
*/
_reportError(message, debugInfo) {
alert(message);
console.error(message, debugInfo);
}
_freezeSuites() {
Object.freeze(this.#suites);
this.#suites.forEach((suite) => {
if (!suite.tags)
suite.tags = [];
if (suite.url.startsWith("experimental/"))
suite.tags.unshift("all", "experimental");
else
suite.tags.unshift("all");
suite.enabled = suite.tags.includes("default");
Object.freeze(suite.tags);
Object.freeze(suite.steps);
});
}
_freezeTags() {
Object.freeze(this.#tags);
}
async init() {
if (params.config) {
try {
const benchmarkUrl = new URL(window.location);
if (DISALLOWED_DOMAINS.some((domain) => benchmarkUrl.hostname.endsWith(domain))) {
console.warn("Configuration fetch not allowed. Loading default suites.");
this._loadSuites();
return;
}
const response = await fetch(params.config);
if (!response.ok)
throw new Error(`Could not fetch config: ${response.status}`);
const config = await response.json();
if (!config || !Array.isArray(config.suites))
throw new Error("Could not find a valid config structure!");
config.suites.flatMap((suite) => suite.tags || []).forEach((tag) => this.#tags.add(tag));
config.suites.forEach((suite) => {
if (suite && suite.url && this._isValidUrl(suite.url))
this.#suites.push(suite);
else
throw new Error("Invalid suite data");
});
} catch (error) {
console.warn(`Error loading custom configuration: ${error.message}. Loading default suites.`);
this._loadSuites();
}
} else {
this._loadSuites();
}
this._freezeTags();
this._freezeSuites();
}
_loadSuites() {
DefaultSuites.forEach((suite) => this._loadSuite(suite));
ExperimentalSuites.forEach((suite) => this._loadSuite(suite));
}
_loadSuite(suite) {
suite.tags.forEach((tag) => this.#tags.add(tag));
this.#suites.push(suite);
}
enableSuites(names, tags) {
if (names?.length) {
const lowerCaseNames = names.map((each) => each.toLowerCase());
this.#suites.forEach((suite) => {
suite.enabled = lowerCaseNames.includes(suite.name.toLowerCase());
});
} else if (tags?.length) {
tags.forEach((tag) => {
if (!this.#tags.has(tag))
console.error(`Unknown Suites tag: "${tag}"`);
});
const tagsSet = new Set(tags);
this.#suites.forEach((suite) => {
suite.enabled = suite.tags.some((tag) => tagsSet.has(tag));
});
} else {
console.warn("Neither names nor tags provided. Enabling all default suites.");
this.#suites.forEach((suite) => {
suite.enabled = suite.tags.includes("default");
});
}
if (this.#suites.some((suite) => suite.enabled))
return;
if (names?.length) {
this._reportError(`Suites "${names}" does not match any Suite. No tests to run.`, {
providedNames: names,
validNames: this.#suites.map((each) => each.name),
});
} else if (tags?.length) {
this._reportError(`Tags "${tags}" does not match any Suite. No tests to run.`, {
providedTags: tags,
validTags: Array.from(this.#tags),
});
}
}
}
const benchmarkConfigurator = new BenchmarkConfigurator();
await benchmarkConfigurator.init();
export { benchmarkConfigurator };

View file

@ -0,0 +1,539 @@
import { Metric } from "./metric.mjs";
import { params } from "./shared/params.mjs";
import { forceLayout } from "./shared/helpers.mjs";
import { SUITE_RUNNER_LOOKUP } from "./suite-runner.mjs";
const performance = globalThis.performance;
export class BenchmarkTestStep {
constructor(testName, testFunction) {
this.name = testName;
this.run = testFunction;
}
}
function getParent(lookupStartNode, path) {
const parent = path.reduce((root, selector) => {
const node = root.querySelector(selector);
return node.shadowRoot ?? node;
}, lookupStartNode);
return parent;
}
class Page {
constructor(frame) {
this._frame = frame;
}
getLocalStorage() {
return this._frame.contentWindow.localStorage;
}
layout() {
const body = this._frame ? this._frame.contentDocument.body : document.body;
const value = forceLayout(body, params.layoutMode);
body._leakedLayoutValue = value; // Prevent dead code elimination.
}
async waitForElement(selector) {
return new Promise((resolve) => {
const resolveIfReady = () => {
const element = this.querySelector(selector);
let callback = resolveIfReady;
if (element)
callback = () => resolve(element);
window.requestAnimationFrame(callback);
};
resolveIfReady();
});
}
/**
* Returns the first element within the document that matches the specified selector, or group of selectors.
* If no matches are found, null is returned.
*
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
*
* @example
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
* // return PageElement(<todo-item>)
* querySelector("todo-item", ["todo-app", "todo-list"]);
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns PageElement | null
*/
querySelector(selector, path = []) {
const lookupStartNode = this._frame.contentDocument;
const element = getParent(lookupStartNode, path).querySelector(selector);
if (element === null)
return null;
return this._wrapElement(element);
}
/**
* Returns all elements within the document that matches the specified selector, or group of selectors.
* If no matches are found, null is returned.
*
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
*
* @example
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
* // return [PageElement(<todo-item>), PageElement(<todo-item>)]
* querySelectorAll("todo-item", ["todo-app", "todo-list"]);
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns array
*/
querySelectorAll(selector, path = []) {
const lookupStartNode = this._frame.contentDocument;
const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector));
for (let i = 0; i < elements.length; i++)
elements[i] = this._wrapElement(elements[i]);
return elements;
}
getElementById(id) {
const element = this._frame.contentDocument.getElementById(id);
if (element === null)
return null;
return this._wrapElement(element);
}
call(functionName) {
this._frame.contentWindow[functionName]();
return null;
}
callAsync(functionName) {
setTimeout(() => {
this._frame.contentWindow[functionName]();
}, 0);
}
callToGetElement(functionName) {
return this._wrapElement(this._frame.contentWindow[functionName]());
}
setWidth(widthPx) {
this._frame.style.width = `${widthPx}px`;
}
_wrapElement(element) {
return new PageElement(element);
}
addEventListener(name, listener, options) {
this._frame.contentWindow.addEventListener(name, listener, options);
}
}
const NATIVE_OPTIONS = {
bubbles: true,
cancellable: true,
};
class PageElement {
#node;
constructor(node) {
this.#node = node;
}
setValue(value) {
this.#node.value = value;
}
click() {
this.#node.click();
}
focus() {
this.#node.focus();
}
getElementByMethod(name) {
return new PageElement(this.#node[name]());
}
scrollIntoView(options) {
this.#node.scrollIntoView(options);
}
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
if (eventName === "submit")
// FIXME FireFox doesn't like `new Event('submit')
this._dispatchSubmitEvent();
else
this.#node.dispatchEvent(new eventType(eventName, options));
}
_dispatchSubmitEvent() {
const submitEvent = document.createEvent("Event");
submitEvent.initEvent("submit", true, true);
this.#node.dispatchEvent(submitEvent);
}
enter(type, options = undefined) {
const ENTER_KEY_CODE = 13;
return this.dispatchKeyEvent(type, ENTER_KEY_CODE, "Enter", options);
}
dispatchKeyEvent(type, keyCode, key, options) {
let eventOptions = { bubbles: true, cancelable: true, keyCode, which: keyCode, key };
if (options !== undefined)
eventOptions = Object.assign(eventOptions, options);
const event = new KeyboardEvent(type, eventOptions);
this.#node.dispatchEvent(event);
}
dispatchMouseEvent(type, offsetX, offsetY, options) {
const boundingRect = this.#node.getBoundingClientRect();
const clientX = offsetX + boundingRect.x;
const clientY = offsetY + boundingRect.y;
const contentWindow = this.#node.ownerDocument.defaultView;
const screenX = clientX + contentWindow.screenX;
const screenY = clientY + contentWindow.screenY;
let eventOptions = { bubbles: true, cancelable: true, clientX, clientY, screenX, screenY };
if (options !== undefined)
eventOptions = Object.assign(eventOptions, options);
const event = new contentWindow.MouseEvent(type, eventOptions);
this.#node.dispatchEvent(event);
}
/**
* Returns the first element found in a node of a PageElement that matches the specified selector, or group of selectors. If a shadow DOM is present in the node, the shadow DOM is used to query.
* If no matches are found, null is returned.
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns PageElement | null
*/
querySelectorInShadowRoot(selector, path = []) {
const lookupStartNode = this.#node.shadowRoot ?? this.#node;
const element = getParent(lookupStartNode, path).querySelector(selector);
if (element === null)
return null;
return new PageElement(element);
}
querySelector(selector) {
const element = this.#node.querySelector(selector);
if (element === null)
return null;
return new PageElement(element);
}
}
function geomeanToScore(geomean) {
return 1000 / geomean;
}
// The WarmupSuite is used to make sure all runner helper functions and
// classes are compiled, to avoid unnecessary pauses due to delayed
// compilation of runner methods in the middle of the measuring cycle.
export const WarmupSuite = {
name: "Warmup",
url: "warmup/index.html",
async prepare(page) {
await page.waitForElement("#testItem");
},
tests: [
// Make sure to run ever page.method once at least
new BenchmarkTestStep("WarmingUpPageMethods", (page) => {
let results = [];
results.push(page.querySelector(".testItem"));
results.push(page.querySelectorAll(".item"));
results.push(page.getElementById("testItem"));
}),
new BenchmarkTestStep("WarmingUpPageElementMethods", (page) => {
const item = page.getElementById("testItem");
item.setValue("value");
item.click();
item.focus();
item.dispatchEvent("change");
item.enter("keypress");
item.dispatchEvent("input");
item.enter("keyup");
}),
new BenchmarkTestStep("WarmingUpPageElementMouseMethods", (page) => {
const item = page.getElementById("testItem");
const mouseEventOptions = { clientX: 100, clientY: 100, bubbles: true, cancelable: true };
const wheelEventOptions = {
clientX: 200,
clientY: 200,
deltaMode: 0,
delta: -10,
deltaY: -10,
bubbles: true,
cancelable: true,
};
item.dispatchEvent("mousedown", mouseEventOptions, MouseEvent);
item.dispatchEvent("mousemove", mouseEventOptions, MouseEvent);
item.dispatchEvent("mouseup", mouseEventOptions, MouseEvent);
item.dispatchEvent("wheel", wheelEventOptions, WheelEvent);
}),
],
};
// https://stackoverflow.com/a/47593316
function seededHashRandomNumberGenerator(a) {
return function () {
var t = a += 0x6d2b79f5;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return (t ^ (t >>> 14)) >>> 0;
};
}
class WakeLock {
#wakeLockSentinel = undefined;
async request() {
if (!navigator.wakeLock)
return;
try {
this.#wakeLockSentinel = await navigator.wakeLock.request("screen");
} catch (err) {
console.error(`${err.name}, ${err.message}`);
}
}
async release() {
if (!this.#wakeLockSentinel)
return;
try {
await this.#wakeLockSentinel.release();
} catch (err) {
console.error(`${err.name}, ${err.message}`);
} finally {
this.#wakeLockSentinel = undefined;
}
}
}
export class BenchmarkRunner {
constructor(suites, client) {
this._suites = suites;
if (params.useWarmupSuite)
this._suites = [WarmupSuite, ...suites];
this._client = client;
this._page = null;
this._metrics = null;
this._iterationCount = params.iterationCount;
if (params.shuffleSeed !== "off")
this._suiteOrderRandomNumberGenerator = seededHashRandomNumberGenerator(params.shuffleSeed);
this._wakeLock = new WakeLock();
}
async runMultipleIterations(iterationCount) {
this._iterationCount = iterationCount;
await this._client?.willStartFirstIteration?.(iterationCount);
try {
await this._runMultipleIterations();
} catch (error) {
console.error(error);
if (this._client?.handleError) {
await this._client.handleError(error);
return;
}
}
await this._client?.didFinishLastIteration?.(this._metrics);
}
async _runMultipleIterations() {
const iterationStartLabel = "iteration-start";
const iterationEndLabel = "iteration-end";
for (let i = 0; i < this._iterationCount; i++) {
performance.mark(iterationStartLabel);
await this.runAllSuites();
performance.mark(iterationEndLabel);
performance.measure(`iteration-${i}`, iterationStartLabel, iterationEndLabel);
}
}
_removeFrame() {
if (this._frame) {
this._frame.parentNode.removeChild(this._frame);
this._frame = null;
}
}
async _appendFrame() {
const frame = document.createElement("iframe");
const style = frame.style;
style.width = `${params.viewport.width}px`;
style.height = `${params.viewport.height}px`;
style.border = "0px none";
style.position = "absolute";
frame.setAttribute("scrolling", "no");
frame.className = "test-runner";
style.left = "50%";
style.top = "50%";
style.transform = "translate(-50%, -50%)";
await this._client?.willAddTestFrame?.(frame);
document.body.insertBefore(frame, document.body.firstChild);
this._frame = frame;
return frame;
}
async _prepareAllSuites() {
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
await this._wakeLock.request();
const prepareStartLabel = "runner-prepare-start";
const prepareEndLabel = "runner-prepare-end";
performance.mark(prepareStartLabel);
let suites = [...this._suites];
if (this._suiteOrderRandomNumberGenerator)
this._shuffleSuites(suites);
performance.mark(prepareEndLabel);
performance.measure("runner-prepare", prepareStartLabel, prepareEndLabel);
return suites;
}
_shuffleSuites(suites) {
// We just do a simple Fisher-Yates shuffle based on the repeated hash of the
// seed. This is not a high quality RNG, but it's plenty good enough.
for (let i = 0; i < suites.length - 1; i++) {
const j = i + (this._suiteOrderRandomNumberGenerator() % (suites.length - i));
const tmp = suites[i];
suites[i] = suites[j];
suites[j] = tmp;
}
}
async runAllSuites() {
const suites = await this._prepareAllSuites();
try {
for (const suite of suites) {
if (!suite.enabled)
continue;
try {
await this._appendFrame();
this._page = new Page(this._frame);
await this.runSuite(suite);
} finally {
this._removeFrame();
}
}
} finally {
await this._finishRunAllSuites();
}
}
async _finishRunAllSuites() {
const finalizeStartLabel = "runner-finalize-start";
const finalizeEndLabel = "runner-finalize-end";
performance.mark(finalizeStartLabel);
await this._finalize();
performance.mark(finalizeEndLabel);
performance.measure("runner-finalize", finalizeStartLabel, finalizeEndLabel);
await this._wakeLock.release();
}
async runSuite(suite) {
// FIXME: Encapsulate more state in the SuiteRunner.
// FIXME: Return and use measured values from SuiteRunner.
const type = suite.type ?? ((params.useAsyncSteps && "async") || "default");
const suiteRunnerClass = SUITE_RUNNER_LOOKUP[type];
const suiteRunner = new suiteRunnerClass(this._frame, this._page, params, suite, this._client, this._measuredValues);
await suiteRunner.run();
}
async _finalize() {
this._appendIterationMetrics();
if (this._client?.didRunSuites) {
let product = 1;
const values = [];
for (const suiteName in this._measuredValues.tests) {
const suiteTotal = this._measuredValues.tests[suiteName].total;
product *= suiteTotal;
values.push(suiteTotal);
}
values.sort((a, b) => a - b); // Avoid the loss of significance for the sum.
const total = values.reduce((a, b) => a + b);
const geomean = Math.pow(product, 1 / values.length);
this._measuredValues.total = total;
this._measuredValues.mean = total / values.length;
this._measuredValues.geomean = geomean;
this._measuredValues.score = geomeanToScore(geomean);
await this._client.didRunSuites(this._measuredValues);
}
}
_appendIterationMetrics() {
const getMetric = (name, unit = "ms") => this._metrics[name] || (this._metrics[name] = new Metric(name, unit));
const iterationMetric = (i, name) => {
if (i >= params.iterationCount)
throw new Error(`Requested iteration=${i} does not exist.`);
return getMetric(`Iteration-${i}-${name}`);
};
const collectSubMetrics = (prefix, items, parent) => {
for (let name in items) {
const results = items[name];
const metric = getMetric(prefix + name);
metric.add(results.total ?? results);
if (metric.parent !== parent)
parent.addChild(metric);
if (results.tests)
collectSubMetrics(`${metric.name}${Metric.separator}`, results.tests, metric);
}
};
const initializeMetrics = this._metrics === null;
if (initializeMetrics)
this._metrics = { __proto__: null };
const iterationResults = this._measuredValues.tests;
collectSubMetrics("", iterationResults);
if (initializeMetrics) {
// Prepare all iteration metrics so they are listed at the end of
// of the _metrics object, before "Total" and "Score".
for (let i = 0; i < this._iterationCount; i++)
iterationMetric(i, "Total").description = `Test totals for iteration ${i}`;
getMetric("Geomean", "ms").description = "Geomean of test totals";
getMetric("Score", "score").description = "Scaled inverse of the Geomean";
if (params.measurePrepare)
getMetric("Prepare", "ms").description = "Geomean of workload prepare times";
}
const geomean = getMetric("Geomean");
const iteration = geomean.length;
const iterationTotal = iterationMetric(iteration, "Total");
for (const results of Object.values(iterationResults))
iterationTotal.add(results.total);
iterationTotal.computeAggregatedMetrics();
geomean.add(iterationTotal.geomean);
getMetric("Score").add(geomeanToScore(iterationTotal.geomean));
if (params.measurePrepare) {
const iterationPrepare = iterationMetric(iteration, "Prepare");
for (const results of Object.values(iterationResults))
iterationPrepare.add(results.prepare);
iterationPrepare.computeAggregatedMetrics();
const prepare = getMetric("Prepare");
prepare.add(iterationPrepare.geomean);
}
for (const metric of Object.values(this._metrics))
metric.computeAggregatedMetrics();
}
_initializeMetrics() {}
}

13
resources/config.json Normal file
View file

@ -0,0 +1,13 @@
{
"suites": [
{
"name": "NewsSite-PostMessage",
"url": "resources/newssite/news-next/dist/index.html",
"tags": ["newssite", "language"],
"type": "remote",
"config": {
"name": "default"
}
}
]
}

View file

@ -0,0 +1,353 @@
import { params, LAYOUT_MODES, defaultParams } from "./shared/params.mjs";
import { benchmarkConfigurator } from "./benchmark-configurator.mjs";
import { handleComplexityChange } from "./shared/todomvc-utils.mjs";
const { suites, tags } = benchmarkConfigurator;
export function createDeveloperModeContainer() {
const container = document.createElement("div");
container.className = "developer-mode";
const details = document.createElement("details");
const summary = document.createElement("summary");
summary.textContent = "Developer Mode";
details.append(summary);
const content = document.createElement("div");
content.className = "developer-mode-content";
content.append(createUIForSuites());
const settings = document.createElement("div");
settings.className = "settings";
settings.append(createUIForIterationCount());
settings.append(createUIForMeasurePrepare());
settings.append(createUIForWarmupSuite());
settings.append(createUIForWarmupBeforeSync());
settings.append(createUIForWaitAfterSetup());
settings.append(createUIForSyncStepDelay());
settings.append(createUIForWaitAfterSuite());
settings.append(createUIForAsyncSteps());
settings.append(createUIForLayoutMode());
settings.append(createUIForComplexity());
content.append(document.createElement("hr"));
content.append(settings);
content.append(document.createElement("hr"));
content.append(createUIForRun());
details.append(content);
container.append(details);
return container;
}
function span(text) {
const span = document.createElement("span");
span.textContent = text;
return span;
}
function createUIForWarmupSuite() {
return createCheckboxUI("Use Warmup Suite", params.useWarmupSuite, (isChecked) => {
params.useWarmupSuite = isChecked;
});
}
function createUIForMeasurePrepare() {
return createCheckboxUI("Measure Prepare", params.measurePrepare, (isChecked) => {
params.measurePrepare = isChecked;
});
}
function createUIForAsyncSteps() {
return createCheckboxUI("Use Async Steps", params.useAsyncSteps, (isChecked) => {
params.useAsyncSteps = isChecked;
});
}
function createCheckboxUI(labelValue, initialValue, paramsUpdateCallback) {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = !!initialValue;
checkbox.onchange = () => {
paramsUpdateCallback(checkbox.checked);
handleParamsChange();
};
const label = document.createElement("label");
label.append(checkbox, span(labelValue));
return label;
}
function createUIForIterationCount() {
return createLinearRangeUI("Iterations: ", "iterationCount", "#", 1, 200);
}
function createUIForWarmupBeforeSync() {
return createLinearRangeUI("Warmup time: ", "warmupBeforeSync");
}
function createUIForSyncStepDelay() {
return createLinearRangeUI("Sync step delay: ", "waitBeforeSync");
}
function createUIForWaitAfterSetup() {
return createLinearRangeUI("Post setup delay: ", "waitAfterSetup");
}
function createUIForWaitAfterSuite() {
return createLinearRangeUI("Post suite delay: ", "waitAfterSuite");
}
function createUIForComplexity() {
return createExpRangeUI("Relative complexity: ", "complexity", "x", 0.01, 100, 0.01);
}
function createLinearRangeUI(labelText, paramKey, unit = "ms", min = 0, max = 1000, step = 1) {
const initialValue = params[paramKey];
const linearMap = (value) => value;
const { range, label } = createTimeRangeUI(labelText, paramKey, unit, linearMap, 0);
range.min = min;
range.max = max;
range.step = step;
range.value = initialValue;
return label;
}
function createExpRangeUI(labelText, paramKey, unit = "ms", min = 0, max = 1000, step = 1) {
const defaultValue = defaultParams[paramKey];
const initialValue = params[paramKey];
const b = defaultValue - 1;
const a = -Math.log(min - b);
const logMap = (value) => Math.round((Math.exp(value * a) + b) / step) * step;
const { range, label } = createTimeRangeUI(labelText, paramKey, unit, logMap, 2);
range.min = -1;
range.max = Math.log(max - b) / a;
range.step = 0.01;
range.value = Math.log(initialValue - b) / a;
return label;
}
function createTimeRangeUI(labelText, paramKey, unit = "ms", map, decimals) {
const range = document.createElement("input");
range.type = "range";
const rangeValueAndUnit = document.createElement("span");
rangeValueAndUnit.className = "range-label-data";
const rangeValue = document.createElement("span");
rangeValue.textContent = params[paramKey];
rangeValueAndUnit.append(rangeValue, " ", unit);
const label = document.createElement("label");
label.append(span(labelText), range, rangeValueAndUnit);
range.oninput = () => {
rangeValue.textContent = map(Number(range.value)).toFixed(decimals);
};
range.onchange = () => {
params[paramKey] = map(Number(range.value));
handleParamsChange();
};
return { range, label };
}
function createUIForLayoutMode() {
return createSelectUI("Force layout mode", params.layoutMode, LAYOUT_MODES, (value) => {
params.layoutMode = value;
});
}
function createSelectUI(labelValue, initialValue, choices, paramsUpdateCallback) {
const select = document.createElement("select");
select.onchange = () => {
paramsUpdateCallback(select.value);
handleParamsChange();
};
choices.forEach((choice) => {
const option = new Option(choice, choice);
select.add(option);
});
select.value = initialValue;
const label = document.createElement("label");
label.append(span(labelValue), select);
return label;
}
function createUIForSuites() {
const control = document.createElement("nav");
control.className = "suites";
const checkboxes = [];
const setSuiteEnabled = (suiteIndex, enabled) => {
suites[suiteIndex].enabled = enabled;
checkboxes[suiteIndex].checked = enabled;
};
control.appendChild(createSuitesGlobalSelectButtons(setSuiteEnabled));
const ol = document.createElement("ol");
for (const suite of suites) {
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.id = suite.name;
checkbox.type = "checkbox";
checkbox.checked = suite.enabled;
checkbox.onchange = () => {
suite.enabled = checkbox.checked;
handleParamsChange();
};
checkboxes.push(checkbox);
const label = document.createElement("label");
label.append(checkbox, " ", suite.name);
li.appendChild(label);
label.onclick = (event) => {
if (event?.ctrlKey || event?.metaKey) {
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
if (suites[suiteIndex] !== suite)
setSuiteEnabled(suiteIndex, false);
else
setSuiteEnabled(suiteIndex, true);
}
}
};
const link = document.createElement("a");
link.href = suite.url;
link.target = "_blank";
link.title = "Raw workload URL";
link.className = "workload-url-link";
link.textContent = "\u{1F517}";
li.appendChild(link);
ol.appendChild(li);
}
control.appendChild(ol);
control.appendChild(createSuitesTagsButton(setSuiteEnabled));
return control;
}
function createSuitesGlobalSelectButtons(setSuiteEnabled) {
const buttons = document.createElement("div");
buttons.className = "button-bar";
let button = document.createElement("button");
button.className = "select-all";
button.textContent = "Select all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, true);
handleParamsChange();
};
buttons.appendChild(button);
button = document.createElement("button");
button.textContent = "Unselect all";
button.className = "unselect-all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, false);
handleParamsChange();
};
buttons.appendChild(button);
return buttons;
}
function createSuitesTagsButton(setSuiteEnabled) {
let container = document.createElement("div");
let buttons = container.appendChild(document.createElement("div"));
buttons.className = "button-bar";
let i = 0;
const kTagsPerLine = 3;
for (const tag of tags) {
if (tag === "all")
continue;
if (!(i % kTagsPerLine)) {
buttons = container.appendChild(document.createElement("div"));
buttons.className = "button-bar";
}
i++;
const button = document.createElement("button");
button.className = "tag";
button.textContent = `#${tag}`;
button.dataTag = tag;
button.onclick = (event) => {
const extendSelection = event?.shiftKey;
const invertSelection = event?.ctrlKey || event?.metaKey;
const selectedTag = event.target.dataTag;
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
let enabled = suites[suiteIndex].tags.includes(selectedTag);
if (invertSelection)
enabled = !enabled;
if (extendSelection && !enabled)
continue;
setSuiteEnabled(suiteIndex, enabled);
}
handleParamsChange();
};
buttons.appendChild(button);
}
return container;
}
function createUIForRun() {
const stepTestButton = document.createElement("button");
stepTestButton.className = "step-button";
stepTestButton.innerHTML = "Step Test<span>\u23EF</span>";
stepTestButton.onclick = (event) => {
globalThis.benchmarkClient.step();
};
const startTestButton = document.createElement("button");
startTestButton.innerHTML = "Start Test<span>\u23F5</span>";
startTestButton.onclick = (event) => {
globalThis.benchmarkClient.start();
};
const buttons = document.createElement("div");
buttons.className = "button-bar";
buttons.appendChild(stepTestButton);
buttons.appendChild(startTestButton);
return buttons;
}
function updateParamsSuitesAndTags() {
params.suites = [];
params.tags = [];
// If less than all suites are selected then change the URL "Suites" GET parameter
// to comma separate only the selected
const selectedSuites = suites.filter((suite) => suite.enabled);
if (!selectedSuites.length)
return;
// Try finding common tags that would result in the current suite selection.
let commonTags = new Set(selectedSuites[0].tags);
for (const suite of suites) {
if (suite.enabled)
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
else
suite.tags.forEach((tag) => commonTags.delete(tag));
}
if (selectedSuites.length > 1 && commonTags.size)
params.tags = [...commonTags];
else
params.suites = selectedSuites.map((suite) => suite.name);
}
function handleParamsChange() {
updateParamsSuitesAndTags();
handleComplexityChange();
const url = new URL(window.location.href);
url.search = params.toSearchParams();
// Only push state if changed
if (url.href !== window.location.href)
window.history.pushState({}, "", url);
}

BIN
resources/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
resources/gauge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
resources/gauge@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
resources/logo@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

1003
resources/main.css Normal file

File diff suppressed because it is too large Load diff

547
resources/main.mjs Normal file
View file

@ -0,0 +1,547 @@
import { BenchmarkRunner } from "./benchmark-runner.mjs";
import * as Statistics from "./statistics.mjs";
import { renderMetricView } from "./metric-ui.mjs";
import { defaultParams, params } from "./shared/params.mjs";
import { createDeveloperModeContainer } from "./developer-mode.mjs";
const BENCHMARK_STATE = Object.freeze({
IDLE: "IDLE",
READY: "READY",
RUNNING: "RUNNING",
DONE: "DONE",
ERROR: "ERROR",
});
// FIXME(camillobruni): Add base class
class MainBenchmarkClient {
developerMode = false;
stepCount = null;
suitesCount = null;
_measuredValuesList = [];
_finishedTestCount = 0;
_progressCompleted = null;
_state = BENCHMARK_STATE.IDLE;
_developerModeContainer = null;
_metrics = Object.create(null);
_steppingPromise = null;
_steppingResolver = null;
_benchmarkConfiguratorPromise = null;
constructor() {
this._benchmarkConfiguratorPromise = import("./benchmark-configurator.mjs");
this.prepareUI();
this.evaluateParams();
this._showSection(window.location.hash);
this._benchmarkConfiguratorPromise.then(() => {
window.dispatchEvent(new Event("SpeedometerReady"));
});
}
isRunning() {
return this._state === BENCHMARK_STATE.RUNNING;
}
hasFinished() {
return this._state === BENCHMARK_STATE.DONE || this._state === BENCHMARK_STATE.ERROR;
}
async start() {
if (this._isStepping())
this._clearStepping();
else if (await this._startBenchmark())
this._showSection("#running");
}
async step() {
const currentSteppingResolver = this._steppingResolver;
this._steppingPromise = new Promise((resolve) => {
this._steppingResolver = resolve;
});
currentSteppingResolver?.();
if (!this.isRunning()) {
await this._startBenchmark();
this._showSection("#running");
}
}
_clearStepping() {
const currentSteppingResolver = this._steppingResolver;
this._steppingPromise = null;
this._steppingResolver = null;
currentSteppingResolver?.();
}
async _awaitNextStep(suite, test) {
console.log(`Next Step: ${suite.name} ${test.name}`, { suite, test });
await this._steppingPromise;
}
_isStepping() {
return this._steppingResolver !== null;
}
async _startBenchmark() {
if (this.isRunning())
return false;
const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise;
const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled);
const totalSuitesCount = enabledSuites.length;
if (totalSuitesCount === 0) {
const message = `No suites selected - "${params.suites}" does not exist.`;
alert(message);
console.error(
message,
params.suites,
"\nValid values:",
benchmarkConfigurator.suites.map((each) => each.name)
);
return false;
}
if (!this._isStepping())
this._developerModeContainer?.remove();
this._progressCompleted = document.getElementById("progress-completed");
if (params.iterationCount < 50) {
const progressNode = document.getElementById("progress");
for (let i = 1; i < params.iterationCount; i++) {
const iterationMarker = progressNode.appendChild(document.createElement("div"));
iterationMarker.className = "iteration-marker";
iterationMarker.style.left = `${(i / params.iterationCount) * 100}%`;
}
}
this._metrics = Object.create(null);
this.stepCount = params.iterationCount * totalSuitesCount;
this._progressCompleted.max = this.stepCount;
this.suitesCount = enabledSuites.length;
this._setBenchmarkState(BENCHMARK_STATE.RUNNING);
const runner = new BenchmarkRunner(benchmarkConfigurator.suites, this);
runner.runMultipleIterations(params.iterationCount);
return true;
}
get metrics() {
return this._metrics;
}
_assertNotAborted() {
if (this._state === BENCHMARK_STATE.ERROR)
throw new Error("Benchmark aborted by another process.");
}
willAddTestFrame(frame) {
this._assertNotAborted();
frame.style.left = "50%";
frame.style.top = "50%";
frame.style.transform = "translate(-50%, -50%)";
}
async willRunTest(suite, test) {
this._assertNotAborted();
document.getElementById("info-label").textContent = suite.name;
document.getElementById("info-progress").textContent = `${this._finishedTestCount} / ${this.stepCount}`;
if (this._steppingPromise)
await this._awaitNextStep(suite, test);
}
async didFinishSuite(suite) {
this._finishedTestCount++;
this._progressCompleted.value = this._finishedTestCount;
if (this._steppingPromise)
await this._awaitNextStep(suite, { name: "Finished" });
}
didRunSuites(measuredValues) {
this._measuredValuesList.push(measuredValues);
}
willStartFirstIteration() {
this._measuredValuesList = [];
this._finishedTestCount = 0;
}
didFinishLastIteration(metrics) {
console.assert(this.isRunning());
this._metrics = metrics;
this._setBenchmarkState(BENCHMARK_STATE.DONE);
const scoreResults = this._computeResults(this._measuredValuesList, "score");
if (scoreResults.isValid)
this._populateValidScore(scoreResults);
else
this._populateInvalidScore();
this._populateDetailedResults(metrics);
if (params.developerMode)
this.showResultsDetails();
else
this.showResultsSummary();
globalThis.dispatchEvent(new Event("SpeedometerDone"));
}
handleError(error) {
if (this._state === BENCHMARK_STATE.ERROR)
return;
this._metrics = Object.create(null);
this._setBenchmarkState(BENCHMARK_STATE.ERROR);
this._populateErrorMessage(error.message);
this.showResultsSummary();
throw error;
}
_populateValidScore(scoreResults) {
document.getElementById("summary").className = "valid";
this._updateGaugeNeedle(scoreResults.mean);
document.getElementById("result-number").textContent = scoreResults.formattedMean;
if (scoreResults.formattedDelta)
document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`;
}
_populateInvalidScore() {
this._populateErrorMessage(undefined);
}
_populateErrorMessage(errorText) {
document.getElementById("summary").className = "invalid";
document.getElementById("result-number").textContent = "Error";
document.getElementById("confidence-number").textContent = "";
if (errorText === undefined)
return;
const errorMessage = document.getElementById("invalid-score-text");
errorMessage.textContent = errorText;
}
_computeResults(measuredValuesList, displayUnit) {
function valueForUnit(measuredValues) {
if (displayUnit === "ms")
return measuredValues.geomean;
return measuredValues.score;
}
function sigFigFromPercentDelta(percentDelta) {
return Math.ceil(-Math.log(percentDelta) / Math.log(10)) + 3;
}
function toSigFigPrecision(number, sigFig) {
const nonDecimalDigitCount = number < 1 ? 0 : Math.floor(Math.log(number) / Math.log(10)) + 1;
return number.toPrecision(Math.max(nonDecimalDigitCount, Math.min(6, sigFig)));
}
const values = measuredValuesList.map(valueForUnit);
const sum = values.reduce((a, b) => a + b, 0);
const arithmeticMean = sum / values.length;
let meanSigFig = 4;
let formattedDelta;
let formattedPercentDelta;
const delta = Statistics.confidenceIntervalDelta(0.95, values.length, sum, Statistics.squareSum(values));
if (!isNaN(delta)) {
const percentDelta = (delta * 100) / arithmeticMean;
meanSigFig = sigFigFromPercentDelta(percentDelta);
formattedDelta = toSigFigPrecision(delta, 2);
formattedPercentDelta = `${toSigFigPrecision(percentDelta, 2)}%`;
}
const formattedMean = toSigFigPrecision(arithmeticMean, Math.max(meanSigFig, 3));
return {
formattedValues: values.map((value) => {
return `${toSigFigPrecision(value, 4)} ${displayUnit}`;
}),
mean: arithmeticMean,
formattedMean: formattedMean,
formattedDelta: formattedDelta,
formattedMeanAndDelta: formattedMean + (formattedDelta ? ` \xb1 ${formattedDelta} (${formattedPercentDelta})` : ""),
isValid: values.length > 0 && isFinite(sum) && sum > 0,
};
}
_addDetailedResultsRow(table, iterationNumber, value) {
const row = document.createElement("tr");
const th = document.createElement("th");
th.textContent = `Iteration ${iterationNumber + 1}`;
const td = document.createElement("td");
td.textContent = value;
row.appendChild(th);
row.appendChild(td);
table.appendChild(row);
}
_updateGaugeNeedle(score) {
const needleAngle = Math.max(0, Math.min(score, 140)) - 70;
const needleRotationValue = `rotate(${needleAngle}deg)`;
const gaugeNeedleElement = document.querySelector("#summary > .gauge .needle");
gaugeNeedleElement.style.setProperty("-webkit-transform", needleRotationValue);
gaugeNeedleElement.style.setProperty("-moz-transform", needleRotationValue);
gaugeNeedleElement.style.setProperty("-ms-transform", needleRotationValue);
gaugeNeedleElement.style.setProperty("transform", needleRotationValue);
}
_populateDetailedResults(metrics) {
this._populateNonStandardParams();
const trackHeight = 24;
document.documentElement.style.setProperty("--metrics-line-height", `${trackHeight}px`);
const plotWidth = (params.viewport.width - 120) / 2;
const aggregateMetrics = [metrics.Geomean];
if (params.measurePrepare)
aggregateMetrics.push(metrics.Prepare);
document.getElementById("aggregate-chart").innerHTML = renderMetricView({
metrics: aggregateMetrics,
width: plotWidth,
trackHeight,
renderChildren: false,
colors: ["white"],
});
const toplevelMetrics = Object.values(metrics).filter((each) => !each.parent && each.children.length > 0);
document.getElementById("tests-chart").innerHTML = renderMetricView({
metrics: toplevelMetrics,
width: plotWidth,
trackHeight,
renderChildren: false,
});
let html = "";
for (const metric of toplevelMetrics) {
html += renderMetricView({
metrics: metric.children,
width: plotWidth,
trackHeight,
title: metric.name,
});
}
document.getElementById("metrics-results").innerHTML = html;
const filePrefix = `speedometer-3-${new Date().toISOString()}`;
let jsonData = this._formattedJSONResult({ modern: false });
let jsonLink = document.getElementById("download-classic-json");
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
jsonLink.setAttribute("download", `${filePrefix}.json`);
jsonLink = document.getElementById("download-full-json");
jsonData = this._formattedJSONResult({ modern: true });
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
jsonLink.setAttribute("download", `${filePrefix}.json`);
const csvData = this._formattedCSVResult();
const csvLink = document.getElementById("download-csv");
csvLink.href = URL.createObjectURL(new Blob([csvData], { type: "text/csv" }));
csvLink.setAttribute("download", `${filePrefix}.csv`);
}
_populateNonStandardParams() {
if (params.isDefault())
return;
const paramsDiff = [];
const usedSearchparams = params.toSearchParamsObject();
const defaultSearchParams = defaultParams.toCompleteSearchParamsObject(false);
for (const [key, value] of usedSearchparams.entries()) {
if (key === "developerMode")
continue;
const defaultValue = defaultSearchParams.get(key);
if (value !== defaultValue)
paramsDiff.push({ key, value, defaultValue });
}
if (paramsDiff.length === 0)
return;
const body = document.createElement("tbody");
for (const { key, value, defaultValue } of paramsDiff) {
const row = body.insertRow();
row.insertCell().textContent = key;
row.insertCell().textContent = value;
row.insertCell().textContent = defaultValue;
}
const table = document.getElementById("non-standard-params-table");
table.replaceChild(body, table.tBodies[0]);
document.querySelector(".non-standard-params").style.display = "block";
}
prepareUI() {
window.addEventListener("hashchange", this._hashChangeHandler.bind(this));
window.addEventListener("resize", this._resizeScreeHandler.bind(this));
this._resizeScreeHandler();
document.querySelectorAll("logo").forEach((button) => {
button.onclick = this._logoClickHandler.bind(this);
});
document.getElementById("copy-full-json").onclick = this.copyJsonResults.bind(this);
document.getElementById("copy-csv").onclick = this.copyCSVResults.bind(this);
document.querySelectorAll(".start-tests-button").forEach((button) => {
button.onclick = this._startBenchmarkHandler.bind(this);
button.disabled = true;
});
}
async evaluateParams() {
const { benchmarkConfigurator } = await this._benchmarkConfiguratorPromise;
if (params.suites.length > 0 || params.tags.length > 0)
benchmarkConfigurator.enableSuites(params.suites, params.tags);
if (params.developerMode) {
this._developerModeContainer = createDeveloperModeContainer();
document.body.append(this._developerModeContainer);
}
if (params.startAutomatically)
this.start();
else
this._setBenchmarkState(BENCHMARK_STATE.READY);
}
async _setBenchmarkState(state) {
this._state = state;
document.body.setAttribute("data-benchmark-state", state);
const startButtons = document.querySelectorAll(".start-tests-button");
if (state !== BENCHMARK_STATE.RUNNING) {
startButtons.forEach((btn) => {
btn.innerHTML = "<div>Start Test</div>";
});
if (state === BENCHMARK_STATE.READY) {
startButtons.forEach((btn) => {
btn.disabled = false;
});
}
}
}
_hashChangeHandler() {
this._showSection(window.location.hash);
}
_resizeScreeHandler() {
// FIXME: Detect when the window size changes during the test.
const mainSize = document.querySelector("main").getBoundingClientRect();
const screenIsTooSmall = window.innerWidth < mainSize.width || window.innerHeight < mainSize.height;
document.getElementById("min-screen-width").textContent = `${params.viewport.width + 50}px`;
document.getElementById("min-screen-height").textContent = `${params.viewport.height + 50}px`;
document.getElementById("screen-size").textContent = `${window.innerWidth}px by ${window.innerHeight}px`;
document.getElementById("screen-size-warning").style.display = screenIsTooSmall ? null : "none";
}
_startBenchmarkHandler() {
this.start();
}
_logoClickHandler(event) {
// Prevent any accidental UI changes during benchmark runs.
if (!this.isRunning())
this._showSection("#home");
event.preventDefault();
return false;
}
showResultsSummary() {
this._showSection("#summary");
}
showResultsDetails() {
this._showSection("#details");
}
_formattedJSONResult({ modern = false }) {
const indent = " ";
if (modern)
return JSON.stringify(this._metrics, undefined, indent);
return JSON.stringify(this._measuredValuesList, undefined, indent);
}
_formattedCSVResult() {
// The CSV format is similar to the details view table. Each measurement is a row with
// the name and N columns with the measurement for each iteration:
// ```
// Measurement,#1,...,#N
// TodoMVC-JavaScript-ES5/Total,num,...,num
// TodoMVC-JavaScript-ES5/Adding100Items,num,...,num
// ...
const labels = ["Name"];
for (let i = 0; i < params.iterationCount; i++)
labels.push(`#${i + 1}`);
labels.push("Mean");
const metrics = Array.from(Object.values(this._metrics)).filter((metric) => !metric.name.startsWith("Iteration-"));
const metricsValues = metrics.map((metric) => [metric.name, ...metric.values, metric.mean].join(","));
const csv = [labels.join(","), ...metricsValues];
return csv.join("\n");
}
copyJsonResults() {
navigator.clipboard.writeText(this._formattedJSONResult({ modern: true }));
}
copyCSVResults() {
navigator.clipboard.writeText(this._formattedCSVResult());
}
_showSection(hash) {
if (this.isRunning()) {
this._setLocationHash("#running");
return;
} else if (this.hasFinished()) {
if (hash !== "#summary" && hash !== "#details") {
this._setLocationHash("#summary");
return;
}
} else if (hash !== "#home" && hash !== "") {
// Redirect invalid views to #home directly.
this._setLocationHash("#home");
return;
}
this._setLocationHash(hash);
}
_setLocationHash(hash) {
if (hash === "#home" || hash === "") {
if (window.location.hash !== hash)
window.location.hash = "#home";
hash = "#home";
this._removeLocationHash();
} else {
window.location.hash = hash;
}
this._updateVisibleSectionAttribute(hash);
this._updateDocumentTitle(hash);
}
_updateVisibleSectionAttribute(hash) {
const sectionId = hash.substring(1);
document.documentElement.setAttribute("data-visible-section", sectionId);
}
_updateDocumentTitle(hash) {
const maybeSection = document.querySelector(hash);
const sectionTitle = maybeSection?.getAttribute("data-title") ?? "";
document.title = `Speedometer 3 ${sectionTitle}`.trimEnd();
}
_removeLocationHash() {
const location = window.location;
window.history.pushState("", document.title, location.pathname + location.search);
}
}
function init() {
const rootStyle = document.documentElement.style;
rootStyle.setProperty("--viewport-width", `${params.viewport.width}px`);
rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`);
globalThis.benchmarkClient = new MainBenchmarkClient();
window.addEventListener("error", (event) => {
globalThis.benchmarkClient.handleError(event.error || new Error(event.message));
});
window.addEventListener("unhandledrejection", (event) => {
globalThis.benchmarkClient.handleError(event.reason || new Error("Unhandled promise rejection"));
});
}
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", init);
else
init();

View file

@ -0,0 +1,88 @@
<svg fill="none" viewBox="0 0 780 140" width="780" height="140" xmlns="http://www.w3.org/2000/svg">
<style>
:root {
--foreground: rgb(235, 235, 235);
--background: rgb(46, 51, 55);
font-family: "system-ui";
background-color: var(--background);
color: var(--foreground);
}
table {
padding: 1em;
}
th {
text-align: left;
}
td {
white-space: nowrap;
border: 1px solid var(--foreground);
border-radius: 2px;
padding: 0 2px 0 2px;
}
.measured {
padding: 0 2px 0 2px;
border-radius: 2px;
color: var(--background);
background-color: var(--foreground);
font-weight: bold;
}
.unmeasured {
padding: 0 2px 0 2px;
border-radius: 2px;
border-style: dashed !important;
font-style: italic;
}
td.hidden {
border: none;
}
</style>
<foreignObject width="100%" height="100%">
<table xmlns="http://www.w3.org/1999/xhtml" width="100%">
<tbody>
<tr>
<th>Iteration:</th>
<td colspan="9">Iteration 1</td>
<td>...</td>
<td colspan="3">Iteration N</td>
</tr>
<tr>
<th>Test:</th>
<td colspan="6">TodoMVC-JavaScript-ES5</td>
<td>...</td>
<td colspan="2">Perf-Dashboard</td>
<td class="hidden"></td>
<td colspan="2">TodoMVC-...</td>
<td>...</td>
</tr>
<tr>
<th>Steps:</th>
<td class="unmeasured">Prepare</td>
<td colspan="2" class="measured">Adding</td>
<td colspan="2" class="measured">Completing</td>
<td class="measured">...</td>
<td class="hidden"></td>
<td class="unmeasured">Prepare</td>
<td class="measured">...</td>
<td class="hidden"></td>
<td class="unmeasured">Prepare</td>
<td class="measured">...</td>
</tr>
<tr>
<th></th>
<td class="hidden"></td>
<td class="measured">Sync</td>
<td class="measured">Async</td>
<td class="measured">Sync</td>
<td class="measured">Async</td>
</tr>
</tbody>
</table>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

327
resources/metric-ui.mjs Normal file
View file

@ -0,0 +1,327 @@
import { Metric } from "./metric.mjs";
export const COLORS = Object.freeze(["blue", "blue-light", "green-light", "green", "yellow", "orange", "red", "magenta", "violet", "purple", "blue-dark", "green-dark", "ochre", "rust"]);
export function renderMetricView(viewParams) {
let { metrics, width = 500, trackHeight = 20, subMetricMargin = 35, title = "", colors = COLORS } = viewParams;
// Make sure subMetricMargin is set for use in renderSubMetrics.
viewParams.subMetricMargin = subMetricMargin;
const scatterPlotParams = { width, trackHeight, colors };
scatterPlotParams.xAxisPositiveOnly = false;
scatterPlotParams.xAxisShowZero = true;
scatterPlotParams.values = prepareScatterPlotValues(metrics, true);
scatterPlotParams.unit = "%";
scatterPlotParams.xAxisLabel = "Spread Normalized";
const normalizedScatterPlot = renderScatterPlot(scatterPlotParams);
scatterPlotParams.xAxisPositiveOnly = true;
scatterPlotParams.xAxisShowZero = false;
scatterPlotParams.values = prepareScatterPlotValues(metrics, false);
scatterPlotParams.unit = metrics[0].unit;
scatterPlotParams.xAxisLabel = metrics[0].unit;
const absoluteScatterPlot = renderScatterPlot(scatterPlotParams);
const legend = metrics
.map(
(metric, i) => `
<tr >
<td class="${colors[i % colors.length]} no-select" >●</td>
<td class="label">${metric.shortName}</td>
<td class="number">${metric.mean.toFixed(2)}</td>
<td>±</td>
<td>${metric.deltaString}</td>
<td>${metric.unit}</td>
</tr>`
)
.join("");
return `
<dl class="metric">
<dt><h3>${title}<h3></dt>
<dd>
<div class="metric-chart"">
<div onclick="document.body.classList.toggle('relative-charts')">
<div class="metric-chart-absolute">
${absoluteScatterPlot}
</div>
<div class="metric-chart-relative">
${normalizedScatterPlot}
</div>
</div>
<table class="chart chart-legend">${legend}</table>
</div>
${renderSubMetrics(viewParams)}
</dd>
</dl>
`;
}
function renderSubMetrics(viewParams) {
const { metrics, width, subMetricMargin, colors = COLORS, renderChildren = true } = viewParams;
const valuesTable = `
<label class="details-toggle">
<input type="checkbox"
onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
Table
</label>
<div class="submetrics">
${renderMetricsTable(metrics)}
</div>`;
const hasChildMetric = metrics.length > 0 && metrics[0].children.length > 0;
if (!hasChildMetric || !renderChildren)
return valuesTable;
const subMetricWidth = width - subMetricMargin;
const childColors = [...colors];
const subMetrics = metrics
.map((metric) => {
// Rotate colors to get different colors for sub-plots.
for (let i = 0; i < metric.children.length; i++) {
const color = childColors.pop();
childColors.unshift(color);
}
const subMetricParams = {
...viewParams,
parentMetric: metric,
metrics: metric.children,
title: metric.name,
width: subMetricWidth,
colors: childColors,
};
return renderMetricView(subMetricParams);
})
.join("");
return `${valuesTable}
<label class="details-toggle">
<input type="checkbox"
onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
Submetrics
</label>
<div class="submetrics">
${subMetrics}
</div>
`;
}
function renderMetricsTable(metrics, min, max) {
let numRows = 0;
let columnHeaders = "";
let commonPrefixes = metrics[0].name.split(Metric.separator);
for (const metric of metrics) {
const prefixes = metric.name.split(Metric.separator);
for (let i = commonPrefixes.length - 1; i >= 0; i--) {
if (commonPrefixes[i] !== prefixes[i])
commonPrefixes.pop();
}
}
const commonPrefix = commonPrefixes.join(Metric.separator);
let commonPrefixHeader = "";
if (commonPrefix) {
commonPrefixHeader = `
<tr>
<td></td>
<td colspan="${metrics.length}" class="prefix">${commonPrefix}</td>
</tr>`;
}
for (const metric of metrics) {
const name = metric.name.substring(commonPrefix.length);
columnHeaders += `<th>${name} [${metric.unit}]</th>`;
numRows = Math.max(metric.values.length, numRows);
}
let body = "";
for (let row = 0; row < numRows; row++) {
let columns = "";
for (const metric of metrics) {
const value = metric.values[row];
if (value === undefined)
continue;
const delta = metric.max - metric.min;
const percent = Math.max(Math.min((value - metric.min) / delta, 1), 0) * 100;
const percentGradient = `background: linear-gradient(90deg, var(--foreground-alpha) ${percent}%, rgba(0,0,0,0) ${percent}%);`;
columns += `<td style="${percentGradient}">${value.toFixed(2)}</td>`;
}
body += `<tr>
<td>${row}</td>
${columns}
</tr>`;
}
return `<table class="metrics-table" >
<thead onclick="this.classList.toggle('nowrap')" >
${commonPrefixHeader}
<tr>
<th>Iteration</th>
${columnHeaders}
</tr>
</thead>
<tbody>
${body}
<tbody>
</table>`;
}
function prepareScatterPlotValues(metrics, normalize = true) {
let points = [];
// Arrange child-metrics values in a single coordinate system:
// - metric 1: x values are in range [0, 1]
// - metric 2: y values are in range [1, 2]
// - ...
// This way each metric data point is on a separate track in the scatter
// plot.
// If normalize == true:
// All x values are normalized by the mean of each metric and
// centered on 0.
// Example: [90ms, 100ms, 110ms] => [-10%, 0%, +10%]
const toPercent = 100;
let unit;
for (let metricIndex = 0; metricIndex < metrics.length; metricIndex++) {
const metric = metrics[metricIndex];
// If the mean is 0 we can't normalize values properly.
const mean = metric.mean || 1;
if (!unit)
unit = metric.unit;
else if (unit !== metric.unit)
throw new Error("All metrics must have the same unit.");
let width = metric.delta || 1;
let center = mean;
if (normalize) {
width = (metric.delta / mean) * toPercent;
center = 0;
}
const left = center - width / 2;
const y = metricIndex;
const label = `Mean: ${metric.valueString}\n` + `Min: ${metric.min.toFixed(2)}${unit}\n` + `Max: ${metric.max.toFixed(2)}${unit}`;
const rect = [left, y, label, width];
// Add data for individual points:
points.push(rect);
const values = metric.values;
const length = values.length;
for (let i = 0; i < length; i++) {
const value = values[i];
let x = value;
let normalized = (value / mean - 1) * toPercent;
if (normalize)
x = normalized;
const sign = normalized < 0 ? "-" : "+";
normalized = Math.abs(normalized);
// Each value is mapped to a y-coordinate in the range of [metricIndex, metricIndex + 1]
const valueOffsetY = length === 1 ? 0.5 : i / length;
const y = metricIndex + valueOffsetY;
let label = `Iteration ${i}: ${value.toFixed(3)}${unit}\n` + `Normalized: ${metric.mean.toFixed(3)}${unit} ${sign} ${normalized.toFixed(2)}%`;
const point = [x, y, label];
points.push(point);
}
}
return points;
}
function renderScatterPlot({ values, width = 500, height, trackHeight, xAxisPositiveOnly = false, xAxisShowZero = false, xAxisLabel, unit = "", colors = COLORS }) {
if (!height && !trackHeight)
throw new Error("Either height or trackHeight must be specified");
let xMin = Infinity;
let xMax = 0;
let yMin = Infinity;
let yMax = 0;
for (let value of values) {
let [x, y] = value;
xMin = Math.min(xMin, x);
xMax = Math.max(xMax, x);
yMin = Math.min(yMin, y);
yMax = Math.max(yMax, y);
}
if (xAxisPositiveOnly)
xMin = Math.max(xMin, 0);
// Max delta of values across each axis:
const trackCount = Math.ceil(yMax - yMin) || 1;
const spreadX = xMax - xMin;
// Axis + labels height:
const axisHeight = 18;
const axisMarginY = 4;
const trackMargin = 2;
let markerSize = 5;
// Auto-adjust markers to [2px, 5px] for high iteration counts:
const iterationsLimit = 20;
if (values.length > iterationsLimit)
markerSize = 2 + (3 / values.length) * iterationsLimit;
// Recalculate height:
if (height)
trackHeight = (height - axisHeight - axisMarginY) / trackCount;
else
height = trackCount * trackHeight + axisHeight + axisMarginY;
// Horizontal axis position:
const axisY = height - axisHeight + axisMarginY;
const unitToPosX = width / spreadX;
const unitToPosY = trackHeight - trackMargin - markerSize / 2;
const points = values.map(renderValue).join("");
let xAxisZeroLine = "";
if (xAxisShowZero) {
const xZeroPos = (0 - xMin) * unitToPosX;
xAxisZeroLine = `<line x1="${xZeroPos}" x2="${xZeroPos}" y1="${0}" y2="${axisY}" class="axis"/>`;
}
return `
<svg class="scatter-plot chart"
width="${width}" height="${height}"
viewBox="${`0 0 ${width} ${height}`}">
<g class="horizontal-axis no-select">
<line
x1="${0}" x2="${width}"
y1="${axisY - axisMarginY}" y2="${axisY - axisMarginY}"
class="axis" />
<text y="${axisY}" x="0" text-anchor="start">${xMin.toFixed(2)}${unit}</text>
<text y="${axisY}" x="${width / 2}" text-anchor="middle">${xAxisLabel}</text>
<text y="${axisY}" x="${width}" text-anchor="end">${xMax.toFixed(2)}${unit}</text>
</g>
<defs>
<g id="marker">
<circle r="${markerSize / 2}" />
</g>
</defs>
<g class="values">
${xAxisZeroLine}
${points}
</g>
</svg>
`;
function renderValue(value) {
const [rawX, rawY, label, rawWidth = 0] = value;
const trackIndex = rawY | 0;
const y = (rawY - yMin) * unitToPosY + markerSize * trackIndex;
const cssClass = colors[trackIndex % colors.length];
if (value.length <= 3) {
// Render a simple marker:
const x = (rawX - xMin) * unitToPosX;
const adjustedY = y + markerSize / 2;
return `
<use href="#marker" x="${x}" y="${adjustedY}" class="marker ${cssClass}">
<title>${label}</title>
</use>
`;
} else {
// Render a rect with 4 input values:
const x = (rawX - xMin) * unitToPosX + rawWidth / 2;
const w = rawWidth * unitToPosX;
const centerX = x + w / 2;
const top = y;
const height = trackHeight - trackMargin;
const bottom = top + height;
return `
<g class="percentile ${cssClass}">
<rect x="${x}" y="${top}" width="${w}" height="${height}">
<title>${label}</title>
</rect>
<line x1="${x}" x2="${x}" y1="${top}" y2="${bottom}" />
<line
x1="${centerX}" x2="${centerX}"
y1="${top}" y2="${bottom}"
stroke-dasharray="${height / 3}" />
<line x1="${x + w}" x2="${x + w}" y1="${top}" y2="${bottom}" />
</g>
`;
}
}
}

95
resources/metric.mjs Normal file
View file

@ -0,0 +1,95 @@
import * as Statistics from "./statistics.mjs";
export const MILLISECONDS_PER_MINUTE = 60 * 1000;
export class Metric {
static separator = "/";
constructor(name, unit = "ms") {
if (typeof name !== "string")
throw new Error(`Invalid metric.name=${name}, expected string.`);
this.name = name;
this.unit = unit;
this.description = "";
this.mean = 0.0;
// Make "geomean" non-enumerable so we don't serialize it with JSON.stringify
// and avoid some confusion with the top-level Geomean metric.
Object.defineProperty(this, "geomean", {
writable: true,
value: 0,
});
this.delta = 0.0;
this.percentDelta = 0.0;
this.sum = 0.0;
this.min = 0.0;
this.max = 0.0;
this.values = [];
// Mark properties which refer to other Metric objects as
// non-enumerable to avoid issue with JSON.stringify due to circular
// references.
Object.defineProperties(this, {
parent: {
writable: true,
value: undefined,
},
children: {
writable: true,
value: [],
},
});
}
get shortName() {
return this.parent ? this.name.replace(`${this.parent.name}-`, "") : this.name;
}
get valueString() {
const mean = this.mean.toFixed(2);
if (!this.percentDelta || !this.delta)
return `${mean} ${this.unit}`;
return `${mean} ± ${this.deltaString} ${this.unit}`;
}
get deltaString() {
if (!this.percentDelta || !this.delta)
return "";
return `${this.delta.toFixed(2)} (${this.percentDelta.toFixed(1)}%)`;
}
get length() {
return this.values.length;
}
addChild(metric) {
if (metric.parent)
throw new Error("Cannot re-add sub metric");
metric.parent = this;
this.children.push(metric);
}
add(value) {
if (typeof value !== "number")
throw new Error(`Adding invalid value=${value} to metric=${this.name}`);
this.values.push(value);
}
computeAggregatedMetrics() {
// Avoid the loss of significance for the sum.
const sortedValues = this.values.concat().sort((a, b) => a - b);
this.sum = Statistics.sum(sortedValues);
this.min = sortedValues[0];
this.max = sortedValues[sortedValues.length - 1];
this.mean = this.sum / this.values.length;
const product = Statistics.product(sortedValues);
this.geomean = Math.pow(product, 1 / this.values.length);
if (this.values.length > 1) {
const squareSum = Statistics.squareSum(sortedValues);
this.delta = Statistics.confidenceIntervalDelta(0.95, this.values.length, this.sum, squareSum);
this.percentDelta = isNaN(this.delta) ? undefined : (this.delta * 100) / this.mean;
}
}
}

222
resources/score.svg Normal file
View file

@ -0,0 +1,222 @@
<svg fill="none" viewBox="0 0 550 440" width="550" height="440" xmlns="http://www.w3.org/2000/svg">
<style>
:root {
--foreground: rgb(235, 235, 235);
--background: rgb(46, 51, 55);
background-color: var(--background);
color: var(--foreground);
}
mtable {
display: table;
margin: 1em;
}
mtr mtd {
text-align: left;
padding-bottom: 1em;
}
.legend mtd {
padding-bottom: 0.2em;
}
</style>
<foreignObject width="100%" height="100%">
<math xmlns="http://www.w3.org/1998/Math/MathML" width="100%" height="100%" >
<mtable displaystyle="true" >
<mtr>
<mtd>
<mrow>
<mi>Score</mi>
</mrow>
</mtd>
<mtd>
<mrow>
<mo>=</mo>
<mfrac>
<mn>1</mn>
<mi>n</mi>
</mfrac>
<mrow>
<munderover>
<mo stretchy="true">∑</mo>
<mrow>
<mi>i</mi>
<mo>=</mo>
<mn>1</mn>
</mrow>
<mi>n</mi>
</munderover>
</mrow>
<msub>
<mrow>
<mi>IterationScore</mi>
</mrow>
<mi>i</mi>
</msub>
</mrow>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mrow>
<mi>IterationScore</mi>
</mrow>
<mi>i</mi>
</msub>
</mtd>
<mtd>
<mrow>
<mo>=</mo>
<mfrac>
<mn>1000</mn>
<msub>
<mrow>
<mi>IterationGeoMeanTime</mi>
</mrow>
<mi>i</mi>
</msub>
</mfrac>
</mrow>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mrow>
<mi>IterationGeoMeanTime</mi>
</mrow>
<mi>i</mi>
</msub>
</mtd>
<mtd>
<mrow>
<mo>=</mo>
<mroot>
<mrow>
<mrow>
<munderover>
<mo stretchy="true">∏</mo>
<mrow>
<mi>j</mi>
<mo>=</mo>
<mn>1</mn>
</mrow>
<mi>m</mi>
</munderover>
</mrow>
<msub>
<mrow>
<mi>TestTime</mi>
</mrow>
<mrow>
<mi>i</mi>
<mi>j</mi>
</mrow>
</msub>
</mrow>
<mi>m</mi>
</mroot>
</mrow>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mrow>
<mi>TestTime</mi>
</mrow>
<mrow>
<mi>i</mi>
<mi>j</mi>
</mrow>
</msub>
</mtd>
<mtd>
<mrow>
<mo>=</mo>
<mrow>
<munder>
<mo stretchy="true">∑</mo>
<mi>k</mi>
</munder>
</mrow>
<msub>
<mrow>
<mi>TestStepTime</mi>
</mrow>
<mrow>
<mi>i</mi>
<mi>j</mi>
<mi>k</mi>
</mrow>
</msub>
</mrow>
</mtd>
</mtr>
<mtr>
<mtd>
<msub>
<mrow>
<mi>TestStepTime</mi>
</mrow>
<mrow>
<mi>i</mi>
<mi>j</mi>
<mi>k</mi>
</mrow>
</msub>
</mtd>
<mtd>
<mrow>
<mo>=</mo>
<msub>
<mrow>
<mi>TestStepSyncTime</mi>
</mrow>
<mrow>
<mi>i</mi>
<mi>j</mi>
<mi>k</mi>
</mrow>
</msub>
<mo>+</mo>
<msub>
<mrow>
<mi>TestStepAsyncTime</mi>
</mrow>
<mrow>
<mi>i</mi>
<mi>j</mi>
<mi>k</mi>
</mrow>
</msub>
</mrow>
</mtd>
</mtr>
<mtr class="legend">
<mtd><mi>n</mi></mtd>
<mtd>= Number of Iterations (default = 10)</mtd>
</mtr>
<mtr class="legend">
<mtd><mi>i</mi></mtd>
<mtd>= Iteration Index</mtd>
</mtr>
<mtr class="legend">
<mtd><mi>m</mi></mtd>
<mtd>= Number of Suites</mtd>
</mtr>
<mtr class="legend">
<mtd><mi>j</mi></mtd>
<mtd>= Suite Index</mtd>
</mtr>
<mtr class="legend">
<mtd><mi>k</mi></mtd>
<mtd>= Test Index</mtd>
</mtr>
</mtable>
</math>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -0,0 +1,45 @@
const fs = require("fs").promises;
const { resolve } = require("path");
const dirName = "./dist";
// spelling out words that this file doesn't trigger any bad word filters.
const parts = [
["b", "l", "a", "c", "k", "l", "i", "s", "t"],
["w", "h", "i", "t", "e", "l", "i", "s", "t"],
];
const wordsToUse = ["disallowlist", "allowlist"];
const wordsToReplace = parts.map((part) => part.join(""));
const replacements = new Map();
wordsToReplace.forEach((word, index) => replacements.set(word, wordsToUse[index]));
const stringReplaceRegex = new RegExp(wordsToReplace.join("|"), "gi");
async function getFiles(dir) {
const dirents = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
dirents.map((dirent) => {
const res = resolve(dir, dirent.name);
return dirent.isDirectory() ? getFiles(res) : res;
})
);
return Array.prototype.concat(...files);
}
async function readAndReplace(fileName) {
const contents = await fs.readFile(fileName, "utf8");
const sanitized = contents.replace(stringReplaceRegex, function (matched) {
return replacements.get(matched);
});
if (contents !== sanitized)
await fs.writeFile(fileName, sanitized);
}
async function sanitize() {
const dir = process.env.OUTPUT_FOLDER ?? dirName;
const files = await getFiles(dir);
await Promise.all(files.map((file) => readAndReplace(file)));
console.log(`** Done sanitizing ${dir}! **`);
}
sanitize();

View file

@ -0,0 +1,6 @@
{
"name": "sanitize-language",
"version": "1.0.0",
"description": "A script to parse files for non-inclusive words.",
"main": "index.js"
}

View file

@ -0,0 +1,174 @@
/* eslint-disable no-case-declarations */
import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
import { Params } from "./params.mjs";
/**
* BenchmarkStep
*
* A single test step, with a common interface to interact with.
*/
export class BenchmarkStep {
constructor(name, run) {
this.name = name;
this.run = run;
}
async runAndRecordStep(params, suite, step) {
const stepRunner = new StepRunner(null, null, params, suite, step);
const result = await stepRunner.runStep();
return result;
}
}
export class AsyncBenchmarkStep extends BenchmarkStep {
async runAndRecordStep(params, suite, step) {
const stepRunner = new AsyncStepRunner(null, null, params, suite, step);
const result = await stepRunner.runStep();
return result;
}
}
export const BENCHMARK_SUITE_TYPE = Object.freeze({
__proto__: null,
sync: "sync",
async: "async",
});
/**
* BenchmarkSuite
*
* A single test suite that contains one or more test steps.
*/
export class BenchmarkSuite {
constructor(name, steps, type = BENCHMARK_SUITE_TYPE.sync) {
this.name = name;
this.steps = steps;
this.type = type;
console.assert(this.type in BENCHMARK_SUITE_TYPE, "Invalid Type", this.type);
}
async runAndRecordSuite(params, onProgress) {
const measuredValues = {
tests: {},
prepare: 0,
total: 0,
};
const suiteStartLabel = `suite-${this.name}-start`;
const suiteEndLabel = `suite-${this.name}-end`;
performance.mark(suiteStartLabel);
for (const step of this.steps) {
const rawResult = await step.runAndRecordStep(params, this, step);
console.assert(rawResult, "Missing test return value", step);
const { syncTime, asyncTime } = rawResult;
const total = syncTime + asyncTime;
const result = {
tests: { Sync: syncTime, Async: asyncTime },
total: total,
};
if (!step.ignoreResult) {
measuredValues.tests[step.name] = result;
measuredValues.total += total;
}
onProgress?.(step.name);
}
performance.mark(suiteEndLabel);
performance.measure(`suite-${this.name}`, suiteStartLabel, suiteEndLabel);
return {
type: "suite-tests-complete",
status: "success",
result: measuredValues,
suiteName: this.name,
};
}
}
export class AsyncBenchmarkSuite extends BenchmarkSuite {
constructor(name, steps) {
super(name, steps, BENCHMARK_SUITE_TYPE.async);
}
}
export const MESSAGE_TYPE = Object.freeze({
__proto__: null,
appReady: "app-ready",
suiteStart: "suite-start",
stepComplete: "step-complete",
suiteComplete: "suite-complete",
});
export const MESSAGE_STATUS = Object.freeze({
__proto__: null,
success: "success",
error: "error",
});
/** **********************************************************************
* BenchmarkConnector
*
* postMessage is used to communicate between app and benchmark.
* When the app is ready, an 'app-ready' message is sent to signal that the app can receive instructions.
*
* A prepare script within the apps appends window.name and window.version from the package.json file.
* The appId is build by appending name-version
* It's used as an additional safe-guard to ensure the correct app responds to a message.
*************************************************************************/
export class BenchmarkConnector {
constructor(suites, name, version) {
this.suites = suites;
this.name = name;
this.version = version;
if (!name || !version)
console.warn("No name or version supplied, to create a unique appId");
this.appId = name && version ? `${name}-${version}` : -1;
this.onMessage = this.onMessage.bind(this);
}
async onMessage(event) {
const message = event.data;
if (message.appId !== this.appId || message.key !== "benchmark-connector") {
console.warning("Invalid message", message);
return;
}
switch (message.type) {
case MESSAGE_TYPE.suiteStart:
const params = new Params(new URLSearchParams(window.location.search));
const { name } = message.payload;
const suite = this.suites[name];
if (!suite)
console.error(`Suite with the name of "${name}" not found!`);
const onProgress = (step) => this._sendMessage(MESSAGE_TYPE.stepComplete, { name: this.name, step });
const { result } = await suite.runAndRecordSuite(params, onProgress);
this._sendMessage(MESSAGE_TYPE.suiteComplete, { result });
this.disconnect();
break;
default:
console.error(`Message data type not supported: ${message.type}`);
}
}
_sendMessage(type, payload, status = MESSAGE_STATUS.success) {
const message = {
appId: this.appId,
type: type,
payload: payload,
status: status,
};
window.top.postMessage(message, "*");
}
connect() {
window.addEventListener("message", this.onMessage);
this._sendMessage(MESSAGE_TYPE.appReady, { appId: this.appId });
}
disconnect() {
window.removeEventListener("message", this.onMessage);
}
}

View file

@ -0,0 +1,26 @@
import fs from "fs";
import path from "path";
function walkDir(dir, fileList = []) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory())
walkDir(filePath, fileList);
else
fileList.push(filePath);
}
return fileList;
}
export function generateResourcesFile(distPath) {
if (!fs.existsSync(distPath)) {
console.warn(`Directory ${distPath} does not exist, skipping resources.txt generation.`);
return;
}
const absoluteDist = path.resolve(distPath);
const files = walkDir(absoluteDist);
const relativePaths = files.map((f) => path.relative(absoluteDist, f)).filter((f) => f !== "resources.txt");
fs.writeFileSync(path.join(absoluteDist, "resources.txt"), `${relativePaths.join("\n")}\n`, "utf8");
console.log(`Generated resources.txt at ${distPath}`);
}

View file

@ -0,0 +1,61 @@
/**
* Recursively queries the DOM for an element, traversing through shadow DOMs.
*
* @param {Element|ShadowRoot} lookupStartNode The node or shadow root to start the lookup from.
* @param {string[]} path An array of CSS selectors representing the path to the target element.
* @returns {Element|null} The target element if found, otherwise null.
*/
export function recursivelyQuerySelector(lookupStartNode, path) {
lookupStartNode = lookupStartNode.shadowRoot ?? lookupStartNode;
const target = path.reduce((root, selector) => {
const node = root.querySelector(selector);
return node.shadowRoot ?? node;
}, lookupStartNode);
return target;
}
/**
* Retrieves a single DOM element, optionally traversing through shadow DOMs to a specified path before the final selection.
*
* @param {string} selector The CSS selector for the desired element.
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
* @returns {Element|null} The found element, or null if not found.
*/
export function getElement(selector, path = [], lookupStartNode = document) {
const element = recursivelyQuerySelector(lookupStartNode, path).querySelector(selector);
return element;
}
/**
* Retrieves all DOM elements matching a selector, optionally traversing through shadow DOMs to a specified path before the final selection.
*
* @param {string} selector The CSS selector for the desired elements.
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
* @returns {Element[]} An array of found elements.
*/
export function getAllElements(selector, path = [], lookupStartNode = document) {
const elements = Array.from(recursivelyQuerySelector(lookupStartNode, path).querySelectorAll(selector));
return elements;
}
export function forceLayout(body, layoutMode = "getBoundingRectAndElementFromPoint") {
body ??= document.body;
const rect = body.getBoundingClientRect();
switch (layoutMode) {
case "getBoundingRectAndElementFromPoint":
return document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0);
case "getBoundingClientRect":
return rect.height;
default:
throw Error(`Invalid layoutMode: ${layoutMode}`);
}
}
export function skipInShell(context) {
if (typeof window === "undefined" || typeof document === "undefined")
// Skip in non-browser environments
context.skip();
}

View file

@ -0,0 +1,5 @@
{
"name": "speedometer-utils",
"version": "1.0.0",
"description": "Utility files for Speedometer & Workloads"
}

264
resources/shared/params.mjs Normal file
View file

@ -0,0 +1,264 @@
export const LAYOUT_MODES = Object.freeze(["getBoundingClientRect", "getBoundingRectAndElementFromPoint"]);
export class Params {
viewport = {
width: 800,
height: 600,
};
// Enable a detailed developer menu to change the current Params.
developerMode = false;
startAutomatically = false;
iterationCount = 10;
suites = [];
// A list of tags to filter suites
tags = ["default"];
// Toggle running a dummy suite once before the normal test suites.
useWarmupSuite = false;
// toggle async type vs default raf type.
useAsyncSteps = false;
// Change how a test measurement is triggered and async time is measured:
// "timer": The classic (as in Speedometer 2.x) way using setTimeout
// "raf": Using rAF callbacks, both for triggering the sync part and for measuring async time.
measurementMethod = "raf";
// Wait time after suite preparation in ms.
waitAfterSetup = 0;
// Wait time before the sync step in ms.
waitBeforeSync = 0;
// Wait after running a suite.
waitAfterSuite = 0;
// Warmup time before the sync step in ms.
warmupBeforeSync = 0;
// Seed for shuffling the execution order of suites.
// "off": do not shuffle
// "generate": generate a random seed
// <integer>: use the provided integer as a seed
shuffleSeed = "off";
// Param to tweak the relative complexity of all suites.
// The default is 1.0, and for suites supporting this param, the duration
// roughly scales with the complexity.
complexity = 1.0;
// Choices: "getBoundingClientRect" or "getBoundingRectAndElementFromPoint"
layoutMode = LAYOUT_MODES[0];
// Measure more workload prepare time.
measurePrepare = false;
// External config url to override internal tests.
config = "";
constructor(searchParams = undefined, warnUnused = false) {
if (searchParams)
this._copyFromSearchParams(searchParams, warnUnused);
if (!this.developerMode) {
Object.freeze(this.viewport);
Object.freeze(this);
}
}
_parseNumber(value, errorMessage, minValue = 0) {
const number = Number(value);
if (!Number.isFinite(number) && errorMessage)
throw new Error(`Invalid ${errorMessage} param: '${value}', expected Number.`);
if (number < minValue)
throw new Error(`Invalid ${errorMessage} param: '${value}', value must be >= ${minValue}.`);
return number;
}
_parseInt(value, errorMessage, minValue = 0) {
const number = this._parseNumber(value, errorMessage, minValue);
if (!Number.isInteger(number) && errorMessage)
throw new Error(`Invalid ${errorMessage} param: '${value}', expected int.`);
return parseInt(number);
}
_copyFromSearchParams(searchParams, warnUnused) {
this.viewport = this._parseViewport(searchParams);
this.startAutomatically = this._parseBooleanParam(searchParams, "startAutomatically");
this.iterationCount = this._parseIntParam(searchParams, "iterationCount", 1);
this.suites = this._parseSuites(searchParams);
this.tags = this._parseTags(searchParams);
this.developerMode = this._parseBooleanParam(searchParams, "developerMode");
this.useWarmupSuite = this._parseBooleanParam(searchParams, "useWarmupSuite");
this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps");
this.waitAfterSetup = this._parseIntParam(searchParams, "waitAfterSetup", 0);
this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0);
this.waitAfterSuite = this._parseIntParam(searchParams, "waitAfterSuite", 0);
this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0);
this.measurementMethod = this._parseEnumParam(searchParams, "measurementMethod", ["raf"]);
this.shuffleSeed = this._parseShuffleSeed(searchParams);
this.complexity = this._parserNumberParam(searchParams, "complexity", 0);
this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES);
this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare");
this.config = this._parseConfig(searchParams);
if (warnUnused) {
const unused = Array.from(searchParams.keys());
if (unused.length > 0)
console.error(`Got unused search params: ${unused.join(", ")}`);
}
}
_parseBooleanParam(searchParams, paramKey) {
if (!searchParams.has(paramKey))
return false;
searchParams.delete(paramKey);
return true;
}
_parserNumberParam(searchParams, paramKey, minValue) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const parsedValue = this._parseNumber(searchParams.get(paramKey), "waitBeforeSync", minValue);
searchParams.delete(paramKey);
return parsedValue;
}
_parseIntParam(searchParams, paramKey, minValue) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const parsedValue = this._parseInt(searchParams.get(paramKey), "waitBeforeSync", minValue);
searchParams.delete(paramKey);
return parsedValue;
}
_parseViewport(searchParams) {
if (!searchParams.has("viewport"))
return defaultParams.viewport;
const viewportParam = searchParams.get("viewport");
const [width, height] = viewportParam.split("x");
const viewport = {
width: this._parseInt(width, "viewport.width"),
height: this._parseInt(height, "viewport.height"),
};
if (this.viewport.width < 800 || this.viewport.height < 600)
throw new Error(`Invalid viewport param: ${viewportParam}`);
searchParams.delete("viewport");
return viewport;
}
_parseSuites(searchParams) {
if (searchParams.has("suite") || searchParams.has("suites")) {
if (searchParams.has("suite") && searchParams.has("suites"))
throw new Error("Params 'suite' and 'suites' can not be used together.");
const value = searchParams.get("suite") || searchParams.get("suites");
const suites = value.split(",");
if (suites.length === 0)
throw new Error("No suites selected");
searchParams.delete("suite");
searchParams.delete("suites");
return suites;
}
return defaultParams.suites;
}
_parseTags(searchParams) {
if (!searchParams.has("tags"))
return defaultParams.tags;
if (this.suites.length)
throw new Error("'suites' and 'tags' cannot be used together.");
const tags = searchParams.get("tags").split(",");
searchParams.delete("tags");
return tags;
}
_parseEnumParam(searchParams, paramKey, enumArray) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const value = searchParams.get(paramKey);
if (!enumArray.includes(value))
throw new Error(`Got invalid ${paramKey}: '${value}', choices are ${enumArray}`);
searchParams.delete(paramKey);
return value;
}
_parseShuffleSeed(searchParams) {
if (!searchParams.has("shuffleSeed"))
return defaultParams.shuffleSeed;
let shuffleSeed = searchParams.get("shuffleSeed");
if (shuffleSeed !== "off") {
if (shuffleSeed === "generate") {
shuffleSeed = Math.floor((Math.random() * 1) << 16);
console.log(`Generated a random suite order seed: ${shuffleSeed}`);
} else {
shuffleSeed = parseInt(shuffleSeed);
}
if (!Number.isInteger(shuffleSeed))
throw new Error(`Invalid shuffle seed: '${shuffleSeed}', must be either 'off', 'generate' or an integer.`);
}
searchParams.delete("shuffleSeed");
return shuffleSeed;
}
_parseConfig(searchParams) {
const config = searchParams.get("config") ?? "";
searchParams.delete("config");
if (config && !isValidJsonUrl(config))
throw new Error("Invalid config url passed in.");
return config;
}
toCompleteSearchParamsObject() {
return this.toSearchParamsObject(false);
}
toSearchParamsObject(filter = true) {
const rawUrlParams = { __proto__: null };
for (const [key, value] of Object.entries(this)) {
// Handle composite values separately.
if (key === "viewport" || key === "suites" || key === "tags")
continue;
// Skip over default values.
if (filter && value === defaultParams[key])
continue;
rawUrlParams[key] = value;
}
if (this.viewport.width !== defaultParams.viewport.width || this.viewport.height !== defaultParams.viewport.height)
rawUrlParams.viewport = `${this.viewport.width}x${this.viewport.height}`;
if (this.suites.length) {
rawUrlParams.suites = this.suites.join(",");
} else if (this.tags.length) {
if (!(this.tags.length === 1 && this.tags[0] === "default"))
rawUrlParams.tags = this.tags.join(",");
} else {
rawUrlParams.suites = "";
}
return new URLSearchParams(rawUrlParams);
}
toSearchParams() {
return this.toSearchParamsObject().toString();
}
isDefault() {
return this === defaultParams;
}
}
function isValidJsonUrl(url) {
if (typeof url !== "string" || url.length === 0)
return false;
try {
new URL(url, "http://www.example.com");
return true;
} catch (error) {
return false;
}
}
export const defaultParams = new Params();
export let paramsError = null;
let maybeCustomParams = defaultParams;
if (globalThis?.location?.search) {
const searchParams = new URLSearchParams(globalThis.location.search);
try {
maybeCustomParams = new Params(searchParams, true);
} catch (e) {
paramsError = e;
console.error("Invalid URL Param", e, "\nUsing defaults as fallback:", maybeCustomParams);
}
}
export const params = maybeCustomParams;

View file

@ -0,0 +1,109 @@
import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs";
import { forceLayout } from "./helpers.mjs";
export class StepRunner {
#frame;
#page;
#params;
#suite;
#step;
#type;
constructor(frame, page, params, suite, step, type) {
this.#suite = suite;
this.#step = step;
this.#params = params;
this.#page = page;
this.#frame = frame;
this.#type = type;
}
get page() {
return this.#page;
}
get step() {
return this.#step;
}
_runSyncStep(step, page) {
step.run(page);
}
async runStep() {
// Prepare all mark labels outside the measuring loop.
const suiteName = this.#suite.name;
const stepName = this.#step.name;
const syncStartLabel = `${suiteName}.${stepName}-start`;
const syncEndLabel = `${suiteName}.${stepName}-sync-end`;
const asyncEndLabel = `${suiteName}.${stepName}-async-end`;
let syncTime;
let asyncStartTime;
let asyncTime;
const runSync = async () => {
if (this.#params.warmupBeforeSync) {
performance.mark("warmup-start");
const startTime = performance.now();
// Infinite loop for the specified ms.
while (performance.now() - startTime < this.#params.warmupBeforeSync)
continue;
performance.mark("warmup-end");
}
performance.mark(syncStartLabel);
const syncStartTime = performance.now();
if (this.#type === "async")
await this._runSyncStep(this.step, this.page);
else
this._runSyncStep(this.step, this.page);
const mark = performance.mark(syncEndLabel);
const syncEndTime = mark.startTime;
syncTime = syncEndTime - syncStartTime;
asyncStartTime = syncEndTime;
};
const measureAsync = () => {
// Some browsers don't immediately update the layout for paint.
// Force the layout here to ensure we're measuring the layout time.
//
// Note: This matches the behavior of Page.layout() in benchmark-runner.mjs.
// Since shared code cannot depend on Page, we duplicate the logic here,
// falling back to document.body for remote workloads that don't have a frame.
const body = this.#frame?.contentDocument?.body ?? document.body;
const value = forceLayout(body, this.#params.layoutMode);
body._leakedLayoutValue = value; // Prevent dead code elimination.
const asyncEndTime = performance.now();
performance.mark(asyncEndLabel);
asyncTime = asyncEndTime - asyncStartTime;
if (this.#params.warmupBeforeSync)
performance.measure("warmup", "warmup-start", "warmup-end");
performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel);
performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel);
};
const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType];
const scheduler = new schedulerClass(runSync, measureAsync, this.#params);
await scheduler.start();
return { syncTime, asyncTime };
}
}
export class AsyncStepRunner extends StepRunner {
async _runSyncStep(step, page) {
await step.run(page);
}
}
export const STEP_RUNNER_LOOKUP = {
__proto__: null,
default: StepRunner,
async: AsyncStepRunner,
remote: StepRunner,
};

View file

@ -0,0 +1,77 @@
class StepScheduler {
constructor(syncCallback, asyncCallback, params) {
this._syncCallback = syncCallback;
this._asyncCallback = asyncCallback;
this._params = params;
}
start() {
return new Promise((resolve) => {
if (this._params.waitBeforeSync)
setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync);
else
this._scheduleCallbacks(resolve);
});
}
}
class RAFStepScheduler extends StepScheduler {
_scheduleCallbacks(resolve) {
requestAnimationFrame(() => this._syncCallback());
requestAnimationFrame(() => {
setTimeout(() => {
this._asyncCallback();
setTimeout(resolve, 0);
}, 0);
});
}
}
class AsyncRAFStepScheduler extends StepScheduler {
static mc = new MessageChannel();
_scheduleCallbacks(resolve) {
let gotTimer = false;
let gotMessage = false;
let gotPromise = false;
const tryTriggerAsyncCallback = () => {
if (!gotTimer || !gotMessage || !gotPromise)
return;
this._asyncCallback();
setTimeout(resolve, 0);
};
requestAnimationFrame(async () => {
await this._syncCallback();
gotPromise = true;
tryTriggerAsyncCallback();
});
requestAnimationFrame(() => {
setTimeout(async () => {
await Promise.resolve();
gotTimer = true;
tryTriggerAsyncCallback();
});
AsyncRAFStepScheduler.mc.port1.addEventListener(
"message",
async function () {
await Promise.resolve();
gotMessage = true;
tryTriggerAsyncCallback();
},
{ once: true }
);
AsyncRAFStepScheduler.mc.port1.start();
AsyncRAFStepScheduler.mc.port2.postMessage("speedometer");
});
}
}
export const STEP_SCHEDULER_LOOKUP = {
__proto__: null,
raf: RAFStepScheduler,
async: AsyncRAFStepScheduler,
};

View file

@ -0,0 +1,12 @@
import { params } from "./params.mjs";
export const defaultNumberOfItemsToAdd = 100;
export let numberOfItemsToAdd = defaultNumberOfItemsToAdd;
export function getNumberOfItemsToAdd() {
return numberOfItemsToAdd;
}
export function handleComplexityChange() {
numberOfItemsToAdd = Math.round(defaultNumberOfItemsToAdd * params.complexity);
}

View file

@ -0,0 +1,734 @@
export const todos = {
en: [
"Electronic Granite Hat",
"Bespoke Soft Table",
"Ergonomic Fresh Bike",
"Luxurious Wooden Cheese",
"Gorgeous Fresh Pizza",
"Rustic Rubber Shirt",
"Modern Rubber Soap",
"Small Bronze Ball",
"Awesome Bronze Shoes",
"Bespoke Steel Chair",
"Practical Plastic Soap",
"Incredible Granite Bacon",
"Elegant Metal Keyboard",
"Electronic Wooden Sausages",
"Tasty Wooden Gloves",
"Luxurious Metal Cheese",
"Awesome Rubber Gloves",
"Sleek Soft Car",
"Licensed Fresh Salad",
"Ergonomic Frozen Towels",
"Modern Rubber Keyboard",
"Tasty Concrete Pizza",
"Handmade Plastic Chicken",
"Luxurious Rubber Chicken",
"Practical Soft Fish",
"Ergonomic Bronze Shirt",
"Handcrafted Plastic Bacon",
"Unbranded Plastic Pants",
"Modern Wooden Sausages",
"Handmade Steel Shoes",
"Rustic Steel Bike",
"Gorgeous Frozen Salad",
"Handmade Bronze Chicken",
"Sleek Granite Bike",
"Generic Concrete Sausages",
"Incredible Plastic Tuna",
"Bespoke Fresh Cheese",
"Electronic Fresh Bacon",
"Licensed Wooden Car",
"Recycled Fresh Fish",
"Incredible Fresh Shoes",
"Practical Soft Chips",
"Small Soft Chicken",
"Intelligent Fresh Mouse",
"Modern Metal Mouse",
"Tasty Granite Gloves",
"Awesome Rubber Bike",
"Small Steel Shirt",
"Refined Concrete Computer",
"Sleek Frozen Shirt",
"Intelligent Concrete Shoes",
"Handmade Rubber Car",
"Sleek Rubber Towels",
"Unbranded Concrete Hat",
"Incredible Plastic Fish",
"Practical Soft Gloves",
"Organic Stone Pizza",
"Generic Wooden Keyboard",
"Recycled Wooden Chips",
"Incredible Rubber Chips",
"Ergonomic Granite Shirt",
"Tasty Frozen Keyboard",
"Gorgeous Steel Soap",
"Luxurious Plastic Chair",
"Elegant Frozen Bike",
"Recycled Steel Chair",
"Modern Bronze Sausages",
"Elegant Wooden Cheese",
"Small Plastic Sausages",
"Luxurious Frozen Shoes",
"Sleek Plastic Sausages",
"Handcrafted Fresh Sausages",
"Incredible Soft Chair",
"Recycled Wooden Soap",
"Soft Rubber Duck",
"Licensed Concrete Tuna",
"Luxurious Granite Pants",
"Refined Rubber Keyboard",
"Handcrafted Plastic Computer",
"Practical Steel Salad",
"Incredible Soft Bacon",
"Practical Metal Fish",
"Elegant Rubber Shirt",
"Handcrafted Rubber Table",
"Gorgeous Wooden Table",
"Fantastic Steel Sausages",
"Small Soft Keyboard",
"Generic Steel Ball",
"Electronic Frozen Hat",
"Gorgeous Fresh Chair",
"Sleek Soft Sausages",
"Gorgeous Wooden Towels",
"Bespoke Granite Pizza",
"Generic Metal Salad",
"Handmade Rubber Cheese",
"Fantastic Steel Chair",
"Handcrafted Frozen Computer",
"Rustic Rubber Mouse",
"Sleek Granite Pizza",
"Gorgeous Plastic Keyboard",
],
"zh-cn": [
"电子花岗岩帽子",
"定制软桌",
"符合人体工程学新鲜自行车",
"豪华的木制奶酪",
"华丽的新鲜披萨",
"质朴的橡胶衬衫",
"现代橡胶肥皂",
"小青铜球",
"很棒的青铜鞋",
"定制钢椅",
"实用的塑料肥皂",
"令人难以置信的花岗岩培根",
"优雅的金属键盘",
"电子木香肠",
"美味的木制手套",
"豪华的金属奶酪",
"很棒的橡胶手套",
"光滑的柔软汽车",
"有执照的新鲜沙拉",
"人体工程学的冷冻毛巾",
"现代橡胶键盘",
"美味的混凝土披萨",
"手工塑料鸡",
"豪华橡胶鸡",
"实用的软鱼",
"人体工程学的青铜衬衫",
"手工制作的塑料培根",
"未品牌的塑料裤",
"现代木香肠",
"手工钢鞋",
"质朴的钢自行车",
"华丽的冷冻沙拉",
"手工铜鸡",
"光滑的花岗岩自行车",
"通用混凝土香肠",
"令人难以置信的塑料金枪鱼",
"定制新鲜奶酪",
"电子新鲜培根",
"有执照的木车",
"回收的新鲜鱼",
"令人难以置信的新鲜鞋",
"实用的软芯片",
"小鸡肉",
"智能新鲜鼠标",
"现代金属鼠标",
"美味的花岗岩手套",
"很棒的橡胶自行车",
"小钢衬衫",
"精制混凝土计算机",
"光滑的冷冻衬衫",
"智能混凝土鞋",
"手工橡胶车",
"光滑的橡胶毛巾",
"未品牌的混凝土帽子",
"令人难以置信的塑料鱼",
"实用的软手套",
"有机石比萨",
"通用木制键盘",
"再生木芯片",
"令人难以置信的橡胶芯片",
"人体工程学花岗岩衬衫",
"美味的冷冻键盘",
"华丽的钢肥皂",
"豪华的塑料椅",
"优雅的冷冻自行车",
"再生钢椅",
"现代铜香肠",
"优雅的木制奶酪",
"小塑料香肠",
"豪华的冷冻鞋",
"光滑的塑料香肠",
"手工制作的新鲜香肠",
"令人难以置信的柔软椅子",
"回收的木制肥皂",
"软橡皮鸭",
"许可的混凝土金枪鱼",
"豪华的花岗岩裤",
"精制橡胶键盘",
"手工制作的塑料计算机",
"实用的钢沙拉",
"令人难以置信的软培根",
"实用的金属鱼",
"优雅的橡胶衬衫",
"手工橡胶桌",
"华丽的木桌",
"奇妙的钢香肠",
"小软键盘",
"通用钢球",
"电子冷冻帽子",
"华丽的新鲜椅子",
"光滑的软香肠",
"华丽的木制毛巾",
"定制的花岗岩披萨",
"通用金属沙拉",
"手工橡胶奶酪",
"奇妙的钢椅",
"手工制作的冷冻计算机",
"质朴的橡胶老鼠",
"光滑的花岗岩披萨",
"华丽的塑料键盘",
],
ja: [
"電子花崗岩の帽子",
"オーダーメイドのソフトテーブル",
"人間工学に基づいた新鮮な自転車",
"豪華な木製チーズ",
"ゴージャスな新鮮なピザ",
"素朴なラバーシャツ",
"モダンゴム石鹸",
"小さなブロンズボール",
"素晴らしいブロンズシューズ",
"オーダーメイドのスチールチェア",
"実用的なプラスチック石鹸",
"信じられないほどの花崗岩のベーコン",
"エレガントなメタルキーボード",
"電子木製ソーセージ",
"おいしい木製手袋",
"豪華なメタルチーズ",
"素晴らしいラバーグローブ",
"洗練されたソフトカー",
"ライセンスされた新鮮なサラダ",
"人間工学に基づいた冷凍タオル",
"モダンなゴムキーボード",
"おいしいコンクリートピザ",
"手作りのプラスチックチキン",
"豪華なゴム鶏",
"実用的な柔らかい魚",
"人間工学に基づいたブロンズシャツ",
"手作りのプラスチックベーコン",
"ブランドのないプラスチックパンツ",
"モダンな木製ソーセージ",
"手作りのスチールシューズ",
"素朴なスチールバイク",
"ゴージャスな冷凍サラダ",
"手作りのブロンズチキン",
"洗練された花崗岩の自転車",
"一般的なコンクリートソーセージ",
"信じられないほどのプラスチックマグロ",
"オーダーメイドのフレッシュチーズ",
"電子新鮮なベーコン",
"ライセンスされた木製車",
"新鮮な魚をリサイクルしました",
"信じられないほどの新鮮な靴",
"実用的なソフトチップ",
"小さな柔らかい鶏",
"インテリジェントな新鮮なマウス",
"現代の金属マウス",
"おいしい花崗岩の手袋",
"素晴らしいラバーバイク",
"小さなスチールシャツ",
"洗練されたコンクリートコンピューター",
"洗練された冷凍シャツ",
"インテリジェントコンクリートシューズ",
"手作りのラバーカー",
"洗練されたゴムタオル",
"ブランドのないコンクリートの帽子",
"信じられないほどのプラスチックの魚",
"実用的なソフトグローブ",
"オーガニックストーンピザ",
"一般的な木製キーボード",
"リサイクルされた木製チップ",
"信じられないほどのゴムチップ",
"人間工学に基づいた花崗岩のシャツ",
"おいしい冷凍キーボード",
"ゴージャスなスチールソープ",
"豪華なプラスチック製の椅子",
"エレガントな冷凍自転車",
"リサイクルスチールチェア",
"モダンブロンズソーセージ",
"エレガントな木製チーズ",
"小さなプラスチックソーセージ",
"豪華な冷凍靴",
"なめらかなプラスチックソーセージ",
"手作りの新鮮なソーセージ",
"信じられないほどのソフトチェア",
"リサイクルされた木製石鹸",
"柔らかいラバーアヒル",
"ライセンスされたコンクリートマグロ",
"豪華な花崗岩のズボン",
"洗練されたゴムキーボード",
"手作りのプラスチックコンピューター",
"実用的なスチールサラダ",
"信じられないほどのソフトベーコン",
"実用的な金属魚",
"エレガントなラバーシャツ",
"手作りのゴムテーブル",
"ゴージャスな木製のテーブル",
"素晴らしいスチールソーセージ",
"小さなソフトキーボード",
"ジェネリックスチールボール",
"電子冷凍帽子",
"ゴージャスな新鮮な椅子",
"洗練された柔らかいソーセージ",
"ゴージャスな木製タオル",
"オーダーメイドの花崗岩のピザ",
"ジェネリックメタルサラダ",
"手作りのゴムチーズ",
"素晴らしいスチールチェア",
"手作りの冷凍コンピューター",
"素朴なゴムマウス",
"洗練された花崗岩のピザ",
"ゴージャスなプラスチックキーボード",
],
es: [
"Sombrero de granito electrónico",
"Mesa suave a medida",
"Bicicleta ergonómica fresca",
"Lujoso queso de madera",
"Hermosa pizza fresca",
"Camisa de goma rústica",
"Jabón de goma moderno",
"Bola de bronce pequeña",
"Zapatos de bronce impresionantes",
"Silla de acero a medida",
"Jabón de plástico práctico",
"Tocino de granito increíble",
"Teclado de metal elegante",
"Salchichas de madera electrónica",
"Sabrosos guantes de madera",
"Lujoso queso de metal",
"Guantes de goma impresionantes",
"Coche suave y elegante",
"Ensalada fresca con licencia",
"Toallas congeladas ergonómicas",
"Teclado de goma moderno",
"Pizza de hormigón sabrosa",
"Pollo de plástico hecho a mano",
"Pollo de goma lujoso",
"Pescado suave práctico",
"Camisa de bronce ergonómica",
"Tocino de plástico artesanal",
"Pantalones de plástico sin marca",
"Salchichas de madera modernas",
"Zapatos de acero hechos a mano",
"Bicicleta de acero rústica",
"Hermosa ensalada congelada",
"Pollo bronce hecho a mano",
"Bicicleta de granito elegante",
"Salchichas de concreto genérico",
"Atún de plástico increíble",
"Queso fresco a medida",
"Tocino fresco electrónico",
"Coche de madera con licencia",
"Pescado reciclado",
"Increíbles zapatos frescos",
"Chips suaves prácticos",
"Pollo pequeño y suave",
"Ratón fresco inteligente",
"Ratón de metal moderno",
"Sabrosos guantes de granito",
"Impresionante bicicleta de goma",
"Camisa de acero pequeña",
"Ordenador de concreto refinada",
"Camisa elegante congelada",
"Zapatos de concreto inteligentes",
"Coche de goma hecho a mano",
"Toallas de goma elegantes",
"Sombrero de hormigón sin marca",
"Pescado de plástico increíble",
"Guantes suaves prácticos",
"Pizza de piedra orgánica",
"Teclado de madera genérico",
"Chips de madera reciclados",
"Increíbles chips de goma",
"Camisa de granito ergonómico",
"Sabroso teclado congelado",
"Hermoso jabón de acero",
"Lujosa silla de plástico",
"Bicicleta congelada elegante",
"Silla de acero reciclada",
"Salchichas de bronce modernas",
"Elegante queso de madera",
"Pequeñas salchichas de plástico",
"Zapatos congelados lujosos",
"Salchichas de plástico elegantes",
"Salchichas frescas artesanales",
"Increíble silla suave",
"Jabón de madera reciclado",
"Pato de goma suave",
"Atún de hormigón con licencia",
"Pantalones de granito lujosos",
"Teclado de goma refinado",
"Computadora de plástico hecha a mano",
"Ensalada de acero práctica",
"Bocino suave increíble",
"Pescado de metal práctico",
"Elegante camisa de goma",
"Mesa de goma artesanal",
"Hermosa mesa de madera",
"Fantásticas salchichas de acero",
"Pequeño teclado suave",
"Bola de acero genérica",
"Sombrero de congelado",
"Hermosa silla fresca",
"Salchichas suaves y elegantes",
"Hermosas toallas de madera",
"Pizza de granito a medida",
"Ensalada de metal genérica",
"Queso de goma hecho a mano",
"Fantástica silla de acero",
"Computadora congelada hecha a mano",
"Ratón de goma rústica",
"Pizza de granito elegante",
"Hermoso teclado de plástico",
],
de: [
"Elektronischer Granithut",
"Maßgeschneiderter weicher Tisch",
"Ergonomisches frisches Fahrrad",
"Luxuriöser Holzkäse",
"Wunderschöne frische Pizza",
"Rustikales Gummihemd",
"Moderne Gummiseife",
"Kleine Bronzekugel",
"Tolle Bronzeschuhe",
"Maßgeschneiderter Stahlstuhl",
"Praktische Plastikseife",
"Unglaublicher Granitspeck",
"Elegante Metall -Tastatur",
"Elektronische Holzwurst",
"Leckere Holzhandschuhe",
"Luxuriöser Metallkäse",
"Super Gummihandschuhe",
"Schlankes weiches Auto",
"Lizenzierter frischer Salat",
"Ergonomische gefrorene Handtücher",
"Moderne Gummi Tastatur",
"Leckere Betonpizza",
"Handgefertigtes Plastikhuhn",
"Luxuriöses Gummihähnchen",
"Praktischer weicher Fisch",
"Ergonomisches Bronzehemd",
"Handgefertigter Plastikspeck",
"Plastikhosen ohne Marken",
"Moderne Holz Würste",
"Handgefertigte Stahlschuhe",
"Rustikales Stahlrad",
"Wunderschöner gefrorener Salat",
"Handgefertigtes Bronze Hühnchen",
"Schlankes Granitrad",
"Generische Beton Würste",
"Unglaublicher Plastik Thunfisch",
"Maßgeschneiderter frischer Käse",
"Elektronischer frischer Speck",
"Lizenziertes Holzauto",
"Recycelter frischer Fisch",
"Unglaubliche frische Schuhe",
"Praktische weiche Chips",
"Kleines weiches Huhn",
"Intelligente frische Maus",
"Moderne Metallmaus",
"Leckere Granithandschuhe",
"Super Gummi Fahrrad",
"Kleines Stahlhemd",
"Raffinierter Betoncomputer",
"Schlankes gefrorenes Hemd",
"Intelligente Betonschuhe",
"Handgefertigtes Gummiauto",
"Schlanke Gummi Handtücher",
"Betonhut ohne Markenzeichen",
"Unglaublicher Plastikfisch",
"Praktische weiche Handschuhe",
"Bio Steinpizza",
"Generische hölzerne Tastatur",
"Recycelte Holzchips",
"Unglaubliche Gummischchips",
"Ergonomisches Granithemd",
"Leckere gefrorene Tastatur",
"Wunderschöne Stahlseife",
"Luxuriöser Plastikstuhl",
"Elegantes gefrorenes Fahrrad",
"Recycelter Stahlstuhl",
"Moderne Bronze Würste",
"Eleganter Holzkäse",
"Kleine Plastikwurst",
"Luxuriöse gefrorene Schuhe",
"Schlanke Plastikwurst",
"Handgefertigte frische Würste",
"Unglaublicher weicher Stuhl",
"Recycelte Holzseife",
"Weiche Gummi Ente",
"Lizenzierter Beton Thunfisch",
"Luxuriöse Granithosen",
"Raffinierte Gummi Tastatur",
"Handgefertigter Kunststoffcomputer",
"Praktischer Stahlsalat",
"Unglaublicher weicher Speck",
"Praktische Metallfische",
"Elegantes Gummihemd",
"Handgefertigter Gummi Tisch",
"Wunderschöner Holztisch",
"Fantastische Stahlwurst",
"Kleine weiche Tastatur",
"Generischer Stahlkugel",
"Elektronischer Gefrorenhut",
"Wunderschöner frischer Stuhl",
"Schlanke weiche Würste",
"Wunderschöne Holztücher",
"Maßgeschneiderte Granitpizza",
"Generischer Metallsalat",
"Handgefertigter Gummi Käse",
"Fantastischer Stahlstuhl",
"Handgefertigter gefrorener Computer",
"Rustikale Gummi Maus",
"Schlanke Granitpizza",
"Wunderschöne Plastiktastatur",
],
ru: [
"Электронная гранитная шляпа",
"Беспокойный мягкий стол",
"Эргономичный свежий велосипед",
"Роскошный деревянный сыр",
"Великолепная свежая пицца",
"Деревенская резиновая рубашка",
"Современное резиновое мыло",
"Маленький бронзовый мяч",
"Потрясающие бронзовые туфли",
"Стальное кресло на заказ",
"Практическое пластиковое мыло",
"Невероятный гранитный бекон",
"Элегантная металлическая клавиатура",
"Электронные деревянные колбасы",
"Вкусные деревянные перчатки",
"Роскошный металлический сыр",
"Потрясающие резиновые перчатки",
"Гладкая мягкая машина",
"Лицензированный свежий салат",
"Эргономичные замороженные полотенца",
"Современная резиновая клавиатура",
"Вкусная бетонная пицца",
"Пластиковая курица ручной работы",
"Роскошная резиновая курица",
"Практическая мягкая рыба",
"Эргономичная бронзовая рубашка",
"Пластиковый бекон из ручной работы",
"Непревзойденные пластиковые брюки",
"Современные деревянные колбасы",
"Стальные туфли ручной работы",
"Деревенский стальный велосипед",
"Великолепный замороженный салат",
"Бронзовая курица ручной работы",
"Гладкий гранитный велосипед",
"Общие бетонные колбасы",
"Невероятный пластиковый тунец",
"Созданный свежий сыр",
"Электронный свежий бекон",
"Лицензированный деревянный автомобиль",
"Переработанная свежая рыба",
"Невероятные свежие туфли",
"Практические мягкие чипсы",
"Маленькая мягкая курица",
"Интеллектуальная свежая мышь",
"Современная металлическая мышь",
"Вкусные гранитные перчатки",
"Потрясающий резиновый велосипед",
"Маленькая стальная рубашка",
"Изысканный бетонный компьютер",
"Гладкая замороженная рубашка",
"Интеллектуальные бетонные туфли",
"Резиновый автомобиль ручной работы",
"Гладкие резиновые полотенца",
"Непревзойденная бетонная шляпа",
"Невероятная пластиковая рыба",
"Практические мягкие перчатки",
"Органическая камня пицца",
"Общая деревянная клавиатура",
"Переработанные деревянные чипсы",
"Невероятные резиновые чипсы",
"Эргономичная гранитная рубашка",
"Вкусная замороженная клавиатура",
"Великолепное стальное мыло",
"Роскошное пластиковое кресло",
"Элегантный замороженный велосипед",
"Переработанное стальное кресло",
"Современные бронзовые колбасы",
"Элегантный деревянный сыр",
"Маленькие пластиковые колбаски",
"Роскошные замороженные туфли",
"Гладкие пластиковые колбаски",
"Свежие колбаски ручной работы",
"Невероятный мягкий стул",
"Переработанное деревянное мыло",
"Мягкая резиновая утка",
"Лицензированный бетонный тунец",
"Роскошные гранитные штаны",
"Рафинированная резиновая клавиатура",
"Пластиковый компьютер ручной работы",
"Практический стальной салат",
"Невероятный мягкий бекон",
"Практическая металлическая рыба",
"Элегантная резиновая рубашка",
"Ручной резиновый стол",
"Великолепный деревянный стол",
"Фантастические стальные колбаски",
"Небольшая мягкая клавиатура",
"Общий стальной шар",
"Электронная замороженная шляпа",
"Великолепный свежий стул",
"Гладкие мягкие колбаски",
"Великолепные деревянные полотенца",
"Сделанная на заказ гранитная пицца",
"Общий металлический салат",
"Резиновый сыр ручной работы",
"Фантастическое стальное кресло",
"Замороженный компьютер ручной работы",
"Деревенская резиновая мышь",
"Главная гранитная пицца",
"Великолепная пластиковая клавиатура",
],
emoji: [
"Electronic Granite Hat 👆🏻",
"Bespoke Soft 🍷 Table",
"Ergonomic Fresh Bike 😚😚",
"Luxurious 🍉 Wooden Cheese 🍮",
"Gorgeous Fresh Pizza ⛔",
"Rustic 💪🏽 Rubber Shirt",
"Modern Rubber 🍀 Soap",
"👍 Small Bronze Ball 👍",
"Awesome Bronze Shoes 😎",
"Bespoke 👈🏽 Steel Chair",
"Practical Plastic 💋 💋 Soap",
"🙌🏻 Incredible Granite Bacon",
"🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃",
"Electronic Wooden Sausages 🌷",
"Tasty 🍺 Wooden Gloves",
"🏖️ Luxurious Metal Cheese",
"Awesome Rubber 😉 Gloves",
"Sleek Soft Car 💁🏻‍♂️",
"Licensed 👏👏👏 Fresh Salad",
"Ergonomic Frozen Towels 🐇",
"🖐🏻 Modern Rubber Keyboard",
"Tasty Concrete Pizza ✨✨",
"Handmade 😘 Plastic 😘 Chicken 😘",
"🏁 Luxurious Rubber Chicken 🏁",
"Practical Soft Fish 🤍",
"Ergonomic Bronze Shirt 😍",
"😸😸 Handcrafted 🐻 Plastic Bacon",
"Unbranded 🐭 Plastic Pants",
"🤘 Modern 🤘 Wooden 🤘 Sausages",
"Handmade Steel Shoes 👍",
"Rustic 🧁🧁 Steel Bike",
"Gorgeous Frozen Salad 👩‍💻",
"Handmade Bronze Chicken 😮😸",
"Sleek 🍐 Granite Bike",
" ❌ ❌ Generic Concrete Sausages",
"Incredible 🍉 Plastic Tuna",
"Bespoke Fresh Cheese 😘",
"💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡",
"Licensed 🍐 Wooden 🍅 Car ",
"Recycled Fresh Fish 🤡",
"📞 Incredible Fresh Shoes",
"Practical 🐻🐻 Soft Chips",
"Small 💝 Soft Chicken",
" 💝 Intelligent Fresh Mouse 💝",
"Modern Metal 🧵 Mouse",
"🦈 Tasty Granite Gloves",
"Awesome Rubber Bike 😡😡😡😡",
"🚮 Small Steel Shirt 🚮",
"Refined 🌲 Concrete Computer",
"Sleek Frozen Shirt 👨‍🦰",
"Intelligent Concrete ➗➗➗ Shoes",
"🏅Handmade Rubber Car",
"Sleek 👨🏼‍🌾 Rubber 👨🏼‍🌾 Towels",
"Unbranded Concrete Hat🎇",
"🌀🌀Incredible Plastic Fish",
"Practical Soft Gloves🌶️ 🌶️",
"Organic 🍞 Stone 🔽 Pizza 🥴",
"Generic Wooden Keyboard 💙",
"Recycled 🔴 Wooden Chips",
"Incredible Rubber Chips 🍹",
"🌵 Ergonomic Granite Shirt",
"Tasty Frozen 🦄 Keyboard",
"🍣 Gorgeous Steel 🥯 Soap",
"Luxurious Plastic Chair 🧑‍🦰",
"Elegant Frozen 🧑‍🦰 Bike",
"Recycled 🟠🟠 Steel Chair",
"⭐⭐ Modern ⭐ Bronze ⭐ Sausages",
"Elegant Wooden Cheese🤘",
"Small 🎎 Plastic 🛩️ Sausages",
"*️⃣*️⃣*️⃣*️⃣ Luxurious Frozen Shoes",
"Sleek Plastic Sausages 🚩",
"Handcrafted Fresh 💮 Sausages",
"Incredible 🤢🤢 Soft Chair",
"🇬🇪 Recycled 🇲🇺 Wooden Soap",
"Soft 🦌 Rubber Duck 🐥",
"Licensed Concrete Tuna 👎👎",
"Luxurious Granite 💝 Pants",
"Refined Rubber Keyboard 💝",
"👌🏻👌🏻 Handcrafted Plastic Computer",
"Practical Steel 🐪 Salad",
"Incredible Soft Bacon 🌺",
"Practical Metal 🥊 Fish",
"Elegant 👩🏾‍❤️‍💋‍👨🏽👩🏾‍❤️‍💋‍👨🏽 Rubber Shirt",
"🛺 Handcrafted Rubber Table",
"Gorgeous 🦙 Wooden Table 🦙",
"🍉 Fantastic Steel Sausages",
"Small Soft Keyboard👟",
"Generic 🦙🦙 Steel Ball",
"Electronic Frozen Hat ✌🏾✌🏾✌🏾✌🏾✌🏾",
"Gorgeous 🍏 Fresh Chair",
"Sleek Soft 💧 Sausages",
"Gorgeous Wooden Towels 🍿",
"Bespoke 🌅 Granite Pizza",
"Generic Metal Salad 🎗️",
"✨ Handmade ✨ Rubber ✨ Cheese ✨",
"Fantastic 🐥 🌺 🤷🏾 Steel Chair",
"Handcrafted Frozen Computer 🛡️ 🧸 🐓",
"🐹 Rustic Rubber Mouse",
"💠 Sleek Granite Pizza 💠",
"Gorgeous 🧝🏻‍♂️ Plastic Keyboard",
],
};
export const defaultTodoText = {
en: "Something to do",
"zh-cn": "做某事",
ja: "何かをする必要がある",
es: "Algo que hacer",
de: "Etwas zu tun",
ru: "Кое-что сделать",
emoji: "Something to do 😊",
};
export const defaultLanguage = "en";
export function getTodoText(lang = "en", index) {
const todosSelection = todos[lang];
const currentIndex = index % todosSelection.length;
return todosSelection[currentIndex];
}

View file

@ -0,0 +1,2 @@
export * from "./helpers.mjs";
export * from "./benchmark.mjs";

127
resources/statistics.mjs Normal file
View file

@ -0,0 +1,127 @@
/*
* Copyright (C) 2012, 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
export function max(values) {
return Math.max.apply(Math, values);
}
export function min(values) {
return Math.min.apply(Math, values);
}
export function sum(values) {
return values.reduce((a, b) => a + b, 0);
}
export function product(values) {
return values.reduce((a, b) => a * b, 1);
}
export function squareSum(values) {
return values.reduce((sum, value) => sum + value * value, 0);
}
// With sum and sum of squares, we can compute the sample standard deviation in O(1).
// See https://rniwa.com/2012-11-10/sample-standard-deviation-in-terms-of-sum-and-square-sum-of-samples/
export function sampleStandardDeviation(numberOfSamples, sum, squareSum) {
if (numberOfSamples < 2)
return 0;
return Math.sqrt(squareSum / (numberOfSamples - 1) - (sum * sum) / (numberOfSamples - 1) / numberOfSamples);
}
export function supportedConfidenceLevels() {
const supportedLevels = [];
for (let quantile in tDistributionInverseCDF)
supportedLevels.push((1 - (1 - quantile) * 2).toFixed(2));
return supportedLevels;
}
// Computes the delta d s.t. (mean - d, mean + d) is the confidence interval with the specified confidence level in O(1).
export function confidenceIntervalDelta(confidenceLevel, numberOfSamples, sum, squareSum) {
const probability = 1 - (1 - confidenceLevel) / 2;
if (!(probability in tDistributionInverseCDF)) {
const supportedIntervals = supportedConfidenceLevels().map((level) => `${level * 100}%`);
throw `We only support ${supportedIntervals.join(", ")} confidence intervals.`;
}
if (numberOfSamples - 2 < 0)
return NaN;
const cdfForProbability = tDistributionInverseCDF[probability];
let degreesOfFreedom = numberOfSamples - 1;
if (degreesOfFreedom > cdfForProbability.length)
degreesOfFreedom = cdfForProbability.length - 1;
// tDistributionQuantile(degreesOfFreedom, confidenceLevel) * sampleStandardDeviation / sqrt(numberOfSamples) * S/sqrt(numberOfSamples)
const quantile = cdfForProbability[degreesOfFreedom - 1]; // The first entry is for the one degree of freedom.
return (quantile * sampleStandardDeviation(numberOfSamples, sum, squareSum)) / Math.sqrt(numberOfSamples);
}
export function confidenceInterval(values, probability) {
const sumValue = sum(values);
const mean = sumValue / values.length;
const delta = confidenceIntervalDelta(probability || 0.95, values.length, sumValue, squareSum(values));
return [mean - delta, mean + delta];
}
// See http://en.wikipedia.org/wiki/Student's_t-distribution#Table_of_selected_values
// This table contains one sided (a.k.a. tail) values.
var tDistributionInverseCDF = {
0.9: [
3.077684, 1.885618, 1.637744, 1.533206, 1.475884, 1.439756, 1.414924, 1.396815, 1.383029, 1.372184, 1.36343, 1.356217, 1.350171, 1.34503, 1.340606, 1.336757, 1.333379, 1.330391, 1.327728, 1.325341, 1.323188, 1.321237, 1.31946, 1.317836,
1.316345, 1.314972, 1.313703, 1.312527, 1.311434, 1.310415, 1.309464, 1.308573, 1.307737, 1.306952, 1.306212, 1.305514, 1.304854, 1.30423, 1.303639, 1.303077, 1.302543, 1.302035, 1.301552, 1.30109, 1.300649, 1.300228, 1.299825, 1.299439,
1.299069, 1.298714,
1.298373, 1.298045, 1.29773, 1.297426, 1.297134, 1.296853, 1.296581, 1.296319, 1.296066, 1.295821, 1.295585, 1.295356, 1.295134, 1.29492, 1.294712, 1.294511, 1.294315, 1.294126, 1.293942, 1.293763, 1.293589, 1.293421, 1.293256, 1.293097,
1.292941, 1.29279, 1.292643, 1.2925, 1.29236, 1.292224, 1.292091, 1.291961, 1.291835, 1.291711, 1.291591, 1.291473, 1.291358, 1.291246, 1.291136, 1.291029, 1.290924, 1.290821, 1.290721, 1.290623, 1.290527, 1.290432, 1.29034, 1.29025,
1.290161, 1.290075,
],
0.95: [
6.313752, 2.919986, 2.353363, 2.131847, 2.015048, 1.94318, 1.894579, 1.859548, 1.833113, 1.812461, 1.795885, 1.782288, 1.770933, 1.76131, 1.75305, 1.745884, 1.739607, 1.734064, 1.729133, 1.724718, 1.720743, 1.717144, 1.713872, 1.710882,
1.708141, 1.705618, 1.703288, 1.701131, 1.699127, 1.697261, 1.695519, 1.693889, 1.69236, 1.690924, 1.689572, 1.688298, 1.687094, 1.685954, 1.684875, 1.683851, 1.682878, 1.681952, 1.681071, 1.68023, 1.679427, 1.67866, 1.677927, 1.677224,
1.676551, 1.675905,
1.675285, 1.674689, 1.674116, 1.673565, 1.673034, 1.672522, 1.672029, 1.671553, 1.671093, 1.670649, 1.670219, 1.669804, 1.669402, 1.669013, 1.668636, 1.668271, 1.667916, 1.667572, 1.667239, 1.666914, 1.6666, 1.666294, 1.665996, 1.665707,
1.665425, 1.665151, 1.664885, 1.664625, 1.664371, 1.664125, 1.663884, 1.663649, 1.66342, 1.663197, 1.662978, 1.662765, 1.662557, 1.662354, 1.662155, 1.661961, 1.661771, 1.661585, 1.661404, 1.661226, 1.661052, 1.660881, 1.660715, 1.660551,
1.660391, 1.660234,
],
0.975: [
12.706205, 4.302653, 3.182446, 2.776445, 2.570582, 2.446912, 2.364624, 2.306004, 2.262157, 2.228139, 2.200985, 2.178813, 2.160369, 2.144787, 2.13145, 2.119905, 2.109816, 2.100922, 2.093024, 2.085963, 2.079614, 2.073873, 2.068658, 2.063899,
2.059539, 2.055529, 2.051831, 2.048407, 2.04523, 2.042272, 2.039513, 2.036933, 2.034515, 2.032245, 2.030108, 2.028094, 2.026192, 2.024394, 2.022691, 2.021075, 2.019541, 2.018082, 2.016692, 2.015368, 2.014103, 2.012896, 2.011741, 2.010635,
2.009575, 2.008559,
2.007584, 2.006647, 2.005746, 2.004879, 2.004045, 2.003241, 2.002465, 2.001717, 2.000995, 2.000298, 1.999624, 1.998972, 1.998341, 1.99773, 1.997138, 1.996564, 1.996008, 1.995469, 1.994945, 1.994437, 1.993943, 1.993464, 1.992997, 1.992543,
1.992102, 1.991673, 1.991254, 1.990847, 1.99045, 1.990063, 1.989686, 1.989319, 1.98896, 1.98861, 1.988268, 1.987934, 1.987608, 1.98729, 1.986979, 1.986675, 1.986377, 1.986086, 1.985802, 1.985523, 1.985251, 1.984984, 1.984723, 1.984467,
1.984217, 1.983972,
],
0.99: [
31.820516, 6.964557, 4.540703, 3.746947, 3.36493, 3.142668, 2.997952, 2.896459, 2.821438, 2.763769, 2.718079, 2.680998, 2.650309, 2.624494, 2.60248, 2.583487, 2.566934, 2.55238, 2.539483, 2.527977, 2.517648, 2.508325, 2.499867, 2.492159,
2.485107, 2.47863, 2.47266, 2.46714, 2.462021, 2.457262, 2.452824, 2.448678, 2.444794, 2.44115, 2.437723, 2.434494, 2.431447, 2.428568, 2.425841, 2.423257, 2.420803, 2.41847, 2.41625, 2.414134, 2.412116, 2.410188, 2.408345, 2.406581,
2.404892, 2.403272,
2.401718, 2.400225, 2.39879, 2.39741, 2.396081, 2.394801, 2.393568, 2.392377, 2.391229, 2.390119, 2.389047, 2.388011, 2.387008, 2.386037, 2.385097, 2.384186, 2.383302, 2.382446, 2.381615, 2.380807, 2.380024, 2.379262, 2.378522, 2.377802,
2.377102, 2.37642, 2.375757, 2.375111, 2.374482, 2.373868, 2.37327, 2.372687, 2.372119, 2.371564, 2.371022, 2.370493, 2.369977, 2.369472, 2.368979, 2.368497, 2.368026, 2.367566, 2.367115, 2.366674, 2.366243, 2.365821, 2.365407, 2.365002,
2.364606, 2.364217,
],
};

268
resources/suite-runner.mjs Normal file
View file

@ -0,0 +1,268 @@
import { STEP_RUNNER_LOOKUP } from "./shared/step-runner.mjs";
import { MESSAGE_TYPE } from "./shared/benchmark.mjs";
import { WarmupSuite } from "./benchmark-runner.mjs";
function delay(ms) {
if (ms > 0)
return new Promise((resolve) => setTimeout(resolve, ms));
return undefined;
}
export class SuiteRunner {
#frame;
#page;
#params;
#suite;
#client;
#suiteResults;
#prepareTime = 0;
constructor(frame, page, params, suite, client, measuredValues) {
// FIXME: Create SuiteRunner-local measuredValues.
this.#suiteResults = measuredValues.tests[suite.name];
if (!this.#suiteResults) {
this.#suiteResults = { tests: {}, prepare: 0, total: 0 };
measuredValues.tests[suite.name] = this.#suiteResults;
}
this.#frame = frame;
this.#page = page;
this.#client = client;
this.#suite = suite;
this.#params = params;
}
get frame() {
return this.#frame;
}
get page() {
return this.#page;
}
get params() {
return this.#params;
}
get suite() {
return this.#suite;
}
get client() {
return this.#client;
}
get suiteResults() {
return this.#suiteResults;
}
async run() {
await this._prepareSuite();
await this._runSuite();
}
async _prepareSuite() {
const suiteName = this.#suite.name;
const suitePrepareStartLabel = `suite-${suiteName}-prepare-start`;
const suitePrepareEndLabel = `suite-${suiteName}-prepare-end`;
performance.mark(suitePrepareStartLabel);
await this._loadFrame();
await this.#suite.prepare(this.#page);
performance.mark(suitePrepareEndLabel);
const entry = performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
this.#prepareTime = entry.duration;
}
async _runSuite() {
const suiteName = this.#suite.name;
const suiteStartLabel = `suite-${suiteName}-start`;
const suiteEndLabel = `suite-${suiteName}-end`;
await delay(this.#params.waitAfterSetup);
performance.mark(suiteStartLabel);
for (const step of this.#suite.tests) {
await this.#client?.willRunTest?.(this.#suite, step);
const stepRunnerType = this.#suite.type ?? this.params.useAsyncSteps ? "async" : "default";
const stepRunnerClass = STEP_RUNNER_LOOKUP[stepRunnerType];
const stepRunner = new stepRunnerClass(this.#frame, this.#page, this.#params, this.#suite, step, stepRunnerType);
let { syncTime, asyncTime } = await stepRunner.runStep();
this._recordTestResults(step, syncTime, asyncTime);
}
performance.mark(suiteEndLabel);
await delay(this.#params.waitAfterSuite);
performance.measure(`suite-${suiteName}`, suiteStartLabel, suiteEndLabel);
this._validateSuiteResults();
await this._updateClient();
}
_validateSuiteResults() {
// When the test is fast and the precision is low (for example with Firefox'
// privacy.resistFingerprinting preference), it's possible that the measured
// total duration for an entire is 0.
const suiteTotal = this.#suiteResults.total;
const suitePrepare = this.#suiteResults.prepare;
if (suiteTotal === 0)
throw new Error(`Got invalid 0-time total for suite ${this.#suite.name}: ${suiteTotal}`);
if (this.#params.measurePrepare && suitePrepare === 0)
throw new Error(`Got invalid 0-time prepare time for suite ${this.#suite.name}: ${suitePrepare}`);
}
async _loadFrame() {
return new Promise((resolve, reject) => {
const frame = this.#frame;
frame.onload = () => resolve();
frame.onerror = () => reject();
const url = new URL(this.#suite.url, document.baseURI);
for (const [key, value] of this.#params.toSearchParamsObject())
url.searchParams.append(key, value);
frame.src = url.href;
});
}
async _recordTestResults(step, syncTime, asyncTime) {
// Skip reporting updates for the warmup suite.
if (this.#suite === WarmupSuite)
return;
let total = syncTime + asyncTime;
this.#suiteResults.tests[step.name] = {
tests: { Sync: syncTime, Async: asyncTime },
total: total,
};
this.#suiteResults.prepare = this.#prepareTime;
this.#suiteResults.total += total;
}
async _updateClient(suite = this.#suite) {
await this.#client?.didFinishSuite?.(suite);
}
}
export class RemoteSuiteRunner extends SuiteRunner {
#appId;
#prepareTime;
get appId() {
return this.#appId;
}
set appId(id) {
this.#appId = id;
}
async run() {
this.postMessageCallbacks = new Map();
const handler = this._handlePostMessage.bind(this);
window.addEventListener("message", handler);
// FIXME: use this._suite in all SuiteRunner methods directly.
try {
await this._prepareSuite();
await this._runSuite();
} finally {
window.removeEventListener("message", handler);
}
}
async _prepareSuite() {
const suiteName = this.suite.name;
const suitePrepareStartLabel = `suite-${suiteName}-prepare-start`;
const suitePrepareEndLabel = `suite-${suiteName}-prepare-end`;
performance.mark(suitePrepareStartLabel);
// Wait for the app-ready message from the workload.
const appReadyPromise = this._subscribeOnce(MESSAGE_TYPE.appReady);
await this._loadFrame(this.suite);
// The suite will send out the app-ready message.
const response = await appReadyPromise;
this._initializeAppId(response.appId);
await this.suite.prepare?.(this.page);
performance.mark(suitePrepareEndLabel);
const entry = performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
this.#prepareTime = entry.duration;
}
async _runSuite() {
await delay(this.params.waitAfterSetup);
// Ask workload to run its own tests.
this._sendMessage(MESSAGE_TYPE.suiteStart, { name: this.suite.config?.name || "default" });
// Capture metrics from the completed tests.
const { result } = await this._subscribeOnce(MESSAGE_TYPE.suiteComplete);
await delay(this.params.waitAfterSuite);
this.suiteResults.tests = {
...this.suiteResults.tests,
...result.tests,
};
this.suiteResults.prepare = this.#prepareTime;
this.suiteResults.total = result.total;
this._validateSuiteResults();
await this._updateClient();
}
_sendMessage(type, payload) {
const message = {
appId: this.appId,
key: "benchmark-connector",
type: type,
payload: payload,
};
this.frame.contentWindow.postMessage(message, "*");
}
_handlePostMessage(event) {
const message = event.data;
const callback = this.postMessageCallbacks.get(message.type);
if (callback)
callback(event);
}
_startSubscription(type, callback) {
if (this.postMessageCallbacks.has(type))
throw new Error("Callback exists already");
this.postMessageCallbacks.set(type, callback);
}
_stopSubscription(type) {
if (!this.postMessageCallbacks.has(type))
throw new Error("Callback does not exist");
this.postMessageCallbacks.delete(type);
}
_subscribeOnce(type) {
return new Promise((resolve) => {
this._startSubscription(type, (e) => {
const message = e.data;
if (type !== MESSAGE_TYPE.appReady && message.appId !== this.appId)
throw new Error(`Got message for invalid app: ${message.appId} instead of ${this.appId}`);
this._stopSubscription(type);
resolve(message.payload);
});
});
}
_initializeAppId(appId) {
console.assert(!this.appId, `Cannot receive ${MESSAGE_TYPE.appReady} twice`);
console.assert(appId, `${MESSAGE_TYPE.appReady} sent no appId`);
this.appId = appId;
}
}
export const SUITE_RUNNER_LOOKUP = {
__proto__: null,
default: SuiteRunner,
async: SuiteRunner,
remote: RemoteSuiteRunner,
};

View file

@ -0,0 +1,11 @@
export function freezeSuites(suites) {
suites = suites.map((suite) => {
// FIXME: freeze tags
// suite.tags = Object.freeze(suite.tags);
suite.steps = Object.freeze(suite.steps);
// FIXME: freeze suite after fixing the benchmark-configurator.
// return Object.freeze(suite);
return suite;
});
return suites;
}

View file

@ -0,0 +1,31 @@
# Experimental Workloads
Experimental Speedometer workloads can be used for workloads that do not
directly meet the criteria for official workloads:
- Incubation of new workloads
- Edge-cases that don't work well in the official setup
- Testing features that aren't fully shipped in all browsers
The main purpose is to have a space for low-friction contributions to
experiment with new ideas. There is no explicit requirement to promote
experimental workloads to the official selection.
## Review Process
Contributions to experimental workloads can be merged with the approval from a
single vendor.
This is the opposite from official workloads that are scored, where we require
approval from all vendors.
## Requirements
- Stored in the `/experimental` folder
- Disabled by default and thus do not contribute to the official score
- Use the `experimental` tag in the Suites declaration
## Hosting
Experimental workloads will not be officially hosted on Speedometer releases on
<https://browserbench.org>, unlike the official workloads.

View file

@ -0,0 +1,2 @@
.DS_Store
/node_modules

View file

@ -0,0 +1,72 @@
# Speedometer 3.0: TodoMVC: Web Components IndexedDB
## Description
A todoMVC application implemented with native web components and indexedDB as the backing storage.
It utilizes custom elements and html templates to build reusable components.
In contrast to other workloads, this application uses an updated set of css rules and an optimized dom structure to ensure the application follows best practices in regards to accessibility.
### Benchmark steps
In contrast to other versions of the todoMVC workload, this one only shows 10 todo items at a time.
#### Add 100 items.
All the items are added to the DOM and to the database, it uses CSS to show only 10 of the items on screen.
The measured time stops when the last item has been added to the DOM, it doesn't measure the time spent to complete the database update.
#### Complete 100 items.
The benchmark runs a loop of 10 iterations. On each iteration 10 items are marked completed (in the DOM and in the database), and the "Next page" button is clicked. When moving to the next page the items in the "current page" are deleted from the DOM.
The measured time stops when the last item has been marked as completed, it doesn't measure the time spent to complete the database update.
#### Delete 100 items.
The benchmarks runs a loop of 10 iterations. On each iteration the 10 items in the current page are deleted (from the DOM and the database), and the "Previous page" button is clicked.
When moving to the previous page the previous 10 items are loaded from the database, this is included in the measured time.
## Storage Options
This application supports two different IndexedDB implementations that can be selected via URL search parameters:
- **Vanilla IndexedDB** (default): Uses the native IndexedDB API
- Access via: `http://localhost:7005/?storageType=vanilla` or `http://localhost:7005/`
- **Dexie.js**: Uses the Dexie.js wrapper library for IndexedDB
- Access via: `http://localhost:7005/?storageType=dexie`
Simply use the URL parameters to switch between implementations. The database will be reset when switching between storage types.
Navigation within the app uses simple hash-based routes like `#/active`, `#/completed`, or `#/` for all todos.
## Built steps
A simple build script copies all necessary files to a `dist` folder.
It does not rely on compilers or transpilers and serves raw html, css and js files to the user.
```
npm run build
```
## Requirements
The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server.
```
* Node (min version: 18.13.0)
* NPM (min version: 8.19.3)
```
## Local preview
```
terminal:
1. npm install
2. npm run dev
browser:
1. http://localhost:7005/
```

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
<title>TodoMVC: JavaScript Web Components</title>
<link rel="stylesheet" href="styles/global.css" />
<link rel="stylesheet" href="styles/header.css" />
<link rel="stylesheet" href="styles/footer.css" />
<!-- load these first, so that they are registered by the time the app components needs them -->
<script type="module" src="src/components/todo-topbar/todo-topbar.component.js"></script>
<script type="module" src="src/components/todo-list/todo-list.component.js"></script>
<script type="module" src="src/components/todo-bottombar/todo-bottombar.component.js"></script>
<script type="module" src="src/components/todo-app/todo-app.component.js"></script>
</head>
<body>
<header class="header">
<a href="#" style="text-decoration: none"><h1 class="title">todos</h1></a>
</header>
<todo-app></todo-app>
<footer class="footer">
<p class="footer-text">Click on input field to write your todo.</p>
<p class="footer-text">At least one character is needed to be a valid entry.</p>
<p class="footer-text">Press 'enter' to add the todo.</p>
<p class="footer-text">Double-click to edit a todo</p>
</footer>
<script type="module" src="src/index.mjs"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
index.html
libs/dexie.mjs
src/components/todo-app/todo-app.component.js
src/components/todo-app/todo-app.template.js
src/components/todo-bottombar/todo-bottombar.component.js
src/components/todo-bottombar/todo-bottombar.template.js
src/components/todo-item/todo-item.component.js
src/components/todo-item/todo-item.template.js
src/components/todo-list/todo-list.component.js
src/components/todo-list/todo-list.template.js
src/components/todo-topbar/todo-topbar.component.js
src/components/todo-topbar/todo-topbar.template.js
src/hooks/useDoubleClick.js
src/hooks/useKeyListener.js
src/hooks/useRouter.js
src/index.mjs
src/speedometer-utils/benchmark.mjs
src/speedometer-utils/helpers.mjs
src/speedometer-utils/params.mjs
src/speedometer-utils/step-runner.mjs
src/speedometer-utils/step-scheduler.mjs
src/speedometer-utils/todomvc-utils.mjs
src/speedometer-utils/translations.mjs
src/storage/base-storage-manager.js
src/storage/dexieDB-manager.js
src/storage/indexedDB-manager.js
src/storage/storage-factory.js
src/utils/nanoid.js
src/workload-test.mjs
styles/app.constructable.js
styles/bottombar.constructable.js
styles/footer.css
styles/global.constructable.js
styles/global.css
styles/header.css
styles/main.constructable.js
styles/todo-item.constructable.js
styles/todo-list.constructable.js
styles/topbar.constructable.js

View file

@ -0,0 +1,156 @@
import template from "./todo-app.template.js";
import { useRouter } from "../../hooks/useRouter.js";
import globalStyles from "../../../styles/global.constructable.js";
import appStyles from "../../../styles/app.constructable.js";
import mainStyles from "../../../styles/main.constructable.js";
class TodoApp extends HTMLElement {
#isReady = false;
#numberOfItems = 0;
#numberOfCompletedItems = 0;
constructor() {
super();
const node = document.importNode(template.content, true);
this.topbar = node.querySelector("todo-topbar");
this.list = node.querySelector("todo-list");
this.bottombar = node.querySelector("todo-bottombar");
this.shadow = this.attachShadow({ mode: "open" });
this.htmlDirection = document.dir || "ltr";
this.setAttribute("dir", this.htmlDirection);
this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles];
this.shadow.append(node);
this.addItem = this.addItem.bind(this);
this.toggleItem = this.toggleItem.bind(this);
this.removeItem = this.removeItem.bind(this);
this.updateItem = this.updateItem.bind(this);
this.toggleItems = this.toggleItems.bind(this);
this.clearCompletedItems = this.clearCompletedItems.bind(this);
this.routeChange = this.routeChange.bind(this);
this.moveToNextPage = this.moveToNextPage.bind(this);
this.moveToPreviousPage = this.moveToPreviousPage.bind(this);
this.router = useRouter();
}
get isReady() {
return this.#isReady;
}
getInstance() {
return this;
}
addItem(event) {
const { detail: item } = event;
this.list.addItem(item, this.#numberOfItems++);
this.update();
}
toggleItem(event) {
if (event.detail.completed)
this.#numberOfCompletedItems++;
else
this.#numberOfCompletedItems--;
this.list.toggleItem(event.detail.itemNumber, event.detail.completed);
this.update();
}
removeItem(event) {
if (event.detail.completed)
this.#numberOfCompletedItems--;
this.#numberOfItems--;
this.update();
this.list.removeItem(event.detail.itemNumber);
}
updateItem(event) {
this.update();
}
toggleItems(event) {
this.list.toggleItems(event.detail.completed);
}
clearCompletedItems() {
this.list.removeCompletedItems();
}
moveToNextPage() {
this.list.moveToNextPage();
}
async moveToPreviousPage() {
await this.list.moveToPreviousPage();
this.bottombar.reenablePreviousPageButton();
window.dispatchEvent(new CustomEvent("previous-page-loaded", {}));
}
update() {
const totalItems = this.#numberOfItems;
const completedItems = this.#numberOfCompletedItems;
const activeItems = totalItems - completedItems;
this.list.setAttribute("total-items", totalItems);
this.topbar.setAttribute("total-items", totalItems);
this.topbar.setAttribute("active-items", activeItems);
this.topbar.setAttribute("completed-items", completedItems);
this.bottombar.setAttribute("total-items", totalItems);
this.bottombar.setAttribute("active-items", activeItems);
}
addListeners() {
this.topbar.addEventListener("toggle-all", this.toggleItems);
this.topbar.addEventListener("add-item", this.addItem);
this.list.listNode.addEventListener("toggle-item", this.toggleItem);
this.list.listNode.addEventListener("remove-item", this.removeItem);
this.list.listNode.addEventListener("update-item", this.updateItem);
this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems);
this.bottombar.addEventListener("next-page", this.moveToNextPage);
this.bottombar.addEventListener("previous-page", this.moveToPreviousPage);
}
removeListeners() {
this.topbar.removeEventListener("toggle-all", this.toggleItems);
this.topbar.removeEventListener("add-item", this.addItem);
this.list.listNode.removeEventListener("toggle-item", this.toggleItem);
this.list.listNode.removeEventListener("remove-item", this.removeItem);
this.list.listNode.removeEventListener("update-item", this.updateItem);
this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems);
this.bottombar.removeEventListener("next-page", this.moveToNextPage);
this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage);
}
routeChange(route) {
const routeName = route.split("/")[1] || "all";
this.list.updateRoute(routeName);
this.bottombar.updateRoute(routeName);
this.topbar.updateRoute(routeName);
}
connectedCallback() {
this.update();
this.addListeners();
this.router.initRouter(this.routeChange);
this.#isReady = true;
}
disconnectedCallback() {
this.removeListeners();
this.#isReady = false;
}
}
customElements.define("todo-app", TodoApp);
export default TodoApp;

View file

@ -0,0 +1,14 @@
const template = document.createElement("template");
template.id = "todo-app-template";
template.innerHTML = `
<section class="app">
<todo-topbar></todo-topbar>
<main class="main">
<todo-list></todo-list>
</main>
<todo-bottombar></todo-bottombar>
</section>
`;
export default template;

View file

@ -0,0 +1,126 @@
import template from "./todo-bottombar.template.js";
import globalStyles from "../../../styles/global.constructable.js";
import bottombarStyles from "../../../styles/bottombar.constructable.js";
const customStyles = new CSSStyleSheet();
customStyles.replaceSync(`
.clear-completed-button, .clear-completed-button:active,
.todo-status,
.filter-list
{
position: unset;
transform: unset;
}
.bottombar {
display: grid;
grid-template-columns: repeat(7, 1fr);
align-items: center;
justify-items: center;
}
.bottombar > * {
grid-column: span 1;
}
.filter-list {
grid-column: span 3;
}
:host([total-items="0"]) > .bottombar {
display: none;
}
`);
class TodoBottombar extends HTMLElement {
static get observedAttributes() {
return ["total-items", "active-items"];
}
constructor() {
super();
const node = document.importNode(template.content, true);
this.element = node.querySelector(".bottombar");
this.clearCompletedButton = node.querySelector(".clear-completed-button");
this.todoStatus = node.querySelector(".todo-status");
this.filterLinks = node.querySelectorAll(".filter-link");
this.shadow = this.attachShadow({ mode: "open" });
this.htmlDirection = document.dir || "ltr";
this.setAttribute("dir", this.htmlDirection);
this.shadow.adoptedStyleSheets = [globalStyles, bottombarStyles, customStyles];
this.shadow.append(node);
this.clearCompletedItems = this.clearCompletedItems.bind(this);
this.MoveToNextPage = this.MoveToNextPage.bind(this);
this.MoveToPreviousPage = this.MoveToPreviousPage.bind(this);
}
updateDisplay() {
this.todoStatus.textContent = `${this["active-items"]} ${this["active-items"] === "1" ? "item" : "items"} left!`;
}
updateRoute(route) {
this.filterLinks.forEach((link) => {
if (link.dataset.route === route)
link.classList.add("selected");
else
link.classList.remove("selected");
});
}
clearCompletedItems() {
this.dispatchEvent(new CustomEvent("clear-completed-items"));
}
MoveToNextPage() {
this.dispatchEvent(new CustomEvent("next-page"));
}
MoveToPreviousPage() {
this.element.querySelector(".previous-page-button").disabled = true;
this.dispatchEvent(new CustomEvent("previous-page"));
}
addListeners() {
this.clearCompletedButton.addEventListener("click", this.clearCompletedItems);
this.element.querySelector(".next-page-button").addEventListener("click", this.MoveToNextPage);
this.element.querySelector(".previous-page-button").addEventListener("click", this.MoveToPreviousPage);
}
removeListeners() {
this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems);
this.getElementById("next-page-button").removeEventListener("click", this.MoveToNextPage);
this.getElementById("previous-page-button").removeEventListener("click", this.MoveToPreviousPage);
}
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue)
return;
this[property] = newValue;
if (this.isConnected)
this.updateDisplay();
}
reenablePreviousPageButton() {
this.element.querySelector(".previous-page-button").disabled = false;
window.dispatchEvent(new CustomEvent("previous-page-button-enabled", {}));
}
connectedCallback() {
this.updateDisplay();
this.addListeners();
}
disconnectedCallback() {
this.removeListeners();
}
}
customElements.define("todo-bottombar", TodoBottombar);
export default TodoBottombar;

View file

@ -0,0 +1,24 @@
const template = document.createElement("template");
template.id = "todo-bottombar-template";
template.innerHTML = `
<footer class="bottombar">
<div class="todo-status"><span class="todo-count">0</span> item left</div>
<ul class="filter-list">
<li class="filter-item">
<a id="filter-link-all" class="filter-link selected" href="#/" data-route="all">All</a>
</li>
<li class="filter-item">
<a id="filter-link-active" class="filter-link" href="#/active" data-route="active">Active</a>
</li>
<li class="filter-item">
<a id="filter-link-completed" class="filter-link" href="#/completed" data-route="completed">Completed</a>
</li>
</ul>
<button id="clear-completed" class="clear-completed-button">Clear completed</button>
<button id="previous-page" class="previous-page-button"> Previous </button>
<button id="next-page" class="next-page-button"> Next </button>
</footer>
`;
export default template;

View file

@ -0,0 +1,182 @@
import template from "./todo-item.template.js";
import { useDoubleClick } from "../../hooks/useDoubleClick.js";
import { useKeyListener } from "../../hooks/useKeyListener.js";
import globalStyles from "../../../styles/global.constructable.js";
import itemStyles from "../../../styles/todo-item.constructable.js";
class TodoItem extends HTMLElement {
static get observedAttributes() {
return ["itemid", "itemtitle", "itemcompleted"];
}
constructor() {
super();
// Renamed this.id to this.itemid and this.title to this.itemtitle.
// When the component assigns to this.id or this.title, this causes the browser's implementation of the existing setters to run, which convert these property sets into internal setAttribute calls. This can have surprising consequences.
// [Issue]: https://github.com/WebKit/Speedometer/issues/313
this.itemid = "";
this.itemtitle = "Todo Item";
this.itemcompleted = "false";
this.itemIndex = null;
const node = document.importNode(template.content, true);
this.item = node.querySelector(".todo-item");
this.toggleLabel = node.querySelector(".toggle-todo-label");
this.toggleInput = node.querySelector(".toggle-todo-input");
this.todoText = node.querySelector(".todo-item-text");
this.todoButton = node.querySelector(".remove-todo-button");
this.editLabel = node.querySelector(".edit-todo-label");
this.editInput = node.querySelector(".edit-todo-input");
this.shadow = this.attachShadow({ mode: "open" });
this.htmlDirection = document.dir || "ltr";
this.setAttribute("dir", this.htmlDirection);
this.shadow.adoptedStyleSheets = [globalStyles, itemStyles];
this.shadow.append(node);
this.keysListeners = [];
this.updateItem = this.updateItem.bind(this);
this.toggleItem = this.toggleItem.bind(this);
this.removeItem = this.removeItem.bind(this);
this.startEdit = this.startEdit.bind(this);
this.stopEdit = this.stopEdit.bind(this);
this.cancelEdit = this.cancelEdit.bind(this);
if (window.extraTodoItemCssToAdopt) {
let extraAdoptedStyleSheet = new CSSStyleSheet();
extraAdoptedStyleSheet.replaceSync(window.extraTodoItemCssToAdopt);
this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet);
}
}
update(...args) {
args.forEach((argument) => {
switch (argument) {
case "itemid":
if (this.itemid !== undefined)
this.item.id = `todo-item-${this.itemid}`;
break;
case "itemtitle":
if (this.itemtitle !== undefined) {
this.todoText.textContent = this.itemtitle;
this.editInput.value = this.itemtitle;
}
break;
case "itemcompleted":
this.toggleInput.checked = this.itemcompleted === "true";
break;
}
});
}
startEdit() {
this.item.classList.add("editing");
this.editInput.value = this.itemtitle;
this.editInput.focus();
}
stopEdit() {
this.item.classList.remove("editing");
}
cancelEdit() {
this.editInput.blur();
}
toggleItem() {
// The todo-list checks the "completed" attribute to filter based on route
// (therefore the completed state needs to already be updated before the check)
this.setAttribute("itemcompleted", this.toggleInput.checked);
this.dispatchEvent(
new CustomEvent("toggle-item", {
detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
bubbles: true,
})
);
}
removeItem() {
this.dispatchEvent(
new CustomEvent("remove-item", {
detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
bubbles: true,
})
);
this.remove();
}
updateItem(event) {
if (event.target.value !== this.itemtitle) {
if (!event.target.value.length)
this.removeItem();
else
this.setAttribute("itemtitle", event.target.value);
}
this.cancelEdit();
}
addListeners() {
this.toggleInput.addEventListener("change", this.toggleItem);
this.todoText.addEventListener("click", useDoubleClick(this.startEdit, 500));
this.editInput.addEventListener("blur", this.stopEdit);
this.todoButton.addEventListener("click", this.removeItem);
this.keysListeners.forEach((listener) => listener.connect());
}
removeListeners() {
this.toggleInput.removeEventListener("change", this.toggleItem);
this.todoText.removeEventListener("click", this.startEdit);
this.editInput.removeEventListener("blur", this.stopEdit);
this.todoButton.removeEventListener("click", this.removeItem);
this.keysListeners.forEach((listener) => listener.disconnect());
}
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue)
return;
this[property] = newValue;
if (this.isConnected)
this.update(property);
}
connectedCallback() {
this.update("itemid", "itemtitle", "itemcompleted");
this.keysListeners.push(
useKeyListener({
target: this.editInput,
event: "keyup",
callbacks: {
["Enter"]: this.updateItem,
["Escape"]: this.cancelEdit,
},
}),
useKeyListener({
target: this.todoText,
event: "keyup",
callbacks: {
[" "]: this.startEdit, // this feels weird
},
})
);
this.addListeners();
}
disconnectedCallback() {
this.removeListeners();
this.keysListeners = [];
}
}
customElements.define("todo-item", TodoItem);
export default TodoItem;

View file

@ -0,0 +1,19 @@
const template = document.createElement("template");
template.id = "todo-item-template";
template.innerHTML = `
<li class="todo-item">
<div class="display-todo">
<label for="toggle-todo" class="toggle-todo-label visually-hidden">Toggle Todo</label>
<input id="toggle-todo" class="toggle-todo-input" type="checkbox" />
<span class="todo-item-text truncate-singleline" tabindex="0">Placeholder Text</span>
<button class="remove-todo-button" title="Remove Todo"></button>
</div>
<div class="edit-todo-container">
<label for="edit-todo" class="edit-todo-label visually-hidden">Edit todo</label>
<input id="edit-todo" class="edit-todo-input" />
</div>
</li>
`;
export default template;

View file

@ -0,0 +1,175 @@
import template from "./todo-list.template.js";
import TodoItem from "../todo-item/todo-item.component.js";
import { createStorageManager } from "../../storage/storage-factory.js";
import globalStyles from "../../../styles/global.constructable.js";
import listStyles from "../../../styles/todo-list.constructable.js";
const MAX_ON_SCREEN_ITEMS = 10;
const customListStyles = new CSSStyleSheet();
customListStyles.replaceSync(`
.todo-list[route="completed"] > [itemcompleted="false"] {
display: none;
}
.todo-list[route="active"] > [itemcompleted="true"] {
display: none;
}
:nth-child(${MAX_ON_SCREEN_ITEMS}) ~ todo-item {
display: none;
}
`);
class TodoList extends HTMLElement {
static get observedAttributes() {
return ["total-items"];
}
#route = undefined;
#firstItemIndexOnScreen = 0;
constructor() {
super();
const node = document.importNode(template.content, true);
this.listNode = node.querySelector(".todo-list");
this.shadow = this.attachShadow({ mode: "open" });
this.htmlDirection = document.dir || "ltr";
this.setAttribute("dir", this.htmlDirection);
this.shadow.adoptedStyleSheets = [globalStyles, listStyles, customListStyles];
this.shadow.append(node);
this.classList.add("show-priority");
this.storageManager = createStorageManager();
if (window.extraTodoListCssToAdopt) {
let extraAdoptedStyleSheet = new CSSStyleSheet();
extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt);
this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet);
}
}
addItem(entry, itemIndex) {
const { id, title, completed } = entry;
const priority = 4 - (itemIndex % 5);
const element = new TodoItem();
element.setAttribute("itemid", id);
element.setAttribute("itemtitle", title);
element.setAttribute("itemcompleted", completed);
element.setAttribute("data-priority", priority);
element.itemIndex = itemIndex;
this.listNode.append(element);
this.#addItemToStorage(itemIndex, id, title, priority, completed);
}
removeItem(itemIndex) {
this.storageManager.removeTodo(itemIndex);
}
addItems(items) {
items.forEach((entry) => this.addItem(entry));
}
removeCompletedItems() {
Array.from(this.listNode.children).forEach((element) => {
if (element.itemcompleted === "true")
element.removeItem();
});
}
toggleItems(completed) {
Array.from(this.listNode.children).forEach((element) => {
if (completed && element.itemcompleted === "false")
element.toggleInput.click();
else if (!completed && element.itemcompleted === "true")
element.toggleInput.click();
});
}
toggleItem(itemNumber, completed) {
// Update the item in the IndexedDB
this.storageManager.toggleTodo(itemNumber, completed);
}
updateStyles() {
if (parseInt(this["total-items"]) !== 0)
this.listNode.style.display = "block";
else
this.listNode.style.display = "none";
}
updateRoute(route) {
this.#route = route;
switch (route) {
case "completed":
this.listNode.setAttribute("route", "completed");
break;
case "active":
this.listNode.setAttribute("route", "active");
break;
default:
this.listNode.setAttribute("route", "all");
}
}
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue)
return;
this[property] = newValue;
if (this.isConnected)
this.updateStyles();
}
connectedCallback() {
this.updateStyles();
}
moveToNextPage() {
for (let i = 0; i < MAX_ON_SCREEN_ITEMS; i++) {
const child = this.listNode.firstChild;
if (!child)
break;
child.remove();
}
this.#firstItemIndexOnScreen = this.listNode.firstChild.itemIndex;
}
async moveToPreviousPage() {
const items = await this.storageManager.getTodos(this.#firstItemIndexOnScreen, MAX_ON_SCREEN_ITEMS);
const elements = items.map((item) => {
const { id, title, completed, priority } = item;
const element = new TodoItem();
element.setAttribute("itemid", id);
element.setAttribute("itemtitle", title);
element.setAttribute("itemcompleted", completed);
element.setAttribute("data-priority", priority);
element.itemIndex = item.itemNumber;
return element;
});
this.#firstItemIndexOnScreen = items[0].itemNumber;
this.listNode.replaceChildren(...elements);
}
#addItemToStorage(itemIndex, id, title, priority, completed) {
// Create a todo object with the structure expected by IndexedDB
const todoItem = {
itemNumber: itemIndex,
id,
title,
completed,
priority,
};
// Add the item to IndexedDB and handle the Promise
this.storageManager.addTodo(todoItem);
}
}
customElements.define("todo-list", TodoList);
export default TodoList;

View file

@ -0,0 +1,8 @@
const template = document.createElement("template");
template.id = "todo-list-template";
template.innerHTML = `
<ul class="todo-list"></ul>
`;
export default template;

View file

@ -0,0 +1,138 @@
import template from "./todo-topbar.template.js";
import { useKeyListener } from "../../hooks/useKeyListener.js";
import { nanoid } from "../../utils/nanoid.js";
import globalStyles from "../../../styles/global.constructable.js";
import topbarStyles from "../../../styles/topbar.constructable.js";
const customListStyles = new CSSStyleSheet();
customListStyles.replaceSync(`
:host([total-items="0"]) .toggle-all-container {
display: none;
}
`);
class TodoTopbar extends HTMLElement {
static get observedAttributes() {
return ["active-items", "completed-items"];
}
#route = undefined;
constructor() {
super();
const node = document.importNode(template.content, true);
this.todoInput = node.querySelector("#new-todo");
this.toggleInput = node.querySelector("#toggle-all");
this.toggleContainer = node.querySelector(".toggle-all-container");
this.shadow = this.attachShadow({ mode: "open" });
this.htmlDirection = document.dir || "ltr";
this.setAttribute("dir", this.htmlDirection);
this.shadow.adoptedStyleSheets = [globalStyles, topbarStyles, customListStyles];
this.shadow.append(node);
this.keysListeners = [];
this.toggleAll = this.toggleAll.bind(this);
this.addItem = this.addItem.bind(this);
}
toggleAll(event) {
this.dispatchEvent(
new CustomEvent("toggle-all", {
detail: { completed: event.target.checked },
})
);
}
addItem(event) {
if (!event.target.value.length)
return;
this.dispatchEvent(
new CustomEvent("add-item", {
detail: {
id: nanoid(),
title: event.target.value,
completed: false,
},
})
);
event.target.value = "";
}
updateDisplay() {
if (!parseInt(this["total-items"])) {
this.toggleContainer.style.display = "none";
return;
}
this.toggleContainer.style.display = "block";
switch (this.#route) {
case "active":
this.toggleInput.checked = false;
this.toggleInput.disabled = !parseInt(this["active-items"]);
break;
case "completed":
this.toggleInput.checked = parseInt(this["completed-items"]);
this.toggleInput.disabled = !parseInt(this["completed-items"]);
break;
default:
this.toggleInput.checked = !parseInt(this["active-items"]);
this.toggleInput.disabled = false;
}
}
updateRoute(route) {
this.#route = route;
this.updateDisplay();
}
addListeners() {
this.toggleInput.addEventListener("change", this.toggleAll);
this.keysListeners.forEach((listener) => listener.connect());
}
removeListeners() {
this.toggleInput.removeEventListener("change", this.toggleAll);
this.keysListeners.forEach((listener) => listener.disconnect());
}
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue)
return;
this[property] = newValue;
if (this.isConnected)
this.updateDisplay();
}
connectedCallback() {
this.keysListeners.push(
useKeyListener({
target: this.todoInput,
event: "keyup",
callbacks: {
["Enter"]: this.addItem,
},
})
);
this.updateDisplay();
this.addListeners();
this.todoInput.focus();
}
disconnectedCallback() {
this.removeListeners();
this.keysListeners = [];
}
}
customElements.define("todo-topbar", TodoTopbar);
export default TodoTopbar;

View file

@ -0,0 +1,17 @@
const template = document.createElement("template");
template.id = "todo-topbar-template";
template.innerHTML = `
<header class="topbar">
<div class="new-todo-display">
<label for="new-todo" class="visually-hidden">Enter a new todo.</label>
<input id="new-todo" class="new-todo-input" placeholder="What needs to be done?" autofocus />
</div>
<div class="toggle-all-container">
<input id="toggle-all" class="toggle-all-input" type="checkbox" />
<label for="toggle-all" class="toggle-all-label">Mark all todos as complete.</label>
</div>
</header>
`;
export default template;

View file

@ -0,0 +1,19 @@
/**
* A simple function to normalize a double-click and a double-tab action.
* There is currently no comparable tab action to dblclick.
*
* @param {Function} fn
* @param {number} delay
* @returns
*/
export function useDoubleClick(fn, delay) {
let last = 0;
return function (...args) {
const now = new Date().getTime();
const difference = now - last;
if (difference < delay && difference > 0)
fn.apply(this, args);
last = now;
};
}

View file

@ -0,0 +1,23 @@
export function useKeyListener(props) {
const { target, event, callbacks } = props;
function handleEvent(event) {
Object.keys(callbacks).forEach((key) => {
if (event.key === key)
callbacks[key](event);
});
}
function connect() {
target.addEventListener(event, handleEvent);
}
function disconnect() {
target.removeEventListener(event, handleEvent);
}
return {
connect,
disconnect,
};
}

View file

@ -0,0 +1,43 @@
/**
* Listens for hash change of the url and calls onChange if available.
*
* @param {Function} callback
* @returns Methods to interact with useRouter.
*/
export const useRouter = (callback) => {
let onChange = callback;
let current = "";
/**
* Change event handler.
*/
const handleChange = () => {
current = document.location.hash;
/* istanbul ignore else */
if (onChange)
onChange(document.location.hash);
};
/**
* Initializes router and adds listeners.
*
* @param {Function} callback
*/
const initRouter = (callback) => {
onChange = callback;
window.addEventListener("hashchange", handleChange);
window.addEventListener("load", handleChange);
};
/**
* Removes listeners
*/
const disableRouter = () => {
window.removeEventListener("hashchange", handleChange);
window.removeEventListener("load", handleChange);
};
const getRoute = () => current.split("/").slice(-1)[0];
return { initRouter, getRoute, disableRouter };
};

View file

@ -0,0 +1,12 @@
import { BenchmarkConnector } from "./speedometer-utils/benchmark.mjs";
import suites, { appName, appVersion } from "./workload-test.mjs";
window.addEventListener("db-ready", () => {
/*
Paste below into dev console for manual testing:
window.addEventListener("message", (event) => console.log(event.data));
window.postMessage({ id: "todomvc-postmessage-1.0.0", key: "benchmark-connector", type: "benchmark-suite", name: "default" }, "*");
*/
const benchmarkConnector = new BenchmarkConnector(suites, appName, appVersion);
benchmarkConnector.connect();
});

View file

@ -0,0 +1,154 @@
/* eslint-disable no-case-declarations */
import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
import { Params } from "./params.mjs";
/**
* BenchmarkStep
*
* A single test step, with a common interface to interact with.
*/
export class BenchmarkStep {
constructor(name, run, ignoreResult = false) {
this.name = name;
this.run = run;
this.ignoreResult = ignoreResult;
}
async runAndRecord(params, suite, test) {
const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner;
const type = params.useAsyncSteps ? "async" : "sync";
const stepRunner = new StepRunnerClass(null, null, params, suite, test, type);
const result = await stepRunner.runStep();
return result;
}
}
/**
* BenchmarkSuite
*
* A single test suite that contains one or more test steps.
*/
export class BenchmarkSuite {
constructor(name, tests) {
this.name = name;
this.tests = tests;
}
async runAndRecord(params, onProgress) {
const measuredValues = {
tests: {},
total: 0,
};
const suiteStartLabel = `suite-${this.name}-start`;
const suiteEndLabel = `suite-${this.name}-end`;
performance.mark(suiteStartLabel);
for (const test of this.tests) {
const { syncTime, asyncTime } = await test.runAndRecord(params, this, test);
const total = syncTime + asyncTime;
const result = {
tests: { Sync: syncTime, Async: asyncTime },
total: total,
};
if (!test.ignoreResult) {
measuredValues.tests[test.name] = result;
measuredValues.total += total;
}
onProgress?.(test.name);
}
performance.mark(suiteEndLabel);
performance.measure(`suite-${this.name}`, suiteStartLabel, suiteEndLabel);
return {
type: "suite-tests-complete",
status: "success",
result: measuredValues,
suitename: this.name,
};
}
}
export const MESSAGE_TYPE = Object.freeze({
__proto__: null,
appReady: "app-ready",
suiteStart: "suite-start",
stepComplete: "step-complete",
suiteComplete: "suite-complete",
});
export const MESSAGE_STATUS = Object.freeze({
__proto__: null,
success: "success",
error: "error",
});
/** **********************************************************************
* BenchmarkConnector
*
* postMessage is used to communicate between app and benchmark.
* When the app is ready, an 'app-ready' message is sent to signal that the app can receive instructions.
*
* A prepare script within the apps appends window.name and window.version from the package.json file.
* The appId is build by appending name-version
* It's used as an additional safe-guard to ensure the correct app responds to a message.
*************************************************************************/
export class BenchmarkConnector {
constructor(suites, name, version) {
this.suites = suites;
this.name = name;
this.version = version;
if (!name || !version)
console.warn("No name or version supplied, to create a unique appId");
this.appId = name && version ? `${name}-${version}` : -1;
this.onMessage = this.onMessage.bind(this);
}
async onMessage(event) {
const message = event.data;
if (message.appId !== this.appId || message.key !== "benchmark-connector") {
console.warn("Invalid message", message);
return;
}
switch (message.type) {
case MESSAGE_TYPE.suiteStart:
const params = new Params(new URLSearchParams(window.location.search));
const { name } = message.payload;
const suite = this.suites[name];
if (!suite)
console.error(`Suite with the name of "${name}" not found!`);
const onProgress = (step) => this._sendMessage(MESSAGE_TYPE.stepComplete, { name: this.name, step });
const { result } = await suite.runAndRecord(params, onProgress);
this._sendMessage(MESSAGE_TYPE.suiteComplete, { result });
this.disconnect();
break;
default:
console.error(`Message data type not supported: ${message.type}`);
}
}
_sendMessage(type, payload, status = MESSAGE_STATUS.success) {
const message = {
appId: this.appId,
key: "benchmark-connector",
type: type,
payload: payload,
status: status,
};
window.top.postMessage(message, "*");
}
connect() {
window.addEventListener("message", this.onMessage);
this._sendMessage(MESSAGE_TYPE.appReady, { appId: this.appId });
}
disconnect() {
window.removeEventListener("message", this.onMessage);
}
}

View file

@ -0,0 +1,55 @@
/**
* Recursively queries the DOM for an element, traversing through shadow DOMs.
*
* @param {Element|ShadowRoot} lookupStartNode The node or shadow root to start the lookup from.
* @param {string[]} path An array of CSS selectors representing the path to the target element.
* @returns {Element|null} The target element if found, otherwise null.
*/
export function recursivelyQuerySelector(lookupStartNode, path) {
lookupStartNode = lookupStartNode.shadowRoot ?? lookupStartNode;
const target = path.reduce((root, selector) => {
const node = root.querySelector(selector);
return node.shadowRoot ?? node;
}, lookupStartNode);
return target;
}
/**
* Retrieves a single DOM element, optionally traversing through shadow DOMs to a specified path before the final selection.
*
* @param {string} selector The CSS selector for the desired element.
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
* @returns {Element|null} The found element, or null if not found.
*/
export function getElement(selector, path = [], lookupStartNode = document) {
const element = recursivelyQuerySelector(lookupStartNode, path).querySelector(selector);
return element;
}
/**
* Retrieves all DOM elements matching a selector, optionally traversing through shadow DOMs to a specified path before the final selection.
*
* @param {string} selector The CSS selector for the desired elements.
* @param {string[]} [path=[]] An optional array of CSS selectors to reach the desired shadowRoot or parent element.
* @param {Element|ShadowRoot} [lookupStartNode=document] The starting node for the lookup.
* @returns {Element[]} An array of found elements.
*/
export function getAllElements(selector, path = [], lookupStartNode = document) {
const elements = Array.from(recursivelyQuerySelector(lookupStartNode, path).querySelectorAll(selector));
return elements;
}
export function forceLayout(body, layoutMode = "getBoundingRectAndElementFromPoint") {
body ??= document.body;
const rect = body.getBoundingClientRect();
switch (layoutMode) {
case "getBoundingRectAndElementFromPoint":
return document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0);
case "getBoundingClientRect":
return rect.height;
default:
throw Error(`Invalid layoutMode: ${layoutMode}`);
}
}

View file

@ -0,0 +1,241 @@
export const LAYOUT_MODES = Object.freeze(["getBoundingClientRect", "getBoundingRectAndElementFromPoint"]);
export class Params {
viewport = {
width: 800,
height: 600,
};
// Enable a detailed developer menu to change the current Params.
developerMode = false;
startAutomatically = false;
iterationCount = 10;
suites = [];
// A list of tags to filter suites
tags = ["default"];
// Toggle running a dummy suite once before the normal test suites.
useWarmupSuite = false;
// toggle async type vs default raf type.
useAsyncSteps = false;
// Change how a test measurement is triggered and async time is measured:
// "timer": The classic (as in Speedometer 2.x) way using setTimeout
// "raf": Using rAF callbacks, both for triggering the sync part and for measuring async time.
measurementMethod = "raf";
// Wait time after suite preparation in ms.
waitAfterSetup = 0;
// Wait time before the sync step in ms.
waitBeforeSync = 0;
// Wait after running a suite.
waitAfterSuite = 0;
// Warmup time before the sync step in ms.
warmupBeforeSync = 0;
// Seed for shuffling the execution order of suites.
// "off": do not shuffle
// "generate": generate a random seed
// <integer>: use the provided integer as a seed
shuffleSeed = "off";
// Choices: "getBoundingClientRect" or "getBoundingRectAndElementFromPoint"
layoutMode = LAYOUT_MODES[0];
// Measure more workload prepare time.
measurePrepare = false;
// External config url to override internal tests.
config = "";
constructor(searchParams = undefined, warnUnused = false) {
if (searchParams)
this._copyFromSearchParams(searchParams, warnUnused);
if (!this.developerMode) {
Object.freeze(this.viewport);
Object.freeze(this);
}
}
_parseInt(value, errorMessage) {
const number = Number(value);
if (!Number.isInteger(number) && errorMessage)
throw new Error(`Invalid ${errorMessage} param: '${value}', expected int.`);
return parseInt(number);
}
_copyFromSearchParams(searchParams, warnUnused) {
this.viewport = this._parseViewport(searchParams);
this.startAutomatically = this._parseBooleanParam(searchParams, "startAutomatically");
this.iterationCount = this._parseIntParam(searchParams, "iterationCount", 1);
this.suites = this._parseSuites(searchParams);
this.tags = this._parseTags(searchParams);
this.developerMode = this._parseBooleanParam(searchParams, "developerMode");
this.useWarmupSuite = this._parseBooleanParam(searchParams, "useWarmupSuite");
this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps");
this.waitAfterSetup = this._parseIntParam(searchParams, "waitAfterSetup", 0);
this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0);
this.waitAfterSuite = this._parseIntParam(searchParams, "waitAfterSuite", 0);
this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0);
this.measurementMethod = this._parseEnumParam(searchParams, "measurementMethod", ["raf"]);
this.shuffleSeed = this._parseShuffleSeed(searchParams);
this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES);
this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare");
this.config = this._parseConfig(searchParams);
if (warnUnused) {
const unused = Array.from(searchParams.keys());
if (unused.length > 0)
console.error(`Got unused search params: ${unused.join(", ")}`);
}
}
_parseBooleanParam(searchParams, paramKey) {
if (!searchParams.has(paramKey))
return false;
searchParams.delete(paramKey);
return true;
}
_parseIntParam(searchParams, paramKey, minValue) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const parsedValue = this._parseInt(searchParams.get(paramKey), "waitBeforeSync");
if (parsedValue < minValue)
throw new Error(`Invalid ${paramKey} param: '${parsedValue}', value must be >= ${minValue}.`);
searchParams.delete(paramKey);
return parsedValue;
}
_parseViewport(searchParams) {
if (!searchParams.has("viewport"))
return defaultParams.viewport;
const viewportParam = searchParams.get("viewport");
const [width, height] = viewportParam.split("x");
const viewport = {
width: this._parseInt(width, "viewport.width"),
height: this._parseInt(height, "viewport.height"),
};
if (this.viewport.width < 800 || this.viewport.height < 600)
throw new Error(`Invalid viewport param: ${viewportParam}`);
searchParams.delete("viewport");
return viewport;
}
_parseSuites(searchParams) {
if (searchParams.has("suite") || searchParams.has("suites")) {
if (searchParams.has("suite") && searchParams.has("suites"))
throw new Error("Params 'suite' and 'suites' can not be used together.");
const value = searchParams.get("suite") || searchParams.get("suites");
const suites = value.split(",");
if (suites.length === 0)
throw new Error("No suites selected");
searchParams.delete("suite");
searchParams.delete("suites");
return suites;
}
return defaultParams.suites;
}
_parseTags(searchParams) {
if (!searchParams.has("tags"))
return defaultParams.tags;
if (this.suites.length)
throw new Error("'suites' and 'tags' cannot be used together.");
const tags = searchParams.get("tags").split(",");
searchParams.delete("tags");
return tags;
}
_parseEnumParam(searchParams, paramKey, enumArray) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const value = searchParams.get(paramKey);
if (!enumArray.includes(value))
throw new Error(`Got invalid ${paramKey}: '${value}', choices are ${enumArray}`);
searchParams.delete(paramKey);
return value;
}
_parseShuffleSeed(searchParams) {
if (!searchParams.has("shuffleSeed"))
return defaultParams.shuffleSeed;
let shuffleSeed = searchParams.get("shuffleSeed");
if (shuffleSeed !== "off") {
if (shuffleSeed === "generate") {
shuffleSeed = Math.floor((Math.random() * 1) << 16);
console.log(`Generated a random suite order seed: ${shuffleSeed}`);
} else {
shuffleSeed = parseInt(shuffleSeed);
}
if (!Number.isInteger(shuffleSeed))
throw new Error(`Invalid shuffle seed: '${shuffleSeed}', must be either 'off', 'generate' or an integer.`);
}
searchParams.delete("shuffleSeed");
return shuffleSeed;
}
_parseConfig(searchParams) {
const config = searchParams.get("config") ?? "";
searchParams.delete("config");
if (config && !isValidJsonUrl(config))
throw new Error("Invalid config url passed in.");
return config;
}
toCompleteSearchParamsObject() {
return this.toSearchParamsObject(false);
}
toSearchParamsObject(filter = true) {
const rawUrlParams = { __proto__: null };
for (const [key, value] of Object.entries(this)) {
// Handle composite values separately.
if (key === "viewport" || key === "suites" || key === "tags")
continue;
// Skip over default values.
if (filter && value === defaultParams[key])
continue;
rawUrlParams[key] = value;
}
if (this.viewport.width !== defaultParams.viewport.width || this.viewport.height !== defaultParams.viewport.height)
rawUrlParams.viewport = `${this.viewport.width}x${this.viewport.height}`;
if (this.suites.length) {
rawUrlParams.suites = this.suites.join(",");
} else if (this.tags.length) {
if (!(this.tags.length === 1 && this.tags[0] === "default"))
rawUrlParams.tags = this.tags.join(",");
} else {
rawUrlParams.suites = "";
}
return new URLSearchParams(rawUrlParams);
}
toSearchParams() {
return this.toSearchParamsObject().toString();
}
}
function isValidJsonUrl(url) {
if (typeof url !== "string" || url.length === 0)
return false;
try {
new URL(url, "http://www.example.com");
return true;
} catch (error) {
return false;
}
}
export const defaultParams = new Params();
export let paramsError = null;
let maybeCustomParams = defaultParams;
if (globalThis?.location?.search) {
const searchParams = new URLSearchParams(globalThis.location.search);
try {
maybeCustomParams = new Params(searchParams, true);
} catch (e) {
paramsError = e;
console.error("Invalid URL Param", e, "\nUsing defaults as fallback:", maybeCustomParams);
}
}
export const params = maybeCustomParams;

View file

@ -0,0 +1,109 @@
import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs";
import { forceLayout } from "./helpers.mjs";
export class StepRunner {
#frame;
#page;
#params;
#suite;
#step;
#type;
constructor(frame, page, params, suite, step, type) {
this.#suite = suite;
this.#step = step;
this.#params = params;
this.#page = page;
this.#frame = frame;
this.#type = type;
}
get page() {
return this.#page;
}
get step() {
return this.#step;
}
_runSyncStep(step, page) {
step.run(page);
}
async runStep() {
// Prepare all mark labels outside the measuring loop.
const suiteName = this.#suite.name;
const stepName = this.#step.name;
const syncStartLabel = `${suiteName}.${stepName}-start`;
const syncEndLabel = `${suiteName}.${stepName}-sync-end`;
const asyncEndLabel = `${suiteName}.${stepName}-async-end`;
let syncTime;
let asyncStartTime;
let asyncTime;
const runSync = async () => {
if (this.#params.warmupBeforeSync) {
performance.mark("warmup-start");
const startTime = performance.now();
// Infinite loop for the specified ms.
while (performance.now() - startTime < this.#params.warmupBeforeSync)
continue;
performance.mark("warmup-end");
}
performance.mark(syncStartLabel);
const syncStartTime = performance.now();
if (this.#type === "async")
await this._runSyncStep(this.step, this.page);
else
this._runSyncStep(this.step, this.page);
const mark = performance.mark(syncEndLabel);
const syncEndTime = mark.startTime;
syncTime = syncEndTime - syncStartTime;
asyncStartTime = syncEndTime;
};
const measureAsync = () => {
// Some browsers don't immediately update the layout for paint.
// Force the layout here to ensure we're measuring the layout time.
//
// Note: This matches the behavior of Page.layout() in benchmark-runner.mjs.
// Since shared code cannot depend on Page, we duplicate the logic here,
// falling back to document.body for remote workloads that don't have a frame.
const body = this.#frame?.contentDocument?.body ?? document.body;
const value = forceLayout(body, this.#params.layoutMode);
body._leakedLayoutValue = value; // Prevent dead code elimination.
const asyncEndTime = performance.now();
performance.mark(asyncEndLabel);
asyncTime = asyncEndTime - asyncStartTime;
if (this.#params.warmupBeforeSync)
performance.measure("warmup", "warmup-start", "warmup-end");
performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel);
performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel);
};
const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType];
const scheduler = new schedulerClass(runSync, measureAsync, this.#params);
await scheduler.start();
return { syncTime, asyncTime };
}
}
export class AsyncStepRunner extends StepRunner {
async _runSyncStep(step, page) {
await step.run(page);
}
}
export const STEP_RUNNER_LOOKUP = {
__proto__: null,
default: StepRunner,
async: AsyncStepRunner,
remote: StepRunner,
};

View file

@ -0,0 +1,77 @@
class StepScheduler {
constructor(syncCallback, asyncCallback, params) {
this._syncCallback = syncCallback;
this._asyncCallback = asyncCallback;
this._params = params;
}
start() {
return new Promise((resolve) => {
if (this._params.waitBeforeSync)
setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync);
else
this._scheduleCallbacks(resolve);
});
}
}
class RAFStepScheduler extends StepScheduler {
_scheduleCallbacks(resolve) {
requestAnimationFrame(() => this._syncCallback());
requestAnimationFrame(() => {
setTimeout(() => {
this._asyncCallback();
setTimeout(resolve, 0);
}, 0);
});
}
}
class AsyncRAFStepScheduler extends StepScheduler {
static mc = new MessageChannel();
_scheduleCallbacks(resolve) {
let gotTimer = false;
let gotMessage = false;
let gotPromise = false;
const tryTriggerAsyncCallback = () => {
if (!gotTimer || !gotMessage || !gotPromise)
return;
this._asyncCallback();
setTimeout(resolve, 0);
};
requestAnimationFrame(async () => {
await this._syncCallback();
gotPromise = true;
tryTriggerAsyncCallback();
});
requestAnimationFrame(() => {
setTimeout(async () => {
await Promise.resolve();
gotTimer = true;
tryTriggerAsyncCallback();
});
AsyncRAFStepScheduler.mc.port1.addEventListener(
"message",
async function () {
await Promise.resolve();
gotMessage = true;
tryTriggerAsyncCallback();
},
{ once: true }
);
AsyncRAFStepScheduler.mc.port1.start();
AsyncRAFStepScheduler.mc.port2.postMessage("speedometer");
});
}
}
export const STEP_SCHEDULER_LOOKUP = {
__proto__: null,
raf: RAFStepScheduler,
async: AsyncRAFStepScheduler,
};

View file

@ -0,0 +1 @@
export const numberOfItemsToAdd = 100;

View file

@ -0,0 +1,734 @@
export const todos = {
en: [
"Electronic Granite Hat",
"Bespoke Soft Table",
"Ergonomic Fresh Bike",
"Luxurious Wooden Cheese",
"Gorgeous Fresh Pizza",
"Rustic Rubber Shirt",
"Modern Rubber Soap",
"Small Bronze Ball",
"Awesome Bronze Shoes",
"Bespoke Steel Chair",
"Practical Plastic Soap",
"Incredible Granite Bacon",
"Elegant Metal Keyboard",
"Electronic Wooden Sausages",
"Tasty Wooden Gloves",
"Luxurious Metal Cheese",
"Awesome Rubber Gloves",
"Sleek Soft Car",
"Licensed Fresh Salad",
"Ergonomic Frozen Towels",
"Modern Rubber Keyboard",
"Tasty Concrete Pizza",
"Handmade Plastic Chicken",
"Luxurious Rubber Chicken",
"Practical Soft Fish",
"Ergonomic Bronze Shirt",
"Handcrafted Plastic Bacon",
"Unbranded Plastic Pants",
"Modern Wooden Sausages",
"Handmade Steel Shoes",
"Rustic Steel Bike",
"Gorgeous Frozen Salad",
"Handmade Bronze Chicken",
"Sleek Granite Bike",
"Generic Concrete Sausages",
"Incredible Plastic Tuna",
"Bespoke Fresh Cheese",
"Electronic Fresh Bacon",
"Licensed Wooden Car",
"Recycled Fresh Fish",
"Incredible Fresh Shoes",
"Practical Soft Chips",
"Small Soft Chicken",
"Intelligent Fresh Mouse",
"Modern Metal Mouse",
"Tasty Granite Gloves",
"Awesome Rubber Bike",
"Small Steel Shirt",
"Refined Concrete Computer",
"Sleek Frozen Shirt",
"Intelligent Concrete Shoes",
"Handmade Rubber Car",
"Sleek Rubber Towels",
"Unbranded Concrete Hat",
"Incredible Plastic Fish",
"Practical Soft Gloves",
"Organic Stone Pizza",
"Generic Wooden Keyboard",
"Recycled Wooden Chips",
"Incredible Rubber Chips",
"Ergonomic Granite Shirt",
"Tasty Frozen Keyboard",
"Gorgeous Steel Soap",
"Luxurious Plastic Chair",
"Elegant Frozen Bike",
"Recycled Steel Chair",
"Modern Bronze Sausages",
"Elegant Wooden Cheese",
"Small Plastic Sausages",
"Luxurious Frozen Shoes",
"Sleek Plastic Sausages",
"Handcrafted Fresh Sausages",
"Incredible Soft Chair",
"Recycled Wooden Soap",
"Soft Rubber Duck",
"Licensed Concrete Tuna",
"Luxurious Granite Pants",
"Refined Rubber Keyboard",
"Handcrafted Plastic Computer",
"Practical Steel Salad",
"Incredible Soft Bacon",
"Practical Metal Fish",
"Elegant Rubber Shirt",
"Handcrafted Rubber Table",
"Gorgeous Wooden Table",
"Fantastic Steel Sausages",
"Small Soft Keyboard",
"Generic Steel Ball",
"Electronic Frozen Hat",
"Gorgeous Fresh Chair",
"Sleek Soft Sausages",
"Gorgeous Wooden Towels",
"Bespoke Granite Pizza",
"Generic Metal Salad",
"Handmade Rubber Cheese",
"Fantastic Steel Chair",
"Handcrafted Frozen Computer",
"Rustic Rubber Mouse",
"Sleek Granite Pizza",
"Gorgeous Plastic Keyboard",
],
"zh-cn": [
"电子花岗岩帽子",
"定制软桌",
"符合人体工程学新鲜自行车",
"豪华的木制奶酪",
"华丽的新鲜披萨",
"质朴的橡胶衬衫",
"现代橡胶肥皂",
"小青铜球",
"很棒的青铜鞋",
"定制钢椅",
"实用的塑料肥皂",
"令人难以置信的花岗岩培根",
"优雅的金属键盘",
"电子木香肠",
"美味的木制手套",
"豪华的金属奶酪",
"很棒的橡胶手套",
"光滑的柔软汽车",
"有执照的新鲜沙拉",
"人体工程学的冷冻毛巾",
"现代橡胶键盘",
"美味的混凝土披萨",
"手工塑料鸡",
"豪华橡胶鸡",
"实用的软鱼",
"人体工程学的青铜衬衫",
"手工制作的塑料培根",
"未品牌的塑料裤",
"现代木香肠",
"手工钢鞋",
"质朴的钢自行车",
"华丽的冷冻沙拉",
"手工铜鸡",
"光滑的花岗岩自行车",
"通用混凝土香肠",
"令人难以置信的塑料金枪鱼",
"定制新鲜奶酪",
"电子新鲜培根",
"有执照的木车",
"回收的新鲜鱼",
"令人难以置信的新鲜鞋",
"实用的软芯片",
"小鸡肉",
"智能新鲜鼠标",
"现代金属鼠标",
"美味的花岗岩手套",
"很棒的橡胶自行车",
"小钢衬衫",
"精制混凝土计算机",
"光滑的冷冻衬衫",
"智能混凝土鞋",
"手工橡胶车",
"光滑的橡胶毛巾",
"未品牌的混凝土帽子",
"令人难以置信的塑料鱼",
"实用的软手套",
"有机石比萨",
"通用木制键盘",
"再生木芯片",
"令人难以置信的橡胶芯片",
"人体工程学花岗岩衬衫",
"美味的冷冻键盘",
"华丽的钢肥皂",
"豪华的塑料椅",
"优雅的冷冻自行车",
"再生钢椅",
"现代铜香肠",
"优雅的木制奶酪",
"小塑料香肠",
"豪华的冷冻鞋",
"光滑的塑料香肠",
"手工制作的新鲜香肠",
"令人难以置信的柔软椅子",
"回收的木制肥皂",
"软橡皮鸭",
"许可的混凝土金枪鱼",
"豪华的花岗岩裤",
"精制橡胶键盘",
"手工制作的塑料计算机",
"实用的钢沙拉",
"令人难以置信的软培根",
"实用的金属鱼",
"优雅的橡胶衬衫",
"手工橡胶桌",
"华丽的木桌",
"奇妙的钢香肠",
"小软键盘",
"通用钢球",
"电子冷冻帽子",
"华丽的新鲜椅子",
"光滑的软香肠",
"华丽的木制毛巾",
"定制的花岗岩披萨",
"通用金属沙拉",
"手工橡胶奶酪",
"奇妙的钢椅",
"手工制作的冷冻计算机",
"质朴的橡胶老鼠",
"光滑的花岗岩披萨",
"华丽的塑料键盘",
],
ja: [
"電子花崗岩の帽子",
"オーダーメイドのソフトテーブル",
"人間工学に基づいた新鮮な自転車",
"豪華な木製チーズ",
"ゴージャスな新鮮なピザ",
"素朴なラバーシャツ",
"モダンゴム石鹸",
"小さなブロンズボール",
"素晴らしいブロンズシューズ",
"オーダーメイドのスチールチェア",
"実用的なプラスチック石鹸",
"信じられないほどの花崗岩のベーコン",
"エレガントなメタルキーボード",
"電子木製ソーセージ",
"おいしい木製手袋",
"豪華なメタルチーズ",
"素晴らしいラバーグローブ",
"洗練されたソフトカー",
"ライセンスされた新鮮なサラダ",
"人間工学に基づいた冷凍タオル",
"モダンなゴムキーボード",
"おいしいコンクリートピザ",
"手作りのプラスチックチキン",
"豪華なゴム鶏",
"実用的な柔らかい魚",
"人間工学に基づいたブロンズシャツ",
"手作りのプラスチックベーコン",
"ブランドのないプラスチックパンツ",
"モダンな木製ソーセージ",
"手作りのスチールシューズ",
"素朴なスチールバイク",
"ゴージャスな冷凍サラダ",
"手作りのブロンズチキン",
"洗練された花崗岩の自転車",
"一般的なコンクリートソーセージ",
"信じられないほどのプラスチックマグロ",
"オーダーメイドのフレッシュチーズ",
"電子新鮮なベーコン",
"ライセンスされた木製車",
"新鮮な魚をリサイクルしました",
"信じられないほどの新鮮な靴",
"実用的なソフトチップ",
"小さな柔らかい鶏",
"インテリジェントな新鮮なマウス",
"現代の金属マウス",
"おいしい花崗岩の手袋",
"素晴らしいラバーバイク",
"小さなスチールシャツ",
"洗練されたコンクリートコンピューター",
"洗練された冷凍シャツ",
"インテリジェントコンクリートシューズ",
"手作りのラバーカー",
"洗練されたゴムタオル",
"ブランドのないコンクリートの帽子",
"信じられないほどのプラスチックの魚",
"実用的なソフトグローブ",
"オーガニックストーンピザ",
"一般的な木製キーボード",
"リサイクルされた木製チップ",
"信じられないほどのゴムチップ",
"人間工学に基づいた花崗岩のシャツ",
"おいしい冷凍キーボード",
"ゴージャスなスチールソープ",
"豪華なプラスチック製の椅子",
"エレガントな冷凍自転車",
"リサイクルスチールチェア",
"モダンブロンズソーセージ",
"エレガントな木製チーズ",
"小さなプラスチックソーセージ",
"豪華な冷凍靴",
"なめらかなプラスチックソーセージ",
"手作りの新鮮なソーセージ",
"信じられないほどのソフトチェア",
"リサイクルされた木製石鹸",
"柔らかいラバーアヒル",
"ライセンスされたコンクリートマグロ",
"豪華な花崗岩のズボン",
"洗練されたゴムキーボード",
"手作りのプラスチックコンピューター",
"実用的なスチールサラダ",
"信じられないほどのソフトベーコン",
"実用的な金属魚",
"エレガントなラバーシャツ",
"手作りのゴムテーブル",
"ゴージャスな木製のテーブル",
"素晴らしいスチールソーセージ",
"小さなソフトキーボード",
"ジェネリックスチールボール",
"電子冷凍帽子",
"ゴージャスな新鮮な椅子",
"洗練された柔らかいソーセージ",
"ゴージャスな木製タオル",
"オーダーメイドの花崗岩のピザ",
"ジェネリックメタルサラダ",
"手作りのゴムチーズ",
"素晴らしいスチールチェア",
"手作りの冷凍コンピューター",
"素朴なゴムマウス",
"洗練された花崗岩のピザ",
"ゴージャスなプラスチックキーボード",
],
es: [
"Sombrero de granito electrónico",
"Mesa suave a medida",
"Bicicleta ergonómica fresca",
"Lujoso queso de madera",
"Hermosa pizza fresca",
"Camisa de goma rústica",
"Jabón de goma moderno",
"Bola de bronce pequeña",
"Zapatos de bronce impresionantes",
"Silla de acero a medida",
"Jabón de plástico práctico",
"Tocino de granito increíble",
"Teclado de metal elegante",
"Salchichas de madera electrónica",
"Sabrosos guantes de madera",
"Lujoso queso de metal",
"Guantes de goma impresionantes",
"Coche suave y elegante",
"Ensalada fresca con licencia",
"Toallas congeladas ergonómicas",
"Teclado de goma moderno",
"Pizza de hormigón sabrosa",
"Pollo de plástico hecho a mano",
"Pollo de goma lujoso",
"Pescado suave práctico",
"Camisa de bronce ergonómica",
"Tocino de plástico artesanal",
"Pantalones de plástico sin marca",
"Salchichas de madera modernas",
"Zapatos de acero hechos a mano",
"Bicicleta de acero rústica",
"Hermosa ensalada congelada",
"Pollo bronce hecho a mano",
"Bicicleta de granito elegante",
"Salchichas de concreto genérico",
"Atún de plástico increíble",
"Queso fresco a medida",
"Tocino fresco electrónico",
"Coche de madera con licencia",
"Pescado reciclado",
"Increíbles zapatos frescos",
"Chips suaves prácticos",
"Pollo pequeño y suave",
"Ratón fresco inteligente",
"Ratón de metal moderno",
"Sabrosos guantes de granito",
"Impresionante bicicleta de goma",
"Camisa de acero pequeña",
"Ordenador de concreto refinada",
"Camisa elegante congelada",
"Zapatos de concreto inteligentes",
"Coche de goma hecho a mano",
"Toallas de goma elegantes",
"Sombrero de hormigón sin marca",
"Pescado de plástico increíble",
"Guantes suaves prácticos",
"Pizza de piedra orgánica",
"Teclado de madera genérico",
"Chips de madera reciclados",
"Increíbles chips de goma",
"Camisa de granito ergonómico",
"Sabroso teclado congelado",
"Hermoso jabón de acero",
"Lujosa silla de plástico",
"Bicicleta congelada elegante",
"Silla de acero reciclada",
"Salchichas de bronce modernas",
"Elegante queso de madera",
"Pequeñas salchichas de plástico",
"Zapatos congelados lujosos",
"Salchichas de plástico elegantes",
"Salchichas frescas artesanales",
"Increíble silla suave",
"Jabón de madera reciclado",
"Pato de goma suave",
"Atún de hormigón con licencia",
"Pantalones de granito lujosos",
"Teclado de goma refinado",
"Computadora de plástico hecha a mano",
"Ensalada de acero práctica",
"Bocino suave increíble",
"Pescado de metal práctico",
"Elegante camisa de goma",
"Mesa de goma artesanal",
"Hermosa mesa de madera",
"Fantásticas salchichas de acero",
"Pequeño teclado suave",
"Bola de acero genérica",
"Sombrero de congelado",
"Hermosa silla fresca",
"Salchichas suaves y elegantes",
"Hermosas toallas de madera",
"Pizza de granito a medida",
"Ensalada de metal genérica",
"Queso de goma hecho a mano",
"Fantástica silla de acero",
"Computadora congelada hecha a mano",
"Ratón de goma rústica",
"Pizza de granito elegante",
"Hermoso teclado de plástico",
],
de: [
"Elektronischer Granithut",
"Maßgeschneiderter weicher Tisch",
"Ergonomisches frisches Fahrrad",
"Luxuriöser Holzkäse",
"Wunderschöne frische Pizza",
"Rustikales Gummihemd",
"Moderne Gummiseife",
"Kleine Bronzekugel",
"Tolle Bronzeschuhe",
"Maßgeschneiderter Stahlstuhl",
"Praktische Plastikseife",
"Unglaublicher Granitspeck",
"Elegante Metall -Tastatur",
"Elektronische Holzwurst",
"Leckere Holzhandschuhe",
"Luxuriöser Metallkäse",
"Super Gummihandschuhe",
"Schlankes weiches Auto",
"Lizenzierter frischer Salat",
"Ergonomische gefrorene Handtücher",
"Moderne Gummi Tastatur",
"Leckere Betonpizza",
"Handgefertigtes Plastikhuhn",
"Luxuriöses Gummihähnchen",
"Praktischer weicher Fisch",
"Ergonomisches Bronzehemd",
"Handgefertigter Plastikspeck",
"Plastikhosen ohne Marken",
"Moderne Holz Würste",
"Handgefertigte Stahlschuhe",
"Rustikales Stahlrad",
"Wunderschöner gefrorener Salat",
"Handgefertigtes Bronze Hühnchen",
"Schlankes Granitrad",
"Generische Beton Würste",
"Unglaublicher Plastik Thunfisch",
"Maßgeschneiderter frischer Käse",
"Elektronischer frischer Speck",
"Lizenziertes Holzauto",
"Recycelter frischer Fisch",
"Unglaubliche frische Schuhe",
"Praktische weiche Chips",
"Kleines weiches Huhn",
"Intelligente frische Maus",
"Moderne Metallmaus",
"Leckere Granithandschuhe",
"Super Gummi Fahrrad",
"Kleines Stahlhemd",
"Raffinierter Betoncomputer",
"Schlankes gefrorenes Hemd",
"Intelligente Betonschuhe",
"Handgefertigtes Gummiauto",
"Schlanke Gummi Handtücher",
"Betonhut ohne Markenzeichen",
"Unglaublicher Plastikfisch",
"Praktische weiche Handschuhe",
"Bio Steinpizza",
"Generische hölzerne Tastatur",
"Recycelte Holzchips",
"Unglaubliche Gummischchips",
"Ergonomisches Granithemd",
"Leckere gefrorene Tastatur",
"Wunderschöne Stahlseife",
"Luxuriöser Plastikstuhl",
"Elegantes gefrorenes Fahrrad",
"Recycelter Stahlstuhl",
"Moderne Bronze Würste",
"Eleganter Holzkäse",
"Kleine Plastikwurst",
"Luxuriöse gefrorene Schuhe",
"Schlanke Plastikwurst",
"Handgefertigte frische Würste",
"Unglaublicher weicher Stuhl",
"Recycelte Holzseife",
"Weiche Gummi Ente",
"Lizenzierter Beton Thunfisch",
"Luxuriöse Granithosen",
"Raffinierte Gummi Tastatur",
"Handgefertigter Kunststoffcomputer",
"Praktischer Stahlsalat",
"Unglaublicher weicher Speck",
"Praktische Metallfische",
"Elegantes Gummihemd",
"Handgefertigter Gummi Tisch",
"Wunderschöner Holztisch",
"Fantastische Stahlwurst",
"Kleine weiche Tastatur",
"Generischer Stahlkugel",
"Elektronischer Gefrorenhut",
"Wunderschöner frischer Stuhl",
"Schlanke weiche Würste",
"Wunderschöne Holztücher",
"Maßgeschneiderte Granitpizza",
"Generischer Metallsalat",
"Handgefertigter Gummi Käse",
"Fantastischer Stahlstuhl",
"Handgefertigter gefrorener Computer",
"Rustikale Gummi Maus",
"Schlanke Granitpizza",
"Wunderschöne Plastiktastatur",
],
ru: [
"Электронная гранитная шляпа",
"Беспокойный мягкий стол",
"Эргономичный свежий велосипед",
"Роскошный деревянный сыр",
"Великолепная свежая пицца",
"Деревенская резиновая рубашка",
"Современное резиновое мыло",
"Маленький бронзовый мяч",
"Потрясающие бронзовые туфли",
"Стальное кресло на заказ",
"Практическое пластиковое мыло",
"Невероятный гранитный бекон",
"Элегантная металлическая клавиатура",
"Электронные деревянные колбасы",
"Вкусные деревянные перчатки",
"Роскошный металлический сыр",
"Потрясающие резиновые перчатки",
"Гладкая мягкая машина",
"Лицензированный свежий салат",
"Эргономичные замороженные полотенца",
"Современная резиновая клавиатура",
"Вкусная бетонная пицца",
"Пластиковая курица ручной работы",
"Роскошная резиновая курица",
"Практическая мягкая рыба",
"Эргономичная бронзовая рубашка",
"Пластиковый бекон из ручной работы",
"Непревзойденные пластиковые брюки",
"Современные деревянные колбасы",
"Стальные туфли ручной работы",
"Деревенский стальный велосипед",
"Великолепный замороженный салат",
"Бронзовая курица ручной работы",
"Гладкий гранитный велосипед",
"Общие бетонные колбасы",
"Невероятный пластиковый тунец",
"Созданный свежий сыр",
"Электронный свежий бекон",
"Лицензированный деревянный автомобиль",
"Переработанная свежая рыба",
"Невероятные свежие туфли",
"Практические мягкие чипсы",
"Маленькая мягкая курица",
"Интеллектуальная свежая мышь",
"Современная металлическая мышь",
"Вкусные гранитные перчатки",
"Потрясающий резиновый велосипед",
"Маленькая стальная рубашка",
"Изысканный бетонный компьютер",
"Гладкая замороженная рубашка",
"Интеллектуальные бетонные туфли",
"Резиновый автомобиль ручной работы",
"Гладкие резиновые полотенца",
"Непревзойденная бетонная шляпа",
"Невероятная пластиковая рыба",
"Практические мягкие перчатки",
"Органическая камня пицца",
"Общая деревянная клавиатура",
"Переработанные деревянные чипсы",
"Невероятные резиновые чипсы",
"Эргономичная гранитная рубашка",
"Вкусная замороженная клавиатура",
"Великолепное стальное мыло",
"Роскошное пластиковое кресло",
"Элегантный замороженный велосипед",
"Переработанное стальное кресло",
"Современные бронзовые колбасы",
"Элегантный деревянный сыр",
"Маленькие пластиковые колбаски",
"Роскошные замороженные туфли",
"Гладкие пластиковые колбаски",
"Свежие колбаски ручной работы",
"Невероятный мягкий стул",
"Переработанное деревянное мыло",
"Мягкая резиновая утка",
"Лицензированный бетонный тунец",
"Роскошные гранитные штаны",
"Рафинированная резиновая клавиатура",
"Пластиковый компьютер ручной работы",
"Практический стальной салат",
"Невероятный мягкий бекон",
"Практическая металлическая рыба",
"Элегантная резиновая рубашка",
"Ручной резиновый стол",
"Великолепный деревянный стол",
"Фантастические стальные колбаски",
"Небольшая мягкая клавиатура",
"Общий стальной шар",
"Электронная замороженная шляпа",
"Великолепный свежий стул",
"Гладкие мягкие колбаски",
"Великолепные деревянные полотенца",
"Сделанная на заказ гранитная пицца",
"Общий металлический салат",
"Резиновый сыр ручной работы",
"Фантастическое стальное кресло",
"Замороженный компьютер ручной работы",
"Деревенская резиновая мышь",
"Главная гранитная пицца",
"Великолепная пластиковая клавиатура",
],
emoji: [
"Electronic Granite Hat 👆🏻",
"Bespoke Soft 🍷 Table",
"Ergonomic Fresh Bike 😚😚",
"Luxurious 🍉 Wooden Cheese 🍮",
"Gorgeous Fresh Pizza ⛔",
"Rustic 💪🏽 Rubber Shirt",
"Modern Rubber 🍀 Soap",
"👍 Small Bronze Ball 👍",
"Awesome Bronze Shoes 😎",
"Bespoke 👈🏽 Steel Chair",
"Practical Plastic 💋 💋 Soap",
"🙌🏻 Incredible Granite Bacon",
"🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃",
"Electronic Wooden Sausages 🌷",
"Tasty 🍺 Wooden Gloves",
"🏖️ Luxurious Metal Cheese",
"Awesome Rubber 😉 Gloves",
"Sleek Soft Car 💁🏻‍♂️",
"Licensed 👏👏👏 Fresh Salad",
"Ergonomic Frozen Towels 🐇",
"🖐🏻 Modern Rubber Keyboard",
"Tasty Concrete Pizza ✨✨",
"Handmade 😘 Plastic 😘 Chicken 😘",
"🏁 Luxurious Rubber Chicken 🏁",
"Practical Soft Fish 🤍",
"Ergonomic Bronze Shirt 😍",
"😸😸 Handcrafted 🐻 Plastic Bacon",
"Unbranded 🐭 Plastic Pants",
"🤘 Modern 🤘 Wooden 🤘 Sausages",
"Handmade Steel Shoes 👍",
"Rustic 🧁🧁 Steel Bike",
"Gorgeous Frozen Salad 👩‍💻",
"Handmade Bronze Chicken 😮😸",
"Sleek 🍐 Granite Bike",
" ❌ ❌ Generic Concrete Sausages",
"Incredible 🍉 Plastic Tuna",
"Bespoke Fresh Cheese 😘",
"💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡",
"Licensed 🍐 Wooden 🍅 Car ",
"Recycled Fresh Fish 🤡",
"📞 Incredible Fresh Shoes",
"Practical 🐻🐻 Soft Chips",
"Small 💝 Soft Chicken",
" 💝 Intelligent Fresh Mouse 💝",
"Modern Metal 🧵 Mouse",
"🦈 Tasty Granite Gloves",
"Awesome Rubber Bike 😡😡😡😡",
"🚮 Small Steel Shirt 🚮",
"Refined 🌲 Concrete Computer",
"Sleek Frozen Shirt 👨‍🦰",
"Intelligent Concrete ➗➗➗ Shoes",
"🏅Handmade Rubber Car",
"Sleek 👨🏼‍🌾 Rubber 👨🏼‍🌾 Towels",
"Unbranded Concrete Hat🎇",
"🌀🌀Incredible Plastic Fish",
"Practical Soft Gloves🌶️ 🌶️",
"Organic 🍞 Stone 🔽 Pizza 🥴",
"Generic Wooden Keyboard 💙",
"Recycled 🔴 Wooden Chips",
"Incredible Rubber Chips 🍹",
"🌵 Ergonomic Granite Shirt",
"Tasty Frozen 🦄 Keyboard",
"🍣 Gorgeous Steel 🥯 Soap",
"Luxurious Plastic Chair 🧑‍🦰",
"Elegant Frozen 🧑‍🦰 Bike",
"Recycled 🟠🟠 Steel Chair",
"⭐⭐ Modern ⭐ Bronze ⭐ Sausages",
"Elegant Wooden Cheese🤘",
"Small 🎎 Plastic 🛩️ Sausages",
"*️⃣*️⃣*️⃣*️⃣ Luxurious Frozen Shoes",
"Sleek Plastic Sausages 🚩",
"Handcrafted Fresh 💮 Sausages",
"Incredible 🤢🤢 Soft Chair",
"🇬🇪 Recycled 🇲🇺 Wooden Soap",
"Soft 🦌 Rubber Duck 🐥",
"Licensed Concrete Tuna 👎👎",
"Luxurious Granite 💝 Pants",
"Refined Rubber Keyboard 💝",
"👌🏻👌🏻 Handcrafted Plastic Computer",
"Practical Steel 🐪 Salad",
"Incredible Soft Bacon 🌺",
"Practical Metal 🥊 Fish",
"Elegant 👩🏾‍❤️‍💋‍👨🏽👩🏾‍❤️‍💋‍👨🏽 Rubber Shirt",
"🛺 Handcrafted Rubber Table",
"Gorgeous 🦙 Wooden Table 🦙",
"🍉 Fantastic Steel Sausages",
"Small Soft Keyboard👟",
"Generic 🦙🦙 Steel Ball",
"Electronic Frozen Hat ✌🏾✌🏾✌🏾✌🏾✌🏾",
"Gorgeous 🍏 Fresh Chair",
"Sleek Soft 💧 Sausages",
"Gorgeous Wooden Towels 🍿",
"Bespoke 🌅 Granite Pizza",
"Generic Metal Salad 🎗️",
"✨ Handmade ✨ Rubber ✨ Cheese ✨",
"Fantastic 🐥 🌺 🤷🏾 Steel Chair",
"Handcrafted Frozen Computer 🛡️ 🧸 🐓",
"🐹 Rustic Rubber Mouse",
"💠 Sleek Granite Pizza 💠",
"Gorgeous 🧝🏻‍♂️ Plastic Keyboard",
],
};
export const defaultTodoText = {
en: "Something to do",
"zh-cn": "做某事",
ja: "何かをする必要がある",
es: "Algo que hacer",
de: "Etwas zu tun",
ru: "Кое-что сделать",
emoji: "Something to do 😊",
};
export const defaultLanguage = "en";
export function getTodoText(lang = "en", index) {
const todosSelection = todos[lang];
const currentIndex = index % todosSelection.length;
return todosSelection[currentIndex];
}

View file

@ -0,0 +1,72 @@
import { numberOfItemsToAdd, promisesEventsNames } from "../workload-test.mjs";
// Base class for storage managers that provides common functionality
// for tracking pending operations and dispatching events.
class BaseStorageManager {
constructor() {
this.dbName = "todoDB";
this.storeName = "todos";
this.db = null;
this.finishedAdditions = 0;
this.finishedToggles = 0;
this.finishedDeletions = 0;
this.initDB().then(() => {
this._dispatchReadyEvent();
});
}
_ensureDbConnection() {
if (!this.db)
throw new Error("Database connection is not established");
}
// When runner in Speedometer, additions, completions and removals are
// triggered synchonously in a tight loop, increasing the pending counters.
// The completion events are dispatched only when all pending operations
// of that type are complete.
_handleAddComplete() {
if (++this.finishedAdditions === numberOfItemsToAdd)
window.dispatchEvent(new CustomEvent(promisesEventsNames.add, {}));
}
_handleToggleComplete() {
if (++this.finishedToggles === numberOfItemsToAdd)
window.dispatchEvent(new CustomEvent(promisesEventsNames.toggle, {}));
}
_handleRemoveComplete() {
if (++this.finishedDeletions === numberOfItemsToAdd) {
this.db.close();
window.dispatchEvent(new CustomEvent(promisesEventsNames.delete, {}));
}
}
_dispatchReadyEvent() {
window.dispatchEvent(new CustomEvent("db-ready", {}));
}
// Abstract methods that must be implemented by subclasses
async initDB() {
throw new Error("initDB method must be implemented by subclass");
}
addTodo(todo) {
throw new Error("addTodo method must be implemented by subclass");
}
async getTodos(upperItemNumber, count) {
throw new Error("getTodos method must be implemented by subclass");
}
toggleTodo(itemNumber, completed) {
throw new Error("toggleTodo method must be implemented by subclass");
}
removeTodo(itemNumber) {
throw new Error("removeTodo method must be implemented by subclass");
}
}
export default BaseStorageManager;

View file

@ -0,0 +1,77 @@
import Dexie from "../../libs/dexie.mjs";
import BaseStorageManager from "./base-storage-manager.js";
class DexieDBManager extends BaseStorageManager {
constructor() {
super();
}
async initDB() {
// Delete the existing database first for clean state
await Dexie.delete(this.dbName);
this.db = new Dexie(this.dbName);
this.db.version(1).stores({
todos: "itemNumber, id, title, completed, priority",
});
await this.db.open();
return this.db;
}
addTodo(todo) {
this._ensureDbConnection();
this.db.todos
.add(todo)
.then(() => {
// When running in Speedometer, the event will be dispatched only once
// because all the additions are done in a tight loop.
this._handleAddComplete();
})
.catch((error) => {
throw error;
});
}
async getTodos(upperItemNumber, count) {
this._ensureDbConnection();
// Get items with itemNumber less than upperItemNumber
// Use reverse to get highest first, then limit, then reverse result back to ascending
const items = await this.db.todos.where("itemNumber").below(upperItemNumber).reverse().limit(count).toArray();
// Reverse to get ascending order (lowest itemNumber first) to match IndexedDB implementation
return items.reverse();
}
toggleTodo(itemNumber, completed) {
this._ensureDbConnection();
this.db.todos
.update(itemNumber, { completed: completed })
.then(() => {
this._handleToggleComplete();
})
.catch((error) => {
throw error;
});
}
removeTodo(itemNumber) {
this._ensureDbConnection();
this.db.todos
.delete(itemNumber)
.then(() => {
this._handleRemoveComplete();
})
.catch((error) => {
throw error;
});
}
}
export default DexieDBManager;

View file

@ -0,0 +1,165 @@
import BaseStorageManager from "./base-storage-manager.js";
class IndexedDBManager extends BaseStorageManager {
initDB() {
return new Promise((resolve, reject) => {
// Delete the existing database first for clean state
const deleteRequest = indexedDB.deleteDatabase(this.dbName);
deleteRequest.onerror = (event) => {
reject(event.target.error);
};
deleteRequest.onsuccess = () => {
this.openDatabase(resolve, reject);
};
deleteRequest.onblocked = () => {
reject(new Error("Database deletion blocked - please close other tabs using this database"));
};
});
}
openDatabase(resolve, reject) {
const request = indexedDB.open(this.dbName, 1);
request.onerror = (event) => {
reject(event.target.error);
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore(this.storeName, { keyPath: "itemNumber" });
store.createIndex("id", "id", { unique: true });
store.createIndex("title", "title", { unique: false });
store.createIndex("completed", "completed", { unique: false });
store.createIndex("priority", "priority", { unique: false });
};
}
addTodo(todo) {
this._ensureDbConnection();
const transaction = this.db.transaction(this.storeName, "readwrite");
transaction.oncomplete = () => {
// When running in Speedometer, the event will be dispatched only once
// because all the additions are done in a tight loop.
this._handleAddComplete();
};
transaction.onerror = (event) => {
console.error("Transaction error:", event.target.error);
throw event.target.error;
};
const store = transaction.objectStore(this.storeName);
store.add(todo);
transaction.commit();
}
async getTodos(upperItemNumber, count) {
this._ensureDbConnection();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readonly");
const store = transaction.objectStore(this.storeName);
// Use IDBKeyRange to get items with itemNumber less than upperItemNumber
const range = IDBKeyRange.upperBound(upperItemNumber, true); // true = exclusive bound
// Open a cursor to iterate through records in descending order
const request = store.openCursor(range, "prev");
const items = [];
let itemsProcessed = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
// Check if we have a valid cursor and haven't reached our count limit
if (cursor && itemsProcessed < count) {
items.push(cursor.value);
itemsProcessed++;
cursor.continue(); // Move to next item
} else {
// We're done - sort items by itemNumber in ascending order
// for proper display order (oldest to newest)
items.sort((a, b) => a.itemNumber - b.itemNumber);
transaction.commit();
resolve(items);
}
};
transaction.onerror = (event) => {
reject(event.target.error);
};
});
}
toggleTodo(itemNumber, completed) {
this._ensureDbConnection();
const transaction = this.db.transaction(this.storeName, "readwrite");
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(itemNumber);
getRequest.onsuccess = (event) => {
const todoItem = getRequest.result;
if (!todoItem)
throw new Error(`Todo item with itemNumber '${itemNumber}' not found`);
todoItem.completed = completed;
const updateRequest = store.put(todoItem);
updateRequest.onerror = (event) => {
throw event.target.error;
};
transaction.commit();
};
getRequest.onerror = (event) => {
throw event.target.error;
};
transaction.oncomplete = () => {
this._handleToggleComplete();
};
// Handle transaction errors
transaction.onerror = (event) => {
throw event.target.error;
};
}
removeTodo(itemNumber) {
this._ensureDbConnection();
// Access the todo item directly by its itemNumber (keyPath)
const transaction = this.db.transaction(this.storeName, "readwrite");
const store = transaction.objectStore(this.storeName);
transaction.oncomplete = () => {
this._handleRemoveComplete();
};
transaction.onerror = (event) => {
throw event.target.error;
};
// Delete the todo item directly using its primary key (itemNumber)
store.delete(itemNumber);
transaction.commit();
}
}
export default IndexedDBManager;

View file

@ -0,0 +1,24 @@
import IndexedDBManager from "./indexedDB-manager.js";
import DexieDBManager from "./dexieDB-manager.js";
/**
* Factory function that returns the appropriate storage manager based on URL search parameters
* @returns {IndexedDBManager|DexieDBManager} The storage manager instance
*/
export function createStorageManager() {
const params = new URLSearchParams(window.location.search);
let storageType = params.get("storageType");
if (storageType && storageType !== "vanilla" && storageType !== "dexie")
throw new Error(`Invalid storage type specified in URL parameter: ${storageType}`);
storageType = storageType || "vanilla";
if (storageType === "dexie") {
console.log("Using Dexie.js storage manager");
return new DexieDBManager();
}
// Default to vanilla IndexedDB
console.log("Using vanilla IndexedDB storage manager");
return new IndexedDBManager();
}

View file

@ -0,0 +1,41 @@
/* Borrowed from https://github.com/ai/nanoid/blob/3.0.2/non-secure/index.js
The MIT License (MIT)
Copyright 2017 Andrey Sitnik <andrey@sitnik.ru>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
// This alphabet uses `A-Za-z0-9_-` symbols.
// The order of characters is optimized for better gzip and brotli compression.
// References to the same file (works both for gzip and brotli):
// `'use`, `andom`, and `rict'`
// References to the brotli default dictionary:
// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf`
let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
export function nanoid(size = 21) {
let id = "";
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = size;
while (i--) {
// `| 0` is more compact and faster than `Math.floor()`.
id += urlAlphabet[(Math.random() * 64) | 0];
}
return id;
}

View file

@ -0,0 +1,99 @@
import { BenchmarkStep, BenchmarkSuite } from "./speedometer-utils/benchmark.mjs";
import { getTodoText, defaultLanguage } from "./speedometer-utils/translations.mjs";
import { numberOfItemsToAdd } from "./speedometer-utils/todomvc-utils.mjs";
export const appName = "todomvc-indexeddb";
export const appVersion = "1.0.0";
export { numberOfItemsToAdd };
export const promisesEventsNames = {
add: "db-add-completed",
toggle: "db-toggle-completed",
delete: "db-delete-completed",
};
const addPromise = new Promise((resolve) => {
window.addEventListener(promisesEventsNames.add, () => resolve());
});
const togglePromise = new Promise((resolve) => {
window.addEventListener(promisesEventsNames.toggle, () => resolve());
});
const deletePromise = new Promise((resolve) => {
window.addEventListener(promisesEventsNames.delete, () => resolve());
});
const suites = {
default: new BenchmarkSuite("indexeddb", [
new BenchmarkStep(`Adding${numberOfItemsToAdd}Items`, async () => {
const input = document.querySelector("todo-app").shadowRoot.querySelector("todo-topbar").shadowRoot.querySelector(".new-todo-input");
for (let i = 0; i < numberOfItemsToAdd; i++) {
input.value = getTodoText(defaultLanguage, i);
input.dispatchEvent(new Event("input"));
input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" }));
}
}),
new BenchmarkStep(
"FinishAddingItemsToDB",
async () => {
await addPromise;
},
/* ignoreResult = */ true
),
new BenchmarkStep("CompletingAllItems", async () => {
const numberOfItemsPerIteration = 10;
const numberOfIterations = 10;
for (let j = 0; j < numberOfIterations; j++) {
const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list");
const items = todoList.shadowRoot.querySelectorAll("todo-item");
for (let i = 0; i < numberOfItemsPerIteration; i++) {
const item = items[i].shadowRoot.querySelector(".toggle-todo-input");
item.click();
}
if (j < 9) {
const nextPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".next-page-button");
nextPageButton.click();
}
}
}),
new BenchmarkStep(
"FinishModifyingItemsInDB",
async () => {
await togglePromise;
},
/* ignoreResult = */ true
),
new BenchmarkStep("DeletingAllItems", async () => {
const numberOfItemsPerIteration = 10;
const numberOfIterations = 10;
function iterationFinishedListener() {
iterationFinishedListener.promiseResolve();
}
window.addEventListener("previous-page-loaded", iterationFinishedListener);
for (let j = 0; j < numberOfIterations; j++) {
const iterationFinishedPromise = new Promise((resolve) => {
iterationFinishedListener.promiseResolve = resolve;
});
const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list");
const items = todoList.shadowRoot.querySelectorAll("todo-item");
for (let i = numberOfItemsPerIteration - 1; i >= 0; i--) {
const item = items[i].shadowRoot.querySelector(".remove-todo-button");
item.click();
}
if (j < 9) {
const previousPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".previous-page-button");
previousPageButton.click();
await iterationFinishedPromise;
}
}
}),
new BenchmarkStep(
"FinishDeletingItemsFromDB",
async () => {
await deletePromise;
},
/* ignoreResult = */ true
),
]),
};
export default suites;

View file

@ -0,0 +1,15 @@
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host {
display: block;
box-shadow: none !important;
min-height: 68px;
}
.app {
background: #fff;
margin: 24px 16px 40px 16px;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
`);
export default sheet;

View file

@ -0,0 +1,158 @@
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host {
display: block;
box-shadow: none !important;
}
.bottombar {
padding: 10px 0;
height: 41px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
position: relative;
}
.bottombar::before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
pointer-events: none;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-status {
text-align: left;
padding: 3px;
height: 32px;
line-height: 26px;
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
}
.todo-count {
font-weight: 300;
}
.filter-list {
margin: 0;
padding: 0;
list-style: none;
display: inline-block;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.filter-item {
display: inline-block;
}
.filter-link {
color: inherit;
margin: 3px;
padding: 0 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
display: block;
height: 26px;
line-height: 26px;
}
.filter-link:hover {
border-color: #db7676;
}
.filter-link.selected {
border-color: #ce4646;
}
.clear-completed-button,
.clear-completed-button:active {
text-decoration: none;
cursor: pointer;
padding: 3px;
height: 32px;
line-height: 26px;
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
}
.clear-completed-button:hover {
text-decoration: underline;
}
/* rtl support */
html[dir="rtl"] .todo-status,
:host([dir="rtl"]) .todo-status {
right: 12px;
left: unset;
}
html[dir="rtl"] .clear-completed-button,
:host([dir="rtl"]) .clear-completed-button {
left: 12px;
right: unset;
}
@media (max-width: 430px) {
.bottombar {
height: 120px;
}
.todo-status {
display: block;
text-align: center;
position: relative;
left: unset;
right: unset;
top: unset;
transform: unset;
}
.filter-list {
display: block;
position: relative;
left: unset;
right: unset;
top: unset;
transform: unset;
}
.clear-completed-button,
.clear-completed-button:active {
display: block;
margin: 0 auto;
position: relative;
left: unset;
right: unset;
top: unset;
transform: unset;
}
html[dir="rtl"] .todo-status,
:host([dir="rtl"]) .todo-status {
right: unset;
left: unset;
}
html[dir="rtl"] .clear-completed-button,
:host([dir="rtl"]) .clear-completed-button {
left: unset;
right: unset;
}
}
`);
export default sheet;

View file

@ -0,0 +1,26 @@
:host {
display: block;
box-shadow: none !important;
}
.footer {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.footer-text {
line-height: 1;
}
.footer-link {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.footer-link:hover {
text-decoration: underline;
}

View file

@ -0,0 +1,86 @@
const sheet = new CSSStyleSheet();
sheet.replaceSync(`*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111;
min-width: 300px;
max-width: 582px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
box-shadow: inset 0 0 2px 2px #cf7d7d !important;
outline: 0 !important;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input {
position: relative;
margin: 0;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
color: inherit;
padding: 0;
border: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input:placeholder-shown {
text-overflow: ellipsis;
}
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
.truncate-singleline {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block !important;
}
`);
export default sheet;

View file

@ -0,0 +1,88 @@
/** defaults */
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111;
min-width: 300px;
max-width: 582px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
box-shadow: inset 0 0 2px 2px #cf7d7d !important;
outline: 0 !important;
}
/** resets */
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input {
position: relative;
margin: 0;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
color: inherit;
padding: 0;
border: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input:placeholder-shown {
text-overflow: ellipsis;
}
/* utility classes */
/* used for things that should be hidden in the ui,
but useful for people who use screen readers */
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
.truncate-singleline {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block !important;
}

View file

@ -0,0 +1,21 @@
:host {
display: block;
box-shadow: none !important;
}
.header {
margin-top: 27px;
}
.title {
width: 100%;
font-size: 80px;
line-height: 80px;
margin: 0;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}

View file

@ -0,0 +1,11 @@
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host {
display: block;
box-shadow: none !important;
}
.main {
position: relative;
}
`);
export default sheet;

View file

@ -0,0 +1,147 @@
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host {
display: block;
box-shadow: none !important;
}
:host(:last-child) > .todo-item {
border-bottom: none;
}
.todo-item {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
height: 60px;
}
.todo-item.editing {
border-bottom: none;
padding: 0;
}
.edit-todo-container {
display: none;
}
.todo-item.editing .edit-todo-container {
display: block;
}
.edit-todo-input {
padding: 0 16px 0 60px;
width: 100%;
height: 60px;
font-size: 24px;
line-height: 1.4em;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20%20style%3D%22opacity%3A%200.2%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
.display-todo {
position: relative;
}
.todo-item.editing .display-todo {
display: none;
}
.toggle-todo-input {
text-align: center;
width: 40px;
height: auto;
position: absolute;
top: 0;
bottom: 0;
left: 3px;
margin: auto 0;
border: none; appearance: none;
cursor: pointer;
}
.todo-item-text {
overflow-wrap: break-word;
padding: 0 60px;
display: block;
line-height: 60px;
transition: color 0.4s;
font-weight: 400;
color: #484848;
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
.toggle-todo-input:checked + .todo-item-text {
color: #949494;
text-decoration: line-through;
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E");
}
.remove-todo-button {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #949494;
transition: color 0.2s ease-out;
cursor: pointer;
}
.remove-todo-button:hover,
.remove-todo-button:focus {
color: #c18585;
}
.remove-todo-button::after {
content: "×";
display: block;
height: 100%;
line-height: 1.1;
}
.todo-item:hover .remove-todo-button {
display: block;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-todo-input {
background: none;
height: 40px;
}
}
@media (max-width: 430px) {
.remove-todo-button {
display: block;
}
}
html[dir="rtl"] .toggle-todo-input,
:host([dir="rtl"]) .toggle-todo-input {
right: 3px;
left: unset;
}
html[dir="rtl"] .todo-item-text,
:host([dir="rtl"]) .todo-item-text {
background-position: center right 6px;
}
html[dir="rtl"] .remove-todo-button,
:host([dir="rtl"]) .remove-todo-button {
left: 10px;
right: unset;
}
`);
export default sheet;

View file

@ -0,0 +1,15 @@
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host {
display: block;
box-shadow: none !important;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
display: block;
border-top: 1px solid #e6e6e6;
}
`);
export default sheet;

View file

@ -0,0 +1,90 @@
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host {
display: block;
box-shadow: none !important;
}
.topbar {
position: relative;
}
.new-todo-input {
padding: 0 32px 0 60px;
width: 100%;
height: 68px;
font-size: 24px;
line-height: 1.4em;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.new-todo-input::placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.toggle-all-container {
width: 45px;
height: 68px;
position: absolute;
left: 0;
top: 0;
}
.toggle-all-input {
width: 45px;
height: 45px;
font-size: 0;
position: absolute;
top: 11.5px;
left: 0;
border: none;
appearance: none;
cursor: pointer;
}
.toggle-all-label {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 68px;
font-size: 0;
position: absolute;
top: 0;
left: 0;
cursor: pointer;
}
.toggle-all-label::before {
content: "❯";
display: inline-block;
font-size: 22px;
color: #949494;
padding: 10px 27px 10px 27px;
transform: rotate(90deg);
}
.toggle-all-input:checked + .toggle-all-label::before {
color: #484848;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all-input {
background: none;
}
}
html[dir="rtl"] .new-todo-input,
:host([dir="rtl"]) .new-todo-input {
padding: 0 60px 0 32px;
}
html[dir="rtl"] .toggle-all-container,
:host([dir="rtl"]) .toggle-all-container {
right: 0;
left: unset;
}
`);
export default sheet;

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
<title>TodoMVC: JavaScript Web Components</title>
<link rel="stylesheet" href="node_modules/todomvc-css/dist/global.css" />
<link rel="stylesheet" href="node_modules/todomvc-css/dist/header.css" />
<link rel="stylesheet" href="node_modules/todomvc-css/dist/footer.css" />
<!-- load these first, so that they are registered by the time the app components needs them -->
<script type="module" src="src/components/todo-topbar/todo-topbar.component.js"></script>
<script type="module" src="src/components/todo-list/todo-list.component.js"></script>
<script type="module" src="src/components/todo-bottombar/todo-bottombar.component.js"></script>
<script type="module" src="src/components/todo-app/todo-app.component.js"></script>
</head>
<body>
<header class="header">
<a href="#" style="text-decoration: none"><h1 class="title">todos</h1></a>
</header>
<todo-app></todo-app>
<footer class="footer">
<p class="footer-text">Click on input field to write your todo.</p>
<p class="footer-text">At least one character is needed to be a valid entry.</p>
<p class="footer-text">Press 'enter' to add the todo.</p>
<p class="footer-text">Double-click to edit a todo</p>
</footer>
<script type="module" src="src/index.mjs"></script>
</body>
</html>

View file

@ -0,0 +1,840 @@
{
"name": "todomvc-javascript-web-components-indexeddb",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "todomvc-javascript-web-components-indexeddb",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"dexie": "^4.2.1",
"http-server": "^14.1.1",
"speedometer-utils": "../../resources/shared",
"todomvc-css": "file:../../suites/todomvc/todomvc-css"
},
"engines": {
"node": ">=24.0.0",
"npm": ">=8.19.3"
}
},
"../../../resources/shared": {
"extraneous": true
},
"../../../shared": {
"extraneous": true
},
"../../resources/shared": {
"name": "speedometer-utils",
"version": "1.0.0"
},
"../../resources/todomvc/todomvc-css": {
"version": "1.0.0",
"extraneous": true,
"license": "ISC",
"devDependencies": {
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-html": "^1.0.2",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/pluginutils": "^5.0.2",
"fs-extra": "^11.1.1",
"globby": "^13.2.0",
"http-server": "^14.1.1",
"rollup": "^3.23.0",
"rollup-plugin-cleaner": "^1.0.0",
"rollup-plugin-copy-merge": "^1.0.2",
"rollup-plugin-import-css": "^3.2.1",
"strip-comments": "^2.0.1",
"stylelint": "^15.6.2",
"stylelint-config-standard": "^33.0.0"
},
"engines": {
"node": ">=18.13.0",
"npm": ">=8.19.3"
}
},
"../../suites/todomvc/todomvc-css": {
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-html": "^1.0.2",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/pluginutils": "^5.0.2",
"fs-extra": "^11.1.1",
"globby": "^13.2.0",
"http-server": "^14.1.1",
"rollup": "^3.23.0",
"rollup-plugin-cleaner": "^1.0.0",
"rollup-plugin-copy-merge": "^1.0.2",
"rollup-plugin-import-css": "^3.2.1",
"strip-comments": "^2.0.1",
"stylelint": "^15.6.2",
"stylelint-config-standard": "^33.0.0"
},
"engines": {
"node": ">=18.13.0",
"npm": ">=8.19.3"
}
},
"../../todomvc-css": {
"version": "1.0.0",
"extraneous": true,
"license": "ISC",
"devDependencies": {
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-html": "^1.0.2",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/pluginutils": "^5.0.2",
"fs-extra": "^11.1.1",
"globby": "^13.2.0",
"http-server": "^14.1.1",
"rollup": "^3.23.0",
"rollup-plugin-cleaner": "^1.0.0",
"rollup-plugin-copy-merge": "^1.0.2",
"rollup-plugin-import-css": "^3.2.1",
"strip-comments": "^2.0.1",
"stylelint": "^15.6.2",
"stylelint-config-standard": "^33.0.0"
},
"engines": {
"node": ">=18.13.0",
"npm": ">=8.19.3"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/dexie": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz",
"integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==",
"license": "Apache-2.0"
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"dependencies": {
"whatwg-encoding": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-server": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
"dependencies": {
"basic-auth": "^2.0.1",
"chalk": "^4.1.2",
"corser": "^2.0.1",
"he": "^1.2.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy": "^1.18.1",
"mime": "^1.6.0",
"minimist": "^1.2.6",
"opener": "^1.5.1",
"portfinder": "^1.0.28",
"secure-compare": "3.0.1",
"union": "~0.5.0",
"url-join": "^4.0.1"
},
"bin": {
"http-server": "bin/http-server"
},
"engines": {
"node": ">=12"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"bin": {
"opener": "bin/opener-bin.js"
}
},
"node_modules/portfinder": {
"version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
"dependencies": {
"async": "^2.6.4",
"debug": "^3.2.7",
"mkdirp": "^0.5.6"
},
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/secure-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/speedometer-utils": {
"resolved": "../../resources/shared",
"link": true
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/todomvc-css": {
"resolved": "../../suites/todomvc/todomvc-css",
"link": true
},
"node_modules/union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"dependencies": {
"qs": "^6.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=12"
}
}
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
},
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ=="
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"dexie": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz",
"integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"requires": {
"whatwg-encoding": "^2.0.0"
}
},
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
},
"http-server": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
"requires": {
"basic-auth": "^2.0.1",
"chalk": "^4.1.2",
"corser": "^2.0.1",
"he": "^1.2.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy": "^1.18.1",
"mime": "^1.6.0",
"minimist": "^1.2.6",
"opener": "^1.5.1",
"portfinder": "^1.0.28",
"secure-compare": "3.0.1",
"union": "~0.5.0",
"url-join": "^4.0.1"
}
},
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="
},
"opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="
},
"portfinder": {
"version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
"requires": {
"async": "^2.6.4",
"debug": "^3.2.7",
"mkdirp": "^0.5.6"
}
},
"qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"requires": {
"side-channel": "^1.0.4"
}
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"secure-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw=="
},
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"requires": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
}
},
"speedometer-utils": {
"version": "file:../../resources/shared"
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
},
"todomvc-css": {
"version": "file:../../suites/todomvc/todomvc-css",
"requires": {
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-html": "^1.0.2",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/pluginutils": "^5.0.2",
"fs-extra": "^11.1.1",
"globby": "^13.2.0",
"http-server": "^14.1.1",
"rollup": "^3.23.0",
"rollup-plugin-cleaner": "^1.0.0",
"rollup-plugin-copy-merge": "^1.0.2",
"rollup-plugin-import-css": "^3.2.1",
"strip-comments": "^2.0.1",
"stylelint": "^15.6.2",
"stylelint-config-standard": "^33.0.0"
}
},
"union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"requires": {
"qs": "^6.4.0"
}
},
"url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
},
"whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"requires": {
"iconv-lite": "0.6.3"
}
}
}
}

View file

@ -0,0 +1,24 @@
{
"name": "todomvc-javascript-web-components-indexeddb",
"version": "1.0.0",
"description": "TodoMVC app written with JavaScript using web components.",
"engines": {
"node": ">=24.0.0",
"npm": ">=8.19.3"
},
"private": true,
"scripts": {
"dev": "http-server ./ -p 7005 -c-1 --cors -o",
"build": "node scripts/build.mjs",
"serve": "http-server ./dist -p 7006 -c-1 --cors -o"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dexie": "^4.2.1",
"http-server": "^14.1.1",
"todomvc-css": "file:../../suites/todomvc/todomvc-css",
"speedometer-utils": "../../resources/shared"
}
}

View file

@ -0,0 +1,170 @@
import path from "path";
import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs";
import fs from "fs/promises";
import { dirname } from "path";
/**
* createDirectory
*
* Removes and recreates a directory.
*
* @param {string} directory Directory name.
*/
async function createDirectory(directory) {
await fs.rm(directory, { recursive: true, force: true });
await fs.mkdir(directory);
}
/**
* copyDirectory
*
* Copies a source folder to a destination folder.
*
* @param {string} src Source directory.
* @param {string} dest Destination directory.
*/
async function copyDirectory(src, dest) {
await fs.cp(src, dest, { recursive: true }, (err) => {
if (err)
console.error(err);
});
}
/**
* copyFile
*
* Copies a file from a source to a destination.
*
* @param {string} src Source file.
* @param {string} dest Destination file.
*/
async function copyFile(src, dest) {
await fs.mkdir(dirname(dest), { recursive: true });
await fs.copyFile(src, dest);
}
/**
* copyFiles
*
* Copies multiple files from a source to a destination.
*
* @param {string[]} files Array of files to copy.
*/
async function copyFiles(files) {
for (const file of files)
await copyFile(file.src, file.dest);
}
/**
* updateImportsInFile
*
* Reads a file and replaces a source path with a destination path.
*
* @param {Object} config - Config to update imports
* @param {string} config.src - The source path.
* @param {string} config.dest - The destination path.
* @param {string} config.file - File to read from.
*/
async function updateImportsInFile({ file, src, dest }) {
let contents = await fs.readFile(file, "utf8");
contents = contents.replaceAll(src, dest);
await fs.writeFile(file, contents);
}
/**
* updateImports
*
* Updates imports in multiple files.
*
* @param {Object} config - Config to update imports
* @param {string} config.src - The source path.
* @param {string} config.dest - The destination path.
* @param {string} config.file - Files to read from.
*/
async function updateImports({ files, src, dest }) {
for (const file of files)
await updateImportsInFile({ file, src, dest });
}
const filesToMove = [
{ src: "index.html", dest: "./dist/index.html" },
{ src: "node_modules/todomvc-css/dist/global.css", dest: "./dist/styles/global.css" },
{ src: "node_modules/todomvc-css/dist/header.css", dest: "./dist/styles/header.css" },
{ src: "node_modules/todomvc-css/dist/footer.css", dest: "./dist/styles/footer.css" },
{ src: "node_modules/todomvc-css/dist/global.constructable.js", dest: "./dist/styles/global.constructable.js" },
{ src: "node_modules/todomvc-css/dist/app.constructable.js", dest: "./dist/styles/app.constructable.js" },
{ src: "node_modules/todomvc-css/dist/topbar.constructable.js", dest: "./dist/styles/topbar.constructable.js" },
{ src: "node_modules/todomvc-css/dist/main.constructable.js", dest: "./dist/styles/main.constructable.js" },
{ src: "node_modules/todomvc-css/dist/bottombar.constructable.js", dest: "./dist/styles/bottombar.constructable.js" },
{ src: "node_modules/todomvc-css/dist/todo-list.constructable.js", dest: "./dist/styles/todo-list.constructable.js" },
{ src: "node_modules/todomvc-css/dist/todo-item.constructable.js", dest: "./dist/styles/todo-item.constructable.js" },
{ src: "node_modules/dexie/dist/modern/dexie.mjs", dest: "./dist/libs/dexie.mjs" },
{ src: "node_modules/speedometer-utils/step-scheduler.mjs", dest: "./dist/src/speedometer-utils/step-scheduler.mjs" },
{ src: "node_modules/speedometer-utils/step-runner.mjs", dest: "./dist/src/speedometer-utils/step-runner.mjs" },
{ src: "node_modules/speedometer-utils/params.mjs", dest: "./dist/src/speedometer-utils/params.mjs" },
{ src: "src/speedometer-utils/benchmark.mjs", dest: "./dist/src/speedometer-utils/benchmark.mjs" },
{ src: "node_modules/speedometer-utils/helpers.mjs", dest: "./dist/src/speedometer-utils/helpers.mjs" },
{ src: "node_modules/speedometer-utils/translations.mjs", dest: "./dist/src/speedometer-utils/translations.mjs" },
{ src: "node_modules/speedometer-utils/todomvc-utils.mjs", dest: "./dist/src/speedometer-utils/todomvc-utils.mjs" },
];
const importsToRename = [
{
src: "node_modules/todomvc-css/dist/",
dest: "styles/",
files: ["./dist/index.html"],
},
{
src: "../../../node_modules/todomvc-css/dist/",
dest: "../../../styles/",
files: [
"./dist/src/components/todo-app/todo-app.component.js",
"./dist/src/components/todo-bottombar/todo-bottombar.component.js",
"./dist/src/components/todo-item/todo-item.component.js",
"./dist/src/components/todo-list/todo-list.component.js",
"./dist/src/components/todo-topbar/todo-topbar.component.js",
],
},
{
src: "../../../node_modules/dexie/dist/modern/dexie.mjs",
dest: "../../libs/dexie.mjs",
files: ["./dist/src/storage/dexieDB-manager.js"],
},
{
src: "/src/",
dest: "./",
files: ["./dist/src/index.mjs"],
},
{
src: "/node_modules/speedometer-utils/",
dest: "./speedometer-utils/",
files: ["./dist/src/index.mjs", "./dist/src/workload-test.mjs"],
},
{
src: "/node_modules/speedometer-utils/",
dest: "./",
files: ["./dist/src/speedometer-utils/benchmark.mjs"],
},
];
const build = async () => {
// create dist folder
await createDirectory("./dist");
// copy src folder
await copyDirectory("./src", "./dist/src");
// copy files to Move
await copyFiles(filesToMove);
// rename imports files
for (const entry of importsToRename) {
const { files, src, dest } = entry;
await updateImports({ files, src, dest });
}
console.log("Done with building!");
};
await build();
await generateResourcesFile(path.join(import.meta.dirname, "../dist"));

View file

@ -0,0 +1,156 @@
import template from "./todo-app.template.js";
import { useRouter } from "../../hooks/useRouter.js";
import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js";
import appStyles from "../../../node_modules/todomvc-css/dist/app.constructable.js";
import mainStyles from "../../../node_modules/todomvc-css/dist/main.constructable.js";
class TodoApp extends HTMLElement {
#isReady = false;
#numberOfItems = 0;
#numberOfCompletedItems = 0;
constructor() {
super();
const node = document.importNode(template.content, true);
this.topbar = node.querySelector("todo-topbar");
this.list = node.querySelector("todo-list");
this.bottombar = node.querySelector("todo-bottombar");
this.shadow = this.attachShadow({ mode: "open" });
this.htmlDirection = document.dir || "ltr";
this.setAttribute("dir", this.htmlDirection);
this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles];
this.shadow.append(node);
this.addItem = this.addItem.bind(this);
this.toggleItem = this.toggleItem.bind(this);
this.removeItem = this.removeItem.bind(this);
this.updateItem = this.updateItem.bind(this);
this.toggleItems = this.toggleItems.bind(this);
this.clearCompletedItems = this.clearCompletedItems.bind(this);
this.routeChange = this.routeChange.bind(this);
this.moveToNextPage = this.moveToNextPage.bind(this);
this.moveToPreviousPage = this.moveToPreviousPage.bind(this);
this.router = useRouter();
}
get isReady() {
return this.#isReady;
}
getInstance() {
return this;
}
addItem(event) {
const { detail: item } = event;
this.list.addItem(item, this.#numberOfItems++);
this.update();
}
toggleItem(event) {
if (event.detail.completed)
this.#numberOfCompletedItems++;
else
this.#numberOfCompletedItems--;
this.list.toggleItem(event.detail.itemNumber, event.detail.completed);
this.update();
}
removeItem(event) {
if (event.detail.completed)
this.#numberOfCompletedItems--;
this.#numberOfItems--;
this.update();
this.list.removeItem(event.detail.itemNumber);
}
updateItem(event) {
this.update();
}
toggleItems(event) {
this.list.toggleItems(event.detail.completed);
}
clearCompletedItems() {
this.list.removeCompletedItems();
}
moveToNextPage() {
this.list.moveToNextPage();
}
async moveToPreviousPage() {
await this.list.moveToPreviousPage();
this.bottombar.reenablePreviousPageButton();
window.dispatchEvent(new CustomEvent("previous-page-loaded", {}));
}
update() {
const totalItems = this.#numberOfItems;
const completedItems = this.#numberOfCompletedItems;
const activeItems = totalItems - completedItems;
this.list.setAttribute("total-items", totalItems);
this.topbar.setAttribute("total-items", totalItems);
this.topbar.setAttribute("active-items", activeItems);
this.topbar.setAttribute("completed-items", completedItems);
this.bottombar.setAttribute("total-items", totalItems);
this.bottombar.setAttribute("active-items", activeItems);
}
addListeners() {
this.topbar.addEventListener("toggle-all", this.toggleItems);
this.topbar.addEventListener("add-item", this.addItem);
this.list.listNode.addEventListener("toggle-item", this.toggleItem);
this.list.listNode.addEventListener("remove-item", this.removeItem);
this.list.listNode.addEventListener("update-item", this.updateItem);
this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems);
this.bottombar.addEventListener("next-page", this.moveToNextPage);
this.bottombar.addEventListener("previous-page", this.moveToPreviousPage);
}
removeListeners() {
this.topbar.removeEventListener("toggle-all", this.toggleItems);
this.topbar.removeEventListener("add-item", this.addItem);
this.list.listNode.removeEventListener("toggle-item", this.toggleItem);
this.list.listNode.removeEventListener("remove-item", this.removeItem);
this.list.listNode.removeEventListener("update-item", this.updateItem);
this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems);
this.bottombar.removeEventListener("next-page", this.moveToNextPage);
this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage);
}
routeChange(route) {
const routeName = route.split("/")[1] || "all";
this.list.updateRoute(routeName);
this.bottombar.updateRoute(routeName);
this.topbar.updateRoute(routeName);
}
connectedCallback() {
this.update();
this.addListeners();
this.router.initRouter(this.routeChange);
this.#isReady = true;
}
disconnectedCallback() {
this.removeListeners();
this.#isReady = false;
}
}
customElements.define("todo-app", TodoApp);
export default TodoApp;

View file

@ -0,0 +1,14 @@
const template = document.createElement("template");
template.id = "todo-app-template";
template.innerHTML = `
<section class="app">
<todo-topbar></todo-topbar>
<main class="main">
<todo-list></todo-list>
</main>
<todo-bottombar></todo-bottombar>
</section>
`;
export default template;

Some files were not shown because too many files have changed in this diff Show more