Compare commits

...

267 commits

Author SHA1 Message Date
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
1118 changed files with 492554 additions and 1 deletions

48
.eslintignore Normal file
View file

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

101
.eslintrc Normal file
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
}
}

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

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

33
.gitignore vendored
View file

@ -2,3 +2,36 @@
*.pyc
.DS_Store
.directory
/node_modules
.vscode
.angular
/resources/todomvc/architecture-examples/angular/node_modules/
/resources/todomvc/architecture-examples/angular-complex/node_modules/
/resources/todomvc/architecture-examples/backbone/node_modules/
/resources/todomvc/architecture-examples/backbone-complex/node_modules/
/resources/todomvc/architecture-examples/jquery/node_modules/
/resources/todomvc/architecture-examples/jquery-complex/node_modules/
/resources/todomvc/architecture-examples/preact/node_modules/
/resources/todomvc/architecture-examples/preact-complex/node_modules/
/resources/todomvc/architecture-examples/react/node_modules/
/resources/todomvc/architecture-examples/react-complex/node_modules/
/resources/todomvc/architecture-examples/react-redux/node_modules/
/resources/todomvc/architecture-examples/react-redux-complex/node_modules/
/resources/todomvc/architecture-examples/svelte/node_modules/
/resources/todomvc/architecture-examples/svelte-complex/node_modules/
/resources/todomvc/architecture-examples/vue/node_modules/
/resources/todomvc/architecture-examples/vue-complex/node_modules/
/resources/todomvc/architecture-examples/lit-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-es5/node_modules/
/resources/todomvc/vanilla-examples/javascript-es5-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6-webpack/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-web-components-complex/node_modules/
/resources/charts/node_modules/
/resources/todomvc/big-dom-generator/node_modules/

1
.npmrc Normal file
View file

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

48
.prettierignore Normal file
View file

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

12
.prettierrc Normal file
View file

@ -0,0 +1,12 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"printWidth": 250,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "es5",
"useTabs": false
}

