├── .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 |
// Website for Ladybird's JavaScript engine, LibJS.
35 | //
36 | // For details visit https://github.com/LadybirdBrowser/ladybird.
37 | //
38 | // Continuously updated test262 results are available at /test262.
39 | // Continuously updated Wasm results are available at /wasm.
40 | //
41 | // If you're a Ladybird contributor and want to change something
42 | // here, open a PR in this repository.
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 | 42 |
43 | Loading... 44 | 45 |
46 |
47 | 56 | 62 | 68 | 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 |
48 |

Per-file results

49 |

50 | Click here! 51 |

52 |
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 | 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 | 24 | 28 |

Per-file results

29 |
30 | Loading... 31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 | 56 | 57 | 58 | 59 | 68 | 69 | 70 | 85 | 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 |
50 |

Per-file results

51 |

52 | Click here! 53 |

54 |
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 | 24 | 28 |

Per-file results

29 |
30 | Loading... 31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 | 56 | 57 | 58 | 59 | 72 | 73 | 74 | 93 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /wasm/per-file/wasm-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LadybirdBrowser/libjs-website/7466d45dfe1e42867eefe219d3f2478b0dc42720/wasm/per-file/wasm-icon.png --------------------------------------------------------------------------------