Skip to content

Commit 0e3c13d

Browse files
authored
Merge pull request #13 from lpgauth/ci/benchmark-page-improvements
Improve benchmark page with comparison tables and torque-only trend c…
2 parents 17ca9a0 + 903329c commit 0e3c13d

3 files changed

Lines changed: 385 additions & 5 deletions

File tree

.github/workflows/bench.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,40 @@ jobs:
5656
env:
5757
BENCH_OUTPUT: json
5858

59-
- name: Store benchmark result
59+
- name: Store trend data
6060
uses: benchmark-action/github-action-benchmark@v1
6161
with:
6262
name: Torque Benchmarks
6363
tool: customBiggerIsBetter
64-
output-file-path: bench_results.json
64+
output-file-path: bench_torque.json
6565
github-token: ${{ secrets.GITHUB_TOKEN }}
6666
auto-push: true
6767
benchmark-data-dir-path: dev/bench
6868
comment-on-alert: true
6969
alert-threshold: "130%"
7070
fail-on-alert: false
7171

72+
- name: Deploy comparison data and custom page
73+
run: |
74+
git fetch origin gh-pages
75+
git worktree add /tmp/gh-pages -B gh-pages origin/gh-pages
76+
mkdir -p /tmp/gh-pages/dev/bench
77+
cp bench_comparison.json /tmp/gh-pages/dev/bench/comparison.json
78+
cp bench/index.html /tmp/gh-pages/dev/bench/index.html
79+
cd /tmp/gh-pages
80+
git config user.name "github-actions[bot]"
81+
git config user.email "github-actions[bot]@users.noreply.github.com"
82+
git add dev/bench/comparison.json dev/bench/index.html
83+
if ! git diff --staged --quiet; then
84+
git commit -m "Update comparison data and benchmark page"
85+
git push origin gh-pages
86+
fi
87+
7288
- name: Upload raw results
7389
uses: actions/upload-artifact@v4
7490
with:
7591
name: benchmark-results
76-
path: bench_results.json
92+
path: |
93+
bench_torque.json
94+
bench_comparison.json
7795
retention-days: 90