34
Development.md Normal file
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
npm run dev
```
2. Open your browser of choice and navigate to [http://127.0.0.1:7000](http://127.0.0.1:7000)
## Local Server
Speedometer uses [http-server](https://github.com/http-party/http-server), which is a static HTTP server. Meaning it does not provide hot-reloading. By default, the dev script disables caching and local changes can be viewed by simply refreshing your browser window. Additional options of the http-server can be found [here](https://github.com/http-party/http-server#available-options).

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

40
InteractiveRunner.html Normal file
View file

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer 3.0 Interactive Runner</title>
<script src="resources/interactive.mjs" type="module"></script>
<link rel="icon" href="resources/favicon.png" />
<style>
iframe {
border: 1px solid black;
}
ol {
list-style: none;
margin: 0;
padding: 0;
}
ol ol {
margin-left: 2em;
list-style-position: outside;
}
.running {
text-decoration: underline;
}
.ran {
color: grey;
}
nav {
position: absolute;
right: 10px;
height: 600px;
}
nav > ol {
height: 100%;
overflow-y: scroll;
}
</style>
</head>
<body></body>
</html>

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

16
README.md Normal file
View file

@ -0,0 +1,16 @@
# What is Speedometer?
Speedometer is a benchmark for web browsers that measures Web application responsiveness
by timing simulated user interactions on various workloads. Our primary goal is to make it
reflect the real-world Web as much as possible. When a browser improves its score on the
benchmark, actual users should benefit. In order to achieve this, it should:
- Test end-to-end user journeys instead of testing specific features in a tight loop. Each
test should exercise the full set of what’s needed from the engine in order for a user to
accomplish a task.
- Evolve over time, adapting to the present Web on a regular basis. This should be informed
by current usage data, and by consensus about features which are important for engines to
optimize to provide a consistent experience for users and site authors.
- Be accessible to the public and useful to browser engineers. It should run in every modern
browser by visiting a normal web page. It should run relatively quickly, while providing
enough test coverage to be reflective of the real-world Web.

29
Testing.md Normal file
View file

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

105
about.html Normal file
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 3.0 About</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
</head>
<body>
<main>
<section id="about" class="visible" data-title="About">
<a href="./" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div class="section-grid">
<h1 class="section-header">About Speedometer 3</h1>
<div class="section-content">
<p>Speedometer 3 is a benchmark for web browsers that measures Web application responsiveness by timing simulated user interactions on various workloads.</p>
<p>
It's developed as an open source project, with decisions being made under a <a href="https://github.com/WebKit/Speedometer/blob/main/Governance.md">multistakeholder governance model</a> between the three widely distributed
web browser engine projects.
</p>
<p>
The following high level user journeys are implemented in the current version. Each of these journeys has one or more workloads which test important aspects of it — for example commonly used patterns, frameworks, or
technologies.
</p>
<ul>
<li>Working with a todo list</li>
<ul>
<li>Measures the time to add, complete, and remove 100 todo items in a basic list.</li>
<li>Each example implements the same todo application (TodoMVC) using different techniques and frameworks.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.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">
<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
}
]
]
}

106
index.html Normal file
View file

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html data-visible-section="home">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer 3.0</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
<script src="resources/main.mjs" type="module"></script>
</head>
<body>
<main>
<section id="home">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div class="content">
<p>Speedometer is a browser benchmark that measures the responsiveness of Web applications. It uses demo web applications to simulate user actions such as adding to-do items.</p>
<p id="screen-size-warning">
<strong>
Your browser window is too small. For most accurate results, please make the view port size at least <span id="min-screen-width">850px</span> by <span id="min-screen-height">650px</span>.<br />
It's currently <span id="screen-size"></span>.
</strong>
</p>
</div>
<div class="buttons">
<div class="button-row">
<button class="start-tests-button">Start Test</button>
</div>
<div class="button-row">
<a href="about.html">About Speedometer</a>
<a href="instructions.html">Test Instructions</a>
</div>
</div>
</section>
<section id="running">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</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">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</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 class="buttons">
<div class="button-row">
<a class="button" href="#details" id="show-details" title="Show detailed results data.">Details</a>
</div>
</div>
</section>
<section id="details" data-title="Results Details">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div class="section-grid">
<h1 class="section-header">Detailed Results</h1>
<div class="section-content all-metric-results">
<div class="aggregated-metric-result">
<h2>Geomean</h2>
<div id="geomean-chart"></div>
<h2>Tests</h2>
<div id="tests-chart"></div>
</div>
<br />
<h2>Detailed Metrics</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 3.0 Test Instructions</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
</head>
<body>
<main>
<section id="instructions" class="visible" data-title="Test Instructions">
<a href="./" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div class="section-grid">
<h1 class="section-header">Speedometer 3 General Instructions</h1>
<div class="section-content">
<p>To get reasonably accurate results your device and browser need to be focused on the benchmark. Here are some basic instructions to allow that:</p>
<ul class="compact">
<li>Ensure you are running the latest stable browser version.</li>
<li>Prefer using a separate clean browser profile — extensions and non-default browser settings can have a large impact on the score.</li>
<li>Don't run any programs in the background.</li>
<li>Restart the browser and close all other tabs and windows.</li>
<li>Keep the Speedometer page focused and do not interact with the device while running the benchmark.</li>
<li>Keep your battery-powered device plugged into a charger.</li>
<li>Some devices might need time to cool down between between consecutive runs.</li>
</ul>
</div>
<div class="buttons section-footer">
<div class="button-row">
<a class="button" href="./" title="Show main section.">Home</a>
<a class="button" href="about.html" title="Show about section.">About</a>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

11509
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

52
package.json Normal file
View file

@ -0,0 +1,52 @@
{
"name": "speedometer",
"version": "3.0.0-alpha",
"description": "An open source repository for the Speedometer benchmark.",
"engines": {
"node": ">=18.13.0",
"npm": ">=8.19.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WebKit/Speedometer.git"
},
"bugs": {
"url": "https://github.com/WebKit/Speedometer/issues"
},
"license": "SEE LICENSE IN LICENSE",
"scripts": {
"dev": "http-server ./ -p 7000 -c-1 --cors",
"server": "http-server ./ -p 7000 --cors",
"lint:check": "eslint **/*.{js,mjs,jsx,ts,tsx}",
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix",
"pretty:check": "prettier --check ./",
"pretty:fix": "prettier --write ./",
"format": "npm run pretty:fix && npm run lint:fix",
"test:chrome": "node tests/run.mjs --browser chrome",
"test:firefox": "node tests/run.mjs --browser firefox",
"test:safari": "node tests/run.mjs --browser safari",
"test:edge": "node tests/run.mjs --browser edge"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/eslint-parser": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@nuxt/eslint-config": "^0.1.1",
"@next/eslint-plugin-next": "^13.4.5",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^6.1.3",
"eslint": "^8.38.0",
"eslint-plugin-ember": "^11.4.8",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-vue": "^9.10.0",
"expect.js": "^0.3.1",
"http-server": "^14.1.1",
"mocha": "^10.2.0",
"prettier": "^2.8.3",
"selenium-webdriver": "^4.8.0",
"sinon": "^15.0.1",
"typescript": "^5.0.4"
}
}

View file

@ -0,0 +1,593 @@
import { Metric } from "./metric.mjs";
import { params } from "./params.mjs";
const performance = globalThis.performance;
export class BenchmarkTestStep {
constructor(testName, testFunction) {
this.name = testName;
this.run = testFunction;
}
}
function getParent(lookupStartNode, path) {
const parent = path.reduce((root, selector) => {
const node = root.querySelector(selector);
return node.shadowRoot ?? node;
}, lookupStartNode);
return parent;
}
class Page {
constructor(frame) {
this._frame = frame;
}
layout() {
const body = this._frame.contentDocument.body.getBoundingClientRect();
this.layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
}
async waitForElement(selector) {
return new Promise((resolve) => {
const resolveIfReady = () => {
const element = this.querySelector(selector);
let callback = resolveIfReady;
if (element)
callback = () => resolve(element);
window.requestAnimationFrame(callback);
};
resolveIfReady();
});
}
/**
* Returns the first element within the document that matches the specified selector, or group of selectors.
* If no matches are found, null is returned.
*
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
*
* @example
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
* // return PageElement(<todo-item>)
* querySelector("todo-item", ["todo-app", "todo-list"]);
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns PageElement | null
*/
querySelector(selector, path = []) {
const lookupStartNode = this._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]());
}
_wrapElement(element) {
return new PageElement(element);
}
}
const NATIVE_OPTIONS = {
bubbles: true,
cancellable: true,
};
class PageElement {
#node;
constructor(node) {
this.#node = node;
}
setValue(value) {
this.#node.value = value;
}
click() {
this.#node.click();
}
focus() {
this.#node.focus();
}
getElementByMethod(name) {
return new PageElement(this.#node[name]());
}
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
if (eventName === "submit")
// FIXME FireFox doesn't like `new Event('submit')
this._dispatchSubmitEvent();
else
this.#node.dispatchEvent(new eventType(eventName, options));
}
_dispatchSubmitEvent() {
const submitEvent = document.createEvent("Event");
submitEvent.initEvent("submit", true, true);
this.#node.dispatchEvent(submitEvent);
}
enter(type, options = undefined) {
const ENTER_KEY_CODE = 13;
return this.dispatchKeyEvent(type, ENTER_KEY_CODE, "Enter", options);
}
dispatchKeyEvent(type, keyCode, key, options) {
let eventOptions = { bubbles: true, cancelable: true, keyCode, which: keyCode, key };
if (options !== undefined)
eventOptions = Object.assign(eventOptions, options);
const event = new KeyboardEvent(type, eventOptions);
this.#node.dispatchEvent(event);
}
dispatchMouseEvent(type, offsetX, offsetY, options) {
const boundingRect = this.#node.getBoundingClientRect();
const clientX = offsetX + boundingRect.x;
const clientY = offsetY + boundingRect.y;
const contentWindow = this.#node.ownerDocument.defaultView;
const screenX = clientX + contentWindow.screenX;
const screenY = clientY + contentWindow.screenY;
let eventOptions = { bubbles: true, cancelable: true, clientX, clientY, screenX, screenY };
if (options !== undefined)
eventOptions = Object.assign(eventOptions, options);
const event = new contentWindow.MouseEvent(type, eventOptions);
this.#node.dispatchEvent(event);
}
/**
* Returns the first element found in a node of a PageElement that matches the specified selector, or group of selectors. If a shadow DOM is present in the node, the shadow DOM is used to query.
* If no matches are found, null is returned.
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns PageElement | null
*/
querySelectorInShadowRoot(selector, path = []) {
const lookupStartNode = this.#node.shadowRoot ?? this.#node;
const element = getParent(lookupStartNode, path).querySelector(selector);
if (element === null)
return null;
return new PageElement(element);
}
querySelector(selector) {
const element = this.#node.querySelector(selector);
if (element === null)
return null;
return new PageElement(element);
}
}
function geomeanToScore(geomean) {
return 1000 / geomean;
}
// The WarmupSuite is used to make sure all runner helper functions and
// classes are compiled, to avoid unnecessary pauses due to delayed
// compilation of runner methods in the middle of the measuring cycle.
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);
}),
],
};
class TestInvoker {
constructor(syncCallback, asyncCallback, reportCallback) {
this._syncCallback = syncCallback;
this._asyncCallback = asyncCallback;
this._reportCallback = reportCallback;
}
}
class TimerTestInvoker extends TestInvoker {
start() {
return new Promise((resolve) => {
setTimeout(() => {
this._syncCallback();
setTimeout(() => {
this._asyncCallback();
requestAnimationFrame(async () => {
await this._reportCallback();
resolve();
});
}, 0);
}, params.waitBeforeSync);
});
}
}
class RAFTestInvoker extends TestInvoker {
start() {
return new Promise((resolve) => {
if (params.waitBeforeSync)
setTimeout(() => this._scheduleCallbacks(resolve), params.waitBeforeSync);
else
this._scheduleCallbacks(resolve);
});
}
_scheduleCallbacks(resolve) {
requestAnimationFrame(() => this._syncCallback());
requestAnimationFrame(() => {
setTimeout(() => {
this._asyncCallback();
setTimeout(async () => {
await this._reportCallback();
resolve();
}, 0);
}, 0);
});
}
}
// https://stackoverflow.com/a/47593316
function seededHashRandomNumberGenerator(a) {
return function () {
var t = a += 0x6d2b79f5;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return (t ^ (t >>> 14)) >>> 0;
};
}
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);
}
async runMultipleIterations(iterationCount) {
this._iterationCount = iterationCount;
if (this._client?.willStartFirstIteration)
await this._client.willStartFirstIteration(iterationCount);
const iterationStartLabel = "iteration-start";
const iterationEndLabel = "iteration-end";
for (let i = 0; i < iterationCount; i++) {
performance.mark(iterationStartLabel);
await this._runAllSuites();
performance.mark(iterationEndLabel);
performance.measure(`iteration-${i}`, iterationStartLabel, iterationEndLabel);
}
if (this._client?.didFinishLastIteration)
await this._client.didFinishLastIteration(this._metrics);
}
_removeFrame() {
if (this._frame) {
this._frame.parentNode.removeChild(this._frame);
this._frame = null;
}
}
async _appendFrame(src) {
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%)";
if (this._client?.willAddTestFrame)
await this._client.willAddTestFrame(frame);
document.body.insertBefore(frame, document.body.firstChild);
this._frame = frame;
return frame;
}
async _runAllSuites() {
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
const prepareStartLabel = "runner-prepare-start";
const prepareEndLabel = "runner-prepare-end";
performance.mark(prepareStartLabel);
this._removeFrame();
await this._appendFrame();
this._page = new Page(this._frame);
let suites = [...this._suites];
if (this._suiteOrderRandomNumberGenerator) {
// We just do a simple Fisher-Yates shuffle based on the repeated hash of the
// seed. This is not a high quality RNG, but it's plenty good enough.
for (let i = 0; i < suites.length - 1; i++) {
let j = i + (this._suiteOrderRandomNumberGenerator() % (suites.length - i));
let tmp = suites[i];
suites[i] = suites[j];
suites[j] = tmp;
}
}
performance.mark(prepareEndLabel);
performance.measure("runner-prepare", prepareStartLabel, prepareEndLabel);
for (const suite of suites) {
if (!suite.disabled)
await this._runSuite(suite);
}
const finalizeStartLabel = "runner-finalize-start";
const finalizeEndLabel = "runner-finalize-end";
performance.mark(finalizeStartLabel);
// Remove frame to clear the view for displaying the results.
this._removeFrame();
await this._finalize();
performance.mark(finalizeEndLabel);
performance.measure("runner-finalize", finalizeStartLabel, finalizeEndLabel);
}
async _runSuite(suite) {
const suitePrepareStartLabel = `suite-${suite.name}-prepare-start`;
const suitePrepareEndLabel = `suite-${suite.name}-prepare-end`;
const suiteStartLabel = `suite-${suite.name}-start`;
const suiteEndLabel = `suite-${suite.name}-end`;
performance.mark(suitePrepareStartLabel);
await this._prepareSuite(suite);
performance.mark(suitePrepareEndLabel);
performance.mark(suiteStartLabel);
for (const test of suite.tests)
await this._runTestAndRecordResults(suite, test);
performance.mark(suiteEndLabel);
performance.measure(`suite-${suite.name}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
performance.measure(`suite-${suite.name}`, suiteStartLabel, suiteEndLabel);
}
async _prepareSuite(suite) {
return new Promise((resolve) => {
const frame = this._page._frame;
frame.onload = async () => {
await suite.prepare(this._page);
resolve();
};
frame.src = `resources/${suite.url}`;
});
}
async _runTestAndRecordResults(suite, test) {
/* eslint-disable-next-line no-async-promise-executor */
if (this._client?.willRunTest)
await this._client.willRunTest(suite, test);
// Prepare all mark labels outside the measuring loop.
const startLabel = `${suite.name}.${test.name}-start`;
const syncEndLabel = `${suite.name}.${test.name}-sync-end`;
const asyncStartLabel = `${suite.name}.${test.name}-async-start`;
const asyncEndLabel = `${suite.name}.${test.name}-async-end`;
let syncTime;
let asyncStartTime;
let asyncTime;
const runSync = () => {
if (params.warmupBeforeSync) {
performance.mark("warmup-start");
const startTime = performance.now();
// Infinite loop for the specified ms.
while (performance.now() - startTime < params.warmupBeforeSync)
continue;
performance.mark("warmup-end");
}
performance.mark(startLabel);
const syncStartTime = performance.now();
test.run(this._page);
const syncEndTime = performance.now();
performance.mark(syncEndLabel);
syncTime = syncEndTime - syncStartTime;
performance.mark(asyncStartLabel);
asyncStartTime = performance.now();
};
const measureAsync = () => {
// Some browsers don't immediately update the layout for paint.
// Force the layout here to ensure we're measuring the layout time.
const height = this._frame.contentDocument.body.getBoundingClientRect().height;
const asyncEndTime = performance.now();
asyncTime = asyncEndTime - asyncStartTime;
this._frame.contentWindow._unusedHeightValue = height; // Prevent dead code elimination.
performance.mark(asyncEndLabel);
if (params.warmupBeforeSync)
performance.measure("warmup", "warmup-start", "warmup-end");
performance.measure(`${suite.name}.${test.name}-sync`, startLabel, syncEndLabel);
performance.measure(`${suite.name}.${test.name}-async`, asyncStartLabel, asyncEndLabel);
};
const report = () => this._recordTestResults(suite, test, syncTime, asyncTime);
const invokerClass = params.measurementMethod === "raf" ? RAFTestInvoker : TimerTestInvoker;
const invoker = new invokerClass(runSync, measureAsync, report);
return invoker.start();
}
async _recordTestResults(suite, test, syncTime, asyncTime) {
// Skip reporting updates for the warmup suite.
if (suite === WarmupSuite)
return;
const suiteResults = this._measuredValues.tests[suite.name] || { tests: {}, total: 0 };
const total = syncTime + asyncTime;
this._measuredValues.tests[suite.name] = suiteResults;
suiteResults.tests[test.name] = { tests: { Sync: syncTime, Async: asyncTime }, total: total };
suiteResults.total += total;
if (this._client?.didRunTest)
await this._client.didRunTest(suite, test);
}
async _finalize() {
this._appendIterationMetrics();
if (this._client?.didRunSuites) {
let product = 1;
const values = [];
for (const suiteName in this._measuredValues.tests) {
const suiteTotal = this._measuredValues.tests[suiteName].total;
product *= suiteTotal;
values.push(suiteTotal);
}
values.sort((a, b) => a - b); // Avoid the loss of significance for the sum.
const total = values.reduce((a, b) => a + b);
const geomean = Math.pow(product, 1 / values.length);
this._measuredValues.total = total;
this._measuredValues.mean = total / values.length;
this._measuredValues.geomean = geomean;
this._measuredValues.score = geomeanToScore(geomean);
await this._client.didRunSuites(this._measuredValues);
}
}
_appendIterationMetrics() {
const getMetric = (name, unit = "ms") => this._metrics[name] || (this._metrics[name] = new Metric(name, unit));
const iterationTotalMetric = (i) => {
if (i >= params.iterationCount)
throw new Error(`Requested iteration=${i} does not exist.`);
return getMetric(`Iteration-${i}-Total`);
};
const 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++)
iterationTotalMetric(i);
getMetric("Geomean");
getMetric("Score", "score");
}
const geomean = getMetric("Geomean");
const iterationTotal = iterationTotalMetric(geomean.length);
for (const results of Object.values(iterationResults))
iterationTotal.add(results.total);
iterationTotal.computeAggregatedMetrics();
geomean.add(iterationTotal.geomean);
getMetric("Score").add(geomeanToScore(iterationTotal.geomean));
for (const metric of Object.values(this._metrics))
metric.computeAggregatedMetrics();
}
}

View file

@ -0,0 +1,78 @@
# Chart benchmarks
## How to run locally
This project uses `npm` to manage dependencies and run scripts. It has been
tested with node v18 but should work with other versions of node.
This uses [vite](https://vitejs.dev/) as builder but this should be fairly
transparent.
### Install dependencies
```
npm i
```
### Run the development server
```
npm run dev
```
### Build the app
```
npm run build
```
### Preview the production build
```
npm run preview
```
## Included benchmarks
All the benchmarks are included in iframes in the index page. But it may be more
convenient to open these benchmarks with their specific files.
### [Observable Plot](https://github.com/observablehq/plot)
You can load this benchmark with the `/observable-plot.html` page, for example
http://localhost:5173/observable-plot.html if you run it locally or
http://localhost:7000/resources/charts/dist/observable-plot.html in the
context of the speedometer.
Observable Plot is D3-based and outputs SVG.
When run in development mode, the page will automatically execute the 3
included graphs:
- a stacked bar graph
- a grouped bar graph
- a graph using dots
In production mode nothing executes by default, the user needs to push the
buttons to run any code. That's how the benchmark exercizes this code.
To build these graphs, we use datasets representing flight information in the
US. You can consult them in the [datasets directory](./datasets).
### [ChartJS](https://github.com/chartjs/Chart.js)
You can load this benchmark with the `/chartjs.html` page, for example
http://localhost:5173/chartjs.html if you run it locally or
http://localhost:7000/resources/charts/dist/chartjs.html in the
context of speedometer.
ChartJS is canvas-based.
When run in development mode, the page will automatically execute the scatter
graph.
In production mode nothing executes by default, the user needs to push the
buttons to run any code. That's how the benchmark exercizes this code.
To build these graphs, we use datasets representing flight information in the
US. You can consult them in the [datasets directory](./datasets).

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chart.js</title>
<style type="text/css" media="all">
label {
user-select: none;
display: inline-flex;
align-items: center;
}
</style>
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
<button id="open-tooltip" type="button">Open tooltips</button>
<button id="reset" type="button">Reset</button>
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
</div>
<canvas id="chart"></canvas>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
<script type="module" src="/chartjs.js"></script>
</body>
</html>

174
resources/charts/chartjs.js Normal file
View file

@ -0,0 +1,174 @@
import Chart from "chart.js/auto";
import { csvParse } from "d3-dsv";
// These 2 imports use some vite machinery to directly import these files as
// strings. Then they are constants and this reduces the load on the GC.
import airportsString from "./datasets/airports.csv?raw";
import flightsString from "./datasets/flights-airports.csv?raw";
/*
* From https://www.omnicalculator.com/other/latitude-longitude-distance:
* d = 2R × sin⁻¹(√[sin²((θ₂ - θ₁)/2) + cosθ₁ × cosθ₂ × sin²((φ₂ - φ₁)/2)])
*
* where:
* θ₁, φ₁ – First point latitude and longitude coordinates;
* θ₂, φ₂ – Second point latitude and longitude coordinates;
* R – Earth's radius (R = 6371 km); and
* d – Distance between them along Earth's surface.
*/
const R = 6371;
function computeDistance(coords1, coords2) {
const long1 = toRadian(coords1.longitude);
const lat1 = toRadian(coords1.latitude);
const long2 = toRadian(coords2.longitude);
const lat2 = toRadian(coords2.latitude);
const longSquareSin = Math.sin((long2 - long1) / 2) ** 2;
const latSquareSin = Math.sin((lat2 - lat1) / 2) ** 2;
const d = 2 * R * Math.asin(Math.sqrt(latSquareSin + Math.cos(lat1) * Math.cos(lat2) * longSquareSin));
return d;
}
const RAD = Math.PI / 180;
function toRadian(deg) {
return deg * RAD;
}
let preparedData = null;
function prepare() {
/**
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
* airports: Array<AirportInformation>
* flights: Array<{ origin, destination, count }>
*/
const airports = csvParse(airportsString);
const flights = csvParse(flightsString);
const airportMap = new Map(airports.map((airport) => [airport.iata, airport]));
for (const flight of flights) {
const origAirport = airportMap.get(flight.origin);
const destAirport = airportMap.get(flight.destination);
flight.distance = computeDistance(origAirport, destAirport);
}
preparedData = { flights, airportMap };
}
const ROOT = document.getElementById("chart");
const opaqueCheckBox = document.getElementById("opaque-color");
let currentChart = null;
function drawScattered(data) {
if (!preparedData)
throw new Error("Please prepare the data first.");
reset();
currentChart = new Chart(ROOT, {
type: "scatter",
data: {
datasets: [
{
data: preparedData.flights,
backgroundColor: opaqueCheckBox.checked ? "rgb(0, 125, 255)" : "rgba(0, 125, 255, .20)",
},
],
},
options: {
animation: false,
parsing: {
xAxisKey: "distance",
yAxisKey: "count",
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: "Number of flights for a distance",
position: "bottom",
},
tooltip: {
displayColors: false,
callbacks: {
label: (item) => {
const orig = preparedData.airportMap.get(item.raw.origin);
const dest = preparedData.airportMap.get(item.raw.destination);
const result = `✈ ${orig.name} (${orig.iata}) → ${dest.name} (${dest.iata}): ${Math.round(item.raw.distance)} km`;
return result;
},
},
},
},
scales: {
x: {
title: {
text: "distance →",
align: "end",
display: true,
},
ticks: {
format: {
style: "unit",
unit: "kilometer",
},
},
},
y: {
type: "logarithmic",
title: {
text: "# of flights →",
align: "end",
display: true,
},
},
},
},
});
}
function openTooltip() {
if (!currentChart)
throw new Error("No chart is present, please draw a chart first");
const renderedDataset = currentChart.getDatasetMeta(0);
const node = currentChart.canvas;
const rect = node.getBoundingClientRect();
// Index 2426 is carefully chosen to display a lot of lines (which depends
// on the zoom level).
const point = renderedDataset.data[2426];
const event = new MouseEvent("mousemove", {
clientX: rect.left + point.x,
clientY: rect.top + point.y,
cancelable: true,
bubbles: true,
});
node.dispatchEvent(event);
}
function reset() {
if (currentChart) {
currentChart.destroy();
currentChart = null;
}
}
async function runAllTheThings() {
[
// Force prettier to use a multiline formatting
"prepare",
"add-scatter-chart-button",
"open-tooltip",
].forEach((id) => document.getElementById(id).click());
}
document.getElementById("prepare").addEventListener("click", prepare);
document.getElementById("add-scatter-chart-button").addEventListener("click", drawScattered);
document.getElementById("open-tooltip").addEventListener("click", openTooltip);
document.getElementById("reset").addEventListener("click", reset);
document.getElementById("run-all").addEventListener("click", runAllTheThings);
if (import.meta.env.DEV)
runAllTheThings();

View file

@ -0,0 +1,4 @@
Origin of the datasets:
airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/airports.csv
flights-airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/flights-airport.csv

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
<iframe src="./observable-plot.html"></iframe>
</details>
<details open>
<summary>
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
</summary>
<iframe src="./chartjs.html"></iframe>
</details>
</div>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"developer-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"index-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

35
resources/charts/dist/chartjs.html vendored Normal file
View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chart.js</title>
<style type="text/css" media="all">
label {
user-select: none;
display: inline-flex;
align-items: center;
}
</style>
<script type="module" crossorigin src="./assets/chartjs-7fd89fd7.js"></script>
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
<button id="open-tooltip" type="button">Open tooltips</button>
<button id="reset" type="button">Reset</button>
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
</div>
<canvas id="chart"></canvas>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
</body>
</html>

33
resources/charts/dist/developer.html vendored Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
<iframe src="./observable-plot.html"></iframe>
</details>
<details open>
<summary>
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
</summary>
<iframe src="./chartjs.html"></iframe>
</details>
</div>
</body>
</html>

15
resources/charts/dist/index.html vendored Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1" />
</head>
<body>
<h1>Charting workloads</h1>
<ul>
<li><a href="./observable-plot.html">Observable Plot</a></li>
<li><a href="./chartjs.html">Chart.js</a></li>
</ul>
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Observable Plot</title>
<style>
label {
display: inline-flex;
align-content: center;
gap: 3px;
margin: 0 3px;
}
#airport-group-size {
min-width: 30px;
}
</style>
<script type="module" crossorigin src="./assets/plot-562fbdb6.js"></script>
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
<button id="reset" type="button">Reset</button>
<label>
Number of airports to keep in each group:
<span id="airport-group-size"></span>
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
</label>
</div>
<div id="chart"></div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1" />
</head>
<body>
<h1>Charting workloads</h1>
<ul>
<li><a href="./observable-plot.html">Observable Plot</a></li>
<li><a href="./chartjs.html">Chart.js</a></li>
</ul>
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
</body>
</html>

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Observable Plot</title>
<style>
label {
display: inline-flex;
align-content: center;
gap: 3px;
margin: 0 3px;
}
#airport-group-size {
min-width: 30px;
}
</style>
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
<button id="reset" type="button">Reset</button>
<label>
Number of airports to keep in each group:
<span id="airport-group-size"></span>
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
</label>
</div>
<div id="chart"></div>
<script type="module" src="/observable-plot.js"></script>
</body>
</html>

View file

@ -0,0 +1,233 @@
import * as Plot from "@observablehq/plot";
import { csvParse } from "d3-dsv";
import * as d3Array from "d3-array";
import { format as d3Format } from "d3-format";
// These 2 imports use some vite machinery to directly import these files as
// strings. Then they are constants and this reduces the load on the GC.
import airportsString from "./datasets/airports.csv?raw";
import flightsString from "./datasets/flights-airports.csv?raw";
const ROOT = document.getElementById("chart");
const DEFAULT_OPTIONS = {
// This width is really a max-width: the plot adjusts using the available
// width as well.
width: 2000,
// The height is especially used for the ratio.
height: 1000,
};
let preparedData;
function prepare() {
/**
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
* airports: Array<AirportInformation>
* flights: Array<{ origin, destination, count }>
*/
const { airports, flights } = parseAirportsInformation();
/**
* flightsByAirport: Map<iata: string, { origin: number, destination: number, total: number}>
*/
const flightsByAirport = groupFlightsByAirports(flights);
/**
* byAirport: Map<iata: string, AirportInformation>
*/
const byAirport = d3Array.index(airports, (d) => d.iata);
/* Array<[state, AirportInformation[]]> */
const airportsGroupedByStateArray = d3Array.groups(airports, (d) => d.state);
/* DescSortedArray<[{ state: string, total: number, mostUsedAirportsInState: AirportInformation[] }]> */
const stateInformationSortedArray = airportsGroupedByStateArray
.map(([state, airportsInState]) => {
const totalFlightsInState = d3Array.sum(airportsInState, ({ iata }) => flightsByAirport.get(iata)?.total);
const sorted = d3Array.sort(airportsInState, ({ iata }) => -flightsByAirport.get(iata)?.total);
const mostUsedAirportsInState = sorted.slice(0, airportCountPerGroup());
return {
state,
total: totalFlightsInState,
mostUsedAirports: mostUsedAirportsInState,
};
})
.sort((stateA, stateB) => stateB.total - stateA.total);
/* Array<state: string> */
const stateSortedArray = stateInformationSortedArray.map(({ state }) => state);
// Flatten the information in preparedData.stateInformationSortedArray, so that we
// have one item == one airport information.
/* Array<{state, iata, name, city, index, origin, destination, total}> */
const plotData = stateInformationSortedArray.flatMap(({ mostUsedAirports, total, state }) => {
const enrichedMostUsedAirports = mostUsedAirports.map(({ iata, name, city }, index) => ({
state,
iata,
name,
city,
index, // This will be used to have consistent colors.
...flightsByAirport.get(iata),
}));
const otherTotal = total - d3Array.sum(mostUsedAirports, ({ iata }) => flightsByAirport.get(iata)?.total);
if (otherTotal > 0) {
enrichedMostUsedAirports.push({
state,
iata: "Other",
total: otherTotal,
index: enrichedMostUsedAirports.length,
});
}
return enrichedMostUsedAirports;
});
preparedData = {
airports,
flights,
flightsByAirport,
byAirport,
stateInformationSortedArray,
stateSortedArray,
plotData,
};
}
function parseAirportsInformation() {
return {
airports: csvParse(airportsString),
flights: csvParse(flightsString),
};
}
function groupFlightsByAirports(flights) {
const flightsByAirport = new Map();
for (const { origin, destination, count } of flights) {
const infoForOriginAirport = flightsByAirport.get(origin) ?? { origin: 0, destination: 0, total: 0 };
const intCount = Number(count);
if (Number.isNaN(intCount)) {
console.error(`Couldn't convert ${count} to number.`);
continue;
}
infoForOriginAirport.origin += intCount;
infoForOriginAirport.total += intCount;
flightsByAirport.set(origin, infoForOriginAirport);
const infoForDestinationAirport = flightsByAirport.get(destination) ?? { origin: 0, destination: 0, total: 0 };
infoForDestinationAirport.destination += intCount;
infoForDestinationAirport.total += intCount;
flightsByAirport.set(destination, infoForDestinationAirport);
}
return flightsByAirport;
}
function isReady() {
return !!preparedData;
}
function addStackedBars() {
if (!isReady())
throw new Error("Please prepare the data first.");
const options = {
...DEFAULT_OPTIONS,
color: { type: "categorical" },
x: {
domain: preparedData.stateSortedArray,
},
y: { grid: true, tickFormat: "~s" },
marks: [
Plot.barY(preparedData.plotData, {
x: "state",
y: "total",
fill: "index",
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(d.total)} flights`,
}),
Plot.text(preparedData.stateInformationSortedArray, { x: "state", y: "total", text: (d) => d3Format(".2~s")(d.total), dy: -10 }),
Plot.ruleY([0]),
],
};
ROOT.append(Plot.plot(options));
}
function addDottedBars() {
if (!isReady())
throw new Error("Please prepare the data first.");
const data = [...preparedData.flightsByAirport]
.flatMap(([iata, { origin, destination }]) => {
const airportInformation = preparedData.byAirport.get(iata);
return [
{ ...airportInformation, value: -origin },
{ ...airportInformation, value: destination },
];
})
.filter((d) => d.value);
const options = {
...DEFAULT_OPTIONS,
color: { type: "threshold", domain: [0] },
x: {
domain: preparedData.stateSortedArray,
},
y: {
grid: true,
label: "← outward Number of flights inward →",
labelAnchor: "center",
tickFormat: (v) => d3Format("~s")(Math.abs(v)),
type: "pow",
exponent: 0.2,
},
marks: [
Plot.dot(data, {
x: "state",
y: "value",
r: 4,
stroke: "value",
strokeWidth: 3,
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(Math.abs(d.value))} ${d.value > 0 ? "inward" : "outward"} flights`,
}),
Plot.ruleY([0]),
],
};
ROOT.append(Plot.plot(options));
}
function reset() {
ROOT.textContent = "";
}
async function runAllTheThings() {
reset();
[
// Force prettier to use a multiline formatting
"prepare",
"add-stacked-chart-button",
"add-dotted-chart-button",
].forEach((id) => document.getElementById(id).click());
}
// This is the number of airports we keep for each state in the stacked bar
// graph. One additional group will be added, that will sum all airports that
// haven't been kept. It's retrieved from the input directly.
function airportCountPerGroup() {
return document.querySelector("#airport-group-size-input").value;
}
function onGroupSizeInputChange() {
document.querySelector("#airport-group-size").textContent = airportCountPerGroup();
if (import.meta.env.DEV) {
// In dev mode, redraw everything
runAllTheThings();
}
}
document.getElementById("prepare").addEventListener("click", prepare);
document.getElementById("add-stacked-chart-button").addEventListener("click", addStackedBars);
document.getElementById("add-dotted-chart-button").addEventListener("click", addDottedBars);
document.getElementById("reset").addEventListener("click", reset);
document.getElementById("run-all").addEventListener("click", runAllTheThings);
document.getElementById("airport-group-size-input").addEventListener("input", onGroupSizeInputChange);
onGroupSizeInputChange();
if (import.meta.env.DEV)
runAllTheThings();

