| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>R8 perf</title> | |
| </head> | |
| <body> | |
| <div id="benchmark-selectors"></div> | |
| <div> | |
| <canvas id="myChart"></canvas> | |
| </div> | |
| <div> | |
| <div style="float: left; width: 50%"> | |
| <button type="button" id="show-more-left" disabled>⇐</button> | |
| <button type="button" id="show-less-left">⇒</button> | |
| </div> | |
| <div style="float: left; text-align: right; width: 50%"> | |
| <button type="button" id="show-less-right">⇐</button> | |
| <button type="button" id="show-more-right" disabled>⇒</button> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script> | |
| <script type="module"> | |
| // Utility methods. | |
| Array.prototype.any = function(predicate) { | |
| for (const element of this.values()) { | |
| if (predicate(element)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| Array.prototype.first = function() { | |
| return this[0]; | |
| }; | |
| Array.prototype.avg = function() { | |
| return this.reduce(function(x, y) { return x + y; }, 0) / this.length; | |
| }; | |
| Array.prototype.min = function() { | |
| return this.reduce(function(x, y) { return x === null ? y : Math.min(x, y); }, null); | |
| }; | |
| Array.prototype.reverseInPlace = function() { | |
| for (var i = 0; i < Math.floor(this.length / 2); i++) { | |
| var temp = this[i]; | |
| this[i] = this[this.length - i - 1]; | |
| this[this.length - i - 1] = temp; | |
| } | |
| }; | |
| Number.prototype.ns_to_s = function() { | |
| const seconds = this/10E8; | |
| const seconds_with_one_decimal = Math.round(seconds*10)/10; | |
| return seconds; | |
| }; | |
| // Import and reverse commits so that newest are last. | |
| import commits from "./benchmark_data.json" with { type: "json" }; | |
| commits.reverseInPlace(); | |
| // Amend the commits with their unique index. | |
| for (var i = 0; i < commits.length; i++) { | |
| commits[i].index = i; | |
| } | |
| // DOM references. | |
| const benchmarkSelectors = document.getElementById('benchmark-selectors'); | |
| const canvas = document.getElementById('myChart'); | |
| const showMoreLeft = document.getElementById('show-more-left'); | |
| const showLessLeft = document.getElementById('show-less-left'); | |
| const showLessRight = document.getElementById('show-less-right'); | |
| const showMoreRight = document.getElementById('show-more-right'); | |
| // Initialize benchmark selectors. | |
| const benchmarks = new Set(); | |
| for (const commit of commits.values()) { | |
| for (const benchmark in commit.benchmarks) { | |
| benchmarks.add(benchmark); | |
| } | |
| } | |
| const selectedBenchmarks = new Set(); | |
| const urlOptions = unescape(window.location.hash.substring(1)).split(','); | |
| for (const benchmark of benchmarks.values()) { | |
| for (const filter of urlOptions.values()) { | |
| if (filter) { | |
| const match = benchmark.match(new RegExp(filter.replace("*", ".*"))); | |
| if (match) { | |
| selectedBenchmarks.add(benchmark); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| if (selectedBenchmarks.size == 0) { | |
| selectedBenchmarks.add(benchmarks.values().next().value); | |
| } | |
| for (const benchmark of benchmarks.values()) { | |
| const input = document.createElement('input'); | |
| input.type = 'checkbox'; | |
| input.name = 'benchmark'; | |
| input.id = benchmark; | |
| input.value = benchmark; | |
| input.checked = selectedBenchmarks.has(benchmark); | |
| input.onchange = function (e) { | |
| if (e.target.checked) { | |
| selectedBenchmarks.add(e.target.value); | |
| } else { | |
| selectedBenchmarks.delete(e.target.value); | |
| } | |
| updateChart(true, false); | |
| }; | |
| const label = document.createElement('label'); | |
| label.id = benchmark + 'Label'; | |
| label.htmlFor = benchmark; | |
| label.innerHTML = benchmark; | |
| benchmarkSelectors.appendChild(input); | |
| benchmarkSelectors.appendChild(label); | |
| } | |
| // Chart data provider. | |
| function getData() { | |
| const filteredCommits = commits.slice(zoom.left, zoom.right); | |
| const labels = filteredCommits.map((c, i) => c.index); | |
| const datasets = []; | |
| for (const selectedBenchmark of selectedBenchmarks.values()) { | |
| const codeSizeData = | |
| filteredCommits.map( | |
| (c, i) => | |
| selectedBenchmark in filteredCommits[i].benchmarks | |
| ? filteredCommits[i] | |
| .benchmarks[selectedBenchmark] | |
| .results | |
| .first() | |
| .code_size | |
| : NaN); | |
| const codeSizeScatterData = []; | |
| for (const commit of filteredCommits.values()) { | |
| if (!(selectedBenchmark in commit.benchmarks)) { | |
| continue; | |
| } | |
| const codeSizes = | |
| commit.benchmarks[selectedBenchmark].results.map(result => result.code_size) | |
| const expectedCodeSize = codeSizes.first(); | |
| if (codeSizes.any(codeSize => codeSize != expectedCodeSize)) { | |
| const seen = new Set(); | |
| seen.add(expectedCodeSize); | |
| for (const codeSize of codeSizes.values()) { | |
| if (!seen.has(codeSize)) { | |
| codeSizeScatterData.push({ x: commit.index, y: codeSize }); | |
| seen.add(codeSize); | |
| } | |
| } | |
| } | |
| } | |
| const runtimeData = | |
| filteredCommits.map( | |
| (c, i) => | |
| selectedBenchmark in filteredCommits[i].benchmarks | |
| ? filteredCommits[i] | |
| .benchmarks[selectedBenchmark] | |
| .results | |
| .map(result => result.runtime) | |
| .min() | |
| .ns_to_s() | |
| : NaN); | |
| const runtimeScatterData = []; | |
| for (const commit of filteredCommits.values()) { | |
| if (!(selectedBenchmark in commit.benchmarks)) { | |
| continue; | |
| } | |
| const runtimes = | |
| commit.benchmarks[selectedBenchmark].results.map(result => result.runtime) | |
| for (const runtime of runtimes.values()) { | |
| runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() }); | |
| } | |
| } | |
| const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined; | |
| datasets.push(...[ | |
| { | |
| benchmark: selectedBenchmark, | |
| type: 'line', | |
| label: 'Code size', | |
| data: codeSizeData, | |
| tension: 0.1, | |
| segment: { | |
| borderColor: ctx => | |
| skipped( | |
| ctx, | |
| myChart | |
| ? myChart.data.datasets[ctx.datasetIndex].backgroundColor | |
| : undefined), | |
| borderDash: ctx => skipped(ctx, [6, 6]), | |
| }, | |
| spanGaps: true | |
| }, | |
| { | |
| benchmark: selectedBenchmark, | |
| type: 'scatter', | |
| label: 'Nondeterminism', | |
| data: codeSizeScatterData, | |
| radius: 6, | |
| pointBackgroundColor: 'red' | |
| }, | |
| { | |
| benchmark: selectedBenchmark, | |
| type: 'line', | |
| label: 'Runtime', | |
| data: runtimeData, | |
| tension: 0.1, | |
| yAxisID: 'y2', | |
| segment: { | |
| borderColor: ctx => | |
| skipped(ctx, myChart.data.datasets[ctx.datasetIndex].backgroundColor), | |
| borderDash: ctx => skipped(ctx, [6, 6]), | |
| }, | |
| spanGaps: true | |
| }, | |
| { | |
| benchmark: selectedBenchmark, | |
| type: 'scatter', | |
| label: 'Runtime variance', | |
| data: runtimeScatterData, | |
| yAxisID: 'y2' | |
| } | |
| ]); | |
| } | |
| return { | |
| labels: labels, | |
| datasets: datasets, | |
| }; | |
| } | |
| // Legend tracking. | |
| const legends = | |
| new Set(['Code size', 'Nondeterminism', 'Runtime', 'Runtime variance']); | |
| const selectedLegends = | |
| new Set( | |
| unescape(window.location.hash.substring(1)) | |
| .split(',') | |
| .filter(l => legends.has(l))); | |
| if (selectedLegends.size == 0) { | |
| legends.forEach(l => selectedLegends.add(l)); | |
| } | |
| // Chart options. | |
| const options = { | |
| onHover: (event, chartElement) => | |
| event.native.target.style.cursor = | |
| chartElement[0] ? 'pointer' : 'default', | |
| plugins: { | |
| legend: { | |
| labels: { | |
| filter: function(legendItem, data) { | |
| // Only retain the legends for the first selected benchmark. If | |
| // multiple benchmarks are selected, then use the legends of the | |
| // first selected benchmark to control all selected benchmarks. | |
| const numUniqueLegends = | |
| data.datasets.length / selectedBenchmarks.size; | |
| return legendItem.datasetIndex < numUniqueLegends; | |
| }, | |
| }, | |
| onClick: function legendClickHandler(e, legendItem, legend) { | |
| const clickedLegend = legendItem.text; | |
| if (selectedLegends.has(clickedLegend)) { | |
| selectedLegends.delete(clickedLegend); | |
| } else { | |
| selectedLegends.add(clickedLegend); | |
| } | |
| updateChart(false, true); | |
| }, | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| title: (context) => { | |
| const elementInfo = context[0]; | |
| var commit; | |
| if (elementInfo.dataset.type == 'line') { | |
| commit = commits[zoom.left + elementInfo.dataIndex]; | |
| } else { | |
| console.assert(elementInfo.dataset.type == 'scatter'); | |
| commit = commits[elementInfo.raw.x]; | |
| } | |
| return commit.title; | |
| }, | |
| footer: (context) => { | |
| const elementInfo = context[0]; | |
| var commit; | |
| if (elementInfo.dataset.type == 'line') { | |
| commit = commits[zoom.left + elementInfo.dataIndex]; | |
| } else { | |
| console.assert(elementInfo.dataset.type == 'scatter'); | |
| commit = commits[elementInfo.raw.x]; | |
| } | |
| const dataset = myChart.data.datasets[elementInfo.datasetIndex]; | |
| return `App: ${dataset.benchmark}\n` | |
| + `Author: ${commit.author}\n` | |
| + `Submitted: ${new Date(commit.submitted * 1000).toLocaleString()}\n` | |
| + `Hash: ${commit.hash}\n` | |
| + `Index: ${commit.index}`; | |
| } | |
| } | |
| } | |
| }, | |
| responsive: true, | |
| scales: { | |
| x: {}, | |
| y: { | |
| position: 'left', | |
| title: { | |
| display: true, | |
| text: 'Code size (bytes)' | |
| } | |
| }, | |
| y2: { | |
| position: 'right', | |
| title: { | |
| display: true, | |
| text: 'Runtime (seconds)' | |
| } | |
| } | |
| } | |
| }; | |
| // Setup click handler. | |
| canvas.onclick = function (event) { | |
| const points = | |
| myChart.getElementsAtEventForMode( | |
| event, 'nearest', { intersect: true }, true); | |
| if (points.length > 0) { | |
| const point = points[0]; | |
| const commit = commits[point.index]; | |
| window.open('https://r8.googlesource.com/r8/+/' + commit.hash, '_blank'); | |
| } | |
| }; | |
| // Setup chart navigation. | |
| var zoom = { left: 0, right: commits.length }; | |
| for (const urlOption of urlOptions.values()) { | |
| if (urlOption.startsWith('L')) { | |
| var left = parseInt(urlOption.substring(1)); | |
| if (isNaN(left)) { | |
| continue; | |
| } | |
| left = left >= 0 ? left : commits.length + left; | |
| if (left < 0) { | |
| zoom.left = 0; | |
| } else if (left >= commits.length) { | |
| zoom.left = commits.length - 1; | |
| } else { | |
| zoom.left = left; | |
| } | |
| } | |
| } | |
| showMoreLeft.onclick = function (event) { | |
| if (zoom.left == 0) { | |
| return; | |
| } | |
| const currentSize = zoom.right - zoom.left; | |
| zoom.left = zoom.left - currentSize; | |
| if (zoom.left < 0) { | |
| zoom.left = 0; | |
| } | |
| updateChart(true, false); | |
| }; | |
| showLessLeft.onclick = function (event) { | |
| const currentSize = zoom.right - zoom.left; | |
| zoom.left = zoom.left + Math.floor(currentSize / 2); | |
| if (zoom.left >= zoom.right) { | |
| zoom.left = zoom.right - 1; | |
| } | |
| updateChart(true, false); | |
| }; | |
| showLessRight.onclick = function (event) { | |
| if (zoom.right == 0) { | |
| return; | |
| } | |
| const currentSize = zoom.right - zoom.left; | |
| zoom.right = zoom.right - Math.floor(currentSize / 2); | |
| if (zoom.right < zoom.left) { | |
| zoom.right = zoom.left; | |
| } | |
| updateChart(true, false); | |
| }; | |
| showMoreRight.onclick = function (event) { | |
| const currentSize = zoom.right - zoom.left; | |
| zoom.right = zoom.right + currentSize; | |
| if (zoom.right > commits.length) { | |
| zoom.right = commits.length; | |
| } | |
| updateChart(true, false); | |
| }; | |
| function updateChart(dataChanged, legendsChanged) { | |
| console.assert(zoom.left <= zoom.right); | |
| // Update datasets. | |
| if (dataChanged) { | |
| const newData = getData(); | |
| Object.assign(myChart.data, newData); | |
| // Update chart. | |
| myChart.update(); | |
| } | |
| // Update legends. | |
| if (legendsChanged || (dataChanged && selectedLegends.size < legends.size)) { | |
| for (var datasetIndex = 0; | |
| datasetIndex < myChart.data.datasets.length; | |
| datasetIndex++) { | |
| const datasetMeta = myChart.getDatasetMeta(datasetIndex); | |
| datasetMeta.hidden = !selectedLegends.has(datasetMeta.label); | |
| } | |
| // Update chart. | |
| myChart.update(); | |
| } | |
| // Update checkbox colors. | |
| const benchmarkColors = {}; | |
| for (var datasetIndex = 0; | |
| datasetIndex < myChart.data.datasets.length; | |
| datasetIndex++) { | |
| if (myChart.getDatasetMeta(datasetIndex).hidden) { | |
| continue; | |
| } | |
| const dataset = myChart.data.datasets[datasetIndex]; | |
| const benchmark = dataset.benchmark; | |
| const benchmarkColor = dataset.borderColor; | |
| if (!(benchmark in benchmarkColors)) { | |
| benchmarkColors[benchmark] = benchmarkColor; | |
| } | |
| } | |
| for (const benchmark of benchmarks.values()) { | |
| const benchmarkLabel = document.getElementById(benchmark + 'Label'); | |
| const benchmarkColor = benchmarkColors[benchmark] || '#000000'; | |
| const benchmarkFontWeight = benchmark in benchmarkColors ? 'bold' : 'normal'; | |
| benchmarkLabel.style.color = benchmarkColor; | |
| benchmarkLabel.style.fontWeight = benchmarkFontWeight; | |
| } | |
| // Update navigation. | |
| showMoreLeft.disabled = zoom.left == 0; | |
| showLessLeft.disabled = zoom.left == zoom.right - 1; | |
| showLessRight.disabled = zoom.left == zoom.right - 1; | |
| showMoreRight.disabled = zoom.right == commits.length; | |
| // Update hash. | |
| window.location.hash = | |
| Array.from(selectedBenchmarks) | |
| .concat( | |
| selectedLegends.size == legends.size | |
| ? [] | |
| : Array.from(selectedLegends)) | |
| .join(','); | |
| } | |
| // Create chart. | |
| const myChart = new Chart(canvas, { | |
| data: getData(), | |
| options: options | |
| }); | |
| // Hide disabled legends. | |
| if (selectedLegends.size < legends.size) { | |
| updateChart(false, true); | |
| } else { | |
| updateChart(false, false); | |
| } | |
| </script> | |
| </body> | |
| </html> |