├── .github
└── workflows
│ └── lint.yml
├── .gitignore
├── .prettierignore
├── LICENSE
├── README.md
├── favicon.png
├── index.html
├── repl
├── index.html
├── main.css
├── main.js
└── repl.js
├── test262
├── fetch.js
├── index.html
├── main.css
├── main.js
├── per-file
│ ├── dir-icon.png
│ ├── index.html
│ ├── js-icon.png
│ ├── main.css
│ └── main.js
└── test-config.js
└── wasm
├── index.html
├── main.js
└── per-file
├── index.html
└── wasm-icon.png
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - name: Run prettier
11 | run: |
12 | npx prettier@2.8.8 --check .
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LadybirdBrowser/libjs-website/7466d45dfe1e42867eefe219d3f2478b0dc42720/.gitignore
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | test262/data/*
2 | wasm/data/*
3 | repl/libjs.js
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Linus Groh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LibJS Website
2 |
3 | Website for Ladybird's JavaScript engine.
4 |
--------------------------------------------------------------------------------
/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LadybirdBrowser/libjs-website/7466d45dfe1e42867eefe219d3f2478b0dc42720/favicon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LibJS JavaScript engine
7 |
8 |
31 |
32 |
33 |
34 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/repl/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LibJS REPL
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 | LibJS REPL
20 |
21 |
press 'ctrl+enter' to run
22 |
press 'enter' to run
23 |
24 |
29 |
34 | <
35 |
36 |
Run!
37 |
40 |
41 |
42 |
43 |
Loading...
44 |
45 |
46 |
47 |
48 | Made by
49 | CxByte .
55 |
56 |
57 |
61 |
62 |
63 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/repl/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-size: auto;
3 | }
4 |
5 | textarea {
6 | font-family: monospace;
7 | background: transparent;
8 | outline-style: dashed;
9 | outline-width: 2px;
10 | outline-color: var(--color-chart-border);
11 | outline-offset: 2px;
12 | border: none;
13 | color: var(--color-text);
14 | font-size: var(--font-size);
15 | }
16 |
17 | textarea[large] {
18 | height: 80vh;
19 | width: 100%;
20 | }
21 |
22 | #input {
23 | flex-grow: 1;
24 | padding: 4px;
25 | }
26 |
27 | #input-toggle {
28 | max-height: 30px;
29 | margin-top: 50%;
30 | background: var(--color-background);
31 | border: 2px solid var(--color-chart-border);
32 | color: var(--color-highlight);
33 | padding: 2px;
34 | }
35 |
36 | #input-toggle:hover {
37 | background: var(--color-highlight);
38 | color: var(--color-background);
39 | }
40 |
41 | #input-toggle.input-hidden {
42 | margin-right: 2px;
43 | }
44 |
45 | #input-toggle.input-shown {
46 | margin-right: 4px;
47 | margin-left: 2px;
48 | }
49 |
50 | #repl-window {
51 | display: flex;
52 | flex-direction: column;
53 | flex-grow: 1;
54 | }
55 |
56 | #repl-contents {
57 | outline-style: dashed;
58 | outline-width: 2px;
59 | outline-color: var(--color-chart-border);
60 | height: 80vh;
61 | padding: 4px;
62 | overflow: scroll;
63 | }
64 |
65 | #repl {
66 | display: flex;
67 | flex-direction: row;
68 | }
69 |
70 | main {
71 | margin: 40px 40px;
72 | }
73 |
74 | .repl-input-line {
75 | display: flex;
76 | flex-direction: row;
77 | }
78 |
79 | .repl-input-line > textarea {
80 | flex-grow: 1;
81 | margin-right: 8px;
82 | margin-top: 8px;
83 | margin-left: 8px;
84 | min-height: 1rem;
85 | }
86 |
87 | .hovered-related {
88 | background-color: var(--color-hover-highlight);
89 | }
90 |
91 | #loading-content {
92 | display: grid;
93 | }
94 |
--------------------------------------------------------------------------------
/repl/main.js:
--------------------------------------------------------------------------------
1 | const inputTemplate = document.getElementById("repl-input-template");
2 | const staticInputTemplate = document.getElementById(
3 | "repl-static-input-template"
4 | );
5 | const outputTemplate = document.getElementById("repl-output-template");
6 | const inputElement = document.getElementById("input");
7 | const inputTextArea = inputElement.querySelector("textarea");
8 | const outputElement = document.getElementById("repl-contents");
9 | const loadingContainer = document.getElementById("loading-content");
10 | const mainContainer = document.getElementById("main-content");
11 | const loadingText = document.getElementById("loading-text");
12 | const loadingProgress = document.getElementById("loading-progress");
13 | const headerDescriptionSpan = document.getElementById("header-description");
14 |
15 | (async function () {
16 | function updateLoading(name, { loaded, total, known }) {
17 | loadingText.innerText = `Loading ${name}...`;
18 | if (known) {
19 | loadingProgress.max = total;
20 | loadingProgress.value = loaded;
21 | } else {
22 | delete loadingProgress.max;
23 | delete loadingProgress.value;
24 | }
25 | }
26 |
27 | const repl = await createREPL({
28 | inputTemplate,
29 | staticInputTemplate,
30 | outputTemplate,
31 | inputElement,
32 | inputTextArea,
33 | outputElement,
34 | updateLoading,
35 | });
36 |
37 | const buildHash = Module.SERENITYOS_COMMIT;
38 | const shortenedBuildHash = buildHash.substring(0, 7);
39 | headerDescriptionSpan.innerHTML = ` (built from ${shortenedBuildHash} )`;
40 |
41 | loadingContainer.style.display = "none";
42 | mainContainer.style.display = "";
43 |
44 | repl.display("Ready!");
45 | inputTextArea.focus();
46 |
47 | const inputToggleButton = document.getElementById("input-toggle");
48 | const inputEditorTip = document.getElementById("input-editor-tip");
49 | const inputTip = document.getElementById("input-tip");
50 |
51 | inputToggleButton.addEventListener("click", () => {
52 | if (inputToggleButton.classList.contains("input-shown")) {
53 | inputToggleButton.classList.remove("input-shown");
54 | inputToggleButton.classList.add("input-hidden");
55 | inputToggleButton.textContent = ">";
56 | inputElement.style.display = "none";
57 | inputEditorTip.style.display = "none";
58 | inputTip.style.display = "";
59 | repl.allowDirectInput();
60 | } else {
61 | inputToggleButton.classList.remove("input-hidden");
62 | inputToggleButton.classList.add("input-shown");
63 | inputToggleButton.textContent = "<";
64 | inputElement.style.display = "";
65 | inputEditorTip.style.display = "";
66 | inputTip.style.display = "none";
67 | repl.prohibitDirectInput();
68 | inputTextArea.focus();
69 | }
70 | });
71 | })();
72 |
--------------------------------------------------------------------------------
/repl/repl.js:
--------------------------------------------------------------------------------
1 | if (typeof Module === "undefined")
2 | throw new Error("LibJS.js must be loaded before repl.js");
3 |
4 | function globalDisplayToUser(text) {
5 | globalDisplayToUser.repl.push(text);
6 | }
7 |
8 | async function createREPL(elements) {
9 | const repl = Object.create(null);
10 | elements.updateLoading("LibJS Runtime", { known: false });
11 |
12 | await new Promise((resolve) => addOnPostRun(resolve));
13 |
14 | elements.updateLoading("LibJS WebAssembly Module", { known: false });
15 | if (!runtimeInitialized) {
16 | initRuntime();
17 | }
18 |
19 | // The REPL only has access to a limited, virtual file system that does not contain time zone
20 | // information. Retrieve the current time zone from the running browser for LibJS to use.
21 | let timeZone;
22 |
23 | try {
24 | const dateTimeFormat = new Intl.DateTimeFormat();
25 | timeZone = Module.allocateUTF8(dateTimeFormat.resolvedOptions().timeZone);
26 | } catch {
27 | timeZone = Module.allocateUTF8("UTC");
28 | }
29 |
30 | if (Module._initialize_repl(timeZone) !== 0)
31 | throw new Error("Failed to initialize REPL");
32 |
33 | Module._free(timeZone);
34 |
35 | repl.private = {
36 | allowingDirectInput: false,
37 | activeInputs: [],
38 | inactiveInputs: [],
39 | outputs: [],
40 | prepareInput() {
41 | let node = elements.inputTemplate.content.children[0].cloneNode(true);
42 | return repl.private.attachInput(node, { directly: true });
43 | },
44 | prepareOutput() {
45 | let node = elements.outputTemplate.cloneNode(true).content.children[0];
46 | node = elements.outputElement.appendChild(node);
47 | node.addEventListener("mouseenter", () => {
48 | if (!node._input) return;
49 |
50 | node._input.classList.add("hovered-related");
51 | node._input._related.forEach((other) => {
52 | other.classList.add("hovered-related");
53 | });
54 | });
55 | node.addEventListener("mouseleave", () => {
56 | if (!node._input) return;
57 |
58 | node._input.classList.remove("hovered-related");
59 | node._input._related.forEach((other) => {
60 | other.classList.remove("hovered-related");
61 | });
62 | });
63 | return node;
64 | },
65 | attachInput(node, { directly }) {
66 | if (directly) {
67 | node = elements.outputElement.appendChild(node);
68 | node._isDirect = true;
69 | }
70 | node._related = [];
71 | const editor = node.querySelector("textarea");
72 | editor.addEventListener("keydown", (event) => {
73 | const requireCtrl = directly;
74 | if (event.keyCode == 13 && requireCtrl ^ event.ctrlKey) {
75 | event.preventDefault();
76 | repl.execute(node, editor.value);
77 | return false;
78 | }
79 | return true;
80 | });
81 | document
82 | .getElementById("run")
83 | .addEventListener("onclick", () => repl.execute(node, editor.value));
84 | node.addEventListener("mouseenter", () => {
85 | node._related.forEach((other) => {
86 | other.classList.add("hovered-related");
87 | });
88 | });
89 | node.addEventListener("mouseleave", () => {
90 | node._related.forEach((other) => {
91 | other.classList.remove("hovered-related");
92 | });
93 | });
94 | return node;
95 | },
96 | execute(text) {
97 | const encodedText = Module.allocateUTF8(text);
98 | let oldRepl = globalDisplayToUser.repl;
99 | try {
100 | globalDisplayToUser.repl = repl.private.outputs;
101 | Module._execute(encodedText);
102 | return repl.private.outputs;
103 | } finally {
104 | globalDisplayToUser.repl = oldRepl;
105 | repl.private.outputs = [];
106 | Module._free(encodedText);
107 | }
108 | },
109 | markRelated(node, input) {
110 | node._input = input;
111 | input._related.push(node);
112 | },
113 | };
114 |
115 | repl.private.attachInput(elements.inputElement, { directly: false });
116 |
117 | repl.display = (text, relatedInput = null) => {
118 | text.split("\n").forEach((line) => {
119 | const node = repl.private.prepareOutput();
120 | node.querySelector("pre").textContent = line;
121 | if (relatedInput !== null) {
122 | repl.private.markRelated(node, relatedInput);
123 | }
124 | });
125 | };
126 | repl.allowDirectInput = () => {
127 | repl.private.allowingDirectInput = true;
128 | repl.private.inactiveInputs.forEach((node) =>
129 | repl.private.attachInput(node, { directly: true })
130 | );
131 | repl.private.activeInputs = repl.private.inactiveInputs;
132 | repl.private.inactiveInputs = [];
133 | if (
134 | repl.private.allowingDirectInput &&
135 | repl.private.activeInputs.length == 0
136 | ) {
137 | repl.addInput();
138 | }
139 | };
140 | repl.prohibitDirectInput = () => {
141 | repl.private.allowingDirectInput = false;
142 | repl.private.activeInputs.forEach((node) => node.remove());
143 | repl.private.inactiveInputs = repl.private.inactiveInputs.concat(
144 | repl.private.activeInputs
145 | );
146 | repl.private.activeInputs = [];
147 | };
148 | repl.addInput = () => {
149 | const input = repl.private.prepareInput();
150 | repl.private.activeInputs.push(input);
151 | input.querySelector("textarea").focus();
152 | };
153 | repl.addStaticInput = (text) => {
154 | const input =
155 | elements.staticInputTemplate.cloneNode(true).content.children[0];
156 | input.querySelector("pre.content").textContent = text;
157 | input._related = [];
158 | return elements.outputElement.appendChild(input);
159 | };
160 | repl.execute = (input, text) => {
161 | repl.private.activeInputs = repl.private.activeInputs.filter(
162 | (i) => i !== input
163 | );
164 | let staticInput = repl.addStaticInput(text);
165 | let outputs = repl.private.execute(text).join("");
166 | if (outputs.endsWith("undefined\n"))
167 | outputs = outputs.substring(0, outputs.length - 10);
168 |
169 | repl.display(outputs, input);
170 |
171 | input._related.forEach((node) =>
172 | repl.private.markRelated(node, staticInput)
173 | );
174 | if (input._isDirect) {
175 | input.remove();
176 | }
177 |
178 | if (
179 | repl.private.allowingDirectInput &&
180 | repl.private.activeInputs.length == 0
181 | ) {
182 | repl.addInput();
183 | }
184 | };
185 |
186 | return repl;
187 | }
188 |
--------------------------------------------------------------------------------
/test262/fetch.js:
--------------------------------------------------------------------------------
1 | const LIBJS_DATA_URL =
2 | "https://raw.githubusercontent.com/LadybirdWebBrowser/libjs-data/master";
3 |
4 | const fetchData = (path) => {
5 | return fetch(`${LIBJS_DATA_URL}/${path}`, {
6 | method: "GET",
7 | cache: "no-cache",
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/test262/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LibJS test262 results
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 | LibJS test262 results
18 |
19 | Introduction
20 |
21 | These are the results of the
22 | Ladybird
28 | JavaScript engine, LibJS, running the official ECMAScript conformance
29 | test suite
30 | test262
36 | as well as the
37 | test262 parser tests . All tests are re-run and the results updated automatically for
43 | every push to the master branch of the repository on GitHub. Dates and
44 | times are shown in your browser's timezone.
45 |
46 |
47 |
53 |
54 | Source code & Data
55 | Source code:
56 |
74 | Data (JSON):
75 |
93 |
94 |
95 | test262
96 | Loading...
97 |
98 |
99 |
100 |
101 |
102 | test262 parser tests
103 | Loading...
104 |
105 |
106 |
107 |
108 |
109 | test262 performance
110 |
111 |
112 |
113 |
114 |
115 | test262 performance per test
116 |
117 |
118 |
119 |
120 |
121 |
122 | Made by
123 | linusg .
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/test262/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-family: "Roboto", sans-serif;
3 | --font-family-heading: "Montserrat", sans-serif;
4 | --color-highlight: #1d60df;
5 | --color-hover-highlight: #1d60df40;
6 | --color-text: #312f2f;
7 | --color-background: #f6fcfc;
8 | --color-background-alt: #dfecec;
9 | --color-chart-border: rgba(0, 0, 0, 0.3);
10 | --color-chart-passed: #19db6e;
11 | --color-chart-failed: #f70c4e;
12 | --color-chart-skipped: #888888;
13 | --color-chart-metadata-error: #5c6bc0;
14 | --color-chart-harness-error: #26a69a;
15 | --color-chart-timeout-error: #26c6da;
16 | --color-chart-process-error: #ab47bc;
17 | --color-chart-runner-exception: #ff7043;
18 | --color-chart-todo-error: #ffca28;
19 | }
20 |
21 | @media (prefers-color-scheme: dark) {
22 | :root {
23 | --color-highlight: #03a9f4;
24 | --color-text: #dddddd;
25 | --color-background: #191922;
26 | --color-background-alt: #262635;
27 | --color-chart-border: rgba(255, 255, 255, 0.5);
28 | --color-chart-passed: #18b55d;
29 | --color-chart-failed: #c80f43;
30 | --color-chart-skipped: #aaaaaa;
31 | --color-chart-metadata-error: #3949ab;
32 | --color-chart-harness-error: #00897b;
33 | --color-chart-timeout-error: #00acc1;
34 | --color-chart-process-error: #8e24aa;
35 | --color-chart-runner-exception: #f4511e;
36 | --color-chart-todo-error: #ffb300;
37 | }
38 | }
39 |
40 | * {
41 | margin: 0;
42 | padding: 0;
43 | box-sizing: border-box;
44 | }
45 |
46 | body {
47 | background: var(--color-background);
48 | color: var(--color-text);
49 | font-family: var(--font-family);
50 | letter-spacing: 0.2px;
51 | line-height: 1.5;
52 | padding: 20px;
53 | }
54 |
55 | main,
56 | footer {
57 | max-width: 1200px;
58 | margin: 40px auto;
59 | }
60 |
61 | footer {
62 | text-align: center;
63 | }
64 |
65 | section {
66 | margin-top: 40px;
67 | }
68 |
69 | p {
70 | margin: 20px 0;
71 | }
72 |
73 | li {
74 | list-style-position: inside;
75 | }
76 |
77 | h1,
78 | h2 {
79 | font-family: var(--font-family-heading);
80 | font-weight: 700;
81 | }
82 |
83 | h1 {
84 | font-size: 40px;
85 | margin-bottom: 40px;
86 | text-align: center;
87 | }
88 |
89 | h2 {
90 | font-size: 30px;
91 | margin-bottom: 20px;
92 | }
93 |
94 | a,
95 | a:active,
96 | a:hover,
97 | a:visited {
98 | color: var(--color-highlight);
99 | }
100 |
101 | h2 a {
102 | text-decoration-thickness: 2px;
103 | }
104 |
105 | code {
106 | display: inline-block;
107 | padding: 0 2px;
108 | border-radius: 4px;
109 | font-size: 1rem;
110 | background: var(--color-background-alt);
111 | }
112 |
113 | .chart-wrapper {
114 | height: max(80vh, 400px);
115 | background: var(--color-background-alt);
116 | border-radius: 10px;
117 | padding: 20px;
118 | }
119 |
--------------------------------------------------------------------------------
/test262/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (() => {
4 | const { DateTime, Duration } = luxon;
5 |
6 | const backgroundColor = style.getPropertyValue("--color-background");
7 | const textColor = style.getPropertyValue("--color-text");
8 | const chartBorderColor = style.getPropertyValue("--color-chart-border");
9 | const fontFamily = style.getPropertyValue("font-family");
10 | const fontSize = parseInt(
11 | style.getPropertyValue("font-size").slice(0, -2),
12 | 10
13 | );
14 |
15 | Chart.defaults.borderColor = textColor;
16 | Chart.defaults.color = textColor;
17 | Chart.defaults.font.family = fontFamily;
18 | Chart.defaults.font.size = fontSize;
19 |
20 | // place tooltip's origin point under the cursor
21 | const tooltipPlugin = Chart.registry.getPlugin("tooltip");
22 | tooltipPlugin.positioners.underCursor = function (elements, eventPosition) {
23 | const pos = tooltipPlugin.positioners.average(elements);
24 |
25 | if (pos === false) {
26 | return false;
27 | }
28 |
29 | return {
30 | x: pos.x,
31 | y: eventPosition.y,
32 | };
33 | };
34 |
35 | class LineWithVerticalHoverLineController extends Chart.LineController {
36 | draw() {
37 | super.draw(arguments);
38 |
39 | if (!this.chart.tooltip._active.length) return;
40 |
41 | const { x } = this.chart.tooltip._active[0].element;
42 | const { top: topY, bottom: bottomY } = this.chart.chartArea;
43 | const ctx = this.chart.ctx;
44 |
45 | ctx.save();
46 | ctx.beginPath();
47 | ctx.moveTo(x, topY);
48 | ctx.lineTo(x, bottomY);
49 | ctx.lineWidth = 1;
50 | ctx.strokeStyle = chartBorderColor;
51 | ctx.stroke();
52 | ctx.restore();
53 | }
54 | }
55 |
56 | LineWithVerticalHoverLineController.id = "lineWithVerticalHoverLine";
57 | LineWithVerticalHoverLineController.defaults = Chart.LineController.defaults;
58 | Chart.register(LineWithVerticalHoverLineController);
59 |
60 | // This is when we started running the tests on Idan's self-hosted runner. Before that,
61 | // durations varied a lot across runs. See https://github.com/SerenityOS/serenity/pull/7718.
62 | const PERFORMANCE_CHART_START_DATE_TIME = DateTime.fromISO("2021-07-04");
63 |
64 | function prepareDataForCharts(data) {
65 | const charts = {
66 | ...Object.fromEntries(
67 | [].concat(
68 | ...["test262"].map((name) => [
69 | [
70 | name,
71 | {
72 | data: {
73 | [TestResult.PASSED]: [],
74 | [TestResult.FAILED]: [],
75 | [TestResult.SKIPPED]: [],
76 | [TestResult.METADATA_ERROR]: [],
77 | [TestResult.HARNESS_ERROR]: [],
78 | [TestResult.TIMEOUT_ERROR]: [],
79 | [TestResult.PROCESS_ERROR]: [],
80 | [TestResult.RUNNER_EXCEPTION]: [],
81 | [TestResult.TODO_ERROR]: [],
82 | [TestResult.DURATION]: [],
83 | },
84 | datasets: [],
85 | metadata: [],
86 | },
87 | ],
88 | [
89 | `${name}-performance`,
90 | {
91 | data: {
92 | [TestResult.DURATION]: [],
93 | },
94 | datasets: [],
95 | metadata: [],
96 | },
97 | ],
98 | [
99 | `${name}-performance-per-test`,
100 | {
101 | data: {
102 | [TestResult.DURATION]: [],
103 | },
104 | datasets: [],
105 | metadata: [],
106 | },
107 | ],
108 | ])
109 | )
110 | ),
111 | ["test262-parser-tests"]: {
112 | data: {
113 | [TestResult.PASSED]: [],
114 | [TestResult.FAILED]: [],
115 | },
116 | datasets: [],
117 | metadata: [],
118 | },
119 | };
120 |
121 | for (const entry of data) {
122 | for (const chart in charts) {
123 | const results = entry.tests[chart]?.results;
124 | if (!results) {
125 | continue;
126 | }
127 | charts[chart].metadata.push({
128 | commitTimestamp: entry.commit_timestamp,
129 | runTimestamp: entry.run_timestamp,
130 | duration: entry.tests[chart].duration,
131 | versions: entry.versions,
132 | total: results.total,
133 | });
134 | for (const testResult in charts[chart].data) {
135 | if (testResult === TestResult.DURATION) {
136 | continue;
137 | }
138 | charts[chart].data[testResult].push({
139 | x: entry.commit_timestamp * 1000,
140 | y: results[testResult] || 0,
141 | });
142 | }
143 | }
144 |
145 | const dt = DateTime.fromSeconds(entry.commit_timestamp);
146 | if (dt < PERFORMANCE_CHART_START_DATE_TIME) {
147 | continue;
148 | }
149 |
150 | for (const suffix of [""]) {
151 | // chart-test262-performance
152 | const performanceTests = entry.tests[`test262${suffix}`];
153 | const performanceChart = charts[`test262${suffix}-performance`];
154 | const performanceResults = performanceTests?.results;
155 | if (performanceResults) {
156 | performanceChart.metadata.push({
157 | commitTimestamp: entry.commit_timestamp,
158 | runTimestamp: entry.run_timestamp,
159 | duration: performanceTests.duration,
160 | versions: entry.versions,
161 | total: performanceResults.total,
162 | });
163 | performanceChart.data["duration"].push({
164 | x: entry.commit_timestamp * 1000,
165 | y: performanceTests.duration,
166 | });
167 | }
168 |
169 | // chart-test262-performance-per-test
170 | const performancePerTestTests = entry.tests[`test262${suffix}`];
171 | const performancePerTestChart =
172 | charts[`test262${suffix}-performance-per-test`];
173 | const performancePerTestResults = performancePerTestTests?.results;
174 | if (performancePerTestResults) {
175 | performancePerTestChart.metadata.push({
176 | commitTimestamp: entry.commit_timestamp,
177 | runTimestamp: entry.run_timestamp,
178 | duration:
179 | performancePerTestTests.duration /
180 | performancePerTestResults.total,
181 | versions: entry.versions,
182 | total: performancePerTestResults.total,
183 | });
184 | performancePerTestChart.data["duration"].push({
185 | x: entry.commit_timestamp * 1000,
186 | y:
187 | performancePerTestTests.duration /
188 | performancePerTestResults.total,
189 | });
190 | }
191 | }
192 | }
193 |
194 | for (const chart in charts) {
195 | for (const testResult in charts[chart].data) {
196 | charts[chart].datasets.push({
197 | label: TestResultLabels[testResult],
198 | data: charts[chart].data[testResult],
199 | backgroundColor: TestResultColors[testResult],
200 | borderWidth: 2,
201 | borderColor: chartBorderColor,
202 | pointRadius: 0,
203 | pointHoverRadius: 0,
204 | fill: true,
205 | });
206 | }
207 | delete charts[chart].data;
208 | }
209 |
210 | return { charts };
211 | }
212 |
213 | function initializeChart(
214 | element,
215 | { datasets, metadata },
216 | { xAxisTitle = "Time", yAxisTitle = "Number of tests" } = {}
217 | ) {
218 | const ctx = element.getContext("2d");
219 |
220 | new Chart(ctx, {
221 | type: "lineWithVerticalHoverLine",
222 | data: {
223 | datasets,
224 | },
225 | options: {
226 | parsing: false,
227 | normalized: true,
228 | responsive: true,
229 | maintainAspectRatio: false,
230 | animation: false,
231 | plugins: {
232 | zoom: {
233 | zoom: {
234 | mode: "x",
235 | wheel: {
236 | enabled: true,
237 | },
238 | },
239 | pan: {
240 | enabled: true,
241 | mode: "x",
242 | },
243 | },
244 | hover: {
245 | mode: "index",
246 | intersect: false,
247 | },
248 | tooltip: {
249 | mode: "index",
250 | intersect: false,
251 | usePointStyle: true,
252 | boxWidth: 12,
253 | boxHeight: 12,
254 | padding: 20,
255 | position: "underCursor",
256 | titleColor: textColor,
257 | bodyColor: textColor,
258 | footerColor: textColor,
259 | footerFont: { weight: "normal" },
260 | footerMarginTop: 20,
261 | backgroundColor: backgroundColor,
262 | callbacks: {
263 | title: () => {
264 | return null;
265 | },
266 | beforeBody: (context) => {
267 | const { dataIndex } = context[0];
268 | const { total } = metadata[dataIndex];
269 | const formattedValue = total.toLocaleString("en-US");
270 | // Leading spaces to make up for missing color circle
271 | return ` Number of tests: ${formattedValue}`;
272 | },
273 | label: (context) => {
274 | // Space as padding between color circle and label
275 | const formattedValue = context.parsed.y.toLocaleString("en-US");
276 | if (
277 | context.dataset.label !==
278 | TestResultLabels[TestResult.DURATION]
279 | ) {
280 | const { total } = metadata[context.dataIndex];
281 | const percentOfTotal = (
282 | (context.parsed.y / total) *
283 | 100
284 | ).toFixed(2);
285 | return ` ${context.dataset.label}: ${formattedValue} (${percentOfTotal}%)`;
286 | } else {
287 | return ` ${context.dataset.label}: ${formattedValue}`;
288 | }
289 | },
290 |
291 | footer: (context) => {
292 | const { dataIndex } = context[0];
293 | const {
294 | commitTimestamp,
295 | duration: durationSeconds,
296 | versions,
297 | } = metadata[dataIndex];
298 | const dateTime = DateTime.fromSeconds(commitTimestamp);
299 | const duration = Duration.fromMillis(durationSeconds * 1000);
300 | const ladybirdVersion = versions.serenity.substring(0, 7);
301 | // prettier-ignore
302 | const libjsTest262Version = versions["libjs-test262"].substring(0, 7);
303 | const test262Version = versions.test262.substring(0, 7);
304 | // prettier-ignore
305 | const test262ParserTestsVersion = versions["test262-parser-tests"].substring(0, 7);
306 | return `\
307 | Committed on ${dateTime.toLocaleString(DateTime.DATETIME_SHORT)}, \
308 | run took ${duration.toISOTime()}
309 |
310 | Versions: ladybird@${ladybirdVersion}, libjs-test262@${libjsTest262Version},
311 | test262@${test262Version}, test262-parser-tests@${test262ParserTestsVersion}`;
312 | },
313 | },
314 | },
315 | legend: {
316 | align: "end",
317 | labels: {
318 | usePointStyle: true,
319 | boxWidth: 10,
320 | // Only include passed, failed, TODO, and crashed in the legend
321 | filter: ({ text }) =>
322 | text === TestResultLabels[TestResult.PASSED] ||
323 | text === TestResultLabels[TestResult.FAILED] ||
324 | text === TestResultLabels[TestResult.TODO_ERROR] ||
325 | text === TestResultLabels[TestResult.PROCESS_ERROR],
326 | },
327 | },
328 | },
329 | scales: {
330 | x: {
331 | type: "time",
332 | title: {
333 | display: true,
334 | text: xAxisTitle,
335 | },
336 | grid: {
337 | borderColor: textColor,
338 | color: "transparent",
339 | borderWidth: 2,
340 | },
341 | },
342 | y: {
343 | stacked: true,
344 | beginAtZero: true,
345 | title: {
346 | display: true,
347 | text: yAxisTitle,
348 | },
349 | grid: {
350 | borderColor: textColor,
351 | color: chartBorderColor,
352 | borderWidth: 2,
353 | },
354 | },
355 | },
356 | },
357 | });
358 | }
359 |
360 | function initializeSummary(
361 | element,
362 | runTimestamp,
363 | commitHash,
364 | durationSeconds,
365 | results
366 | ) {
367 | const dateTime = DateTime.fromSeconds(runTimestamp);
368 | const duration = Duration.fromMillis(durationSeconds * 1000);
369 | const passed = results[TestResult.PASSED];
370 | const total = results.total;
371 | const percent = ((passed / total) * 100).toFixed(2);
372 | element.innerHTML = `
373 | The last test run was on
374 | ${dateTime.toLocaleString(DateTime.DATETIME_SHORT)}
375 | for commit
376 |
377 |
383 | ${commitHash.slice(0, 7)}
384 |
385 |
386 | and took ${duration.toISOTime()} .
387 | ${passed} of ${total} tests passed, i.e. ${percent}% .
388 | `;
389 | }
390 |
391 | function initialize(data) {
392 | const { charts } = prepareDataForCharts(data);
393 | initializeChart(
394 | document.getElementById("chart-test262-parser-tests"),
395 | charts["test262-parser-tests"]
396 | );
397 | for (const suffix of [""]) {
398 | initializeChart(
399 | document.getElementById(`chart-test262${suffix}`),
400 | charts[`test262${suffix}`]
401 | );
402 | initializeChart(
403 | document.getElementById(`chart-test262${suffix}-performance`),
404 | charts[`test262${suffix}-performance`],
405 | { yAxisTitle: TestResultLabels[TestResult.DURATION] }
406 | );
407 | initializeChart(
408 | document.getElementById(`chart-test262${suffix}-performance-per-test`),
409 | charts[`test262${suffix}-performance-per-test`],
410 | { yAxisTitle: TestResultLabels[TestResult.DURATION] }
411 | );
412 | }
413 |
414 | const last = data.slice(-1)[0];
415 | if ("test262" in last.tests) {
416 | initializeSummary(
417 | document.getElementById("summary-test262"),
418 | last.run_timestamp,
419 | last.versions.serenity,
420 | last.tests.test262.duration,
421 | last.tests.test262.results
422 | );
423 | }
424 |
425 | if ("test262-parser-tests" in last.tests) {
426 | initializeSummary(
427 | document.getElementById("summary-test262-parser-tests"),
428 | last.run_timestamp,
429 | last.versions.serenity,
430 | last.tests["test262-parser-tests"].duration,
431 | last.tests["test262-parser-tests"].results
432 | );
433 | }
434 | }
435 |
436 | document.addEventListener("DOMContentLoaded", () => {
437 | fetchData("test262/results.json")
438 | .then((response) => response.json())
439 | .then((data) => {
440 | data.sort((a, b) =>
441 | a.commit_timestamp === b.commit_timestamp
442 | ? 0
443 | : a.commit_timestamp < b.commit_timestamp
444 | ? -1
445 | : 1
446 | );
447 | return data;
448 | })
449 | .then((data) => initialize(data));
450 | });
451 | })();
452 |
--------------------------------------------------------------------------------
/test262/per-file/dir-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LadybirdBrowser/libjs-website/7466d45dfe1e42867eefe219d3f2478b0dc42720/test262/per-file/dir-icon.png
--------------------------------------------------------------------------------
/test262/per-file/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LibJS test262 per-file results
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Search:
22 |
23 |
24 |
25 | Filter mode:
26 |
27 |
28 | Per-file results
29 |
30 |
Loading...
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
56 |
57 |
58 |
59 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | (Open on GitHub )
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
93 |
94 |
95 | (Open on GitHub )
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/test262/per-file/js-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LadybirdBrowser/libjs-website/7466d45dfe1e42867eefe219d3f2478b0dc42720/test262/per-file/js-icon.png
--------------------------------------------------------------------------------
/test262/per-file/main.css:
--------------------------------------------------------------------------------
1 | .hidden {
2 | display: none;
3 | }
4 |
5 | #legend {
6 | float: right;
7 | }
8 |
9 | #search {
10 | float: right;
11 | }
12 |
13 | #search-mode {
14 | float: right;
15 | margin-right: 5px;
16 | }
17 |
18 | .search-warning {
19 | align-content: center;
20 | text-align: center;
21 | border: solid 1px #4d4d0a;
22 | background: #f0db4f;
23 | }
24 |
25 | @media (prefers-color-scheme: dark) {
26 | .search-warning {
27 | border-color: #f0db4f;
28 | background-color: #4d4d0a;
29 | }
30 | }
31 |
32 | .legend-item {
33 | display: inline-flex;
34 | align-items: center;
35 | margin-left: 10px;
36 | }
37 |
38 | .legend-circle {
39 | width: 20px;
40 | height: 20px;
41 | display: inline-block;
42 | border-radius: 100%;
43 | margin-right: 5px;
44 | }
45 |
46 | .result-separator {
47 | height: 60px;
48 | }
49 |
50 | .tree-node-status-container {
51 | flex-grow: 2;
52 | padding-left: 20px;
53 | }
54 |
55 | .tree-node-status {
56 | display: flex;
57 | flex-flow: column;
58 | text-align: right;
59 | }
60 |
61 | .mode-summary-container {
62 | display: flex;
63 | flex-flow: row;
64 | justify-content: space-between;
65 | }
66 |
67 | .mode-bar-container {
68 | display: flex;
69 | flex-flow: row;
70 | width: 25vw;
71 | }
72 |
73 | .mode-result-text {
74 | flex-grow: 1;
75 | padding-right: 5px;
76 | }
77 |
78 | .tree-node-action {
79 | display: flex;
80 | align-items: center;
81 | }
82 |
83 | .tree-node-name {
84 | margin-left: 10px;
85 | }
86 |
87 | .node {
88 | display: flex;
89 | flex-flow: row;
90 | align-items: center;
91 | margin-bottom: 10px;
92 | }
93 |
--------------------------------------------------------------------------------
/test262/per-file/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const initialPathInTree = [window.config.initialPathInTree];
4 |
5 | let resultsNode;
6 | let legendNode;
7 | let searchInputNode;
8 | let summaryLabel;
9 | let summaryStatusLabel;
10 | let leafTreeNodeTemplate;
11 | let nonLeafTreeNodeTemplate;
12 | let pathInTree = new URL(location.href).searchParams
13 | .get("path")
14 | ?.split("/") ?? [...initialPathInTree];
15 | let tree;
16 |
17 | const resultsObject = Object.create(null);
18 | const legendResults = [
19 | TestResult.PASSED,
20 | TestResult.FAILED,
21 | TestResult.PROCESS_ERROR,
22 | TestResult.TODO_ERROR,
23 | ];
24 |
25 | const shownResultTypes = new Set(Object.values(TestResult));
26 | let searchQuery = "";
27 | let filtering = false;
28 |
29 | // Behavior: Initially show all
30 | // Click text -> disable that one
31 | // Click circle -> show only that one
32 |
33 | function initialize(data, modeName) {
34 | let mode = modeName;
35 |
36 | // Do a pass and generate the tree.
37 | for (const testPath in data.results) {
38 | const segments = testPath.split("/");
39 | const fileName = segments.pop();
40 | let testObject = resultsObject;
41 | for (const pathSegment of segments) {
42 | if (!(pathSegment in testObject)) {
43 | testObject[pathSegment] = {
44 | children: Object.create(null),
45 | aggregatedResults: null,
46 | };
47 | }
48 |
49 | testObject = testObject[pathSegment].children;
50 | }
51 |
52 | if (!(fileName in testObject)) {
53 | testObject[fileName] = {
54 | children: null,
55 | results: Object.create(null),
56 | };
57 | }
58 |
59 | testObject[fileName].results[mode] = data.results[testPath].toLowerCase();
60 | }
61 | }
62 |
63 | function generateResults() {
64 | function constructTree(results) {
65 | if (results.children === null) {
66 | results.aggregatedResults = Object.fromEntries(
67 | Object.keys(results.results).map((name) => [
68 | name,
69 | { [results.results[name]]: 1 },
70 | ])
71 | );
72 |
73 | return;
74 | }
75 |
76 | for (const name in results.children) {
77 | constructTree(results.children[name]);
78 | }
79 |
80 | for (const name in results.children) {
81 | const childResults = results.children[name];
82 | results.aggregatedResults = Object.keys(
83 | childResults.aggregatedResults
84 | ).reduce((acc, mode) => {
85 | if (!(mode in acc)) acc[mode] = {};
86 | const modeAcc = acc[mode];
87 | const stats = childResults.aggregatedResults[mode];
88 | for (const name in stats) {
89 | if (name in modeAcc) modeAcc[name] += stats[name];
90 | else modeAcc[name] = stats[name];
91 | }
92 | return acc;
93 | }, results.aggregatedResults || {});
94 | }
95 | }
96 |
97 | // Now do another pass and aggregate the results.
98 | let results = {
99 | children: resultsObject,
100 | aggregatedResults: {},
101 | };
102 | constructTree(results);
103 | tree = results;
104 |
105 | resultsNode = document.getElementById("results");
106 | legendNode = document.getElementById("legend");
107 | searchInputNode = document.getElementById("search-input");
108 | summaryLabel = document.getElementById("summary");
109 | summaryStatusLabel = document.getElementById("summary-status");
110 | leafTreeNodeTemplate = document.getElementById("leaf-tree-node-template");
111 | nonLeafTreeNodeTemplate = document.getElementById(
112 | "nonleaf-tree-node-template"
113 | );
114 |
115 | // Now make a nice lazy-loaded collapsible tree in `resultsNode`.
116 | regenerateResults(resultsNode);
117 |
118 | summaryStatusLabel.classList.remove("hidden");
119 |
120 | legendNode.innerHTML = legendResults
121 | .map((result) => {
122 | const color = TestResultColors[result];
123 | const label = TestResultLabels[result];
124 | return `
125 |
126 |
127 | ${label}
128 |
129 | `;
130 | })
131 | .join(" ");
132 |
133 | function legendChanged() {
134 | legendNode.querySelectorAll(".legend-item").forEach((legendItem) => {
135 | if (shownResultTypes.has(legendItem.getAttribute("data-type")))
136 | legendItem.style.textDecoration = null;
137 | else legendItem.style.textDecoration = "line-through";
138 | });
139 | }
140 |
141 | legendNode.querySelectorAll(".legend-item").forEach((legendItem) => {
142 | legendItem.onclick = (event) => {
143 | const clickedCircle = event.target !== legendItem;
144 | const resultType = legendItem.getAttribute("data-type");
145 | if (clickedCircle) {
146 | if (shownResultTypes.size === 1 && shownResultTypes.has(resultType)) {
147 | Object.values(TestResult).forEach((v) => shownResultTypes.add(v));
148 | } else {
149 | shownResultTypes.clear();
150 | shownResultTypes.add(resultType);
151 | }
152 | } else {
153 | if (shownResultTypes.has(resultType)) {
154 | shownResultTypes.delete(resultType);
155 | } else {
156 | shownResultTypes.add(resultType);
157 | }
158 | }
159 |
160 | legendChanged();
161 | regenerateResults(resultsNode);
162 | };
163 | });
164 |
165 | searchInputNode.oninput = (event) => {
166 | searchQuery = event.target.value.toLowerCase();
167 | regenerateResults(resultsNode);
168 | };
169 |
170 | const filterModeCheckbox = document.getElementById("search-mode-checkbox");
171 | filterModeCheckbox.checked = filtering;
172 | filterModeCheckbox.oninput = (event) => {
173 | filtering = event.target.checked;
174 | regenerateResults(resultsNode);
175 | };
176 |
177 | // We hide the search input and filter mode checkbox until the rest is loaded
178 | document.getElementById("search").classList.remove("hidden");
179 | document.getElementById("search-mode").classList.remove("hidden");
180 | }
181 |
182 | window.onpopstate = (event) => {
183 | pathInTree = event.state?.pathInTree ?? [...initialPathInTree];
184 | regenerateResults(resultsNode);
185 | };
186 |
187 | function navigate() {
188 | if (!filtering) {
189 | searchInputNode.value = "";
190 | searchQuery = "";
191 | }
192 | history.pushState(
193 | { pathInTree },
194 | pathInTree[pathInTree.length - 1],
195 | generateQueryString(pathInTree)
196 | );
197 | regenerateResults(resultsNode);
198 | }
199 |
200 | function goToParentDirectory(count) {
201 | for (let i = 0; i < count; ++i) {
202 | pathInTree.pop();
203 | }
204 | navigate();
205 | }
206 |
207 | function generateQueryString(pathSegments) {
208 | return `?path=${pathSegments.join("/")}`;
209 | }
210 |
211 | function generateSummary(results) {
212 | summaryLabel.innerHTML = "/ ";
213 | for (let i = 0; i < pathInTree.length; ++i) {
214 | const pathSegment = pathInTree[i];
215 | const pathSegmentLink = document.createElement("a");
216 | pathSegmentLink.textContent = pathSegment;
217 | pathSegmentLink.href = generateQueryString(pathInTree.slice(0, i + 1));
218 | pathSegmentLink.onclick = (event) => {
219 | if (event.metaKey || event.ctrlKey) return;
220 | event.preventDefault();
221 | goToParentDirectory(pathInTree.length - i - 1);
222 | };
223 | summaryLabel.appendChild(pathSegmentLink);
224 | if (i < pathInTree.length - 1) {
225 | summaryLabel.insertAdjacentHTML("beforeend", " / ");
226 | }
227 | }
228 | summaryStatusLabel.innerHTML = generateStatus(results.aggregatedResults);
229 | }
230 |
231 | function generateChildNode(childName, child, filepath) {
232 | const template =
233 | child.children === null ? leafTreeNodeTemplate : nonLeafTreeNodeTemplate;
234 | const childNode = template.content.children[0].cloneNode(true);
235 | childNode.querySelector(".tree-node-name").textContent = childName;
236 | childNode.querySelector(".tree-node-name").title = filepath;
237 | childNode.querySelector(".tree-node-status").innerHTML = generateStatus(
238 | child.aggregatedResults
239 | );
240 | childNode.querySelector(".tree-node-github-url").href =
241 | window.config.generateGitHubURLFromTestPath(filepath, childName);
242 | return childNode;
243 | }
244 |
245 | function makeChildNavigable(childNode, extraPathParts) {
246 | const actionNode = childNode.querySelector(".tree-node-action");
247 |
248 | actionNode.href = generateQueryString([...pathInTree, ...extraPathParts]);
249 | actionNode.onclick = function (event) {
250 | if (event.metaKey || event.ctrlKey) return;
251 | event.preventDefault();
252 | for (const part of extraPathParts) pathInTree.push(part);
253 | navigate();
254 | };
255 | }
256 |
257 | function sortResultsByTypeAndName([lhsName, lhsResult], [rhsName, rhsResult]) {
258 | if ((lhsResult.children === null) === (rhsResult.children === null))
259 | return lhsName.localeCompare(rhsName);
260 | return lhsResult.children === null ? 1 : -1;
261 | }
262 |
263 | // Setting this to false means filters check both AST and BC.
264 | let checkASTOnly = true;
265 |
266 | function entryHasFilteredResultType([, child]) {
267 | if (checkASTOnly && "AST" in child.aggregatedResults) {
268 | return Object.keys(child.aggregatedResults.AST).some((type) =>
269 | shownResultTypes.has(type)
270 | );
271 | }
272 |
273 | return Object.values(child.aggregatedResults).some((testType) =>
274 | Object.keys(testType).some((type) => shownResultTypes.has(type))
275 | );
276 | }
277 |
278 | function regenerateResults(targetNode) {
279 | const needle = searchQuery.length >= 3 ? searchQuery : "";
280 |
281 | for (const child of Array.prototype.slice.call(targetNode.children)) {
282 | child.remove();
283 | }
284 |
285 | const results = pathInTree.reduce((acc, x) => acc.children[x], tree);
286 | generateSummary(results);
287 |
288 | let nodes;
289 | if (!needle) {
290 | nodes = Object.entries(results.children)
291 | .filter(entryHasFilteredResultType)
292 | .sort(sortResultsByTypeAndName)
293 | .map(([childName, child]) => {
294 | const childNode = generateChildNode(
295 | childName,
296 | child,
297 | `${pathInTree.join("/")}/${childName}`
298 | );
299 |
300 | const isLeaf = child.children === null;
301 | if (!isLeaf) {
302 | makeChildNavigable(childNode, [childName]);
303 | }
304 | return childNode;
305 | });
306 | } else {
307 | function searchResults(result, allChildren = false, extraPath = "") {
308 | return Object.entries(result)
309 | .filter(entryHasFilteredResultType)
310 | .flatMap(([childName, child]) => {
311 | const isLeaf = child.children === null;
312 |
313 | let isSearchedFor = childName.toLowerCase().includes(needle);
314 | let relativePath = extraPath + childName;
315 | if (isLeaf) {
316 | if (isSearchedFor) return [[childName, child, relativePath]];
317 | else return [];
318 | }
319 |
320 | const childrenResults = searchResults(
321 | child.children,
322 | false,
323 | relativePath + "/"
324 | );
325 | if (isSearchedFor)
326 | childrenResults.push([childName, child, relativePath]);
327 |
328 | return childrenResults;
329 | })
330 | .sort(sortResultsByTypeAndName);
331 | }
332 |
333 | function filterResults(result) {
334 | function filterInternal(result, allChildren = false, extraPath = "") {
335 | return Object.entries(result)
336 | .filter(entryHasFilteredResultType)
337 | .map(([childName, child]) => {
338 | const isLeaf = child.children === null;
339 |
340 | let isSearchedFor = childName.toLowerCase().includes(needle);
341 | let relativePath = extraPath + childName;
342 |
343 | if (isLeaf) {
344 | if (isSearchedFor || allChildren)
345 | return [childName, child, relativePath, null];
346 | return [];
347 | }
348 | const childrenResults = filterInternal(
349 | child.children,
350 | isSearchedFor,
351 | relativePath + "/"
352 | );
353 | if (!isSearchedFor && childrenResults.length === 0 && !allChildren)
354 | return [];
355 |
356 | return [childName, child, relativePath, childrenResults];
357 | })
358 | .filter((i) => i.length > 0)
359 | .sort(sortResultsByTypeAndName);
360 | }
361 |
362 | let results = filterInternal(
363 | result,
364 | pathInTree.join("/").toLowerCase().includes(needle)
365 | );
366 |
367 | while (results.length === 1 && results[0][3] !== null)
368 | results = results[0][3];
369 |
370 | return results;
371 | }
372 |
373 | const maxResultsShown = 500;
374 | const foundResults = filtering
375 | ? filterResults(results.children)
376 | : searchResults(results.children);
377 | nodes = foundResults
378 | .filter((_, i) => i < maxResultsShown)
379 | .map(([childName, child, relativePath]) => {
380 | const childNode = generateChildNode(
381 | childName,
382 | child,
383 | `${pathInTree.join("/")}/${relativePath}`
384 | );
385 |
386 | if (child.children !== null) {
387 | const extraPathParts = [
388 | ...relativePath.split("/").filter((s) => s.length > 0),
389 | ];
390 | makeChildNavigable(childNode, extraPathParts, targetNode);
391 | }
392 |
393 | return childNode;
394 | });
395 |
396 | if (foundResults.length > maxResultsShown) {
397 | const extraNode = document.createElement("p");
398 | extraNode.innerText = `Only show the first ${maxResultsShown} of ${foundResults.length} results.`;
399 | extraNode.classList.add("search-warning");
400 | nodes.push(extraNode);
401 | }
402 | }
403 |
404 | nodes.forEach((node) => targetNode.appendChild(node));
405 | }
406 |
407 | function color(name) {
408 | return TestResultColors[name] || "black";
409 | }
410 |
411 | function resultAwareSort(names) {
412 | const resultOrder = [
413 | TestResult.PASSED,
414 | TestResult.FAILED,
415 | TestResult.SKIPPED,
416 | TestResult.PROCESS_ERROR,
417 | TestResult.TODO_ERROR,
418 | TestResult.METADATA_ERROR,
419 | TestResult.HARNESS_ERROR,
420 | TestResult.TIMEOUT_ERROR,
421 | TestResult.RUNNER_EXCEPTION,
422 | TestResult.DURATION,
423 | ];
424 |
425 | return names.sort((a, b) => {
426 | const aIndex = resultOrder.indexOf(a);
427 | const bIndex = resultOrder.indexOf(b);
428 | return aIndex - bIndex;
429 | });
430 | }
431 |
432 | function generateStatus(aggregatedResults) {
433 | const status = Object.keys(aggregatedResults)
434 | .sort()
435 | .reduce((acc, mode) => {
436 | const stats = aggregatedResults[mode];
437 | const total = Object.keys(stats).reduce(
438 | (acc, name) => acc + stats[name],
439 | 0
440 | );
441 | if (total === 0) return acc;
442 | acc.push(`
443 |
${mode}
444 |
445 | ${resultAwareSort(Object.keys(stats))
446 | .map((x) => {
447 | const percentTotal = ((100 * stats[x]) / total).toFixed(2);
448 | const toolTip = `${TestResultLabels[x]}: ${stats[x]} / ${total} (${percentTotal}%)`;
449 | const barColor = color(x);
450 | return `
`;
451 | })
452 | .join("")}
453 |
454 |
`);
455 | return acc;
456 | }, []);
457 | return status.join(" ");
458 | }
459 |
460 | document.addEventListener("DOMContentLoaded", () => {
461 | const promises = [];
462 | for (const [path, mode] of window.config.loadPathsAndModes) {
463 | promises.push(
464 | fetchData(path)
465 | .then((response) => response.json())
466 | .then((data) => initialize(data, mode))
467 | );
468 | }
469 | Promise.all(promises).then(() => generateResults());
470 | });
471 |
--------------------------------------------------------------------------------
/test262/test-config.js:
--------------------------------------------------------------------------------
1 | const style = getComputedStyle(document.body);
2 |
3 | const TestResult = {
4 | PASSED: "passed",
5 | FAILED: "failed",
6 | SKIPPED: "skipped",
7 | METADATA_ERROR: "metadata_error",
8 | HARNESS_ERROR: "harness_error",
9 | TIMEOUT_ERROR: "timeout_error",
10 | PROCESS_ERROR: "process_error",
11 | RUNNER_EXCEPTION: "runner_exception",
12 | TODO_ERROR: "todo_error",
13 | DURATION: "duration",
14 | };
15 |
16 | const TestResultColors = {
17 | [TestResult.PASSED]: style.getPropertyValue("--color-chart-passed"),
18 | [TestResult.FAILED]: style.getPropertyValue("--color-chart-failed"),
19 | [TestResult.SKIPPED]: style.getPropertyValue("--color-chart-skipped"),
20 | [TestResult.METADATA_ERROR]: style.getPropertyValue(
21 | "--color-chart-metadata-error"
22 | ),
23 | [TestResult.HARNESS_ERROR]: style.getPropertyValue(
24 | "--color-chart-harness-error"
25 | ),
26 | [TestResult.TIMEOUT_ERROR]: style.getPropertyValue(
27 | "--color-chart-timeout-error"
28 | ),
29 | [TestResult.PROCESS_ERROR]: style.getPropertyValue(
30 | "--color-chart-process-error"
31 | ),
32 | [TestResult.RUNNER_EXCEPTION]: style.getPropertyValue(
33 | "--color-chart-runner-exception"
34 | ),
35 | [TestResult.TODO_ERROR]: style.getPropertyValue("--color-chart-todo-error"),
36 | };
37 |
38 | const TestResultLabels = {
39 | [TestResult.PASSED]: "Passed",
40 | [TestResult.FAILED]: "Failed",
41 | [TestResult.SKIPPED]: "Skipped",
42 | [TestResult.METADATA_ERROR]: "Metadata failed to parse",
43 | [TestResult.HARNESS_ERROR]: "Harness file failed to parse or run",
44 | [TestResult.TIMEOUT_ERROR]: "Timed out",
45 | [TestResult.PROCESS_ERROR]: "Crashed",
46 | [TestResult.RUNNER_EXCEPTION]: "Unhandled runner exception",
47 | [TestResult.TODO_ERROR]: "Not yet implemented",
48 | [TestResult.DURATION]: "Duration (seconds)",
49 | };
50 |
--------------------------------------------------------------------------------
/wasm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LibWasm Spec Test results
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 | LibWasm Spec test results
18 |
19 | Introduction
20 |
21 | These are the results of the
22 | Ladybird
28 | WebAssembly library, LibWasm, running the WebAssembly
29 | testsuite . All tests are re-run and the results updated automatically for
35 | every push to the master branch of the repository on GitHub. Dates and
36 | times are shown in your browser's timezone.
37 |
38 |
39 | If you have any questions or want to help out with improving these
40 | test scores, feel free to get in touch on the
41 | Ladybird Discord server .
47 |
48 |
49 |
55 |
56 | Source code & Data
57 | Source code:
58 |
84 | Data (JSON):
85 |
103 |
104 |
105 | Results
106 | Loading...
107 |
108 |
109 |
110 |
111 |
112 | Performance
113 |
114 |
115 |
116 |
117 |
118 | Performance per test
119 |
120 |
121 |
122 |
123 |
124 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/wasm/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (() => {
4 | const { DateTime, Duration } = luxon;
5 |
6 | const backgroundColor = style.getPropertyValue("--color-background");
7 | const textColor = style.getPropertyValue("--color-text");
8 | const chartBorderColor = style.getPropertyValue("--color-chart-border");
9 | const fontFamily = style.getPropertyValue("font-family");
10 | const fontSize = parseInt(
11 | style.getPropertyValue("font-size").slice(0, -2),
12 | 10
13 | );
14 |
15 | Chart.defaults.borderColor = textColor;
16 | Chart.defaults.color = textColor;
17 | Chart.defaults.font.family = fontFamily;
18 | Chart.defaults.font.size = fontSize;
19 |
20 | // place tooltip's origin point under the cursor
21 | const tooltipPlugin = Chart.registry.getPlugin("tooltip");
22 | tooltipPlugin.positioners.underCursor = function (elements, eventPosition) {
23 | const pos = tooltipPlugin.positioners.average(elements);
24 |
25 | if (pos === false) {
26 | return false;
27 | }
28 |
29 | return {
30 | x: pos.x,
31 | y: eventPosition.y,
32 | };
33 | };
34 |
35 | class LineWithVerticalHoverLineController extends Chart.LineController {
36 | draw() {
37 | super.draw(arguments);
38 |
39 | if (!this.chart.tooltip._active.length) return;
40 |
41 | const { x } = this.chart.tooltip._active[0].element;
42 | const { top: topY, bottom: bottomY } = this.chart.chartArea;
43 | const ctx = this.chart.ctx;
44 |
45 | ctx.save();
46 | ctx.beginPath();
47 | ctx.moveTo(x, topY);
48 | ctx.lineTo(x, bottomY);
49 | ctx.lineWidth = 1;
50 | ctx.strokeStyle = chartBorderColor;
51 | ctx.stroke();
52 | ctx.restore();
53 | }
54 | }
55 |
56 | LineWithVerticalHoverLineController.id = "lineWithVerticalHoverLine";
57 | LineWithVerticalHoverLineController.defaults = Chart.LineController.defaults;
58 | Chart.register(LineWithVerticalHoverLineController);
59 |
60 | // This is when we started running the tests on Idan's self-hosted runner. Before that,
61 | // durations varied a lot across runs. See https://github.com/SerenityOS/serenity/pull/7718.
62 | const PERFORMANCE_CHART_START_DATE_TIME = DateTime.fromISO("2021-07-04");
63 |
64 | function prepareDataForCharts(data) {
65 | const charts = {
66 | [""]: {
67 | data: {
68 | [TestResult.PASSED]: [],
69 | [TestResult.FAILED]: [],
70 | [TestResult.SKIPPED]: [],
71 | [TestResult.METADATA_ERROR]: [],
72 | [TestResult.HARNESS_ERROR]: [],
73 | [TestResult.TIMEOUT_ERROR]: [],
74 | [TestResult.PROCESS_ERROR]: [],
75 | [TestResult.RUNNER_EXCEPTION]: [],
76 | [TestResult.TODO_ERROR]: [],
77 | [TestResult.DURATION]: [],
78 | },
79 | datasets: [],
80 | metadata: [],
81 | },
82 | ["performance"]: {
83 | data: {
84 | [TestResult.DURATION]: [],
85 | },
86 | datasets: [],
87 | metadata: [],
88 | },
89 | ["performance-per-test"]: {
90 | data: {
91 | [TestResult.DURATION]: [],
92 | },
93 | datasets: [],
94 | metadata: [],
95 | },
96 | };
97 |
98 | console.log(data);
99 | for (const entry of data) {
100 | const test = entry.tests["spectest"];
101 | const results = test.results;
102 | charts[""].metadata.push({
103 | commitTimestamp: entry.commit_timestamp,
104 | runTimestamp: entry.run_timestamp,
105 | duration: test.duration,
106 | versions: entry.versions,
107 | total: results.total,
108 | });
109 | for (const testResult in charts[""].data) {
110 | if (testResult === TestResult.DURATION) {
111 | continue;
112 | }
113 | charts[""].data[testResult].push({
114 | x: entry.commit_timestamp * 1000,
115 | y: results[testResult] || 0,
116 | });
117 | }
118 |
119 | const dt = DateTime.fromSeconds(entry.commit_timestamp);
120 | if (dt < PERFORMANCE_CHART_START_DATE_TIME) {
121 | continue;
122 | }
123 |
124 | // chart-performance
125 | const performanceTests = test;
126 | const performanceChart = charts["performance"];
127 | const performanceResults = performanceTests?.results;
128 | if (performanceResults) {
129 | performanceChart.metadata.push({
130 | commitTimestamp: entry.commit_timestamp,
131 | runTimestamp: entry.run_timestamp,
132 | duration: performanceTests.duration,
133 | versions: entry.versions,
134 | total: performanceResults.total,
135 | });
136 | performanceChart.data["duration"].push({
137 | x: entry.commit_timestamp * 1000,
138 | y: performanceTests.duration,
139 | });
140 | }
141 |
142 | // chart-performance-per-test
143 | const performancePerTestTests = test;
144 | const performancePerTestChart = charts["performance-per-test"];
145 | const performancePerTestResults = performancePerTestTests?.results;
146 | if (performancePerTestResults) {
147 | performancePerTestChart.metadata.push({
148 | commitTimestamp: entry.commit_timestamp,
149 | runTimestamp: entry.run_timestamp,
150 | duration:
151 | performancePerTestTests.duration / performancePerTestResults.total,
152 | versions: entry.versions,
153 | total: performancePerTestResults.total,
154 | });
155 | performancePerTestChart.data["duration"].push({
156 | x: entry.commit_timestamp * 1000,
157 | y: performancePerTestTests.duration / performancePerTestResults.total,
158 | });
159 | }
160 | }
161 |
162 | for (const chart in charts) {
163 | for (const testResult in charts[chart].data) {
164 | charts[chart].datasets.push({
165 | label: TestResultLabels[testResult],
166 | data: charts[chart].data[testResult],
167 | backgroundColor: TestResultColors[testResult],
168 | borderWidth: 2,
169 | borderColor: chartBorderColor,
170 | pointRadius: 0,
171 | pointHoverRadius: 0,
172 | fill: true,
173 | });
174 | }
175 | delete charts[chart].data;
176 | }
177 |
178 | return { charts };
179 | }
180 |
181 | function initializeChart(
182 | element,
183 | { datasets, metadata },
184 | { xAxisTitle = "Time", yAxisTitle = "Number of tests" } = {}
185 | ) {
186 | const ctx = element.getContext("2d");
187 |
188 | new Chart(ctx, {
189 | type: "lineWithVerticalHoverLine",
190 | data: {
191 | datasets,
192 | },
193 | options: {
194 | parsing: false,
195 | normalized: true,
196 | responsive: true,
197 | maintainAspectRatio: false,
198 | animation: false,
199 | plugins: {
200 | zoom: {
201 | zoom: {
202 | mode: "x",
203 | wheel: {
204 | enabled: true,
205 | },
206 | },
207 | pan: {
208 | enabled: true,
209 | mode: "x",
210 | },
211 | },
212 | hover: {
213 | mode: "index",
214 | intersect: false,
215 | },
216 | tooltip: {
217 | mode: "index",
218 | intersect: false,
219 | usePointStyle: true,
220 | boxWidth: 12,
221 | boxHeight: 12,
222 | padding: 20,
223 | position: "underCursor",
224 | titleColor: textColor,
225 | bodyColor: textColor,
226 | footerColor: textColor,
227 | footerFont: { weight: "normal" },
228 | footerMarginTop: 20,
229 | backgroundColor: backgroundColor,
230 | callbacks: {
231 | title: () => {
232 | return null;
233 | },
234 | beforeBody: (context) => {
235 | const { dataIndex } = context[0];
236 | const { total } = metadata[dataIndex];
237 | const formattedValue = total.toLocaleString("en-US");
238 | // Leading spaces to make up for missing color circle
239 | return ` Number of tests: ${formattedValue}`;
240 | },
241 | label: (context) => {
242 | // Space as padding between color circle and label
243 | const formattedValue = context.parsed.y.toLocaleString("en-US");
244 | if (
245 | context.dataset.label !==
246 | TestResultLabels[TestResult.DURATION]
247 | ) {
248 | const { total } = metadata[context.dataIndex];
249 | const percentOfTotal = (
250 | (context.parsed.y / total) *
251 | 100
252 | ).toFixed(2);
253 | return ` ${context.dataset.label}: ${formattedValue} (${percentOfTotal}%)`;
254 | } else {
255 | return ` ${context.dataset.label}: ${formattedValue}`;
256 | }
257 | },
258 |
259 | footer: (context) => {
260 | const { dataIndex } = context[0];
261 | const {
262 | commitTimestamp,
263 | duration: durationSeconds,
264 | versions,
265 | } = metadata[dataIndex];
266 | const dateTime = DateTime.fromSeconds(commitTimestamp);
267 | const duration = Duration.fromMillis(durationSeconds * 1000);
268 | const ladybirdVersion = versions.serenity.substring(0, 7);
269 | return `\
270 | Committed on ${dateTime.toLocaleString(DateTime.DATETIME_SHORT)}, \
271 | run took ${duration.toISOTime()}
272 |
273 | Versions: ladybird@${ladybirdVersion}`;
274 | },
275 | },
276 | },
277 | legend: {
278 | align: "end",
279 | labels: {
280 | usePointStyle: true,
281 | boxWidth: 10,
282 | // Only include passed, failed, TODO, and crashed in the legend
283 | filter: ({ text }) =>
284 | text === TestResultLabels[TestResult.PASSED] ||
285 | text === TestResultLabels[TestResult.FAILED] ||
286 | text === TestResultLabels[TestResult.TODO_ERROR] ||
287 | text === TestResultLabels[TestResult.PROCESS_ERROR],
288 | },
289 | },
290 | },
291 | scales: {
292 | x: {
293 | type: "time",
294 | title: {
295 | display: true,
296 | text: xAxisTitle,
297 | },
298 | grid: {
299 | borderColor: textColor,
300 | color: "transparent",
301 | borderWidth: 2,
302 | },
303 | },
304 | y: {
305 | stacked: true,
306 | beginAtZero: true,
307 | title: {
308 | display: true,
309 | text: yAxisTitle,
310 | },
311 | grid: {
312 | borderColor: textColor,
313 | color: chartBorderColor,
314 | borderWidth: 2,
315 | },
316 | },
317 | },
318 | },
319 | });
320 | }
321 |
322 | function initializeSummary(
323 | element,
324 | runTimestamp,
325 | commitHash,
326 | durationSeconds,
327 | results
328 | ) {
329 | const dateTime = DateTime.fromSeconds(runTimestamp);
330 | const duration = Duration.fromMillis(durationSeconds * 1000);
331 | const passed = results[TestResult.PASSED];
332 | const total = results.total;
333 | const percent = ((passed / total) * 100).toFixed(2);
334 | element.innerHTML = `
335 | The last test run was on
336 | ${dateTime.toLocaleString(DateTime.DATETIME_SHORT)}
337 | for commit
338 |
339 |
345 | ${commitHash.slice(0, 7)}
346 |
347 |
348 | and took ${duration.toISOTime()} .
349 | ${passed} of ${total} tests passed, i.e. ${percent}% .
350 | `;
351 | }
352 |
353 | function initialize(data) {
354 | const { charts } = prepareDataForCharts(data);
355 | initializeChart(document.getElementById("chart"), charts[""]);
356 | initializeChart(
357 | document.getElementById("chart-performance"),
358 | charts["performance"],
359 | { yAxisTitle: TestResultLabels[TestResult.DURATION] }
360 | );
361 | initializeChart(
362 | document.getElementById("chart-performance-per-test"),
363 | charts["performance-per-test"],
364 | { yAxisTitle: TestResultLabels[TestResult.DURATION] }
365 | );
366 | const last = data.slice(-1)[0];
367 | if (last) {
368 | initializeSummary(
369 | document.getElementById("summary"),
370 | last.run_timestamp,
371 | last.versions.serenity,
372 | last.tests["spectest"].duration,
373 | last.tests["spectest"].results
374 | );
375 | }
376 | }
377 |
378 | document.addEventListener("DOMContentLoaded", () => {
379 | fetchData("wasm/results.json")
380 | .then((response) => response.json())
381 | .then((data) => {
382 | data.sort((a, b) =>
383 | a.commit_timestamp === b.commit_timestamp
384 | ? 0
385 | : a.commit_timestamp < b.commit_timestamp
386 | ? -1
387 | : 1
388 | );
389 | return data;
390 | })
391 | .then((data) => initialize(data));
392 | });
393 | })();
394 |
--------------------------------------------------------------------------------
/wasm/per-file/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LibWasm spec test per-file results
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Search:
22 |
23 |
24 |
25 | Filter mode:
26 |
27 |
28 | Per-file results
29 |
30 |
Loading...
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
56 |
57 |
58 |
59 |
72 |
73 |
74 |
75 |
76 |
81 |
82 | (Open on GitHub )
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
101 |
102 |
103 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/wasm/per-file/wasm-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LadybirdBrowser/libjs-website/7466d45dfe1e42867eefe219d3f2478b0dc42720/wasm/per-file/wasm-icon.png
--------------------------------------------------------------------------------