1022
resources/charts/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
{
"name": "charts-benchmark",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.1.0"
},
"dependencies": {
"@observablehq/plot": "^0.6.4",
"chart.js": "^4.2.1",
"d3-dsv": "^3.0.1"
}
}

View file

@ -0,0 +1,18 @@
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
base: "./", // Since this will be loaded from the project root
build: {
modulePreload: { polyfill: false },
minify: false,
sourcemap: true,
rollupOptions: {
input: {
index: resolve(__dirname, "index.html"),
developer: resolve(__dirname, "developer.html"),
plot: resolve(__dirname, "observable-plot.html"),
chartjs: resolve(__dirname, "chartjs.html"),
},
},
},
});

View file

@ -0,0 +1,288 @@
import { Suites, Tags } from "./tests.mjs";
import { params, defaultParams } from "./params.mjs";
export function createDeveloperModeContainer() {
const container = document.createElement("div");
container.className = "developer-mode";
const details = document.createElement("details");
const summary = document.createElement("summary");
summary.textContent = "Developer Mode";
details.append(summary);
const content = document.createElement("div");
content.className = "developer-mode-content";
content.append(createUIForSuites());
const settings = document.createElement("div");
settings.className = "settings";
settings.append(createUIForIterationCount());
settings.append(createUIForMeasurementMethod());
settings.append(createUIForWarmupSuite());
settings.append(createUIForWarmupBeforeSync());
settings.append(createUIForSyncStepDelay());
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 createUIForMeasurementMethod() {
let check = document.createElement("input");
check.type = "checkbox";
check.id = "measurement-method";
check.checked = params.measurementMethod === "raf";
check.onchange = () => {
params.measurementMethod = check.checked ? "raf" : "timer";
updateURL();
};
let label = document.createElement("label");
label.append(check, " ", span("rAF timing"));
return label;
}
function createUIForWarmupSuite() {
let check = document.createElement("input");
check.type = "checkbox";
check.id = "warmup-suite";
check.checked = !!params.useWarmupSuite;
check.onchange = () => {
params.useWarmupSuite = check.checked;
updateURL();
};
let label = document.createElement("label");
label.append(check, " ", span("Use Warmup Suite"));
return label;
}
function createUIForIterationCount() {
const { range, label } = createTimeRangeUI("Iterations: ", params.iterationCount, "#", 1, 200);
range.onchange = () => {
params.iterationCount = parseInt(range.value);
updateURL();
};
return label;
}
function createUIForWarmupBeforeSync() {
const { range, label } = createTimeRangeUI("Warmup time: ", params.warmupBeforeSync);
range.onchange = () => {
params.warmupBeforeSync = parseInt(range.value);
updateURL();
};
return label;
}
function createUIForSyncStepDelay() {
const { range, label } = createTimeRangeUI("Sync step delay: ", params.waitBeforeSync);
range.onchange = () => {
params.waitBeforeSync = parseInt(range.value);
updateURL();
};
return label;
}
function createTimeRangeUI(labelText, initialValue, unit = "ms", min = 0, max = 1000) {
const range = document.createElement("input");
range.type = "range";
range.min = min;
range.max = max;
range.value = initialValue;
const rangeValueAndUnit = document.createElement("span");
rangeValueAndUnit.className = "range-label-data";
const rangeValue = document.createElement("span");
rangeValue.textContent = initialValue;
rangeValueAndUnit.append(rangeValue, " ", unit);
const label = document.createElement("label");
label.append(span(labelText), range, rangeValueAndUnit);
range.oninput = () => {
rangeValue.textContent = range.value;
};
return { range, label };
}
function createUIForSuites() {
const control = document.createElement("nav");
control.className = "suites";
const ol = document.createElement("ol");
const checkboxes = [];
const setSuiteEnabled = (suiteIndex, enabled) => {
Suites[suiteIndex].disabled = !enabled;
checkboxes[suiteIndex].checked = enabled;
};
for (const suite of Suites) {
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.id = suite.name;
checkbox.type = "checkbox";
checkbox.checked = !suite.disabled;
checkbox.onchange = () => {
suite.disabled = !checkbox.checked;
updateURL();
};
checkboxes.push(checkbox);
const label = document.createElement("label");
label.append(checkbox, " ", suite.name);
li.appendChild(label);
label.onclick = (event) => {
if (event?.ctrlKey || event?.metaKey) {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
if (Suites[suiteIndex] !== suite)
setSuiteEnabled(suiteIndex, false);
else
setSuiteEnabled(suiteIndex, true);
}
}
};
ol.appendChild(li);
}
control.appendChild(ol);
let buttons = control.appendChild(document.createElement("div"));
buttons.className = "button-bar";
let button = document.createElement("button");
button.textContent = "Select all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, true);
updateURL();
};
buttons.appendChild(button);
button = document.createElement("button");
button.textContent = "Unselect all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, false);
updateURL();
};
buttons.appendChild(button);
let i = 0;
const kTagsPerLine = 3;
for (const tag of Tags) {
if (tag === "all")
continue;
if (!(i % kTagsPerLine)) {
buttons = control.appendChild(document.createElement("div"));
buttons.className = "button-bar";
}
i++;
button = document.createElement("button");
button.className = "tag";
button.textContent = `#${tag}`;
button.dataTag = tag;
button.onclick = (event) => {
const extendSelection = event?.shiftKey;
const invertSelection = event?.ctrlKey || event?.metaKey;
const selectedTag = event.target.dataTag;
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
let enabled = Suites[suiteIndex].tags.includes(selectedTag);
if (invertSelection)
enabled = !enabled;
if (extendSelection && !enabled)
continue;
setSuiteEnabled(suiteIndex, enabled);
}
updateURL();
};
buttons.appendChild(button);
}
return control;
}
function createUIForRun() {
let button = document.createElement("button");
button.textContent = "Start Test";
button.onclick = (event) => {
globalThis.benchmarkClient.start();
};
let buttons = document.createElement("div");
buttons.className = "button-bar";
buttons.appendChild(button);
return buttons;
}
function updateURL() {
const url = new URL(window.location.href);
// If less than all suites are selected then change the URL "Suites" GET parameter
// to comma separate only the selected
const selectedSuites = Suites.filter((suite) => !suite.disabled);
if (!selectedSuites.length) {
url.searchParams.delete("tags");
url.searchParams.delete("suites");
url.searchParams.delete("suite");
} else {
url.searchParams.delete("tags");
url.searchParams.delete("suite");
// Try finding common tags that would result in the current suite selection.
let commonTags = new Set(selectedSuites[0].tags);
for (const suite of Suites) {
if (suite.disabled)
suite.tags.forEach((tag) => commonTags.delete(tag));
else
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
}
if (commonTags.size) {
const tags = [...commonTags][0];
if (tags === "default")
url.searchParams.delete("tags");
else
url.searchParams.set("tags", tags);
url.searchParams.delete("suites");
} else {
url.searchParams.delete("tags");
url.searchParams.set("suites", selectedSuites.map((suite) => suite.name).join(","));
}
}
if (params.measurementMethod !== "raf")
url.searchParams.set("measurementMethod", "timer");
else
url.searchParams.delete("measurementMethod");
const boolParamKeys = ["iterationCount", "useWarmupSuite", "warmupBeforeSync", "waitBeforeSync"];
for (const paramKey of boolParamKeys) {
if (params[paramKey] !== defaultParams[paramKey])
url.searchParams.set(paramKey, params[paramKey]);
else
url.searchParams.delete(paramKey);
}
// Only push state if changed
url.search = decodeURIComponent(url.search);
if (url.href !== window.location.href)
window.history.pushState({}, "", url);
}

