From ce6f4e4869b388e46b57d4834b8ba5a5b088ce22 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 22 Mar 2026 23:03:38 +0100 Subject: [PATCH 1/2] ci: use trend images in bundle-size PR comments --- scripts/benchmarks/bundle-size/pr-report.mjs | 94 +++++++++++++++----- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/scripts/benchmarks/bundle-size/pr-report.mjs b/scripts/benchmarks/bundle-size/pr-report.mjs index ea7585815ee..44274d3b6bd 100644 --- a/scripts/benchmarks/bundle-size/pr-report.mjs +++ b/scripts/benchmarks/bundle-size/pr-report.mjs @@ -6,6 +6,7 @@ import path from 'node:path' import { parseArgs as parseNodeArgs } from 'node:util' const DEFAULT_MARKER = '' +const TREND_GRAPH_URL = 'https://tiny-graph.florianpellet.com/' const INT_FORMAT = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0, }) @@ -132,29 +133,57 @@ function formatDelta(current, baseline) { return `${formatBytes(delta, { signed: true })} (${sign}${PERCENT_FORMAT.format(ratio)})` } -function sparkline(values) { - if (!values.length) { +function toUnixTimestamp(value) { + if (Number.isFinite(value)) { + return Math.floor(Number(value) / 1000) + } + + if (typeof value !== 'string' || !value) { + return undefined + } + + const parsed = Date.parse(value) + if (Number.isNaN(parsed)) { + return undefined + } + + return Math.floor(parsed / 1000) +} + +function resolveHistoryEntryTimestamp(entry) { + return ( + toUnixTimestamp(entry?.date) ?? toUnixTimestamp(entry?.commit?.timestamp) + ) +} + +function resolveCurrentTimestamp(current) { + return ( + toUnixTimestamp(current?.measuredAt) ?? + toUnixTimestamp(current?.generatedAt) + ) +} + +function buildTrendGraph(points) { + if (!points.length) { return 'n/a' } - const blocks = '▁▂▃▄▅▆▇█' - const min = Math.min(...values) - const max = Math.max(...values) + const coords = [] - if (max === min) { - return '▅'.repeat(values.length) + for (const point of points) { + if (!Number.isFinite(point?.x) || !Number.isFinite(point?.y)) { + continue + } + + coords.push(point.x, point.y) } - return values - .map((value) => { - const normalized = (value - min) / (max - min) - const idx = Math.min( - blocks.length - 1, - Math.max(0, Math.round(normalized * (blocks.length - 1))), - ) - return blocks[idx] - }) - .join('') + if (!coords.length) { + return 'n/a' + } + + const src = `${TREND_GRAPH_URL}?coords=${encodeURIComponent(coords.join(','))}` + return `![Historical gzip bytes trend](${src})` } function normalizeHistoryEntries(history, benchmarkName) { @@ -177,6 +206,12 @@ function buildSeriesByScenario(historyEntries) { const map = new Map() for (const entry of historyEntries) { + const timestamp = resolveHistoryEntryTimestamp(entry) + + if (!Number.isFinite(timestamp)) { + continue + } + for (const bench of entry?.benches || []) { if (typeof bench?.name !== 'string' || !Number.isFinite(bench?.value)) { continue @@ -186,7 +221,10 @@ function buildSeriesByScenario(historyEntries) { map.set(bench.name, []) } - map.get(bench.name).push(Number(bench.value)) + map.get(bench.name).push({ + x: timestamp, + y: Number(bench.value), + }) } } @@ -263,6 +301,7 @@ async function main() { const historyEntries = normalizeHistoryEntries(history, current.benchmarkName) const seriesByScenario = buildSeriesByScenario(historyEntries) + const currentTimestamp = resolveCurrentTimestamp(current) const baseline = baselineCurrent != null @@ -274,15 +313,22 @@ async function main() { for (const metric of current.metrics || []) { const baselineValue = baseline.benchesByName.get(metric.id) const historySeries = (seriesByScenario.get(metric.id) || []).slice( - // Reserve one slot for the current metric so the sparkline stays at trendPoints. + // Reserve one slot for the current metric so the trend stays at trendPoints. -args.trendPoints + 1, ) + const currentPoint = { + x: currentTimestamp, + y: metric.gzipBytes, + } + const lastPoint = historySeries[historySeries.length - 1] if ( - !historySeries.length || - historySeries[historySeries.length - 1] !== metric.gzipBytes + Number.isFinite(currentPoint.x) && + (!lastPoint || + lastPoint.x !== currentPoint.x || + lastPoint.y !== currentPoint.y) ) { - historySeries.push(metric.gzipBytes) + historySeries.push(currentPoint) } rows.push({ @@ -291,7 +337,7 @@ async function main() { raw: metric.rawBytes, brotli: metric.brotliBytes, deltaCell: formatDelta(metric.gzipBytes, baselineValue), - trendCell: sparkline(historySeries.slice(-args.trendPoints)), + trendCell: buildTrendGraph(historySeries.slice(-args.trendPoints)), }) } @@ -321,7 +367,7 @@ async function main() { lines.push('') lines.push( - '_Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better._', + '_Trend chart uses historical gzip bytes plotted by measurement date, ending with this PR measurement; lower is better._', ) const markdown = lines.join('\n') + '\n' From 17f5e10176a09f660ffcda1e497f11161880cf55 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 22 Mar 2026 23:19:48 +0100 Subject: [PATCH 2/2] ci: size trend images for PR comments --- scripts/benchmarks/bundle-size/pr-report.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/benchmarks/bundle-size/pr-report.mjs b/scripts/benchmarks/bundle-size/pr-report.mjs index 44274d3b6bd..b798eba5288 100644 --- a/scripts/benchmarks/bundle-size/pr-report.mjs +++ b/scripts/benchmarks/bundle-size/pr-report.mjs @@ -182,7 +182,12 @@ function buildTrendGraph(points) { return 'n/a' } - const src = `${TREND_GRAPH_URL}?coords=${encodeURIComponent(coords.join(','))}` + const params = new URLSearchParams({ + coords: coords.join(','), + width: '165', + height: '45', + }) + const src = `${TREND_GRAPH_URL}?${params.toString()}` return `![Historical gzip bytes trend](${src})` }