Skip to content

Commit 665f4d6

Browse files
feat: improve stacked chart UX and add timeline drill-down
- Sort stacked chart datasets by last 2 path segments so circuit index sorts before stage name - Highlight hovered line (thicker border + larger points) - Show nearest tooltip with full dataset labels - Click a point to open timeline.html with a stepped-line chart for that single commit (loads data from branch data.js) - Add timeline.html: shareable single-commit memory timeline view
1 parent a913db0 commit 665f4d6

2 files changed

Lines changed: 183 additions & 12 deletions

File tree

bench/index.html

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -788,19 +788,24 @@
788788
"#2471a3", "#a93226", "#229954", "#d4ac0d", "#4ae24a",
789789
];
790790

791-
// Sort: put total_ms first (thicker line), then rest alphabetically
791+
// Sort: put total_ms first (thicker line), then rest by last 2 segments
792+
// (e.g. "06_EcdsaRAccount:entrypoint/0_alloc") so circuit index sorts first
792793
const sorted = [...stackedItems].sort((a, b) => {
793-
const aName = a.fullKey.split("/").pop();
794-
const bName = b.fullKey.split("/").pop();
795-
if (aName === "total_ms") return -1;
796-
if (bName === "total_ms") return 1;
794+
const aParts = a.fullKey.split("/");
795+
const bParts = b.fullKey.split("/");
796+
const aName = aParts.length >= 2 ? aParts.slice(-2).join("/") : aParts.pop();
797+
const bName = bParts.length >= 2 ? bParts.slice(-2).join("/") : bParts.pop();
798+
if (aName.endsWith("total_ms")) return -1;
799+
if (bName.endsWith("total_ms")) return 1;
797800
return aName.localeCompare(bName);
798801
});
799802