26
resources/editors/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,27 @@
## Description
Lots of people edit text content in the browser. Lots of that content, like WYSIWYG content or code, is too rich or complex to represent well with a `<textarea>`. Sites typically rely on advanced editor libraries for this, and we should make sure browsers perform well at common patterns used by them.
## Screenshot
![screenshot](./screenshot.jpg)
## What are we testing
- Basic DOM and editing
- Virtualization (DOM content changing during scroll)
- Basic flex layout with SVG icons
## How are we testing
The test simulates a real-world user flow by loading a number of popular editor libraries. After the initial load is complete, the following steps are timed:
- Setting to a fairly large value
- "Formatting" the text - in code editors this means turning on syntax highlighting, and in WYSISWYG this means bolding all of the contents
- Scrolling to the bottom of the editor
## Developer Documentation
The app was created with `npm create vite@latest editors`, and can be previewed with `npm run dev`. In order to update the files run in the harness you have to use `npm run build` which will recreate the `dist/` directory.
The built test can be loaded within the harness using i.e. http://localhost:7000/?suite=Editor-CodeMirror&startAutomatically=true, or the static versions at http://localhost:7000/resources/editors/dist/.

View file

@ -0,0 +1,2 @@
longtext.html is "Du côté de chez Swann" by Marcel Proust, downloaded from https://www.gutenberg.org/ebooks/2650
longscript.js has been downloaded from https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>CodeMirror Test</title>
<script>
window.requestIdleCallback = undefined;
window.cancelIdleCallback = undefined;
</script>
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
<script type="module">
import { code as shorttext } from "./shorttext.js";
import { code as longtext } from "./longtext.js";
import editor from "./codemirror.js";
let editorContainer = document.querySelector("#editor");
let editorInstance = null;
let buttons = {
create: document.querySelector("#create"),
highlight: document.querySelector("#highlight"),
unhighlight: document.querySelector("#unhighlight"),
long: document.querySelector("#long"),
short: document.querySelector("#short"),
scroll: document.querySelector("#scroll"),
layout: document.querySelector("#layout"),
};
buttons.scroll.addEventListener("click", scroll);
buttons.highlight.addEventListener("click", highlight);
buttons.unhighlight.addEventListener("click", unhighlight);
buttons.long.addEventListener("click", long);
buttons.short.addEventListener("click", short);
buttons.layout.addEventListener("click", layout);
buttons.create.addEventListener("click", (e) => {
if (!editorInstance) {
editorInstance = editor(editorContainer);
editorInstance.ready.then(() => {
buttons.unhighlight.classList.add("active", "true");
buttons.create.setAttribute("disabled", "true");
});
}
});
function layout() {
// Todo - is this necessary with the runner?
const body = document.body.getBoundingClientRect();
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
}
function highlight() {
buttons.unhighlight.classList.toggle("active", false);
buttons.highlight.classList.toggle("active", true);
editorInstance.format(true);
}
function unhighlight() {
buttons.unhighlight.classList.toggle("active", true);
buttons.highlight.classList.toggle("active", false);
editorInstance.format(false);
}
function long() {
buttons.short.classList.toggle("active", false);
buttons.long.classList.toggle("active", true);
editorInstance.setValue(longtext);
}
function short() {
buttons.short.classList.toggle("active", true);
buttons.long.classList.toggle("active", false);
editorInstance.setValue(shorttext);
}
function scroll() {
let isTop = editorInstance.getScrollTop() == 0;
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
}
</script>
</body>
</html>

