diff --git a/bench/index.html b/bench/index.html index f7d473615..d9b4e24a5 100644 --- a/bench/index.html +++ b/bench/index.html @@ -904,6 +904,170 @@ } } + // Renders a stacked area chart where each series fills from the previous, + // showing both individual contributions and total composition. + function renderStackedAreaGraph(parent, name, stackedItems, fullKey) { + const isDark = document.body.classList.contains("dark-theme"); + const textColor = isDark ? "#e0e0e0" : "#4a4a4a"; + + let grid = parent.querySelector(".benchmark-graphs"); + if (!grid) { + grid = document.createElement("div"); + grid.className = "benchmark-graphs"; + parent.appendChild(grid); + } + const container = document.createElement("div"); + container.className = "chart-container"; + container.style.gridColumn = "1 / -1"; + container.style.minHeight = "400px"; + grid.appendChild(container); + + const canvas = document.createElement("canvas"); + canvas.className = "benchmark-chart"; + container.appendChild(canvas); + + // Collect all unique commits across all component series + const commitOrder = []; + const commitSet = new Set(); + const commitInfoMap = new Map(); + stackedItems.forEach((item) => { + item.benches.forEach((entry) => { + if (!commitSet.has(entry.commit.id)) { + commitSet.add(entry.commit.id); + commitOrder.push(entry.commit.id); + commitInfoMap.set(entry.commit.id, entry.commit); + } + }); + }); + + const labels = commitOrder.map((id) => id.slice(0, 7)); + + // Stacked area colors (semi-opaque for fill) + const colors = [ + "#e24a4a", "#4a90e2", "#2ecc71", "#e2c94a", "#9b59b6", + "#1abc9c", "#e67e22", "#3498db", "#e74c3c", "#27ae60", + "#f39c12", "#8e44ad", "#16a085", "#d35400", "#2980b9", + "#c0392b", "#f1c40f", "#7d3c98", "#148f77", "#d68910", + ]; + + // Sort alphabetically (largest categories tend to be named first) + const sorted = [...stackedItems].sort((a, b) => { + const aName = a.fullKey.split("/").pop(); + const bName = b.fullKey.split("/").pop(); + return aName.localeCompare(bName); + }); + + const datasets = sorted.map((item, i) => { + const color = colors[i % colors.length]; + const metricName = item.fullKey.split("/").pop(); + const valueMap = new Map(); + item.benches.forEach((entry) => { + valueMap.set(entry.commit.id, entry.bench.value); + }); + const data = commitOrder.map((id) => valueMap.get(id) ?? 0); + return { + label: metricName, + data, + borderColor: color, + backgroundColor: color + "80", + borderWidth: 1, + pointRadius: 1, + fill: true, + }; + }); + + const unit = stackedItems[0]?.benches[0]?.bench?.unit || "MB"; + + const chart = new Chart(canvas, { + type: "line", + data: { labels, datasets }, + options: { + responsive: true, + title: { + display: true, + text: name, + fontColor: textColor, + fontSize: 14, + }, + legend: { + display: true, + position: "bottom", + labels: { + fontColor: textColor, + fontSize: 11, + padding: 12, + usePointStyle: true, + }, + }, + scales: { + xAxes: [ + { + scaleLabel: { + display: true, + labelString: "commit", + fontColor: textColor, + }, + ticks: { fontColor: textColor }, + }, + ], + yAxes: [ + { + stacked: true, + scaleLabel: { + display: true, + labelString: unit, + fontColor: textColor, + }, + ticks: { beginAtZero: true, fontColor: textColor }, + }, + ], + }, + tooltips: { + mode: "index", + intersect: false, + callbacks: { + title: (tooltipItems) => { + if (tooltipItems.length > 0) { + const idx = tooltipItems[0].index; + const commitId = commitOrder[idx]; + const commit = commitInfoMap.get(commitId); + return commit + ? commitId.slice(0, 7) + " - " + commit.message + : commitId.slice(0, 7); + } + return ""; + }, + label: (item) => { + const dsLabel = datasets[item.datasetIndex].label; + return " " + dsLabel + ": " + item.value + " " + unit; + }, + afterBody: (tooltipItems) => { + const total = tooltipItems.reduce( + (sum, item) => sum + parseFloat(item.value || 0), + 0 + ); + return " Total: " + total.toFixed(1) + " " + unit; + }, + }, + }, + onClick: (_mouseEvent, activeElems) => { + if (activeElems.length === 0) return; + const index = activeElems[0]._index; + const commitId = commitOrder[index]; + const commit = commitInfoMap.get(commitId); + if (commit && commit.url) { + window.open(commit.url, "_blank"); + } + }, + }, + }); + + window.chartInstances.push(chart); + if (fullKey) { + window.chartsByBenchName.set(fullKey, chart); + } + } + function renderBenchSet(name, benchSet, main) { const setElem = document.createElement("div"); setElem.className = "benchmark-set"; @@ -929,14 +1093,23 @@ } // Detect and consolidate stacked chart entries. - // Entries with extra: "stacked:GROUP_NAME" are grouped into a single stacked chart. + // Entries with extra: "stacked:GROUP_NAME" are grouped into overlaid line charts. + // Entries with extra: "stacked-area:GROUP_NAME" are grouped into stacked area charts. const stackedGroups = new Map(); + const stackedAreaGroups = new Map(); const regularItems = []; items.forEach((item) => { const latestBench = item.benches[item.benches.length - 1]?.bench; const extra = latestBench?.extra || ""; + const stackedAreaMatch = extra.match(/^stacked-area:(.+)$/); const stackedMatch = extra.match(/^stacked:(.+)$/); - if (stackedMatch) { + if (stackedAreaMatch) { + const groupName = stackedAreaMatch[1]; + if (!stackedAreaGroups.has(groupName)) { + stackedAreaGroups.set(groupName, []); + } + stackedAreaGroups.get(groupName).push(item); + } else if (stackedMatch) { const groupName = stackedMatch[1]; if (!stackedGroups.has(groupName)) { stackedGroups.set(groupName, []); @@ -947,7 +1120,7 @@ } }); - // Add consolidated stacked chart entries + // Add consolidated stacked line chart entries stackedGroups.forEach((groupItems, groupName) => { const group = groupItems[0].group; const parts = groupName.split("/"); @@ -961,6 +1134,20 @@ }); }); + // Add consolidated stacked area chart entries + stackedAreaGroups.forEach((groupItems, groupName) => { + const group = groupItems[0].group; + const parts = groupName.split("/"); + const chartName = parts[parts.length - 1]; + regularItems.push({ + group, + chartName, + benches: null, + fullKey: groupName, + stackedAreaItems: groupItems, + }); + }); + // Build hierarchical tree from group paths. const tree = new Map(); regularItems.forEach((item) => { @@ -976,6 +1163,7 @@ benches: item.benches, fullKey: item.fullKey, stackedItems: item.stackedItems || null, + stackedAreaItems: item.stackedAreaItems || null, }); } current = current.get(part).children; @@ -1078,7 +1266,9 @@ // Render charts for this node. value.charts.forEach((item) => { - if (item.stackedItems) { + if (item.stackedAreaItems) { + renderStackedAreaGraph(groupContent, item.chartName, item.stackedAreaItems, item.fullKey); + } else if (item.stackedItems) { renderStackedGraph(groupContent, item.chartName, item.stackedItems, item.fullKey); } else { renderGraph(groupContent, item.chartName, item.benches, item.fullKey);