From 33d46046a1aff0adaf9c6685c9b9cf943380c8df Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Fri, 24 Jul 2026 07:17:40 +0800 Subject: [PATCH] Improve LLGo binary-size dashboard --- .github/workflows/llgo-binary-size.yml | 24 +- ci/llgo-size/README.md | 12 +- ci/llgo-size/site/app.js | 484 +++++++++++++++++-------- ci/llgo-size/site/index.html | 56 +-- ci/llgo-size/site/style.css | 170 +++++---- 5 files changed, 470 insertions(+), 276 deletions(-) diff --git a/.github/workflows/llgo-binary-size.yml b/.github/workflows/llgo-binary-size.yml index 4f48fef..8c9c497 100644 --- a/.github/workflows/llgo-binary-size.yml +++ b/.github/workflows/llgo-binary-size.yml @@ -99,31 +99,37 @@ jobs: contents: read runs-on: ubuntu-24.04 outputs: - version_changed: ${{ steps.changed.outputs.version_changed }} + full_matrix: ${{ steps.changed.outputs.full_matrix }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Detect a committed LLGo version change + - name: Detect an LLGo pin or benchmark-case change id: changed env: BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_SHA: ${{ github.event.pull_request.head.sha }} run: | set -euo pipefail - if git diff --name-only "$BASE_SHA" "$PR_SHA" | grep -Fxq 'ci/llgo-size/llgo-version.env'; then - echo 'version_changed=true' >> "$GITHUB_OUTPUT" - echo 'The PR changes the committed LLGo version; the full matrix will run.' + if git diff --quiet "$BASE_SHA" "$PR_SHA" -- \ + ci/llgo-size/llgo-version.env \ + ci/llgo-size/bin/go \ + cmd/bent \ + cmd/bent/configs/benchmarks-llgo-size.toml \ + cmd/bent/configs/configurations-llgo-size.toml \ + cmd/bent/configs/suites.toml; then + echo 'full_matrix=false' >> "$GITHUB_OUTPUT" + echo 'The PR does not change the LLGo pin, Bent, or benchmark cases; only Go validation will run.' else - echo 'version_changed=false' >> "$GITHUB_OUTPUT" - echo 'The PR does not change the committed LLGo version; only Go validation will run.' + echo 'full_matrix=true' >> "$GITHUB_OUTPUT" + echo 'The PR changes the LLGo pin, Bent, or benchmark cases; the full matrix will run without publishing.' fi binary-size: if: >- always() && github.event_name != 'repository_dispatch' && - (github.event_name != 'pull_request' || needs.pr-scope.outputs.version_changed == 'true') + (github.event_name != 'pull_request' || needs.pr-scope.outputs.full_matrix == 'true') needs: pr-scope permissions: contents: write @@ -297,7 +303,7 @@ jobs: pr-validation: if: >- always() && github.event_name == 'pull_request' && - needs.pr-scope.result == 'success' && needs.pr-scope.outputs.version_changed != 'true' + needs.pr-scope.result == 'success' && needs.pr-scope.outputs.full_matrix != 'true' needs: pr-scope permissions: contents: read diff --git a/ci/llgo-size/README.md b/ci/llgo-size/README.md index da9810d..5a1164c 100644 --- a/ci/llgo-size/README.md +++ b/ci/llgo-size/README.md @@ -62,12 +62,12 @@ The benchmark and page-only workflows use separate concurrency queues. A page refresh can therefore wait independently without replacing a pending binary-size run. -Pull requests are split by whether they change `llgo-version.env`. A normal PR -uses a separate Go-only validation job: dependency acquisition, compilation, -and execution are checked without building LLGo or the four LLGo binary-size -variants. A PR that changes the committed LLGo version runs the full five-way -matrix and uploads the `llgo-binary-size` artifact for review, but still does -not publish history. +Pull requests that change the committed LLGo version, Bent, the LLGo-size +benchmark/configuration files, or the suite definitions used by those cases +run the full five-way matrix and upload the `llgo-binary-size` artifact for +review. They do not publish history. A PR outside that scope uses the separate +Go-only validation job, so dependency acquisition, compilation, and execution +remain checked without rebuilding LLGo or the four LLGo binary-size variants. Published history is keyed by the full LLGo commit, so rerunning one commit updates its existing entry instead of adding another build-round entry. diff --git a/ci/llgo-size/site/app.js b/ci/llgo-size/site/app.js index 8f2915a..b40aa71 100644 --- a/ci/llgo-size/site/app.js +++ b/ci/llgo-size/site/app.js @@ -1,7 +1,23 @@ -const state = { index: null, runs: new Map() }; +const state = { + index: null, + runs: new Map(), + benchmarkNames: [], + activeBenchmarks: new Set(), + activeConfigs: new Set(), + page: 1, + pageSize: 20, + query: "", +}; + const newerSelect = document.querySelector("#newer-run"); const baselineSelect = document.querySelector("#baseline-run"); const status = document.querySelector("#status"); +const comparisonGrid = document.querySelector("#comparison-grid"); +const pagination = document.querySelector("#pagination"); +const filterInput = document.querySelector("#commit-filter"); +const pageSizeSelect = document.querySelector("#page-size"); +const historyMetric = document.querySelector("#history-metric"); +const historyRange = document.querySelector("#history-range"); const configs = [ "Go", @@ -27,7 +43,8 @@ const compactConfigLabels = { LLGoFullLTOGlobalDCEPlugin: "LTO + DCE + P", }; -const seriesColors = ["#315efb", "#7c3aed", "#d97706", "#0f766e", "#c92a2a"]; +const seriesColors = ["#2457d6", "#7c3aed", "#d97706", "#0f766e", "#c92a2a"]; +const seriesDashes = ["", "7 3", "2 3"]; function escapeHtml(value) { return String(value == null ? "" : value).replace(/[&<>"']/g, function (char) { @@ -48,8 +65,9 @@ function formatBytes(value) { return (number < 0 ? "-" : "") + scaled.toFixed(3) + " " + units[unit]; } -function deltaClass(delta) { - return delta > 0 ? "bad" : delta < 0 ? "good" : "flat"; +function formatPercent(value, digits) { + if (!Number.isFinite(value)) return "—"; + return (value > 0 ? "+" : "") + value.toFixed(digits == null ? 3 : digits) + "%"; } function percentDelta(value, base) { @@ -57,18 +75,12 @@ function percentDelta(value, base) { return ((Number(value) - Number(base)) / Number(base)) * 100; } -function formatDelta(delta, label, prominent) { - if (!Number.isFinite(delta)) return ""; - const sign = delta > 0 ? "+" : ""; - const className = deltaClass(delta); - if (prominent) { - return '' + sign + delta.toFixed(3) + '%' + escapeHtml(label) + ""; - } - return '' + sign + delta.toFixed(3) + "% " + escapeHtml(label) + ""; +function deltaClass(delta) { + return delta > 0 ? "bad" : delta < 0 ? "good" : "flat"; } function shortSha(value) { - return value ? value.slice(0, 10) : "—"; + return value ? String(value).slice(0, 10) : "—"; } function dateLabel(value) { @@ -88,7 +100,12 @@ function runLabel(run) { return commitLabel(run) + " · " + dateLabel(run.createdAt); } +function benchmarkMap(document) { + return new Map((document.benchmarks || []).map(function (item) { return [item.name, item]; })); +} + async function loadRun(meta) { + if (!meta) return null; if (!state.runs.has(meta.key)) { const response = await fetch("data/" + meta.path, { cache: "no-store" }); if (!response.ok) throw new Error("Cannot load " + meta.path); @@ -97,196 +114,343 @@ async function loadRun(meta) { return state.runs.get(meta.key); } -function fillSelects(runs) { - newerSelect.replaceChildren(...runs.map(function (run) { - const option = document.createElement("option"); - option.value = run.key; - option.textContent = runLabel(run); - return option; - })); - baselineSelect.replaceChildren(); - const none = document.createElement("option"); - none.value = ""; - none.textContent = "No baseline"; - baselineSelect.appendChild(none); - for (const run of runs) { - const option = document.createElement("option"); - option.value = run.key; - option.textContent = runLabel(run); - baselineSelect.appendChild(option); +function findMeta(key) { + return state.index.runs.find(function (run) { return run.key === key; }); +} + +function selection() { + const baselineMeta = findMeta(baselineSelect.value); + const newerMeta = findMeta(newerSelect.value); + return { baselineMeta: baselineMeta, newerMeta: newerMeta }; +} + +function fillRunSelects(runs) { + function options() { + return runs.map(function (run) { + return '"; + }).join(""); } - newerSelect.selectedIndex = 0; - baselineSelect.value = runs.length > 1 ? runs[1].key : ""; + baselineSelect.innerHTML = options(); + newerSelect.innerHTML = options(); + baselineSelect.value = runs.length > 1 ? runs[1].key : runs[0].key; + newerSelect.value = runs[0].key; } -function renderMeta(newer, baseline) { - const run = newer.run; - const workflow = run.workflowUrl - ? 'workflow ' + escapeHtml(runNumber(run)) + "" - : "run " + escapeHtml(runNumber(run)); +function renderRunMeta(baseline, newer) { + const item = function (label, run, extra) { + if (!run) return ""; + const workflow = run.workflowUrl + ? 'workflow ' + escapeHtml(runNumber(run)) + "" + : "run " + escapeHtml(runNumber(run)); + return '
' + label + "" + escapeHtml(commitLabel(run)) + "" + + '' + escapeHtml(dateLabel(run.createdAt)) + (extra ? " · " + extra : "") + "" + + '' + workflow + "
"; + }; document.querySelector("#run-meta").innerHTML = - '
Newer' + escapeHtml(dateLabel(run.createdAt)) + "
" + - '
LLGo' + escapeHtml(shortSha(run.llgoCommit)) + "
" + - '
Toolchain' + escapeHtml(run.goVersion || "—") + " / LLVM " + escapeHtml(run.llvmVersion || "—") + "
" + - '
Link' + workflow + "
" + - '
Baseline' + (baseline ? escapeHtml(dateLabel(baseline.run.createdAt)) : "none") + "
"; + item("A · baseline", baseline, baseline ? escapeHtml(baseline.ref || "—") : "") + + item("B · selected", newer, newer ? escapeHtml(newer.ref || "—") : "") + + '
Toolchain' + escapeHtml(newer && newer.goVersion || "—") + " / LLVM " + escapeHtml(newer && newer.llvmVersion || "—") + "" + + 'Each LLGo cell remains relative to Go from the same commit.
'; } -function benchmarkMap(document) { - return new Map((document.benchmarks || []).map(function (item) { return [item.name, item]; })); +function filteredRuns() { + const query = state.query.trim().toLowerCase(); + if (!query) return state.index.runs; + return state.index.runs.filter(function (run) { + return [commitLabel(run), run.llgoCommit, run.sourceCommit, run.ref, run.createdAt, run.key] + .some(function (value) { return String(value || "").toLowerCase().includes(query); }); + }); } -function comparisonCell(benchmark, config, otherBenchmark) { - if (!benchmark || !benchmark.values || benchmark.values[config] == null) { - return '—'; - } - const value = benchmark.values[config]; - const goValue = benchmark.values.Go; +function cellHtml(benchmark, config, baselineBenchmark, showComparison) { + if (!benchmark || !benchmark.values || benchmark.values[config] == null) return '—'; + const value = Number(benchmark.values[config]); + const goValue = Number(benchmark.values.Go); const relative = config === "Go" ? 'reference' - : formatDelta(percentDelta(value, goValue), "vs Go", true); - const previous = otherBenchmark && otherBenchmark.values && otherBenchmark.values[config] != null - ? formatDelta(percentDelta(value, otherBenchmark.values[config]), "vs old", false) + : '' + formatPercent(percentDelta(value, goValue), 1) + " vs Go"; + const oldValue = baselineBenchmark && baselineBenchmark.values ? Number(baselineBenchmark.values[config]) : NaN; + const comparison = showComparison && Number.isFinite(oldValue) + ? 'Δ A ' + formatPercent(percentDelta(value, oldValue), 1) + "" : ""; - return '' + formatBytes(value) + "" + - '
' + relative + "
" + (previous ? '
' + previous + "
" : "") + ""; -} - -function renderComparison(newer, baseline) { - const grid = document.querySelector("#comparison-grid"); - const newerByName = benchmarkMap(newer); - const baselineByName = baseline ? benchmarkMap(baseline) : new Map(); - const benchmarkNames = Array.from(new Set(Array.from(newerByName.keys()).concat(Array.from(baselineByName.keys())))).sort(); - - const newerHeading = "Newer · " + commitLabel(newer.run); - const baselineHeading = baseline ? "Older · " + commitLabel(baseline.run) : "Older"; - grid.innerHTML = benchmarkNames.map(function (name) { - const newerBenchmark = newerByName.get(name); - const baselineBenchmark = baselineByName.get(name); - const rows = configs.map(function (config) { - const label = configLabels[config] || config; - return "" + - escapeHtml(compactConfigLabels[config] || label) + "" + - comparisonCell(baselineBenchmark, config, null) + - comparisonCell(newerBenchmark, config, baselineBenchmark) + ""; + return '' + formatBytes(value) + "" + relative + "" + comparison + ""; +} + +function pageNumbers(page, pageCount) { + const values = new Set([1, pageCount, page - 1, page, page + 1]); + return Array.from(values).filter(function (value) { return value >= 1 && value <= pageCount; }).sort(function (a, b) { return a - b; }); +} + +function renderPagination(runCount) { + const pageCount = Math.max(1, Math.ceil(runCount / state.pageSize)); + state.page = Math.min(state.page, pageCount); + const numbers = pageNumbers(state.page, pageCount); + let previous = 0; + const pageButtons = numbers.map(function (number) { + const gap = number - previous > 1 ? '' : ""; + previous = number; + return gap + '"; + }).join(""); + pagination.innerHTML = '" + + pageButtons + '"; +} + +async function renderComparison() { + const selected = selection(); + const baseline = await loadRun(selected.baselineMeta); + const newer = await loadRun(selected.newerMeta); + renderRunMeta(selected.baselineMeta, selected.newerMeta); + + const runs = filteredRuns(); + renderPagination(runs.length); + const start = (state.page - 1) * state.pageSize; + const pageRuns = runs.slice(start, start + state.pageSize); + const documents = await Promise.all(pageRuns.map(loadRun)); + const baselineByName = benchmarkMap(baseline || {}); + const columns = state.benchmarkNames.length ? state.benchmarkNames : []; + const span = columns.length * configs.length; + const headerGroups = columns.map(function (name) { + return '' + escapeHtml(name) + ""; + }).join(""); + const headerModes = columns.map(function () { + return configs.map(function (config) { + return '' + escapeHtml(compactConfigLabels[config]) + ""; }).join(""); - return '

' + escapeHtml(name) + "

" + rows + "
Mode" + - escapeHtml(baselineHeading) + "" + escapeHtml(newerHeading) + "
"; }).join(""); - document.querySelector("#table-note").textContent = baseline - ? "Older results are on the left. Newer LLGo results show the prominent difference from Go and the smaller difference from the selected older run." - : "Each LLGo result shows its difference from Go in the same run."; -} - -function renderHistoryTable(runs) { - const body = document.querySelector("#history-body"); - body.replaceChildren(); - for (const run of runs) { - const row = document.createElement("tr"); - row.innerHTML = - "" + (run.workflowUrl ? '' : "") + escapeHtml(commitLabel(run)) + (run.workflowUrl ? "" : "") + "" + - "" + escapeHtml(dateLabel(run.createdAt)) + "" + - "" + escapeHtml(run.goVersion || "—") + "" + - "" + escapeHtml(run.llvmVersion || "—") + "" + - "" + escapeHtml(run.ref || "—") + ""; - body.appendChild(row); - } + + const rows = documents.map(function (document, index) { + const meta = pageRuns[index]; + const byName = benchmarkMap(document || {}); + const isBaseline = selected.baselineMeta && meta.key === selected.baselineMeta.key; + const isNewer = selected.newerMeta && meta.key === selected.newerMeta.key; + const rowClass = (isBaseline ? " baseline-row" : "") + (isNewer ? " selected-row" : ""); + const roleButtons = '
' + + '
'; + const values = columns.map(function (name) { + const benchmark = byName.get(name); + const baselineBenchmark = baselineByName.get(name); + return configs.map(function (config) { + return cellHtml(benchmark, config, baselineBenchmark, isNewer && !isBaseline); + }).join(""); + }).join(""); + const workflow = meta.workflowUrl ? '' + escapeHtml(commitLabel(meta)) + "" : '' + escapeHtml(commitLabel(meta)) + ""; + return '' + roleButtons + '' + workflow + "" + + '' + escapeHtml(dateLabel(meta.createdAt)) + '' + escapeHtml(meta.ref || "—") + "" + values + ""; + }).join(""); + + comparisonGrid.style.minWidth = (560 + span * 124) + "px"; + comparisonGrid.innerHTML = 'PickCommitCreatedBranch' + headerGroups + '' + headerModes + "" + + (rows || 'No commits match this filter.') + ""; + document.querySelector("#commit-count").textContent = runs.length + " commit" + (runs.length === 1 ? "" : "s") + " · showing " + (runs.length ? (start + 1) + "–" + Math.min(start + state.pageSize, runs.length) : "0"); + document.querySelector("#table-note").textContent = "A/B selections persist while you page or filter commits; use the horizontal scrollbar for benchmark columns."; } -function chartPath(points, x, y) { - return points.map(function (point, index) { - return (index === 0 ? "M" : "L") + x(point.index).toFixed(2) + " " + y(point.value).toFixed(2); - }).join(" "); -} - -function renderHistoryChart(name, documents) { - const values = []; - for (const document of documents) { - const benchmark = benchmarkMap(document).get(name); - if (!benchmark) continue; - for (const config of configs) { - if (Number.isFinite(Number(benchmark.values[config]))) values.push(Number(benchmark.values[config])); +function renderChoiceControls() { + const benchmarks = document.querySelector("#benchmark-filter"); + benchmarks.innerHTML = state.benchmarkNames.map(function (name) { + return '"; + }).join(""); + const configFilter = document.querySelector("#config-filter"); + configFilter.innerHTML = configs.map(function (config, index) { + return '"; + }).join(""); +} + +function metricValue(documents, benchmarkName, config, metric) { + let previous = null; + return documents.map(function (document, index) { + const benchmark = benchmarkMap(document).get(benchmarkName); + const value = benchmark && benchmark.values ? Number(benchmark.values[config]) : NaN; + const goValue = benchmark && benchmark.values ? Number(benchmark.values.Go) : NaN; + let plotted = value; + if (metric === "vs-go") plotted = config === "Go" ? 0 : percentDelta(value, goValue); + if (metric === "commit-delta") { + plotted = previous == null ? NaN : percentDelta(value, previous); + previous = Number.isFinite(value) ? value : previous; } + return { index: index, value: Number(plotted), document: document, raw: value, go: goValue }; + }).filter(function (point) { return Number.isFinite(point.value); }); +} + +function chartMetricLabel(metric) { + if (metric === "vs-go") return "vs Go (%)"; + if (metric === "commit-delta") return "change from previous commit (%)"; + return "binary size"; +} + +function chartFormat(value, metric) { + return metric === "absolute" ? formatBytes(value) : formatPercent(value, 1); +} + +function chartPath(points, x, y) { + return points.map(function (point, index) { return (index === 0 ? "M" : "L") + x(point.index).toFixed(2) + " " + y(point.value).toFixed(2); }).join(" "); +} + +function runValue(document, benchmarkName, config) { + const benchmark = benchmarkMap(document || {}).get(benchmarkName); + return benchmark && benchmark.values ? Number(benchmark.values[config]) : NaN; +} + +function renderInspector(baseline, newer) { + const name = Array.from(state.activeBenchmarks)[0]; + const config = Array.from(state.activeConfigs)[0]; + const inspector = document.querySelector("#history-inspector"); + if (!name || !config || !newer) { + inspector.innerHTML = '

Select at least one benchmark and build mode to inspect a series.

'; + return; } - if (!values.length) return ""; - const width = 700; - const height = 260; - const left = 62; - const right = 14; - const top = 18; - const bottom = 42; + const newerValue = runValue(newer, name, config); + const newerGo = runValue(newer, name, "Go"); + const baselineValue = runValue(baseline, name, config); + const relative = config === "Go" ? 0 : percentDelta(newerValue, newerGo); + const comparison = percentDelta(newerValue, baselineValue); + inspector.innerHTML = '

Selected series

' + escapeHtml(name) + " · " + escapeHtml(compactConfigLabels[config]) + '

' + formatBytes(newerValue) + "" + + '

' + (config === "Go" ? "Go reference" : formatPercent(relative, 1) + " vs Go") + "

" + + '
A → B
' + formatPercent(comparison, 1) + "
Baseline
" + escapeHtml(commitLabel(baseline.run || {})) + "
Selected
" + escapeHtml(commitLabel(newer.run || {})) + "
"; +} + +async function renderHistory() { + const range = Number(historyRange.value); + const metas = (range ? state.index.runs.slice(0, range) : state.index.runs).slice().reverse(); + const documents = await Promise.all(metas.map(loadRun)); + const metric = historyMetric.value; + const series = []; + Array.from(state.activeBenchmarks).forEach(function (name, benchmarkIndex) { + Array.from(state.activeConfigs).forEach(function (config) { + const points = metricValue(documents, name, config, metric); + if (points.length) series.push({ name: name, config: config, benchmarkIndex: benchmarkIndex, points: points }); + }); + }); + const chart = document.querySelector("#history-chart"); + const selected = selection(); + const baseline = await loadRun(selected.baselineMeta); + const newer = await loadRun(selected.newerMeta); + renderInspector(baseline, newer); + if (!series.length || !documents.length) { + chart.innerHTML = '

No matching history is available for this selection.

'; + return; + } + const values = series.flatMap(function (item) { return item.points.map(function (point) { return point.value; }); }); + const width = 1080; + const height = 380; + const left = 72; + const right = 22; + const top = 30; + const bottom = 58; const minimum = Math.min.apply(null, values); const maximum = Math.max.apply(null, values); - const range = maximum === minimum ? Math.max(1, maximum * 0.05) : maximum - minimum; - const yMin = Math.max(0, minimum - range * 0.08); - const yMax = maximum + range * 0.08; + const spread = maximum === minimum ? Math.max(1, Math.abs(maximum) * 0.1) : maximum - minimum; + const yMin = metric === "absolute" ? Math.max(0, minimum - spread * 0.08) : minimum - spread * 0.12; + const yMax = maximum + spread * 0.12; const x = function (index) { return documents.length === 1 ? (left + width - right) / 2 : left + index * (width - left - right) / (documents.length - 1); }; const y = function (value) { return top + (yMax - value) * (height - top - bottom) / (yMax - yMin); }; - const grid = [0, 0.5, 1].map(function (ratio) { + const grid = [0, 0.25, 0.5, 0.75, 1].map(function (ratio) { const value = yMin + (yMax - yMin) * ratio; const position = y(value); - return '' + - '' + escapeHtml(formatBytes(value)) + ""; + return '' + escapeHtml(chartFormat(value, metric)) + ""; }).join(""); - const series = configs.map(function (config, configIndex) { - const points = []; - documents.forEach(function (document, index) { - const benchmark = benchmarkMap(document).get(name); - const value = benchmark && benchmark.values ? Number(benchmark.values[config]) : NaN; - if (Number.isFinite(value)) points.push({ index: index, value: value, document: document }); - }); - if (!points.length) return ""; - const color = seriesColors[configIndex % seriesColors.length]; - return '' + points.map(function (point) { - return '' + - escapeHtml(name + " · " + config + " · " + commitLabel(point.document.run) + ": " + formatBytes(point.value)) + ""; + const markers = [[selected.baselineMeta, "A"], [selected.newerMeta, "B"]].map(function (item) { + const index = metas.findIndex(function (meta) { return item[0] && meta.key === item[0].key; }); + if (index < 0) return ""; + const position = x(index); + return '' + item[1] + ""; + }).join(""); + const lines = series.map(function (item) { + const configIndex = configs.indexOf(item.config); + const color = seriesColors[configIndex]; + const dash = seriesDashes[item.benchmarkIndex % seriesDashes.length]; + const label = item.name + " · " + compactConfigLabels[item.config]; + return '' + item.points.map(function (point) { + return '' + escapeHtml(label + " · " + commitLabel(point.document.run) + ": " + chartFormat(point.value, metric)) + ""; }).join(""); }).join(""); const labels = documents.map(function (document, index) { - return '' + escapeHtml(commitLabel(document.run)) + ""; + if (documents.length > 12 && index % Math.ceil(documents.length / 10) !== 0 && index !== documents.length - 1) return ""; + return '' + escapeHtml(commitLabel(document.run)) + ""; }).join(""); - const legend = configs.map(function (config, index) { - return '' + escapeHtml(configLabels[config] || config) + ""; + const legend = series.map(function (item) { + const configIndex = configs.indexOf(item.config); + return '' + escapeHtml(item.name + " · " + compactConfigLabels[item.config]) + ""; }).join(""); - return '

' + escapeHtml(name) + "

' + grid + series + labels + "
" + legend + "
"; -} - -async function renderHistoryCharts(runs) { - const container = document.querySelector("#history-charts"); - const documents = await Promise.all(runs.slice().reverse().map(loadRun)); - const names = Array.from(new Set(documents.flatMap(function (document) { - return (document.benchmarks || []).map(function (benchmark) { return benchmark.name; }); - }))).sort(); - container.innerHTML = names.map(function (name) { return renderHistoryChart(name, documents); }).join("") || '

No benchmark history is available yet.

'; + chart.innerHTML = '
' + escapeHtml(chartMetricLabel(metric)) + '' + documents.length + " commits
' + grid + markers + lines + labels + '
' + legend + "
"; } -async function update() { +async function refresh() { try { - const newerMeta = state.index.runs.find(function (run) { return run.key === newerSelect.value; }); - const baselineMeta = state.index.runs.find(function (run) { return run.key === baselineSelect.value; }); - const newer = await loadRun(newerMeta); - const baseline = baselineMeta && baselineMeta.key !== newerMeta.key ? await loadRun(baselineMeta) : null; - renderMeta(newer, baseline); - renderComparison(newer, baseline); - status.textContent = state.index.runs.length + " run" + (state.index.runs.length === 1 ? "" : "s") + " published"; + await Promise.all([renderComparison(), renderHistory()]); + status.textContent = state.index.runs.length + " published commit" + (state.index.runs.length === 1 ? "" : "s"); + status.className = "status"; } catch (error) { status.textContent = error.message; status.className = "status error"; } } +function attachEvents() { + newerSelect.addEventListener("change", refresh); + baselineSelect.addEventListener("change", function () { + if (baselineSelect.value === newerSelect.value && state.index.runs.length > 1) newerSelect.value = state.index.runs[0].key === baselineSelect.value ? state.index.runs[1].key : state.index.runs[0].key; + refresh(); + }); + filterInput.addEventListener("input", function () { state.query = filterInput.value; state.page = 1; renderComparison(); }); + pageSizeSelect.addEventListener("change", function () { state.pageSize = Number(pageSizeSelect.value); state.page = 1; renderComparison(); }); + pagination.addEventListener("click", function (event) { + const button = event.target.closest("button[data-page]"); + if (!button || button.disabled) return; + state.page = Number(button.dataset.page); + renderComparison(); + }); + comparisonGrid.addEventListener("click", function (event) { + const button = event.target.closest("button[data-select-role]"); + if (!button) return; + const role = button.dataset.selectRole; + const key = button.dataset.runKey; + if (role === "baseline") baselineSelect.value = key; + else newerSelect.value = key; + if (baselineSelect.value === newerSelect.value && state.index.runs.length > 1) { + const alternative = state.index.runs.find(function (run) { return run.key !== key; }); + if (role === "baseline") newerSelect.value = alternative.key; + else baselineSelect.value = alternative.key; + } + refresh(); + }); + document.querySelector("#benchmark-filter").addEventListener("click", function (event) { + const button = event.target.closest("button[data-benchmark]"); + if (!button) return; + const name = button.dataset.benchmark; + if (state.activeBenchmarks.has(name) && state.activeBenchmarks.size > 1) state.activeBenchmarks.delete(name); + else state.activeBenchmarks.add(name); + renderChoiceControls(); + renderHistory(); + }); + document.querySelector("#config-filter").addEventListener("click", function (event) { + const button = event.target.closest("button[data-config]"); + if (!button) return; + const config = button.dataset.config; + if (state.activeConfigs.has(config) && state.activeConfigs.size > 1) state.activeConfigs.delete(config); + else state.activeConfigs.add(config); + renderChoiceControls(); + renderHistory(); + }); + historyMetric.addEventListener("change", renderHistory); + historyRange.addEventListener("change", renderHistory); +} + async function main() { try { const response = await fetch("data/index.json", { cache: "no-store" }); if (!response.ok) throw new Error("No published benchmark results yet"); state.index = await response.json(); - const runs = state.index.runs || []; - if (!runs.length) throw new Error("No published benchmark results yet"); - fillSelects(runs); - renderHistoryTable(runs); - newerSelect.addEventListener("change", update); - baselineSelect.addEventListener("change", update); - await Promise.all([update(), renderHistoryCharts(runs)]); + state.index.runs = (state.index.runs || []).slice().sort(function (a, b) { return String(b.createdAt).localeCompare(String(a.createdAt)); }); + if (!state.index.runs.length) throw new Error("No published benchmark results yet"); + const latest = await loadRun(state.index.runs[0]); + state.benchmarkNames = Array.from(benchmarkMap(latest).keys()).sort(); + state.activeBenchmarks = new Set(state.benchmarkNames.slice(0, Math.min(3, state.benchmarkNames.length))); + state.activeConfigs = new Set(configs); + fillRunSelects(state.index.runs); + renderChoiceControls(); + attachEvents(); + await refresh(); } catch (error) { status.textContent = error.message; status.className = "status error"; diff --git a/ci/llgo-size/site/index.html b/ci/llgo-size/site/index.html index a0f44fd..2ac2c8c 100644 --- a/ci/llgo-size/site/index.html +++ b/ci/llgo-size/site/index.html @@ -4,24 +4,27 @@ LLGo binary-size history - +
-

LLGo compiler benchmarks

-

Binary-size history

-

ELF sizes collected by Bent benchsize. Compare two CI runs side by side and trace each benchmark over time.

+
+

LLGo compiler benchmarks

+

Binary-size history

+

Inspect every published commit across Go and LLGo build modes. Pick two commits to surface their direct size difference without losing the Go baseline.

+

Loading results…

-

Compare runs

-

The newer and baseline runs become adjacent columns, grouped by use case and build mode.

+

commit comparison

+

Choose a baseline and selected commit

+

The selected commit row displays its change from the baseline, while every LLGo value keeps its own difference from Go.

- - + +
@@ -29,27 +32,38 @@

Compare runs

-

total size · LLGo relative to Go

-

Size comparison

+

commits as rows · benchmark modes as columns

+

Commit comparison matrix

-
+
+

+ + + +
+
+
+
-
+
-

one chart per benchmark

-

Size history

+

interactive trends

+

Explore history

+ +
+
+
Benchmarks
+
Build modes
+
-
-
- - - -
LLGo commitCreatedGoLLVMWorkflow
+
+
+
@@ -59,6 +73,6 @@

Size history

Raw run index
- + diff --git a/ci/llgo-size/site/style.css b/ci/llgo-size/site/style.css index 6092af8..c4989b7 100644 --- a/ci/llgo-size/site/style.css +++ b/ci/llgo-size/site/style.css @@ -1,96 +1,106 @@ -:root { - color-scheme: light; - --ink: #10233f; - --muted: #617089; - --line: #dfe6f1; - --panel: #ffffff; - --wash: #f4f7fc; - --accent: #2457d6; - --accent-soft: #eaf0ff; - --good: #08765a; - --bad: #bd3e4b; - --shadow: 0 14px 34px rgba(19, 42, 79, .08); -} - +:root { color-scheme: light; --ink: #10233f; --muted: #617089; --line: #dfe6f1; --panel: #fff; --wash: #f4f7fc; --accent: #2457d6; --accent-soft: #eaf0ff; --good: #08765a; --bad: #bd3e4b; --shadow: 0 14px 34px rgba(19, 42, 79, .08); } * { box-sizing: border-box; } -body { margin: 0; background: radial-gradient(circle at 8% 0, #e7efff 0, transparent 29rem), var(--wash); color: var(--ink); font: 15px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } +body { margin: 0; background: var(--wash); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } a { color: var(--accent); text-underline-offset: 2px; } a:hover { color: #173e9d; } -code { font: .88em/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; } -.shell { max-width: 1400px; margin: 0 auto; padding: 42px 28px 68px; } -.hero { position: relative; margin-bottom: 24px; padding: 34px 36px 30px; overflow: hidden; border: 1px solid #d9e3f7; border-radius: 22px; background: linear-gradient(120deg, #ffffff 0%, #f5f8ff 100%); box-shadow: var(--shadow); } -.hero::after { position: absolute; top: -122px; right: -70px; width: 300px; height: 300px; border-radius: 50%; background: radial-gradient(circle, rgba(47, 101, 231, .16), rgba(47, 101, 231, 0) 68%); content: ""; pointer-events: none; } -.eyebrow, .label { color: var(--accent); font-size: 11px; font-weight: 750; letter-spacing: .12em; text-transform: uppercase; } -h1, h2, p { margin-top: 0; } -h1 { position: relative; z-index: 1; margin-bottom: 9px; font-size: clamp(34px, 5vw, 56px); letter-spacing: -.05em; line-height: .98; } -h2 { margin-bottom: 4px; font-size: 21px; letter-spacing: -.02em; } -.lede { position: relative; z-index: 1; max-width: 680px; margin-bottom: 0; color: var(--muted); font-size: 16px; } +code { font: .9em/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; } +button, input, select { font: inherit; } +button { cursor: pointer; } +.shell { max-width: 1540px; margin: 0 auto; padding: 34px 24px 60px; } +.hero { display: flex; justify-content: space-between; gap: 24px; align-items: end; margin-bottom: 20px; padding: 28px 32px; border: 1px solid #d9e3f7; border-radius: 16px; background: #fff; box-shadow: var(--shadow); } +.eyebrow, .label, .control-label { color: var(--accent); font-size: 10px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; } +h1, h2, h3, p { margin-top: 0; } +h1 { margin-bottom: 7px; font-size: clamp(32px, 4vw, 48px); letter-spacing: -.05em; line-height: 1; } +h2 { margin-bottom: 4px; font-size: 20px; letter-spacing: -.02em; } +h3 { margin-bottom: 7px; } +.lede { max-width: 780px; margin-bottom: 0; color: var(--muted); font-size: 15px; } .muted { color: var(--muted); } -.status { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 6px; margin: 18px 0 0; padding: 6px 11px; border: 1px solid #d6e1fb; border-radius: 999px; background: var(--accent-soft); color: #24469f; font-size: 12px; font-weight: 650; } +.status { display: inline-flex; align-items: center; gap: 6px; flex: none; margin: 0; padding: 6px 11px; border: 1px solid #d6e1fb; border-radius: 999px; background: var(--accent-soft); color: #24469f; font-size: 12px; font-weight: 650; white-space: nowrap; } .status::before { width: 6px; height: 6px; border-radius: 50%; background: currentColor; content: ""; } .status.error { background: #fff0f0; color: var(--bad); } -.controls, .run-meta, .card { border: 1px solid var(--line); border-radius: 18px; background: var(--panel); box-shadow: 0 7px 24px rgba(19, 42, 79, .045); } -.controls { display: grid; grid-template-columns: 1fr minmax(210px, 275px) minmax(210px, 275px); gap: 24px; align-items: end; padding: 22px 24px; margin-bottom: 14px; } -.controls h2 { margin-bottom: 5px; } +.controls, .run-meta, .card { border: 1px solid var(--line); border-radius: 14px; background: var(--panel); box-shadow: 0 7px 24px rgba(19, 42, 79, .045); } +.controls { display: grid; grid-template-columns: 1fr minmax(240px, 310px) minmax(240px, 310px); gap: 22px; align-items: end; padding: 20px 22px; margin-bottom: 12px; } .controls p { margin-bottom: 0; } -label { display: grid; gap: 7px; color: var(--muted); font-size: 11px; font-weight: 750; letter-spacing: .04em; text-transform: uppercase; } -select { width: 100%; padding: 10px 32px 10px 12px; border: 1px solid #cbd7eb; border-radius: 10px; background: #fbfcff; color: var(--ink); font: 600 14px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } -select:focus { border-color: #5c82e8; outline: 3px solid rgba(71, 118, 226, .16); } -.run-meta { display: grid; grid-template-columns: 1.35fr 1fr 1.1fr 1.05fr 1.35fr; gap: 0; padding: 7px 10px; margin-bottom: 18px; } -.run-meta > div { display: grid; gap: 4px; min-width: 0; padding: 10px 13px; border-right: 1px solid var(--line); font-size: 13px; } +label { display: grid; gap: 6px; color: var(--muted); font-size: 10px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; } +select, input { width: 100%; padding: 9px 10px; border: 1px solid #cbd7eb; border-radius: 8px; background: #fbfcff; color: var(--ink); font-weight: 600; letter-spacing: normal; text-transform: none; } +select:focus, input:focus { border-color: #5c82e8; outline: 3px solid rgba(71, 118, 226, .16); } +.run-meta { display: grid; grid-template-columns: 1fr 1fr 1.2fr; margin-bottom: 16px; overflow: hidden; } +.run-meta > div { display: grid; gap: 3px; min-width: 0; padding: 12px 15px; border-right: 1px solid var(--line); font-size: 13px; } .run-meta > div:last-child { border-right: 0; } -.label { color: var(--muted); font-size: 10px; } -.card { overflow: hidden; margin-bottom: 18px; } -.card-heading { display: flex; justify-content: space-between; gap: 24px; align-items: end; padding: 23px 24px 20px; border-bottom: 1px solid var(--line); background: linear-gradient(180deg, #fff, #fcfdff); } -.card-heading p { margin-bottom: 5px; } +.run-meta strong { font-size: 14px; } +.meta-detail { color: var(--muted); font-size: 11px; } +.card { margin-bottom: 17px; overflow: hidden; } +.card-heading { display: flex; justify-content: space-between; gap: 24px; align-items: end; padding: 20px 22px 17px; border-bottom: 1px solid var(--line); background: #fff; } +.card-heading p { margin-bottom: 4px; } .card-heading h2 { margin-bottom: 0; } -.table-wrap { overflow-x: auto; scrollbar-color: #aebddd transparent; scrollbar-width: thin; } -table { width: 100%; border-collapse: collapse; min-width: 860px; } -th, td { padding: 13px 18px; border-bottom: 1px solid var(--line); text-align: right; white-space: nowrap; } -th:first-child, td:first-child { text-align: left; } -thead th { border-bottom: 1px solid #cbd7eb; background: #f7f9fe; color: #5a6b84; font-size: 10px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } -.comparison-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; padding: 20px 24px 24px; background: #fbfcff; } -.benchmark-card { overflow: hidden; border: 1px solid #dce5f2; border-radius: 13px; background: #fff; box-shadow: 0 4px 12px rgba(19, 42, 79, .035); } -.benchmark-card h3 { margin: 0; padding: 13px 15px 10px; color: #23395d; font-size: 14px; letter-spacing: -.01em; } -.benchmark-card table { min-width: 0; table-layout: fixed; } -.benchmark-card th, .benchmark-card td { padding: 9px 11px; } -.benchmark-card thead th { font-size: 9px; } -.benchmark-card thead th:first-child { width: 26%; } -.benchmark-card .config-mode { color: var(--muted); font-size: 10px; font-weight: 750; text-align: left; white-space: normal; } -.config-mode abbr { border-bottom: 0; cursor: help; text-decoration: none; } -.size-value { display: block; color: #1c3156; font-size: 13px; font-variant-numeric: tabular-nums; } -.comparison-cell { min-width: 0; vertical-align: top; } -.relative-delta { display: flex; align-items: baseline; justify-content: flex-end; gap: 4px; margin-top: 3px; min-height: 17px; white-space: nowrap; } -.percent-delta { font-size: 14px; font-weight: 780; letter-spacing: -.02em; font-variant-numeric: tabular-nums; } -.delta-label { color: var(--muted); font-size: 9px; font-weight: 650; } -.previous-delta { min-height: 14px; margin-top: 1px; font-size: 10px; font-weight: 650; } +.matrix-controls { display: grid; grid-template-columns: auto auto 1fr minmax(190px, 260px); gap: 14px; align-items: center; padding: 12px 18px; border-bottom: 1px solid var(--line); background: #fbfcff; } +.commit-count { margin: 0; color: #293f63; font-size: 12px; font-weight: 800; white-space: nowrap; } +.page-size-label, .filter-label { gap: 4px; font-size: 9px; } +.page-size-label select { min-width: 82px; padding: 6px 8px; } +.filter-label input { padding: 7px 9px; } +.pagination { display: flex; justify-content: center; align-items: center; gap: 5px; min-width: 0; } +.pagination button { min-width: 29px; padding: 6px 8px; border: 1px solid #d4deed; border-radius: 7px; background: #fff; color: #3c5275; font-size: 11px; font-weight: 750; } +.pagination button:hover:not(:disabled), .pagination button.active { border-color: var(--accent); background: var(--accent); color: #fff; } +.pagination button:disabled { cursor: not-allowed; opacity: .45; } +.page-gap { color: var(--muted); font-weight: 800; } +.matrix-wrap { overflow: auto; max-height: 650px; background: #fff; scrollbar-color: #9aabc4 transparent; scrollbar-width: thin; } +.comparison-table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; font-variant-numeric: tabular-nums; } +.comparison-table th, .comparison-table td { padding: 8px 9px; border-right: 1px solid #e4e9f2; border-bottom: 1px solid var(--line); background: #fff; vertical-align: top; white-space: nowrap; } +.comparison-table thead th { position: sticky; top: 0; z-index: 3; background: #f7f9fe; color: #566982; font-size: 9px; font-weight: 800; letter-spacing: .03em; text-align: center; } +.comparison-table thead tr:first-child th { top: 0; border-bottom-color: #cbd7eb; color: #2a4269; } +.comparison-table thead tr:nth-child(2) th { top: 33px; } +.comparison-table tbody th { text-align: left; font-weight: 750; } +.comparison-table .sticky { position: sticky; z-index: 2; background: inherit; } +.comparison-table thead .sticky { z-index: 5; background: #f7f9fe; } +.pick-cell { left: 0; width: 64px; min-width: 64px; } +.commit-cell { left: 64px; width: 108px; min-width: 108px; } +.date-cell { left: 172px; width: 144px; min-width: 144px; white-space: normal !important; } +.branch-cell { left: 316px; width: 72px; min-width: 72px; } +.matrix-cell { width: 124px; min-width: 124px; text-align: right; } +.matrix-cell strong { display: block; color: #18365d; font-size: 12px; } +.matrix-cell .go-delta { display: block; margin-top: 2px; font-size: 10px; font-weight: 700; } +.selected-delta { display: block; margin-top: 3px; font-size: 10px; font-weight: 850; } .reference { font-size: 10px; font-weight: 650; } -tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; } -tbody tr:hover > * { background-color: #f8faff; } -td small, tbody td small, tbody th small { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; font-weight: 500; } +.commit-picks { display: flex; gap: 4px; } +.commit-picks button { width: 22px; height: 22px; padding: 0; border: 1px solid #ccd8e9; border-radius: 5px; background: #fff; color: #60728d; font-size: 10px; font-weight: 850; } +.commit-picks button:hover, .commit-picks button.active { border-color: var(--accent); background: var(--accent); color: #fff; } +.comparison-table tbody tr:hover > * { background: #f8faff; } +.comparison-table tbody tr.baseline-row > * { background: #f4f7ff; } +.comparison-table tbody tr.selected-row > * { background: #eef4ff; box-shadow: inset 0 1px 0 #2457d6, inset 0 -1px 0 #2457d6; } +.comparison-table tbody tr.selected-row > *:first-child { box-shadow: inset 2px 0 0 #2457d6, inset 0 1px 0 #2457d6, inset 0 -1px 0 #2457d6; } +.comparison-table tbody tr.selected-row > *:last-child { box-shadow: inset -2px 0 0 #2457d6, inset 0 1px 0 #2457d6, inset 0 -1px 0 #2457d6; } +.comparison-table tbody tr:last-child > * { border-bottom: 0; } +.missing, .empty-state { color: var(--muted); text-align: center; } .good { color: var(--good); } .bad { color: var(--bad); } .flat { color: var(--muted); } -.chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(390px, 1fr)); gap: 18px; padding: 24px; border-bottom: 1px solid var(--line); background: #fbfcff; } -.chart-card { border: 1px solid #dce5f2; border-radius: 14px; padding: 17px 16px 14px; overflow: hidden; background: #fff; box-shadow: 0 4px 12px rgba(19, 42, 79, .035); } -.chart-card h3 { margin: 0 0 10px; color: #23395d; font-size: 14px; letter-spacing: -.01em; } -.chart-card svg { display: block; width: 100%; min-width: 350px; height: auto; } +.history-heading { align-items: center; } +.history-range { min-width: 150px; } +.history-controls { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(300px, 1.35fr) 150px; gap: 16px; align-items: end; padding: 14px 20px; border-bottom: 1px solid var(--line); background: #fbfcff; } +.control-label { display: block; margin-bottom: 6px; color: var(--muted); font-size: 9px; } +.choice-row { display: flex; flex-wrap: wrap; gap: 6px; } +.choice-button { padding: 5px 8px; border: 1px solid #d5deec; border-radius: 6px; background: #fff; color: #617089; font-size: 10px; font-weight: 750; } +.choice-button:hover, .choice-button.active { border-color: #9eb8f2; background: #eef4ff; color: #224da9; } +.config-choice::before { display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: var(--series); content: ""; } +.history-layout { display: grid; grid-template-columns: minmax(0, 1fr) 270px; gap: 16px; padding: 19px; background: #fff; } +.history-chart { min-width: 0; padding: 15px; border: 1px solid #dce5f2; border-radius: 11px; background: #fff; } +.history-chart svg { display: block; width: 100%; height: auto; } +.chart-title { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px; color: var(--muted); font-size: 11px; font-weight: 750; } .chart-grid-line { stroke: #e3e9f3; stroke-width: 1; } .chart-axis-label { fill: var(--muted); font: 10px system-ui, sans-serif; } -.chart-legend { display: flex; flex-wrap: wrap; gap: 7px 12px; margin-top: 10px; color: var(--muted); font-size: 10px; font-weight: 600; } -.chart-legend span::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: 5px; vertical-align: -1px; border-radius: 50%; background: var(--series); } +.history-series { stroke-width: 2.1; } +.selection-marker { stroke: var(--accent); stroke-width: 1.4; stroke-dasharray: 4 4; } +.marker-label { fill: #fff; font: 800 10px system-ui, sans-serif; paint-order: stroke; stroke: var(--accent); stroke-width: 7px; stroke-linejoin: round; } +.history-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 9px; color: var(--muted); font-size: 10px; font-weight: 650; } +.history-legend-item { display: inline-flex; align-items: center; } +.history-legend-item i { width: 17px; height: 0; margin-right: 5px; border-top: 2px solid var(--series); background: repeating-linear-gradient(90deg, var(--series) 0 4px, transparent 4px 7px); } +.history-inspector { align-self: start; min-height: 185px; padding: 17px; border: 1px solid #b7d9d0; border-radius: 11px; background: #fbfffe; } +.history-inspector h3 { font-size: 14px; } +.inspector-value { display: block; font-size: 23px; letter-spacing: -.04em; } +.inspector-delta { margin: 2px 0 13px; font-size: 14px; font-weight: 850; } +.history-inspector dl { display: grid; gap: 8px; margin: 0; } +.history-inspector dl div { display: flex; justify-content: space-between; gap: 10px; } +.history-inspector dt { color: var(--muted); font-size: 10px; font-weight: 750; } +.history-inspector dd { margin: 0; font-size: 11px; font-weight: 800; text-align: right; } footer { display: flex; gap: 10px; flex-wrap: wrap; padding: 5px 2px; color: var(--muted); font-size: 13px; } -@media (max-width: 820px) { - .shell { padding: 22px 14px 48px; } - .hero { padding: 27px 22px 25px; border-radius: 18px; } - .lede { font-size: 15px; } - .controls { grid-template-columns: 1fr; gap: 16px; padding: 20px; } - .run-meta { grid-template-columns: 1fr 1fr; gap: 0; padding: 6px; } - .run-meta > div { border-right: 0; border-bottom: 1px solid var(--line); } - .run-meta > div:nth-last-child(-n + 2) { border-bottom: 0; } - .card-heading { align-items: start; flex-direction: column; } - .comparison-grid { grid-template-columns: 1fr; padding: 14px; } - .benchmark-card th, .benchmark-card td { padding: 10px; } - .chart-grid { grid-template-columns: 1fr; padding: 14px; } - .chart-card svg { min-width: 0; } -} +@media (max-width: 920px) { .shell { padding: 20px 14px 44px; } .hero, .controls, .history-controls, .history-layout { grid-template-columns: 1fr; } .hero { align-items: start; } .status { align-self: start; } .run-meta { grid-template-columns: 1fr; } .run-meta > div { border-right: 0; border-bottom: 1px solid var(--line); } .run-meta > div:last-child { border-bottom: 0; } .matrix-controls { grid-template-columns: 1fr 1fr; } .pagination { justify-content: start; grid-column: 1 / -1; order: 3; } .filter-label { grid-column: 1 / -1; } .history-range { width: 100%; } }