View file

@ -0,0 +1,47 @@
// https://codemirror.net/examples/bundle/
import { EditorView, basicSetup } from "codemirror";
import { StateEffect } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";
let lang = javascript();
let extensions = [basicSetup, EditorView.lineWrapping];
export default function (element, value) {
let view = new EditorView({
extensions,
parent: element,
doc: value,
wordWrapColumn: 80,
});
return {
editor: view,
// Anything before this promise resolves will happen before timing starts
ready: Promise.resolve(),
getScrollHeight() {
return element.scrollHeight;
},
getScrollTop() {
return element.scrollTop;
},
setScrollTop(value) {
element.scrollTop = value;
},
setValue: (value) =>
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value },
}),
format(on) {
// https://codemirror.net/examples/config/
// https://discuss.codemirror.net/t/cm6-dynamically-switching-syntax-theme-w-reconfigure/2858/6
if (on && extensions.length === 2)
extensions.push(lang);
else if (!on && extensions.length === 3)
extensions.pop();
view.dispatch({
effects: StateEffect.reconfigure.of(extensions),
});
},
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,66 @@
:root {
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
line-height: 1.5;
font-weight: 400;
font-size: 13px;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
text-size-adjust: none;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #747bff;
}
body {
margin: 0;
display: flex;
min-width: 320px;
height: 100vh;
padding: 5px;
box-sizing: border-box;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
flex: 1;
display: flex;
flex-direction: column;
}
#editor {
flex: 1;
overflow: auto;
padding: 2px 5px;
}
#controls {
display: flex;
padding-bottom: 5px;
}
#controls button {
display: flex;
align-items: center;
margin: 0 2px;
padding: 2px;
}
#controls button.active {
color: blue;
}

View file

@ -0,0 +1,129 @@
const style = "";
const code$1 = `
function x() {
console.log("Hello world!");
}
`;
const text = `
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
`;
var base = {
8: "Backspace",
9: "Tab",
10: "Enter",
12: "NumLock",
13: "Enter",
16: "Shift",
17: "Control",
18: "Alt",
20: "CapsLock",
27: "Escape",
32: " ",
33: "PageUp",
34: "PageDown",
35: "End",
36: "Home",
37: "ArrowLeft",
38: "ArrowUp",
39: "ArrowRight",
40: "ArrowDown",
44: "PrintScreen",
45: "Insert",
46: "Delete",
59: ";",
61: "=",
91: "Meta",
92: "Meta",
106: "*",
107: "+",
108: ",",
109: "-",
110: ".",
111: "/",
144: "NumLock",
145: "ScrollLock",
160: "Shift",
161: "Shift",
162: "Control",
163: "Control",
164: "Alt",
165: "Alt",
173: "-",
186: ";",
187: "=",
188: ",",
189: "-",
190: ".",
191: "/",
192: "`",
219: "[",
220: "\\",
221: "]",
222: "'"
};
var shift = {
48: ")",
49: "!",
50: "@",
51: "#",
52: "$",
53: "%",
54: "^",
55: "&",
56: "*",
57: "(",
59: ":",
61: "+",
173: "_",
186: ":",
187: "+",
188: "<",
189: "_",
190: ">",
191: "?",
192: "~",
219: "{",
220: "|",
221: "}",
222: '"'
};
var chrome = typeof navigator != "undefined" && /Chrome\/(\d+)/.exec(navigator.userAgent);
var mac = typeof navigator != "undefined" && /Mac/.test(navigator.platform);
var ie = typeof navigator != "undefined" && /MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);
var brokenModifierNames = mac || chrome && +chrome[1] < 57;
for (var i = 0; i < 10; i++)
base[48 + i] = base[96 + i] = String(i);
for (var i = 1; i <= 24; i++)
base[i + 111] = "F" + i;
for (var i = 65; i <= 90; i++) {
base[i] = String.fromCharCode(i + 32);
shift[i] = String.fromCharCode(i);
}
for (var code in base)
if (!shift.hasOwnProperty(code))
shift[code] = base[code];
function keyName(event) {
var ignoreKey = brokenModifierNames && (event.ctrlKey || event.altKey || event.metaKey) || ie && event.shiftKey && event.key && event.key.length == 1 || event.key == "Unidentified";
var name = !ignoreKey && event.key || (event.shiftKey ? shift : base)[event.keyCode] || event.key || "Unidentified";
if (name == "Esc")
name = "Escape";
if (name == "Del")
name = "Delete";
if (name == "Left")
name = "ArrowLeft";
if (name == "Up")
name = "ArrowUp";
if (name == "Right")
name = "ArrowRight";
if (name == "Down")
name = "ArrowDown";
return name;
}
export {
base as b,
code$1 as c,
keyName as k,
shift as s,
text as t
};
//# sourceMappingURL=index.es-02a92ebc.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"main-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

144
resources/editors/dist/codemirror.html vendored Normal file
View file

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeMirror Test</title>
<script>
window.requestIdleCallback = undefined;
window.cancelIdleCallback = undefined;
</script>
<script type="module" crossorigin src="./assets/codemirror-521de7ab.js"></script>
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
<link rel="stylesheet" href="./assets/index-2feebe42.css">
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
</body>
</html>

32
resources/editors/dist/index.html vendored Normal file
View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Code Editors</strong></summary>
<iframe src="./codemirror.html"></iframe>
</details>
<details open>
<summary><strong>WYSIWYG Editors</strong></summary>
<iframe src="./tiptap.html"></iframe>
</details>
</div>
</body>
</html>

134
resources/editors/dist/tiptap.html vendored Normal file
View file

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TipTap Test</title>
<script type="module" crossorigin src="./assets/tiptap-95a40ba8.js"></script>
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
<link rel="stylesheet" href="./assets/index-2feebe42.css">
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
</body>
</html>

1
resources/editors/dist/vite.svg vendored Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Code Editors</strong></summary>
<iframe src="./codemirror.html"></iframe>
</details>
<details open>
<summary><strong>WYSIWYG Editors</strong></summary>
<iframe src="./tiptap.html"></iframe>
</details>
</div>
</body>
</html>

View file

@ -0,0 +1,2 @@
export { default as code } from "./assets/longscript.js?raw";
export { default as text } from "./assets/longtext.html?raw";

2615
resources/editors/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
{
"name": "editors",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@codemirror/lang-javascript": "^6.1.4",
"@tiptap/core": "^2.0.0-beta.217",
"@tiptap/starter-kit": "^2.0.0-beta.217",
"codemirror": "^6.0.1",
"vite": "^4.1.0"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,9 @@
export const code = `
function x() {
console.log("Hello world!");
}
`;
export const text = `
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
`;

View file

@ -0,0 +1,66 @@
:root {
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
line-height: 1.5;
font-weight: 400;
font-size: 13px;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
text-size-adjust: none;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #747bff;
}
body {
margin: 0;
display: flex;
min-width: 320px;
height: 100vh;
padding: 5px;
box-sizing: border-box;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
flex: 1;
display: flex;
flex-direction: column;
}
#editor {
flex: 1;
overflow: auto;
padding: 2px 5px;
}
#controls {
display: flex;
padding-bottom: 5px;
}
#controls button {
display: flex;
align-items: center;
margin: 0 2px;
padding: 2px;
}
#controls button.active {
color: blue;
}

View file

@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>TipTap Test</title>
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
<script type="module">
import { text as shorttext } from "./shorttext.js";
import { text as longtext } from "./longtext.js";
import editor from "./tiptap.js";
let editorContainer = document.querySelector("#editor");
let editorInstance = null;
let buttons = {
create: document.querySelector("#create"),
highlight: document.querySelector("#highlight"),
unhighlight: document.querySelector("#unhighlight"),
long: document.querySelector("#long"),
short: document.querySelector("#short"),
scroll: document.querySelector("#scroll"),
layout: document.querySelector("#layout"),
};
buttons.scroll.addEventListener("click", scroll);
buttons.highlight.addEventListener("click", highlight);
buttons.unhighlight.addEventListener("click", unhighlight);
buttons.long.addEventListener("click", long);
buttons.short.addEventListener("click", short);
buttons.layout.addEventListener("click", layout);
buttons.create.addEventListener("click", (e) => {
if (!editorInstance) {
editorInstance = editor(editorContainer);
editorInstance.ready.then(() => {
buttons.unhighlight.classList.add("active", "true");
buttons.create.setAttribute("disabled", "true");
});
}
});
function layout() {
// Todo - is this necessary with the runner?
const body = document.body.getBoundingClientRect();
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
}
function highlight() {
buttons.unhighlight.classList.toggle("active", false);
buttons.highlight.classList.toggle("active", true);
editorInstance.format(true);
}
function unhighlight() {
buttons.unhighlight.classList.toggle("active", true);
buttons.highlight.classList.toggle("active", false);
editorInstance.format(false);
}
function long() {
buttons.short.classList.toggle("active", false);
buttons.long.classList.toggle("active", true);
editorInstance.setValue(longtext);
}
function short() {
buttons.short.classList.toggle("active", true);
buttons.long.classList.toggle("active", false);
editorInstance.setValue(shorttext);
}
function scroll() {
let isTop = editorInstance.getScrollTop() == 0;
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
}
</script>
</body>
</html>

View file

@ -0,0 +1,42 @@
// https://tiptap.dev/examples/default
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
export default function (element, value) {
let editor = new Editor({
element,
extensions: [StarterKit],
content: value,
editorProps: {
attributes: {
spellcheck: "false",
},
},
});
return {
editor,
// Anything before this promise resolves will happen before timing starts
ready: Promise.resolve(),
getScrollHeight() {
return element.scrollHeight;
},
getScrollTop() {
return element.scrollTop;
},
setScrollTop(value) {
element.scrollTop = value;
},
setValue(value) {
// Recommendation is to call focus before most commands
// https://tiptap.dev/api/commands#chain-commands
editor.chain().focus().setContent(value).setTextSelection(0).run();
element.scrollTop = 0;
},
format(on) {
if (on)
editor.chain().focus().selectAll().setBold().setTextSelection(0).run();
else
editor.chain().focus().selectAll().unsetBold().setTextSelection(0).run();
},
};
}

View file

@ -0,0 +1,17 @@
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
base: "./", // Since this will be loaded from the project root
build: {
modulePreload: { polyfill: false },
minify: false,
sourcemap: true,
rollupOptions: {
input: {
codemirror: resolve(__dirname, "codemirror.html"),
main: resolve(__dirname, "index.html"),
tiptap: resolve(__dirname, "tiptap.html"),
},
},
},
});

