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);