|
| 1 | +#!/usr/bin/env node |
| 2 | +'use strict' |
| 3 | + |
| 4 | +const fs = require('fs') |
| 5 | +const path = require('path') |
| 6 | + |
| 7 | +const dumpPath = process.argv[2] || 'perf-benchmarks.json' |
| 8 | +const outPath = process.argv[3] || 'perf-benchmarks.html' |
| 9 | +const metric = process.argv[4] || 'average' // requests[metric], e.g. average | mean | p50 |
| 10 | + |
| 11 | +if (!fs.existsSync(dumpPath)) { |
| 12 | + console.error(`❌ Cannot find ${dumpPath}`) |
| 13 | + process.exit(1) |
| 14 | +} |
| 15 | + |
| 16 | +const dump = JSON.parse(fs.readFileSync(dumpPath, 'utf8')) |
| 17 | + |
| 18 | +// Normalize + sort commits by date (not shown on axis, but used for order) |
| 19 | +const entries = Object.entries(dump) |
| 20 | + .map(([commit, v]) => ({ |
| 21 | + commit, |
| 22 | + dateISO: v.date, |
| 23 | + date: new Date(v.date), |
| 24 | + benchmarks: v.benchmarks || {} |
| 25 | + })) |
| 26 | + .sort((a, b) => a.date - b.date) |
| 27 | + |
| 28 | +// X-axis: commit IDs (category axis) |
| 29 | +const commits = entries.map(e => e.commit) |
| 30 | +const commitDates = entries.map(e => e.dateISO) |
| 31 | + |
| 32 | +// Collect all benchmark names across commits |
| 33 | +const benchNames = Array.from( |
| 34 | + entries.reduce((s, e) => { |
| 35 | + Object.keys(e.benchmarks).forEach(k => s.add(k)) |
| 36 | + return s |
| 37 | + }, new Set()) |
| 38 | +) |
| 39 | + |
| 40 | +// Build series aligned to commits (null for missing) |
| 41 | +const series = benchNames.map(name => { |
| 42 | + const y = entries.map(e => { |
| 43 | + const req = e.benchmarks[name] |
| 44 | + if (!req) return null |
| 45 | + const v = req[metric] |
| 46 | + return typeof v === 'number' ? v : (v != null ? Number(v) : null) |
| 47 | + }) |
| 48 | + return { name, x: commits, y } |
| 49 | +}) |
| 50 | + |
| 51 | +// HTML with Plotly (commit = x, equally spaced) |
| 52 | +const html = `<!doctype html> |
| 53 | +<html lang="en"> |
| 54 | +<meta charset="utf-8" /> |
| 55 | +<meta name="viewport" content="width=device-width, initial-scale=1"/> |
| 56 | +<title>Perf Benchmarks by Commit</title> |
| 57 | +<style> |
| 58 | + :root { --fg:#111; --muted:#666; } |
| 59 | + body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Arial; margin: 24px; color: var(--fg); } |
| 60 | + h1 { margin: 0 0 8px; font-size: 20px; } |
| 61 | + #chart { width: 100%; height: 72vh; } |
| 62 | + .meta { margin-top: 8px; color: var(--muted); font-size: 12px; } |
| 63 | + label { font-size: 12px; margin-right: 8px; color: var(--muted); } |
| 64 | + select { font-size: 12px; } |
| 65 | +</style> |
| 66 | +<h1>Perf Benchmarks (requests/second) by Commit</h1> |
| 67 | +<div class="meta"> |
| 68 | + Data: ${path.basename(dumpPath)} • Commits: ${entries.length} • Metric: requests.${metric} |
| 69 | +</div> |
| 70 | +<div style="margin:8px 0"> |
| 71 | + <label for="filter">Filter benchmark:</label> |
| 72 | + <select id="filter"><option value="__all__">All</option></select> |
| 73 | +</div> |
| 74 | +<div id="chart"></div> |
| 75 | +
|
| 76 | +<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script> |
| 77 | +<script> |
| 78 | + const SERIES = ${JSON.stringify(series)}; |
| 79 | + const COMMITS = ${JSON.stringify(commits)}; |
| 80 | + const COMMIT_DATES = ${JSON.stringify(commitDates)}; |
| 81 | +
|
| 82 | + // Populate filter |
| 83 | + const filterEl = document.getElementById('filter'); |
| 84 | + SERIES.forEach(s => { |
| 85 | + const opt = document.createElement('option'); |
| 86 | + opt.value = s.name; opt.textContent = s.name; |
| 87 | + filterEl.appendChild(opt); |
| 88 | + }); |
| 89 | +
|
| 90 | + function makeTraces(showName) { |
| 91 | + const base = { |
| 92 | + mode: 'lines+markers', |
| 93 | + connectgaps: false, |
| 94 | + x: COMMITS, |
| 95 | + customdata: COMMIT_DATES, // one per x |
| 96 | + hovertemplate: |
| 97 | + '<b>%{fullData.name}</b><br>' + |
| 98 | + 'commit: %{x}<br>' + |
| 99 | + 'rps: %{y:.0f}<br>' + |
| 100 | + '%{customdata|%Y-%m-%d %H:%M:%S}<extra></extra>' |
| 101 | + }; |
| 102 | + const chosen = showName === '__all__' |
| 103 | + ? SERIES |
| 104 | + : SERIES.filter(s => s.name === showName); |
| 105 | + return chosen.map(s => Object.assign({}, base, { name: s.name, y: s.y })); |
| 106 | + } |
| 107 | +
|
| 108 | + const layout = { |
| 109 | + xaxis: { |
| 110 | + title: 'Commit', |
| 111 | + type: 'category', |
| 112 | + tickangle: -45, |
| 113 | + automargin: true |
| 114 | + }, |
| 115 | + yaxis: { title: 'Requests / second', rangemode: 'tozero' }, |
| 116 | + hovermode: 'x unified', |
| 117 | + legend: { orientation: 'h' }, |
| 118 | + margin: { l: 60, r: 20, t: 10, b: 80 } |
| 119 | + }; |
| 120 | +
|
| 121 | + function render() { |
| 122 | + const name = filterEl.value; |
| 123 | + const traces = makeTraces(name); |
| 124 | + Plotly.newPlot('chart', traces, layout, { displayModeBar: true, responsive: true }); |
| 125 | + } |
| 126 | +
|
| 127 | + filterEl.addEventListener('change', render); |
| 128 | + render(); |
| 129 | +</script> |
| 130 | +</html>` |
| 131 | + |
| 132 | +fs.writeFileSync(outPath, html, 'utf8') |
| 133 | +console.log(`✅ Wrote \${outPath} (\${series.length} series, \${entries.length} commits) using requests.\${metric}\``) |
0 commit comments