BIN
resources/favicon.png Normal file

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

213
resources/interactive.mjs Normal file
View file

@ -0,0 +1,213 @@
import { BenchmarkRunner } from "./benchmark-runner.mjs";
import { params } from "./params.mjs";
import { Suites } from "./tests.mjs";
class InteractiveBenchmarkRunner extends BenchmarkRunner {
_stepPromise = undefined;
_stepPromiseResolve = undefined;
_isRunning = false;
_isStepping = false;
constructor(suites, iterationCount) {
super(suites);
this._client = this._createClient();
if (!Number.isInteger(iterationCount) || iterationCount <= 0)
throw Error("iterationCount must be a positive integer.");
this._iterationCount = iterationCount;
}
_createClient() {
return {
willStartFirstIteration: this._start.bind(this),
willRunTest: this._testStart.bind(this),
didRunTest: this._testDone.bind(this),
didRunSuites: this._iterationDone.bind(this),
didFinishLastIteration: this._done.bind(this),
};
}
_start() {
if (this._isRunning)
throw Error("Runner was not stopped before starting;");
this._isRunning = true;
if (this._isStepping)
this._stepPromise = this._newStepPromise();
}
_step() {
if (!this._stepPromise) {
// Allow switching to stepping mid-run.
this._stepPromise = this._newStepPromise();
} else {
const resolve = this._stepPromiseResolve;
this._stepPromise = this._newStepPromise();
resolve();
}
}
_newStepPromise() {
return new Promise((resolve) => {
this._stepPromiseResolve = resolve;
});
}
_testStart(suite, test) {
test.anchor.classList.add("running");
}
async _testDone(suite, test) {
const classList = test.anchor.classList;
classList.remove("running");
classList.add("ran");
if (this._isStepping)
await this._stepPromise;
}
_iterationDone(measuredValues) {
let results = "";
for (const suiteName in measuredValues.tests) {
let suiteResults = measuredValues.tests[suiteName];
for (const testName in suiteResults.tests) {
let testResults = suiteResults.tests[testName];
for (const subtestName in testResults.tests)
results += `${suiteName} : ${testName} : ${subtestName}: ${testResults.tests[subtestName]} ms\n`;
}
results += `${suiteName} : ${suiteResults.total} ms\n`;
}
results += `Arithmetic Mean : ${measuredValues.mean} ms\n`;
results += `Geometric Mean : ${measuredValues.geomean} ms\n`;
results += `Total : ${measuredValues.total} ms\n`;
results += `Score : ${measuredValues.score} rpm\n`;
if (!results)
return;
const pre = document.createElement("pre");
document.body.appendChild(pre);
pre.textContent = results;
}
_done() {
this.isRunning = false;
}
runStep() {
this._isStepping = true;
if (!this._isRunning)
this.runMultipleIterations(this._iterationCount);
else
this._step();
}
runSuites() {
if (this._isRunning) {
if (this._isStepping) {
// Switch to continuous running only if we've been stepping.
this._isStepping = false;
this._step();
}
} else {
this._isStepping = false;
this.runMultipleIterations(this._iterationCount);
}
}
}
// Expose Suites/BenchmarkRunner for backwards compatibility
globalThis.BenchmarkRunner = InteractiveBenchmarkRunner;
function formatTestName(suiteName, testName) {
return suiteName + (testName ? `/${testName}` : "");
}
function createUIForSuites(suites, onStep, onRunSuites) {
const control = document.createElement("nav");
const ol = document.createElement("ol");
const checkboxes = [];
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
const suite = suites[suiteIndex];
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.id = suite.name;
checkbox.type = "checkbox";
checkbox.checked = !suite.disabled;
checkbox.onchange = () => {
suite.disabled = !checkbox.checked;
};
checkbox.onchange();
checkboxes.push(checkbox);
li.appendChild(checkbox);
var label = document.createElement("label");
label.appendChild(document.createTextNode(formatTestName(suite.name)));
li.appendChild(label);
label.htmlFor = checkbox.id;
const testList = document.createElement("ol");
for (let testIndex = 0; testIndex < suite.tests.length; testIndex++) {
const testItem = document.createElement("li");
const test = suite.tests[testIndex];
const anchor = document.createElement("a");
anchor.id = `${suite.name}-${test.name}`;
test.anchor = anchor;
anchor.appendChild(document.createTextNode(formatTestName(suite.name, test.name)));
testItem.appendChild(anchor);
testList.appendChild(testItem);
}
li.appendChild(testList);
ol.appendChild(li);
}
control.appendChild(ol);
let button = document.createElement("button");
button.textContent = "Step";
button.onclick = onStep;
control.appendChild(button);
button = document.createElement("button");
button.textContent = "Run";
button.id = "runSuites";
button.onclick = onRunSuites;
control.appendChild(button);
button = document.createElement("button");
button.textContent = "Select all";
button.onclick = () => {
for (var suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
suites[suiteIndex].disabled = false;
checkboxes[suiteIndex].checked = true;
}
};
control.appendChild(button);
button = document.createElement("button");
button.textContent = "Unselect all";
button.onclick = () => {
for (var suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
suites[suiteIndex].disabled = true;
checkboxes[suiteIndex].checked = false;
}
};
control.appendChild(button);
return control;
}
function startTest() {
if (params.suites.length > 0 || params.tags.length > 0)
Suites.enable(params.suites, params.tags);
const interactiveRunner = new window.BenchmarkRunner(Suites, params.iterationCount);
if (!(interactiveRunner instanceof InteractiveBenchmarkRunner))
throw Error("window.BenchmarkRunner must be a subclass of InteractiveBenchmarkRunner");
// Don't call step while step is already executing.
document.body.appendChild(createUIForSuites(Suites, interactiveRunner.runStep.bind(interactiveRunner), interactiveRunner.runSuites.bind(interactiveRunner)));
if (params.startAutomatically)
document.getElementById("runSuites").click();
}
window.addEventListener("load", startTest);

BIN
resources/logo.png Normal file

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

815
resources/main.css Normal file
View file