800803
const datasets = sorted.map((item, i) => {
801804
const color = colors[i % colors.length];
802-
const metricName = item.fullKey.split("/").pop();
803-
const isTotal = metricName === "total_ms";
805+
// Use last 2 segments for labels like "06_EcdsaRAccount:entrypoint/after_accumulate"
806+
const parts = item.fullKey.split("/");
807+
const metricName = parts.length >= 2 ? parts.slice(-2).join("/") : parts.pop();
808+
const isTotal = metricName.endsWith("total_ms");
804809
const valueMap = new Map();
805810
item.benches.forEach((entry) => {
806811
valueMap.set(entry.commit.id, entry.bench.value);
@@ -827,6 +832,24 @@
827832
data: { labels, datasets },
828833
options: {
829834
responsive: true,
835+
hover: {
836+
mode: "nearest",
837+
intersect: false,
838+
onHover: function(event, elements) {
839+
// Highlight hovered line by thickening it
840+
datasets.forEach((ds, i) => {
841+
const isTotal = ds.label.endsWith("total_ms");
842+
ds.borderWidth = isTotal ? 3 : 2;
843+
ds.pointRadius = isTotal ? 4 : 2;
844+
});
845+
if (elements.length > 0) {
846+
const idx = elements[0]._datasetIndex;
847+
datasets[idx].borderWidth = 4;
848+
datasets[idx].pointRadius = 5;
849+
}
850+
chart.update({ duration: 0 });
851+
},
852+
},
830853
title: {
831854
display: true,
832855
text: name,
@@ -866,8 +889,13 @@
866889
],
867890
},
868891
tooltips: {
869-
mode: "index",
892+
mode: "nearest",
870893
intersect: false,
894+
itemSort: (a, b) => {
895+
const aLabel = datasets[a.datasetIndex].label;
896+
const bLabel = datasets[b.datasetIndex].label;
897+
return aLabel.localeCompare(bLabel);
898+
},
871899
callbacks: {
872900
title: (tooltipItems) => {
873901
if (tooltipItems.length > 0) {
@@ -890,10 +918,10 @@
890918
if (activeElems.length === 0) return;
891919
const index = activeElems[0]._index;
892920
const commitId = commitOrder[index];
893-
const commit = commitInfoMap.get(commitId);
894-
if (commit && commit.url) {
895-
window.open(commit.url, "_blank");
896-
}
921+
// Navigate to timeline page with data in query params
922+
window.open("timeline.html?branch=" + encodeURIComponent(branch) +
923+
"&commit=" + encodeURIComponent(commitId) +
924+
"&chart=" + encodeURIComponent(fullKey), "_blank");
897925
},
898926
},
899927
});

bench/timeline.html

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Memory Timeline</title>
6+
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.2/dist/Chart.min.js"></script>
7+
<style>
8+
body {
9+
margin: 0; padding: 20px;
10+
background: #1a1a2e; color: #e0e0e0;
11+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
12+
}
13+
h1 { font-size: 18px; margin-bottom: 4px; }
14+
.subtitle { color: #888; font-size: 13px; margin-bottom: 16px; }
15+
.subtitle a { color: #4a90e2; text-decoration: none; }
16+
.subtitle a:hover { text-decoration: underline; }
17+
.chart-wrap { position: relative; width: 100%; height: calc(100vh - 100px); }
18+
canvas { background: #16213e; border-radius: 8px; }
19+
</style>
20+
</head>
21+
<body>
22+
<h1 id="title">Memory Timeline (Peak RSS)</h1>
23+
<div class="subtitle" id="subtitle">Loading...</div>
24+
<div class="chart-wrap"><canvas id="chart"></canvas></div>
25+
<script>
26+
const params = new URLSearchParams(window.location.search);
27+
const branch = params.get("branch") || "master";
28+
const commitId = params.get("commit") || "";
29+
const chartFilter = params.get("chart") || "";
30+
31+
if (!commitId) {
32+
document.getElementById("subtitle").textContent = "No commit specified. Click a point on a stacked chart to view its timeline.";
33+
} else {
34+
// Load data.js for this branch
35+
const script = document.createElement("script");
36+
script.src = branch + "/data.js";
37+
script.onload = function() {
38+
if (!window.BENCHMARK_DATA) {
39+
document.getElementById("subtitle").textContent = "Could not load benchmark data.";
40+
return;
41+
}
42+
// Find the commit entry
43+
const entries = Object.values(window.BENCHMARK_DATA.entries)[0] || [];
44+
const entry = entries.find(e => e.commit && e.commit.id === commitId);
45+
if (!entry) {
46+
document.getElementById("subtitle").textContent = "Commit " + commitId.slice(0, 7) + " not found in data.";
47+
return;
48+
}
49+
50+
// Find all benches belonging to the stacked chart group
51+
const stages = [];
52+
entry.benches.forEach(b => {
53+
const extra = b.extra || "";
54+
const match = extra.match(/^stacked:(.+)$/);
55+
if (match && (!chartFilter || match[1] === chartFilter)) {
56+
// Use last 2 path segments as label
57+
const parts = b.name.split("/");
58+
const label = parts.length >= 2 ? parts.slice(-2).join("/") : parts.pop();
59+
stages.push({ label: label, value: b.value, unit: b.unit || "MB" });
60+
}
61+
});
62+
// Sort by label (circuit index prefix ensures execution order)
63+
stages.sort((a, b) => a.label.localeCompare(b.label));
64+
65+
if (stages.length === 0) {
66+
document.getElementById("subtitle").textContent = "No stacked chart data found for commit " + commitId.slice(0, 7) + ".";
67+
return;
68+
}
69+
70+
const unit = stages[0].unit;
71+
const values = stages.map(s => s.value);
72+
const peakVal = Math.max(...values);
73+
const peakIdx = values.indexOf(peakVal);
74+
const commitMsg = entry.commit.message || "";
75+
76+
document.getElementById("title").textContent = commitId.slice(0, 7) + " Memory Timeline";
77+
const backHref = "index.html?branch=" + encodeURIComponent(branch);
78+
document.getElementById("subtitle").innerHTML =
79+
stages.length + " stages | Peak: " + peakVal + " " + unit +
80+
" at " + stages[peakIdx].label +
81+
' | <a href="' + backHref + '">&larr; Back to dashboard</a>';
82+
83+
const labels = stages.map(s => s.label.length > 55 ? s.label.slice(0, 52) + "..." : s.label);
84+
const fullLabels = stages.map(s => s.label);
85+
const pointColors = values.map(v => v === peakVal ? "#ff4444" : "#4a90e2");
86+
const pointRadii = values.map(v => v === peakVal ? 6 : 2);
87+
const deltas = values.map((v, i) => i === 0 ? 0 : +(v - values[i - 1]).toFixed(1));
88+
89+
new Chart(document.getElementById("chart"), {
90+
type: "line",
91+
data: {
92+
labels: labels,
93+
datasets: [{
94+
label: "Peak RSS (" + unit + ")",
95+
data: values,
96+
borderColor: "#4a90e2",
97+
backgroundColor: "rgba(74,144,226,0.15)",
98+
borderWidth: 2,
99+
fill: true,
100+
steppedLine: "before",
101+
pointBackgroundColor: pointColors,
102+
pointRadius: pointRadii,
103+
pointHoverRadius: 6,
104+
}],
105+
},
106+
options: {
107+
responsive: true,
108+
maintainAspectRatio: false,
109+
legend: { display: false },
110+
scales: {
111+
xAxes: [{
112+
ticks: { fontColor: "#888", fontSize: 10, maxRotation: 90, minRotation: 45, autoSkip: true, maxTicksLimit: 40 },
113+
gridLines: { color: "rgba(255,255,255,0.05)" },
114+
}],
115+
yAxes: [{
116+
scaleLabel: { display: true, labelString: unit, fontColor: "#aaa" },
117+
ticks: { fontColor: "#aaa", beginAtZero: true },
118+
gridLines: { color: "rgba(255,255,255,0.08)" },
119+
}],
120+
},
121+
tooltips: {
122+
mode: "index",
123+
intersect: false,
124+
callbacks: {
125+
title: function(items) { return fullLabels[items[0].index]; },
126+
label: function(item) {
127+
var d = deltas[item.index];
128+
var ds = d > 0 ? " (+" + d + ")" : "";
129+
return " " + item.value + " " + unit + ds;
130+
},
131+
},
132+
},
133+
},
134+
});
135+
};
136+
script.onerror = function() {
137+
document.getElementById("subtitle").textContent = "Could not load data for branch: " + branch;
138+
};
139+
document.head.appendChild(script);
140+
}
141+
</script>
142+
</body>
143+
</html>

0 commit comments

Comments
 (0)