bench/index.html

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Torque Benchmarks</title>
7+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
8+
<style>
9+
* { margin: 0; padding: 0; box-sizing: border-box; }
10+
body {
11+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
12+
color: #24292f;
13+
background: #f6f8fa;
14+
line-height: 1.5;
15+
}
16+
.container {
17+
max-width: 1100px;
18+
margin: 0 auto;
19+
padding: 2rem 1rem;
20+
}
21+
h1 { font-size: 1.75rem; margin-bottom: 0.25rem; }
22+
.subtitle { color: #57606a; margin-bottom: 2rem; font-size: 0.875rem; }
23+
.subtitle a { color: #0969da; text-decoration: none; }
24+
.subtitle a:hover { text-decoration: underline; }
25+
h2 {
26+
font-size: 1.25rem;
27+
margin: 2.5rem 0 1rem;
28+
border-bottom: 1px solid #d0d7de;
29+
padding-bottom: 0.5rem;
30+
}
31+
.section-desc {
32+
color: #57606a;
33+
font-size: 0.85rem;
34+
margin-bottom: 1rem;
35+
}
36+
.card {
37+
background: white;
38+
border: 1px solid #d0d7de;
39+
border-radius: 6px;
40+
margin-bottom: 1.25rem;
41+
overflow: hidden;
42+
}
43+
.card-header {
44+
background: #f6f8fa;
45+
padding: 0.6rem 1rem;
46+
border-bottom: 1px solid #d0d7de;
47+
font-weight: 600;
48+
font-size: 0.875rem;
49+
}
50+
table {
51+
width: 100%;
52+
border-collapse: collapse;
53+
font-size: 0.8125rem;
54+
}
55+
th {
56+
text-align: left;
57+
padding: 0.4rem 1rem;
58+
border-bottom: 1px solid #d0d7de;
59+
font-weight: 600;
60+
color: #57606a;
61+
font-size: 0.75rem;
62+
text-transform: uppercase;
63+
letter-spacing: 0.03em;
64+
}
65+
th.num { text-align: right; }
66+
td {
67+
padding: 0.4rem 1rem;
68+
border-bottom: 1px solid #eaeef2;
69+
}
70+
td.num {
71+
text-align: right;
72+
font-variant-numeric: tabular-nums;
73+
}
74+
tr:last-child td { border-bottom: none; }
75+
tr.torque td { background: #ddf4ff; font-weight: 500; }
76+
.bar-cell { width: 35%; }
77+
.bar {
78+
height: 18px;
79+
border-radius: 3px;
80+
min-width: 2px;
81+
}
82+
.bar-torque { background: #0969da; }
83+
.bar-other { background: #c8d1d9; }
84+
.chart-card {
85+
background: white;
86+
border: 1px solid #d0d7de;
87+
border-radius: 6px;
88+
padding: 1rem 1rem 0.5rem;
89+
margin-bottom: 1.25rem;
90+
}
91+
.chart-title {
92+
font-weight: 600;
93+
font-size: 0.875rem;
94+
margin-bottom: 0.75rem;
95+
}
96+
.empty {
97+
text-align: center;
98+
padding: 2rem;
99+
color: #57606a;
100+
}
101+
</style>
102+
</head>
103+
<body>
104+
<div class="container">
105+
<h1>Torque Benchmarks</h1>
106+
<p class="subtitle" id="meta">
107+
<a href="https://github.com/lpgauth/torque">github.com/lpgauth/torque</a>
108+
</p>
109+
110+
<h2>Library Comparison</h2>
111+
<p class="section-desc">Latest CI results — all libraries, sorted by throughput (higher is better).</p>
112+
<div id="comparison"><div class="empty">Loading...</div></div>
113+
114+
<h2>Performance Over Time</h2>
115+
<p class="section-desc">Torque-only metrics tracked across commits to detect regressions.</p>
116+
<div id="trends"><div class="empty">Loading...</div></div>
117+
</div>
118+
119+
<script>
120+
// ── helpers ──────────────────────────────────────────────────────────
121+
function formatIps(v) {
122+
if (v >= 1e6) return (v / 1e6).toFixed(1) + 'M';
123+
if (v >= 1e3) return (v / 1e3).toFixed(1) + 'K';
124+
return v.toFixed(1);
125+
}
126+
127+
function classifyMetric(name) {
128+
var m = name.match(/^(.+?)\s*\((.+)\)$/);
129+
if (!m) return { type: name, payload: '', variant: name };
130+
var op = m[1].trim(), payload = m[2].trim();
131+
if (op === 'decode') return { type: 'Decode', payload: payload, variant: op };
132+
if (op.lastIndexOf('parse+get', 0) === 0) return { type: 'Parse+Get', payload: payload, variant: op };
133+
if (op.lastIndexOf('encode', 0) === 0) return { type: 'Encode', payload: payload, variant: op.replace('encode ', '') };
134+
return { type: op, payload: payload, variant: op };
135+
}
136+
137+
// ── comparison tables ───────────────────────────────────────────────
138+
function renderComparison(data) {
139+
var container = document.getElementById('comparison');
140+
141+
var metaEl = document.getElementById('meta');
142+
metaEl.innerHTML =
143+
'<a href="https://github.com/lpgauth/torque">github.com/lpgauth/torque</a>' +
144+
' &mdash; last run: ' +
145+
'<a href="https://github.com/lpgauth/torque/commit/' + data.commit + '">' +
146+
data.commit + '</a>' +
147+
' (' + new Date(data.date).toLocaleDateString() + ')';
148+
149+
// group by benchmark group, preserving order
150+
var groups = {}, order = [];
151+
data.results.forEach(function (r) {
152+
if (!groups[r.group]) { groups[r.group] = []; order.push(r.group); }
153+
groups[r.group].push(r);
154+
});
155+
156+
container.innerHTML = '';
157+
order.forEach(function (group) {
158+
var results = groups[group].sort(function (a, b) { return b.value - a.value; });
159+
var max = results[0].value;
160+
161+
var card = document.createElement('div');
162+
card.className = 'card';
163+
164+
var hdr = document.createElement('div');
165+
hdr.className = 'card-header';
166+
hdr.textContent = group;
167+
card.appendChild(hdr);
168+
169+
var html =
170+
'<table><thead><tr>' +
171+
'<th>Library</th><th class="num">IPS</th><th class="num">Relative</th><th class="bar-cell"></th>' +
172+
'</tr></thead><tbody>';
173+
174+
results.forEach(function (r) {
175+
var isTorque = r.name.lastIndexOf('torque', 0) === 0;
176+
var pct = (r.value / max) * 100;
177+
var rel = (r.value / max).toFixed(2) + 'x';
178+
html +=
179+
'<tr class="' + (isTorque ? 'torque' : '') + '">' +
180+
'<td>' + r.name + '</td>' +
181+
'<td class="num">' + formatIps(r.value) + '</td>' +
182+
'<td class="num">' + rel + '</td>' +
183+
'<td class="bar-cell"><div class="bar ' +
184+
(isTorque ? 'bar-torque' : 'bar-other') +
185+
'" style="width:' + pct + '%"></div></td>' +
186+
'</tr>';
187+
});
188+
189+
html += '</tbody></table>';
190+
card.innerHTML += html;
191+
container.appendChild(card);
192+
});
193+
}
194+
195+
// ── trend charts ────────────────────────────────────────────────────
196+
var COLORS = ['#0969da', '#cf222e', '#1a7f37', '#8250df', '#bf8700', '#0550ae', '#6e7781'];
197+
198+
function renderTrends(benchData) {
199+
var container = document.getElementById('trends');
200+
if (!benchData || !benchData.entries) {
201+
container.innerHTML = '<div class="empty">No trend data available yet.</div>';
202+
return;
203+
}
204+
205+
var benchName = Object.keys(benchData.entries)[0];
206+
var entries = benchData.entries[benchName];
207+
if (!entries || entries.length === 0) {
208+
container.innerHTML = '<div class="empty">No trend data available yet.</div>';
209+
return;
210+
}
211+
212+
// collect all metric names from the latest entry
213+
var latest = entries[entries.length - 1].benches;
214+
var metricNames = latest.map(function (b) { return b.name; });
215+
216+
// group by type + payload
217+
var groups = {}, order = [];
218+
metricNames.forEach(function (name) {
219+
var cls = classifyMetric(name);
220+
var key = cls.type + ' \u2014 ' + cls.payload;
221+
if (!groups[key]) { groups[key] = []; order.push(key); }
222+
groups[key].push({ name: name, variant: cls.variant });
223+
});
224+
225+
container.innerHTML = '';
226+
order.forEach(function (groupName, gi) {
227+
var metrics = groups[groupName];
228+
229+
var card = document.createElement('div');
230+
card.className = 'chart-card';
231+
232+
var title = document.createElement('div');
233+
title.className = 'chart-title';
234+
title.textContent = groupName;
235+
card.appendChild(title);
236+
237+
var canvas = document.createElement('canvas');
238+
canvas.height = 260;
239+
card.appendChild(canvas);
240+
container.appendChild(card);
241+
242+
var labels = entries.map(function (e) {
243+
return e.commit.id ? e.commit.id.substring(0, 7) : '';
244+
});
245+
246+
var datasets = metrics.map(function (m, i) {
247+
var values = entries.map(function (e) {
248+
var b = e.benches.find(function (x) { return x.name === m.name; });
249+
return b ? b.value : null;
250+
});
251+
return {
252+
label: m.variant,
253+
data: values,
254+
borderColor: COLORS[i % COLORS.length],
255+
backgroundColor: COLORS[i % COLORS.length] + '18',
256+
tension: 0.15,
257+
pointRadius: 3,
258+
pointHoverRadius: 6,
259+
fill: false
260+
};
261+
});
262+
263+
new Chart(canvas, {
264+
type: 'line',
265+
data: { labels: labels, datasets: datasets },
266+
options: {
267+
responsive: true,
268+
maintainAspectRatio: false,
269+
interaction: { mode: 'index', intersect: false },
270+
plugins: {
271+
tooltip: {
272+
callbacks: {
273+
label: function (ctx) {
274+
return ctx.dataset.label + ': ' + formatIps(ctx.parsed.y) + ' ips';
275+
}
276+
}
277+
},
278+
legend: { display: metrics.length > 1, position: 'bottom' }
279+
},
280+
scales: {
281+
y: {
282+
beginAtZero: false,
283+
title: { display: true, text: 'iterations/s' },
284+
ticks: { callback: function (v) { return formatIps(v); } }
285+
},
286+
x: { title: { display: true, text: 'Commit' } }
287+
}
288+
}
289+
});
290+
});
291+
}
292+
293+
// ── load data ───────────────────────────────────────────────────────
294+
fetch('comparison.json')
295+
.then(function (r) { return r.json(); })
296+
.then(renderComparison)
297+
.catch(function () {
298+
document.getElementById('comparison').innerHTML =
299+
'<div class="empty">No comparison data available yet.</div>';
300+
});
301+
302+
// data.js is optional (missing on first run)
303+
var s = document.createElement('script');
304+
s.src = 'data.js';
305+
s.onload = function () { renderTrends(window.BENCHMARK_DATA); };
306+
s.onerror = function () {
307+
document.getElementById('trends').innerHTML =
308+
'<div class="empty">No trend data available yet. Trends will appear after the second CI run.</div>';
309+
};
310+
document.head.appendChild(s);
311+
</script>
312+
</body>
313+
</html>

0 commit comments

Comments
 (0)