@ -0,0 +1,815 @@
:root {
--viewport-width: 800px;
--viewport-height: 600px;
--foreground: rgb(235, 235, 235);
--foreground-alpha: rgba(235, 235, 235, 0.2);
--inactive-color: rgb(128, 128, 128);
--background: rgb(46, 51, 55);
--running-background: #f5f5f5;
--highlight: rgb(232, 79, 79);
--text-width: 650px;
--metrics-line-height: 25px;
--scrollbar-width: 10px;
}
body {
background-color: var(--background);
color: var(--foreground);
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
}
::selection {
color: var(--background);
background-color: var(--foreground);
}
::-webkit-scrollbar,
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
background-color: var(--background);
}
::-webkit-scrollbar,
::-webkit-scrollbar-track {
background-color: var(--inactive-color);
border-radius: 8px;
width: var(--scrollbar-width);
height: var(--scrollbar-width);
}
::-webkit-scrollbar-thumb {
background-color: var(--foreground);
border-radius: 8px;
cursor: pointer;
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--foreground);
}
thead {
vertical-align: bottom;
}
h1,
button,
.button {
font-family: "Futura-Medium", Futura, "Helvetica Neue", Helvetica, Verdana, sans-serif;
}
.button {
text-decoration: none;
}
code {
font-family: Menlo, Monaco, monospace;
font-size: smaller;
}
hr {
border: 1px solid var(--foreground);
width: 50%;
margin: 40px auto;
}
img {
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
}
.no-select {
user-select: none;
}
main {
}
.logo {
position: absolute;
left: -70px;
top: 155px;
width: 75px;
height: 406px;
cursor: pointer;
text-decoration: none;
}
.version {
transform: rotate(-90deg);
font-size: 65px;
font-family: Futura-Medium, Futura, Roboto, "Helvetica Neue", Helvetica, Verdana, sans-serif;
font-style: italic;
letter-spacing: -0.05em;
position: absolute;
right: 0;
top: -105px;
}
h1 {
margin-top: 30px;
font-size: 40px;
font-weight: normal;
color: var(--foreground);
text-align: center;
}
h2 {
font-weight: normal;
text-align: center;
color: var(--foreground);
}
h3 {
margin: 10px 0 6px 0;
font-weight: normal;
text-align: center;
}
p {
font-size: 16px;
line-height: 21px;
}
a {
color: inherit;
}
li {
font-size: 16px;
line-height: 21px;
}
li + li {
margin-top: 21px;
}
li + ul > li {
margin-top: 0;
}
.compact li {
margin-top: 5px;
}
li + ul {
margin-bottom: 21px;
}
.buttons {
margin-top: 30px;
display: flex;
flex-direction: column;
}
section#home > .buttons {
margin-top: 80px;
}
.button-row {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
margin: 5px 0px;
}
button,
.button {
appearance: none;
border: none;
background-color: transparent;
cursor: pointer;
margin: 0px;
padding: 0px;
text-align: center;
}
.button {
box-sizing: border-box;
text-decoration: none;
}
.buttons button,
.buttons .button {
appearance: none;
border: 3px solid var(--foreground);
border-radius: 10px;
min-width: 200px;
padding: 5px 0px;
margin: 0 20px;
font-size: 25px;
color: var(--foreground);
background-color: transparent;
cursor: pointer;
user-select: none;
}
.buttons button:active {
background-color: var(--foreground);
color: var(--background);
border-color: var(--foreground) !important;
}
.buttons button:focus {
outline: none;
border-color: var(--highlight);
}
.developer-mode {
border-radius: 10px;
padding: 1rem;
background: #602525;
border: 3px solid rgba(255, 255, 255, 0.5);
position: fixed;
left: 10px;
top: 10px;
display: flex;
flex-direction: column;
}
.developer-mode summary {
user-select: none;
cursor: pointer;
padding: 1rem;
margin: -1rem;
}
.developer-mode-content {
flex: 1;
max-height: 80vh;
overflow: auto;
margin-right: calc(0px - var(--scrollbar-width));
padding-right: var(--scrollbar-width);
}
.developer-mode-content ol {
list-style: none;
padding: 0;
margin: 1em 0 0.5em 0;
}
.developer-mode-content .button-bar {
display: flex;
margin-top: 5px;
gap: 3px;
}
.developer-mode-content .settings label {
width: 100%;
display: flex;
}
.developer-mode-content .settings label * {
flex: 1;
}
.developer-mode-content .settings input[type="checkbox"] {
flex: 0;
margin-left: 0px;
}
.developer-mode-content .settings .range-label-data {
flex: 0;
min-width: 5em;
text-align: right;
}
.developer-mode-content li + li {
margin-top: 0px;
}
.developer-mode-content button {
background: white;
flex: auto;
padding: 4px;
appearance: button;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 10px;
}
.developer-mode-content .tag {
border-radius: 100vh;
color: white;
background: rgba(255, 255, 255, 0.1);
}
.developer-mode-content hr {
width: initial;
margin: 10px 0;
}
section {
display: none;
--padding-width: 15px;
--border-width: 6px;
position: absolute;
width: var(--viewport-width);
height: var(--viewport-height);
top: 50%;
left: 50%;
margin-top: calc(var(--viewport-height) / -2 - var(--padding-width) - var(--border-width));
margin-left: calc(var(--viewport-width) / -2 - var(--padding-width) - var(--border-width));
padding: var(--padding-width);
border: var(--border-width) solid var(--foreground);
border-radius: 20px;
}
section.visible,
:root[data-visible-section="home"] #home,
:root[data-visible-section="running"] #running,
:root[data-visible-section="summary"] #summary,
:root[data-visible-section="details"] #details {
display: block;
}
section > p {
margin: 10px 20px;
}
#testContainer {
position: absolute;
top: 15px;
left: 15px;
width: var(--viewport-width);
height: var(--viewport-height);
}
section#home p {
margin: 0 auto;
width: 70%;
text-align: center;
}
section#home .content {
margin-top: 160px;
text-align: center;
}
button.show-about {
margin-top: 100px;
font-size: 16px;
clear: both;
}
#screen-size-warning {
display: none;
}
#progress {
position: absolute;
bottom: -6px;
left: 60px;
right: 60px;
height: 6px;
border-left: 6px solid var(--background);
border-right: 6px solid var(--background);
}
#progress-completed {
position: absolute;
top: 0;
left: 0;
height: 6px;
width: 100%;
appearance: none;
border: none;
background-color: var(--inactive-color);
}
#progress-completed::-webkit-progress-value {
background-color: var(--foreground);
}
#progress-completed::-moz-progress-bar {
background-color: var(--foreground);
}
#progress .iteration-marker {
position: absolute;
width: 6px;
height: 100%;
background-color: var(--background);
}
#info {
position: absolute;
bottom: -25px;
left: 60px;
right: 60px;
height: 12px;
color: var(--inactive-color);
text-align: center;
font-size: 12px;
}
#info-label {
position: absolute;
left: 6px;
}
#info-progress {
position: absolute;
right: 6px;
text-align: right;
}
iframe.test-runner {
background: var(--running-background);
}
section#summary > #result-number,
section#summary > #confidence-number {
font-family: "Futura-CondensedMedium", Futura, "Helvetica Neue", Helvetica, Verdana, sans-serif;
}
section#summary > #result-number {
text-align: center;
font-size: 145px;
line-height: 145px;
}
section#summary > #confidence-number {
text-align: center;
font-size: 36px;
line-height: 36px;
color: var(--inactive-color);
}
section#details {
--viewport-height: max(600px, 90vh);
--viewport-width: max(800px, 80vw);
}
section#details .results-table {
float: left;
width: 50%;
}
section#details .export-buttons {
justify-content: center;
margin-bottom: 0;
}
section#details .export-buttons button,
section#details .export-buttons .button {
padding: 5px 10px;
min-width: auto;
font-size: 14px;
margin: 0 3px;
}
section#details .arithmetic-mean {
clear: both;
padding-top: 32px;
text-align: center;
}
section#details .arithmetic-mean > label {
font-weight: bold;
margin-right: 10px;
}
section#details button.show-about {
margin-top: 30px;
}
section#details h1 {
margin-bottom: 10px;
}
section#about .section-content > *,
section#instructions .section-content > * {
width: var(--text-width);
padding-left: calc((var(--viewport-width) - var(--text-width)) / 2);
}
:root[data-visible-section="details"] section#details {
display: flex;
flex-direction: column;
}
section#details .all-metric-results {
flex: auto;
overflow-y: auto;
padding: 0px 10px;
}
section#details .arithmetic-mean {
clear: both;
padding-top: 32px;
text-align: center;
}
section#details .arithmetic-mean > label {
font-weight: bold;
margin-right: 10px;
}
section#details h1 {
margin-bottom: 10px;
}
section#details .metric {
margin: 0px 0 10px 0;
display: inline-block;
width: 100%;
}
section#details .metric dd {
margin-left: 0;
}
.submetrics h3 {
font-size: medium;
}
.metrics-table thead .prefix {
text-align: center;
}
.metrics-table thead th {
font-weight: normal;
text-align: left;
border-bottom: 1px var(--foreground) solid;
padding: 0 4px 3px 0px;
}
.metrics-table thead th + th {
padding: 0 4px 3px 4px;
}
.nowrap {
white-space: nowrap;
}
table .number {
text-align: right;
}
.chart-legend {
display: inline;
padding-left: 10px;
vertical-align: top;
margin-top: 0;
line-height: var(--metrics-line-height);
border-collapse: collapse;
}
.chart-legend td {
padding: 0 5px 0 0;
max-width: calc(var(--viewport-width) * 0.22);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
}
section#details h1 {
margin-bottom: 10px;
}
section#about h1,
section#instructions h1 {
margin-top: 10px;
margin-bottom: 0px;
font-size: 25px;
}
section#about .note {
color: var(--inactive-color);
}
.section-grid {
display: grid;
width: 100%;
height: 100%;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header"
"content"
"footer";
}
.section-header {
grid-area: header;
}
.section-content {
grid-area: content;
overflow-y: auto;
}
.section-footer {
grid-area: footer;
}
.gauge {
position: relative;
width: 738px;
height: 78px;
background-image: -webkit-image-set(url("gauge@2x.png") 2x, url("gauge.png") 1x);
background-image: image-set(url("gauge@2x.png") 2x, url("gauge.png") 1x);
background-size: 100% 100%;
background-repeat: no-repeat;
margin: 0 auto;
}
.gauge > .window {
position: absolute;
left: 0;
top: 33px;
bottom: 0;
right: 0;
overflow: hidden;
}
.gauge > .window > .needle {
position: absolute;
left: 363px;
bottom: -88px;
width: 4px;
height: 400px;
background-color: rgb(247, 148, 29);
transform: rotate(-70deg);
transform-origin: 2px 400px;
}
.all-metric-results .submetrics {
display: none;
margin-left: 35px;
}
.all-metric-results .submetrics.visible {
display: block;
}
.metric-chart {
display: grid;
grid-template-columns: calc((var(--viewport-width) - 120px) / 2) 1fr;
}
.metric-chart-relative {
display: none;
}
.relative-charts .metric-chart-absolute {
display: none;
}
.relative-charts .metric-chart-relative {
display: block;
}
.details-toggle {
display: inline-block;
cursor: pointer;
user-select: none;
margin-left: -4px;
}
.details-toggle input {
display: inline-block;
appearance: none;
width: 1em;
height: 1em;
border: none;
background: none;
}
.details-toggle input:after {
content: "▶";
color: var(--foreground);
display: inline-block;
transform-origin: 50%;
position: relative;
transition: all 250ms ease;
}
.details-toggle input:checked:after {
transform: rotate(90deg);
}
/* Charts (lib/charts.js) */
.bar-chart text,
.scatter-plot text {
font-size: 11px;
fill: var(--foreground);
dominant-baseline: hanging;
}
.bar-chart .axis,
.scatter-plot .axis {
stroke: #aaa;
}
.bar-chart .minMax {
stroke: var(--foreground);
stroke-width: 1;
stroke-opacity: 0.5;
stroke-dasharray: 2;
}
.bar-chart .mean {
stroke: var(--foreground);
stroke-width: 2;
stroke-opacity: 0.5;
}
.bar-chart .mean:hover {
stroke-opacity: 1;
}
.bar-chart g.bar {
fill: #8ad;
}
.bar-chart g.bar.large text {
display: none;
}
.bar-chart g.bar:hover {
filter: brightness(1.1);
}
.bar-chart g.bar text {
fill: #ddd;
text-anchor: middle;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
}
.bar-chart g.bar:hover text {
display: block;
fill: #fff;
text-shadow: 0 0 5px #000;
}
.bar-chart .label {
text-anchor: end;
dominant-baseline: middle;
}
.scatter-plot .marker {
stroke-width: 0px;
opacity: 0.8;
fill: currentColor;
cursor: crosshair;
}
.scatter-plot .marker:hover {
opacity: 1;
filter: drop-shadow(0 0 2px currentColor);
}
.scatter-plot .percentile {
fill: currentColor;
fill-opacity: 0.3;
stroke-width: 0px;
}
.scatter-plot .percentile line {
stroke-width: 1px;
}
.scatter-plot .percentile:hover {
opacity: 1;
}
/* Chart colors */
.chart .blue {
color: #5b9aff;
stroke: #5b9aff;
}
.chart .blue-light {
color: #5bd6ff;
stroke: #5bd6ff;
}
.chart .green-light {
color: #73d147;
stroke: #73d147;
}
.chart .green {
color: #94bc4b;
stroke: #94bc4b;
}
.chart .yellow {
color: #ded300;
stroke: #ded300;
}
.chart .orange {
color: #fe8f06;
stroke: #fe8f06;
}
.chart .red {
color: #de4040;
stroke: #de4040;
}
.chart .magenta {
color: #cd1e90;
stroke: #cd1e90;
}
.chart .violet {
color: #a41ecd;
stroke: #a41ecd;
}
.chart .purple {
color: #7b3eff;
stroke: #7b3eff;
}
.chart .blue-dark {
color: #2d4ef7;
stroke: #2d4ef7;
}
.chart .green-dark {
color: #349a58;
stroke: #349a58;
}
.chart .ochre {
color: #cc9900;
stroke: #cc9900;
}
.chart .rust {
color: #cc5200;
stroke: #cc5200;
}
.chart .white {
color: #fff;
stroke: #fff;
}

383
resources/main.mjs Normal file
View file

