From 3a4cb0667332c2d2182c4bb7ab5d19f8d4d63d9c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 7 Jul 2026 16:01:27 +0200 Subject: [PATCH 1/6] Improve benchmark chart rendering Merge backend benchmark series into shared charts and align each platform's charts to the same latest-100 commit axis. Use logarithmic y-axes with compact tick labels and skip missing aligned values cleanly. --- index.html | 293 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 249 insertions(+), 44 deletions(-) diff --git a/index.html b/index.html index 64b6f222a7..32e3ef6ccf 100644 --- a/index.html +++ b/index.html @@ -126,25 +126,81 @@ _: '#333333' }; - const benchColors = { - Windows: '#357ec7', - macOS: '#717378', - Linux: '#e95420', + const seriesColors = { + inproc: '#0072b2', + breakpad: '#009e73', + crashpad: '#d55e00', + native: '#cc79a7', }; + const maxVisibleDataPoints = 100; + function init() { - function collectBenchesPerTestCase(entries) { + function parseBenchName(name) { + const match = name.match(/^(.*) \(([^()]+)\)$/); + if (match) { + return { + chartName: match[1], + seriesName: match[2], + }; + } + return { + chartName: name, + seriesName: name, + }; + } + + function createChartData() { + return { + points: [], + series: new Map(), + }; + } + + function entryKey(entry) { + return entry.commit.id + ':' + entry.date; + } + + function ensureSeries(chartData, name) { + let series = chartData.series.get(name); + if (series !== undefined) { + return series; + } + + series = { + name, + values: new Array(chartData.points.length).fill(null), + points: new Array(chartData.points.length).fill(null), + }; + chartData.series.set(name, series); + return series; + } + + function collectBenchesPerChart(entries) { const map = new Map(); + const points = entries.map(entry => ({ + commit: entry.commit, + date: entry.date, + tool: entry.tool, + })); + const pointIndexByEntry = new Map(points.map((point, index) => [entryKey(point), index])); + for (const entry of entries) { const {commit, date, tool, benches} = entry; for (const bench of benches) { const result = { commit, date, tool, bench }; - const arr = map.get(bench.name); - if (arr === undefined) { - map.set(bench.name, [result]); - } else { - arr.push(result); + const {chartName, seriesName} = parseBenchName(bench.name); + let chartData = map.get(chartName); + if (chartData === undefined) { + chartData = createChartData(); + chartData.points = points; + map.set(chartName, chartData); } + + const index = pointIndexByEntry.get(entryKey(result)); + const series = ensureSeries(chartData, seriesName); + series.values[index] = bench.value; + series.points[index] = result; } } return map; @@ -170,30 +226,177 @@ // Prepare data points for charts return Object.keys(data.entries).map(name => ({ name, - dataSet: collectBenchesPerTestCase(data.entries[name]), + dataSet: collectBenchesPerChart(data.entries[name]), })); } function renderAllChars(dataSets) { - function renderGraph(parent, name, dataset, color) { + function colorForSeries(name, index) { + if (seriesColors[name]) { + return seriesColors[name]; + } + const fallbackColors = [ + toolColors._, + '#f0e442', + '#56b4e9', + '#e69f00', + '#000000', + ]; + return fallbackColors[index % fallbackColors.length]; + } + + function firstLine(text) { + const newlineIndex = text.indexOf('\n'); + return newlineIndex === -1 ? text : text.slice(0, newlineIndex); + } + + function firstBench(chartData) { + for (const series of chartData.series.values()) { + for (const point of series.points) { + if (point) { + return point.bench; + } + } + } + return undefined; + } + + function seriesForTooltip(chartData, item) { + return Array.from(chartData.series.values())[item.datasetIndex]; + } + + function pointForTooltip(chartData, item) { + const series = seriesForTooltip(chartData, item); + return series ? series.points[item.index] : undefined; + } + + function chartValues(seriesData) { + const values = []; + for (const series of seriesData) { + for (const value of series.values) { + if (typeof value === 'number' && isFinite(value)) { + values.push(value); + } + } + } + return values; + } + + function shouldUseLogScale(seriesData) { + const values = chartValues(seriesData); + return values.length > 0 && values.every(value => value > 0); + } + + function trimTrailingZeros(value) { + return value.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, ''); + } + + function formatLogTick(value) { + const numericValue = Number(value); + if (!isFinite(numericValue) || numericValue <= 0) { + return ''; + } + + const exponent = Math.floor(Math.log10(numericValue)); + const mantissa = numericValue / Math.pow(10, exponent); + if (![1, 2, 5].some(value => Math.abs(mantissa - value) < 1e-10)) { + return ''; + } + + const absValue = Math.abs(numericValue); + if (absValue >= 1000) { + return trimTrailingZeros((numericValue / 1000).toFixed(absValue >= 10000 ? 0 : 1)) + 'k'; + } + if (absValue >= 100) { + return Math.round(numericValue).toString(); + } + if (absValue >= 10) { + return trimTrailingZeros(numericValue.toFixed(1)); + } + if (absValue >= 1) { + return trimTrailingZeros(numericValue.toFixed(2)); + } + if (absValue >= 0.001) { + return trimTrailingZeros(numericValue.toFixed(4)); + } + return trimTrailingZeros(numericValue.toPrecision(2)); + } + + function medianAxisLabel(unit, useLogScale) { + if (useLogScale) { + return 'Median' + (unit ? ` (${unit}, LOG)` : ' (LOG)'); + } + return 'Median' + (unit ? ` (${unit})` : ''); + } + + function lastVisibleChartData(chartData) { + const startIndex = Math.max(chartData.points.length - maxVisibleDataPoints, 0); + if (startIndex === 0) { + return chartData; + } + + const series = new Map(); + for (const [name, sourceSeries] of chartData.series.entries()) { + series.set(name, { + name, + values: sourceSeries.values.slice(startIndex), + points: sourceSeries.points.slice(startIndex), + }); + } + + return { + points: chartData.points.slice(startIndex), + series, + }; + } + + function renderGraph(parent, name, chartData) { const canvas = document.createElement('canvas'); canvas.className = 'benchmark-chart'; parent.appendChild(canvas); + const visibleData = lastVisibleChartData(chartData); + const seriesData = Array.from(visibleData.series.values()); const data = { - labels: dataset.map(d => d.commit.id.slice(0, 7)), - datasets: [ - { - label: name, - data: dataset.map(d => d.bench.value), + labels: visibleData.points.map(d => d.commit.id.slice(0, 7)), + datasets: seriesData.map((series, index) => { + const color = colorForSeries(series.name, index); + return { + label: series.name, + data: series.values.map(value => value === null ? NaN : value), borderColor: color, backgroundColor: color + '60', // Add alpha for #rrggbbaa - } - ], + fill: false, + spanGaps: false, + }; + }), + }; + const bench = firstBench(visibleData); + const unit = bench ? bench.unit : ''; + const useLogScale = shouldUseLogScale(seriesData); + const yAxis = { + scaleLabel: { + display: true, + labelString: medianAxisLabel(unit, useLogScale), + }, + ticks: {}, }; - const unit = dataset.length > 0 ? dataset[0].bench.unit : ''; + if (useLogScale) { + yAxis.type = 'logarithmic'; + yAxis.ticks.autoSkip = false; + yAxis.ticks.callback = formatLogTick; + } else { + yAxis.ticks.beginAtZero = true; + } const options = { + title: { + display: true, + text: name, + }, + legend: { + display: true, + }, scales: { xAxes: [ { @@ -203,36 +406,36 @@ }, } ], - yAxes: [ - { - scaleLabel: { - display: true, - labelString: 'Median' + (unit ? ` (${unit})` : ''), - }, - ticks: { - beginAtZero: true, - } - } - ], + yAxes: [yAxis], }, tooltips: { callbacks: { afterTitle: items => { - const {index} = items[0]; - const data = dataset[index]; - return '\n' + data.commit.message.slice(0, data.commit.message.indexOf('\n')) + '\n\n' + data.commit.timestamp + ' by @' + data.commit.author.username + '\n'; + const data = pointForTooltip(visibleData, items[0]); + if (!data) { + return ''; + } + return '\n' + firstLine(data.commit.message) + '\n\n' + data.commit.timestamp + ' by @' + data.commit.author.username + '\n'; }, label: item => { - let label = item.value; - const { range, unit } = dataset[item.index].bench; - label += ' ' + unit; + const data = pointForTooltip(visibleData, item); + if (!data) { + return ''; + } + const { range, unit } = data.bench; + let label = data.bench.value + ' ' + unit; if (range) { label += ' (' + range + ')'; } - return label; + const series = seriesForTooltip(visibleData, item); + return (series ? series.name : data.bench.name) + ': ' + label; }, afterLabel: item => { - const { extra } = dataset[item.index].bench; + const data = pointForTooltip(visibleData, item); + if (!data) { + return ''; + } + const { extra } = data.bench; return extra ? '\n' + extra : ''; } } @@ -243,8 +446,11 @@ } // XXX: Undocumented. How can we know the index? const index = activeElems[0]._index; - const url = dataset[index].commit.url; - window.open(url, '_blank'); + const datasetIndex = activeElems[0]._datasetIndex; + const data = seriesData[datasetIndex].points[index]; + if (data) { + window.open(data.commit.url, '_blank'); + } }, }; @@ -269,9 +475,8 @@ graphsElem.className = 'benchmark-graphs'; setElem.appendChild(graphsElem); - const color = benchColors[name]; - for (const [benchName, benches] of benchSet.entries()) { - renderGraph(graphsElem, benchName, benches, color) + for (const [benchName, chartData] of benchSet.entries()) { + renderGraph(graphsElem, benchName, chartData) } } From 09dfcb54a5367afe954a435a082ab8b1df8ed451 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 7 Jul 2026 16:18:42 +0200 Subject: [PATCH 2/6] fix --- index.html | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/index.html b/index.html index 32e3ef6ccf..ff3575465e 100644 --- a/index.html +++ b/index.html @@ -157,10 +157,6 @@ }; } - function entryKey(entry) { - return entry.commit.id + ':' + entry.date; - } - function ensureSeries(chartData, name) { let series = chartData.series.get(name); if (series !== undefined) { @@ -183,9 +179,8 @@ date: entry.date, tool: entry.tool, })); - const pointIndexByEntry = new Map(points.map((point, index) => [entryKey(point), index])); - for (const entry of entries) { + for (const [index, entry] of entries.entries()) { const {commit, date, tool, benches} = entry; for (const bench of benches) { const result = { commit, date, tool, bench }; @@ -197,7 +192,6 @@ map.set(chartName, chartData); } - const index = pointIndexByEntry.get(entryKey(result)); const series = ensureSeries(chartData, seriesName); series.values[index] = bench.value; series.points[index] = result; From 3987cc022801b8602dc4ccb153d4f9f04f6d4e59 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 7 Jul 2026 16:34:12 +0200 Subject: [PATCH 3/6] fix++ --- index.html | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index ff3575465e..73571e4784 100644 --- a/index.html +++ b/index.html @@ -262,7 +262,7 @@ function pointForTooltip(chartData, item) { const series = seriesForTooltip(chartData, item); - return series ? series.points[item.index] : undefined; + return series ? series.renderPoints[item.index] : undefined; } function chartValues(seriesData) { @@ -351,14 +351,26 @@ parent.appendChild(canvas); const visibleData = lastVisibleChartData(chartData); + const labels = visibleData.points.map(d => d.commit.id.slice(0, 7)); const seriesData = Array.from(visibleData.series.values()); const data = { - labels: visibleData.points.map(d => d.commit.id.slice(0, 7)), + labels, datasets: seriesData.map((series, index) => { const color = colorForSeries(series.name, index); + series.renderData = []; + series.renderPoints = []; + for (let pointIndex = 0; pointIndex < series.values.length; pointIndex++) { + const value = series.values[pointIndex]; + if (typeof value !== 'number' || !isFinite(value)) { + continue; + } + + series.renderData.push({ x: labels[pointIndex], y: value }); + series.renderPoints.push(series.points[pointIndex]); + } return { label: series.name, - data: series.values.map(value => value === null ? NaN : value), + data: series.renderData, borderColor: color, backgroundColor: color + '60', // Add alpha for #rrggbbaa fill: false, From 3a08cfaac965af7e52f5541592590030c6f63f28 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 7 Jul 2026 17:06:15 +0200 Subject: [PATCH 4/6] click --- index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 73571e4784..a01fb0c488 100644 --- a/index.html +++ b/index.html @@ -453,7 +453,8 @@ // XXX: Undocumented. How can we know the index? const index = activeElems[0]._index; const datasetIndex = activeElems[0]._datasetIndex; - const data = seriesData[datasetIndex].points[index]; + const series = seriesData[datasetIndex]; + const data = series ? series.renderPoints[index] : undefined; if (data) { window.open(data.commit.url, '_blank'); } From 0be184f62189a6fff8e63663c95819c58db4fa0e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 7 Jul 2026 17:15:01 +0200 Subject: [PATCH 5/6] fix --- index.html | 97 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index a01fb0c488..3ae1485d26 100644 --- a/index.html +++ b/index.html @@ -256,12 +256,12 @@ return undefined; } - function seriesForTooltip(chartData, item) { - return Array.from(chartData.series.values())[item.datasetIndex]; + function seriesForTooltip(renderSeriesData, item) { + return renderSeriesData[item.datasetIndex]; } - function pointForTooltip(chartData, item) { - const series = seriesForTooltip(chartData, item); + function pointForTooltip(renderSeriesData, item) { + const series = seriesForTooltip(renderSeriesData, item); return series ? series.renderPoints[item.index] : undefined; } @@ -351,32 +351,56 @@ parent.appendChild(canvas); const visibleData = lastVisibleChartData(chartData); - const labels = visibleData.points.map(d => d.commit.id.slice(0, 7)); + const labels = visibleData.points.map(d => d.commit.id + ':' + d.date); + const tickLabels = visibleData.points.map(d => d.commit.id.slice(0, 7)); const seriesData = Array.from(visibleData.series.values()); - const data = { - labels, - datasets: seriesData.map((series, index) => { - const color = colorForSeries(series.name, index); - series.renderData = []; - series.renderPoints = []; - for (let pointIndex = 0; pointIndex < series.values.length; pointIndex++) { - const value = series.values[pointIndex]; - if (typeof value !== 'number' || !isFinite(value)) { - continue; - } - - series.renderData.push({ x: labels[pointIndex], y: value }); - series.renderPoints.push(series.points[pointIndex]); + const renderSeriesData = []; + const datasets = []; + for (const [seriesIndex, series] of seriesData.entries()) { + const color = colorForSeries(series.name, seriesIndex); + let segmentData = []; + let segmentPoints = []; + let segmentIndex = 0; + + const flushSegment = () => { + if (segmentData.length === 0) { + return; } - return { + + renderSeriesData.push({ + name: series.name, + renderPoints: segmentPoints, + }); + datasets.push({ label: series.name, - data: series.renderData, + data: segmentData, borderColor: color, backgroundColor: color + '60', // Add alpha for #rrggbbaa fill: false, spanGaps: false, - }; - }), + _hideLegend: segmentIndex > 0, + _seriesName: series.name, + }); + segmentData = []; + segmentPoints = []; + segmentIndex++; + }; + + for (let pointIndex = 0; pointIndex < series.values.length; pointIndex++) { + const value = series.values[pointIndex]; + if (typeof value !== 'number' || !isFinite(value)) { + flushSegment(); + continue; + } + + segmentData.push({ x: labels[pointIndex], y: value }); + segmentPoints.push(series.points[pointIndex]); + } + flushSegment(); + } + const data = { + labels, + datasets, }; const bench = firstBench(visibleData); const unit = bench ? bench.unit : ''; @@ -402,6 +426,20 @@ }, legend: { display: true, + labels: { + filter: (legendItem, chartData) => !chartData.datasets[legendItem.datasetIndex]._hideLegend, + }, + onClick: function(_mouseEvent, legendItem) { + const chart = this.chart; + const seriesName = chart.data.datasets[legendItem.datasetIndex]._seriesName; + const hidden = chart.isDatasetVisible(legendItem.datasetIndex); + for (let index = 0; index < chart.data.datasets.length; index++) { + if (chart.data.datasets[index]._seriesName === seriesName) { + chart.getDatasetMeta(index).hidden = hidden; + } + } + chart.update(); + }, }, scales: { xAxes: [ @@ -410,6 +448,9 @@ display: true, labelString: 'commit', }, + ticks: { + callback: (_value, index) => tickLabels[index], + }, } ], yAxes: [yAxis], @@ -417,14 +458,14 @@ tooltips: { callbacks: { afterTitle: items => { - const data = pointForTooltip(visibleData, items[0]); + const data = pointForTooltip(renderSeriesData, items[0]); if (!data) { return ''; } return '\n' + firstLine(data.commit.message) + '\n\n' + data.commit.timestamp + ' by @' + data.commit.author.username + '\n'; }, label: item => { - const data = pointForTooltip(visibleData, item); + const data = pointForTooltip(renderSeriesData, item); if (!data) { return ''; } @@ -433,11 +474,11 @@ if (range) { label += ' (' + range + ')'; } - const series = seriesForTooltip(visibleData, item); + const series = seriesForTooltip(renderSeriesData, item); return (series ? series.name : data.bench.name) + ': ' + label; }, afterLabel: item => { - const data = pointForTooltip(visibleData, item); + const data = pointForTooltip(renderSeriesData, item); if (!data) { return ''; } @@ -453,7 +494,7 @@ // XXX: Undocumented. How can we know the index? const index = activeElems[0]._index; const datasetIndex = activeElems[0]._datasetIndex; - const series = seriesData[datasetIndex]; + const series = renderSeriesData[datasetIndex]; const data = series ? series.renderPoints[index] : undefined; if (data) { window.open(data.commit.url, '_blank'); From d94de4c579499a9d629e8f83de420ff6d4beabcc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 7 Jul 2026 17:43:26 +0200 Subject: [PATCH 6/6] logarithmic --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 3ae1485d26..84a18da4b0 100644 --- a/index.html +++ b/index.html @@ -319,7 +319,7 @@ function medianAxisLabel(unit, useLogScale) { if (useLogScale) { - return 'Median' + (unit ? ` (${unit}, LOG)` : ' (LOG)'); + return 'Median' + (unit ? ` (${unit}, logarithmic)` : ' (logarithmic)'); } return 'Median' + (unit ? ` (${unit})` : ''); }