@ -0,0 +1,383 @@
import { BenchmarkRunner } from "./benchmark-runner.mjs";
import * as Statistics from "./statistics.mjs";
import { Suites } from "./tests.mjs";
import { renderMetricView } from "./metric-ui.mjs";
import { params } from "./params.mjs";
import { createDeveloperModeContainer } from "./developer-mode.mjs";
// FIXME(camillobruni): Add base class
class MainBenchmarkClient {
developerMode = false;
stepCount = null;
suitesCount = null;
_measuredValuesList = [];
_finishedTestCount = 0;
_progressCompleted = null;
_isRunning = false;
_hasResults = false;
_developerModeContainer = null;
_metrics = Object.create(null);
constructor() {
window.addEventListener("DOMContentLoaded", () => this.prepareUI());
this._showSection(window.location.hash);
}
start() {
if (this._startBenchmark())
this._showSection("#running");
}
_startBenchmark() {
if (this._isRunning)
return false;
if (Suites.every((suite) => suite.disabled)) {
const message = `No suites selected - "${params.suites}" does not exist.`;
alert(message);
console.error(
message,
params.suites,
"\nValid values:",
Suites.map((each) => each.name)
);
return false;
}
this._developerModeContainer?.remove();
this._progressCompleted = document.getElementById("progress-completed");
if (params.iterationCount < 50) {
const progressNode = document.getElementById("progress");
for (let i = 1; i < params.iterationCount; i++) {
const iterationMarker = progressNode.appendChild(document.createElement("div"));
iterationMarker.className = "iteration-marker";
iterationMarker.style.left = `${(i / params.iterationCount) * 100}%`;
}
}
this._metrics = Object.create(null);
this._isRunning = true;
const enabledSuites = Suites.filter((suite) => !suite.disabled);
const totalSubtestsCount = enabledSuites.reduce((testsCount, suite) => {
return testsCount + suite.tests.length;
}, 0);
this.stepCount = params.iterationCount * totalSubtestsCount;
this._progressCompleted.max = this.stepCount;
this.suitesCount = enabledSuites.length;
const runner = new BenchmarkRunner(Suites, this);
runner.runMultipleIterations(params.iterationCount);
return true;
}
get metrics() {
return this._metrics;
}
willAddTestFrame(frame) {
frame.style.left = "50%";
frame.style.top = "50%";
frame.style.transform = "translate(-50%, -50%)";
}
willRunTest(suite, test) {
document.getElementById("info-label").textContent = suite.name;
document.getElementById("info-progress").textContent = `${this._finishedTestCount} / ${this.stepCount}`;
}
didRunTest() {
this._finishedTestCount++;
this._progressCompleted.value = this._finishedTestCount;
}
didRunSuites(measuredValues) {
this._measuredValuesList.push(measuredValues);
}
willStartFirstIteration() {
this._measuredValuesList = [];
this._finishedTestCount = 0;
}
didFinishLastIteration(metrics) {
console.assert(this._isRunning);
this._isRunning = false;
this._hasResults = true;
this._metrics = metrics;
const scoreResults = this._computeResults(this._measuredValuesList, "score");
this._updateGaugeNeedle(scoreResults.mean);
document.getElementById("result-number").textContent = scoreResults.formattedMean;
if (scoreResults.formattedDelta)
document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`;
this._populateDetailedResults(metrics);
if (params.developerMode)
this.showResultsDetails();
else
this.showResultsSummary();
}
_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) => {
return 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})` : ""),
};
}
_addDetailedResultsRow(table, iterationNumber, value) {
const row = document.createElement("tr");
const th = document.createElement("th");
th.textContent = `Iteration ${iterationNumber + 1}`;
const td = document.createElement("td");
td.textContent = value;
row.appendChild(th);
row.appendChild(td);
table.appendChild(row);
}
_updateGaugeNeedle(score) {
const needleAngle = Math.max(0, Math.min(score, 140)) - 70;
const needleRotationValue = `rotate(${needleAngle}deg)`;
const gaugeNeedleElement = document.querySelector("#summary > .gauge .needle");
gaugeNeedleElement.style.setProperty("-webkit-transform", needleRotationValue);
gaugeNeedleElement.style.setProperty("-moz-transform", needleRotationValue);
gaugeNeedleElement.style.setProperty("-ms-transform", needleRotationValue);
gaugeNeedleElement.style.setProperty("transform", needleRotationValue);
}
_populateDetailedResults(metrics) {
const trackHeight = 24;
document.documentElement.style.setProperty("--metrics-line-height", `${trackHeight}px`);
const plotWidth = (params.viewport.width - 120) / 2;
document.getElementById("geomean-chart").innerHTML = renderMetricView({
metrics: [metrics.Geomean],
width: plotWidth,
trackHeight,
renderChildren: false,
colors: ["white"],
});
const toplevelMetrics = Object.values(metrics).filter((each) => !each.parent && each.children.length > 0);
document.getElementById("tests-chart").innerHTML = renderMetricView({
metrics: toplevelMetrics,
width: plotWidth,
trackHeight,
renderChildren: false,
});
let html = "";
for (const metric of toplevelMetrics) {
html += renderMetricView({
metrics: metric.children,
width: plotWidth,
trackHeight,
title: metric.name,
});
}
document.getElementById("metrics-results").innerHTML = html;
const filePrefix = `speedometer-3-${new Date().toISOString()}`;
let jsonData = this._formattedJSONResult({ modern: false });
let jsonLink = document.getElementById("download-classic-json");
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
jsonLink.setAttribute("download", `${filePrefix}.json`);
jsonLink = document.getElementById("download-full-json");
jsonData = this._formattedJSONResult({ modern: true });
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
jsonLink.setAttribute("download", `${filePrefix}.json`);
const csvData = this._formattedCSVResult();
const csvLink = document.getElementById("download-csv");
csvLink.href = URL.createObjectURL(new Blob([csvData], { type: "text/csv" }));
csvLink.setAttribute("download", `${filePrefix}.csv`);
}
prepareUI() {
window.addEventListener("hashchange", this._hashChangeHandler.bind(this));
window.addEventListener("resize", this._resizeScreeHandler.bind(this));
this._resizeScreeHandler();
document.querySelectorAll("logo").forEach((button) => {
button.onclick = this._logoClickHandler.bind(this);
});
document.getElementById("copy-full-json").onclick = this.copyJsonResults.bind(this);
document.getElementById("copy-csv").onclick = this.copyCSVResults.bind(this);
document.querySelectorAll(".start-tests-button").forEach((button) => {
button.onclick = this._startBenchmarkHandler.bind(this);
});
if (params.suites.length > 0 || params.tags.length > 0)
Suites.enable(params.suites, params.tags);
if (params.developerMode) {
this._developerModeContainer = createDeveloperModeContainer(Suites);
document.body.append(this._developerModeContainer);
}
if (params.startAutomatically)
this.start();
}
_hashChangeHandler() {
this._showSection(window.location.hash);
}
_resizeScreeHandler() {
// FIXME: Detect when the window size changes during the test.
const mainSize = document.querySelector("main").getBoundingClientRect();
const screenIsTooSmall = window.innerWidth < mainSize.width || window.innerHeight < mainSize.height;
document.getElementById("min-screen-width").textContent = `${params.viewport.width + 50}px`;
document.getElementById("min-screen-height").textContent = `${params.viewport.height + 50}px`;
document.getElementById("screen-size").textContent = `${window.innerWidth}px by ${window.innerHeight}px`;
document.getElementById("screen-size-warning").style.display = screenIsTooSmall ? null : "none";
}
_startBenchmarkHandler() {
this.start();
}
_logoClickHandler(event) {
// Prevent any accidental UI changes during benchmark runs.
if (!this._isRunning)
this._showSection("#home");
event.preventDefault();
return false;
}
showResultsSummary() {
this._showSection("#summary");
}
showResultsDetails() {
this._showSection("#details");
}
_formattedJSONResult({ modern = false }) {
const indent = " ";
if (modern)
return JSON.stringify(this._metrics, undefined, indent);
return JSON.stringify(this._measuredValuesList, undefined, indent);
}
_formattedCSVResult() {
// The CSV format is similar to the details view table. Each measurement is a row with
// the name and N columns with the measurement for each iteration:
// ```
// Measurement,#1,...,#N
// TodoMVC-JavaScript-ES5/Total,num,...,num
// TodoMVC-JavaScript-ES5/Adding100Items,num,...,num
// ...
const labels = ["Name"];
for (let i = 0; i < params.iterationCount; i++)
labels.push(`#${i + 1}`);
labels.push("Mean");
const metrics = Array.from(Object.values(this._metrics)).filter((metric) => !metric.name.startsWith("Iteration-"));
const metricsValues = metrics.map((metric) => [metric.name, ...metric.values, metric.mean].join(","));
const csv = [labels.join(","), ...metricsValues];
return csv.join("\n");
}
copyJsonResults() {
navigator.clipboard.writeText(this._formattedJSONResult({ modern: true }));
}
copyCSVResults() {
navigator.clipboard.writeText(this._formattedCSVResult());
}
_showSection(hash) {
if (this._isRunning) {
this._setLocationHash("#running");
return;
} else if (this._hasResults) {
if (hash !== "#summary" && hash !== "#details") {
this._setLocationHash("#summary");
return;
}
} else if (hash !== "#home" && hash !== "") {
// Redirect invalid views to #home directly.
this._setLocationHash("#home");
return;
}
this._setLocationHash(hash);
}
_setLocationHash(hash) {
if (hash === "#home" || hash === "") {
if (window.location.hash !== hash)
window.location.hash = "#home";
hash = "#home";
this._removeLocationHash();
} else {
window.location.hash = hash;
}
this._updateVisibleSectionAttribute(hash);
this._updateDocumentTitle(hash);
}
_updateVisibleSectionAttribute(hash) {
const sectionId = hash.substring(1);
document.documentElement.setAttribute("data-visible-section", sectionId);
}
_updateDocumentTitle(hash) {
const maybeSection = document.querySelector(hash);
const sectionTitle = maybeSection?.getAttribute("data-title") ?? "";
document.title = `Speedometer 3 ${sectionTitle}`.trimEnd();
}
_removeLocationHash() {
const location = window.location;
window.history.pushState("", document.title, location.pathname + location.search);
}
}
const rootStyle = document.documentElement.style;
rootStyle.setProperty("--viewport-width", `${params.viewport.width}px`);
rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`);
globalThis.benchmarkClient = new MainBenchmarkClient();

327
resources/metric-ui.mjs Normal file
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>
`;
}
}
}

86
resources/metric.mjs Normal file
View file

@ -0,0 +1,86 @@
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.mean = 0.0;
this.geomean = 0.0;
this.delta = 0.0;
this.percentDelta = 0.0;
this.sum = 0.0;
this.min = 0.0;
this.max = 0.0;
this.values = [];
this.children = [];
// 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,
},
});
}
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;
}
}
}

View file

@ -0,0 +1,8 @@
{
"extends": "plugin:@next/next/recommended",
"settings": {
"next": {
"rootDir": "resources/newssite/news-next/"
}
}
}

38
resources/newssite/news-next/.gitignore vendored Normal file
View file

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
/dist/cache
/dist/server
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View file

@ -0,0 +1,38 @@
# The Daily Broadcast
> **_NOTE:_** This is not a typical use-case for Next.js and we encourage developers to follow the [official documentation](https://vercel.com/docs) for recommended usage of the framework.
This app is a news-site built with [Next.js](https://nextjs.org/). It utilizes the [News Site Template](https://github.com/flashdesignory/news-site-template) as the basis for styling and functionality.
Since Speedometer expects static files for all apps included, this project's build step uses [static html export](https://nextjs.org/docs/pages/building-your-application/deploying/static-exports).
<br>With this implementation, some features of Next.js are not available and therefore omitted to ensure compatibility with Speedometer.
## Local Development
Start the local dev server:
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Deployment of changes
To ensure Next.js lint rules are followed, run:
```bash
npm run lint
```
To create new build files, run:
```bash
npm run build
```
Add, commit and push changes to the working branch.
## Test steps
The Speedometer test consists of navigating between the different pages of the news site.
It includes interactions with the navigation drop-down menu to ensure state changes happen in between the page navigations.

View file

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/pages/_error-54de1933a164a1ff.js" defer=""></script><script src="./_next/static/hVjtt3DRTxwTZ0w7LdK0j/_buildManifest.js" defer=""></script><script src="./_next/static/hVjtt3DRTxwTZ0w7LdK0j/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"hVjtt3DRTxwTZ0w7LdK0j","assetPrefix":".","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{1118:function(n,e,i){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return i(8375)}])},8375:function(n,e,i){"use strict";i.r(e);var t=i(5893);i(6026),i(858),i(6669),i(3454),i(9917);var u=i(7294),o=i(4298),a=i.n(o);e.default=function(n){let{Component:e,pageProps:i}=n,[o,r]=(0,u.useState)(!1);return(0,u.useEffect)(()=>r(!0),[]),o?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(a(),{id:"raf-mock",children:"// This hack allows to capture the work normally happening in a rAF. We\n// may be able to remove it if the runner improves.\nwindow.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);\nwindow.cancelAnimationFrame = window.clearTimeout;\n// Disable requestIdleCallback until WebKit / Safari supports it.\nwindow.requestIdleCallback = undefined;\nwindow.cancelIdleCallback = undefined;"}),(0,t.jsx)(e,{...i})]}):null}},6669:function(){},858:function(){},3454:function(){},9917:function(){},6026:function(){},4298:function(n,e,i){n.exports=i(5442)}},function(n){var e=function(e){return n(n.s=e)};n.O(0,[774,179],function(){return e(1118),e(6885)}),_N_E=n.O()}]);

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(3499)}])}},function(n){n.O(0,[774,888,179],function(){return n(n.s=1981)}),_N_E=n.O()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
!function(){"use strict";var e,n,r,t,o={},u={};function i(e){var n=u[e];if(void 0!==n)return n.exports;var r=u[e]={exports:{}},t=!0;try{o[e](r,r.exports,i),t=!1}finally{t&&delete u[e]}return r.exports}i.m=o,e=[],i.O=function(n,r,t,o){if(r){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[r,t,o];return}for(var f=1/0,u=0;u<e.length;u++){for(var r=e[u][0],t=e[u][1],o=e[u][2],l=!0,c=0;c<r.length;c++)f>=o&&Object.keys(i.O).every(function(e){return i.O[e](r[c])})?r.splice(c--,1):(l=!1,o<f&&(f=o));if(l){e.splice(u--,1);var a=t();void 0!==a&&(n=a)}}return n},i.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(n,{a:n}),n},i.d=function(e,n){for(var r in n)i.o(n,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},i.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.p=".//_next/",n={272:0},i.O.j=function(e){return 0===n[e]},r=function(e,r){var t,o,u=r[0],f=r[1],l=r[2],c=0;if(u.some(function(e){return 0!==n[e]})){for(t in f)i.o(f,t)&&(i.m[t]=f[t]);if(l)var a=l(i)}for(e&&e(r);c<u.length;c++)o=u[c],i.o(n,o)&&n[o]&&n[o][0](),n[o]=0;return i.O(a)},(t=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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