mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-28 10:40:11 +01:00
425 lines
21 KiB
HTML
425 lines
21 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Fastfetch · Benchmark</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
--page: #f5f7fb;
|
|
--card: #fff;
|
|
--text: #172033;
|
|
--muted: #64748b;
|
|
--border: #dbe3ef;
|
|
--accent: #2563eb;
|
|
--accent-soft: #dbeafe;
|
|
--chart-text: #334155;
|
|
--chart-grid: #cbd5e1;
|
|
}
|
|
:root[data-theme="dark"] {
|
|
color-scheme: dark;
|
|
--page: #0f172a; --card: #172033; --text: #e5edf8; --muted: #9aabc1; --border: #334155; --accent-soft: #1e3a8a;
|
|
--chart-text: #f1f5f9;
|
|
--chart-grid: #475569;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { max-width: 1440px; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; background: var(--page); color: var(--text); font: 16px/1.5 system-ui, -apple-system, sans-serif; }
|
|
header { margin-bottom: 1.75rem; }
|
|
.header-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
h1 { margin: 0; font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -.04em; }
|
|
h2 { margin: 0; font-size: 1.25rem; }
|
|
p { color: var(--muted); }
|
|
.intro { margin: .6rem 0 1rem; }
|
|
.links a { color: var(--accent); font-weight: 600; text-decoration: none; }
|
|
.links a:hover { text-decoration: underline; }
|
|
.theme-toggle, .fullscreen { border: 1px solid var(--border); border-radius: .55rem; padding: .5rem .8rem; background: var(--card); color: var(--text); cursor: pointer; font: inherit; font-weight: 600; }
|
|
.theme-toggle:hover, .fullscreen:hover { border-color: var(--accent); color: var(--accent); }
|
|
.tabs { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1.5rem 0; padding: .35rem; border: 1px solid var(--border); border-radius: .8rem; background: var(--card); }
|
|
.tab { border: 0; border-radius: .55rem; padding: .65rem 1.15rem; background: transparent; color: var(--muted); cursor: pointer; font: inherit; font-weight: 700; }
|
|
.tab:hover, .tab[aria-selected="true"] { background: var(--accent); color: #fff; }
|
|
.tab-panel[hidden] { display: none; }
|
|
.platform-title { display: flex; align-items: center; gap: .65rem; margin: 1.5rem 0 1rem; }
|
|
.platform-title::before { content: ''; width: .35rem; height: 1.4rem; border-radius: 1rem; background: var(--accent); }
|
|
.charts { display: flex; flex-direction: column; gap: 1.25rem; }
|
|
.chart { min-width: 0; padding: 1.25rem; border: 1px solid var(--border); border-radius: 1rem; background: var(--card); box-shadow: 0 8px 28px #0f172a0c; }
|
|
.chart:fullscreen { overflow: auto; padding: 2rem; background: var(--page); }
|
|
.chart:fullscreen .canvas-wrap { height: calc(100vh - 13rem); }
|
|
.chart-header { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: .75rem; }
|
|
.chart-header span { color: var(--muted); font-size: .85rem; }
|
|
.canvas-wrap { position: relative; height: 360px; }
|
|
.chart-actions { display: flex; align-items: center; gap: .75rem; }
|
|
.legend-actions { display: flex; flex-wrap: wrap; gap: .45rem; margin-top: 1rem; }
|
|
.legend-action { border: 1px solid var(--border); border-radius: .45rem; padding: .35rem .6rem; background: var(--card); color: var(--muted); cursor: pointer; font: inherit; font-size: .8rem; }
|
|
.legend-action:hover { border-color: var(--accent); color: var(--accent); }
|
|
.legend { display: flex; flex-wrap: wrap; gap: .35rem .9rem; margin-top: 1rem; }
|
|
.legend-item { display: inline-flex; align-items: center; gap: .4rem; border: 0; padding: .15rem 0; background: transparent; color: var(--text); cursor: pointer; font: inherit; font-size: .85rem; }
|
|
.legend-item.is-hidden { color: var(--muted); }
|
|
.legend-swatch { width: .75rem; height: .75rem; border-radius: 50%; }
|
|
.legend-item.is-hidden .legend-swatch { opacity: .35; }
|
|
.chart-tooltip { position: fixed; z-index: 1000; pointer-events: none; width: min(90vw, 900px); max-height: calc(100vh - 24px); overflow: hidden; padding: .7rem .85rem; border: 1px solid var(--border); border-radius: .6rem; background: var(--card); color: var(--text); box-shadow: 0 10px 30px #0f172a33; font-size: .8rem; opacity: 0; transition: opacity .1s ease; }
|
|
.chart-tooltip-title { margin-bottom: .35rem; font-weight: 700; white-space: normal; overflow-wrap: anywhere; }
|
|
.chart-tooltip-message { margin-bottom: .35rem; color: var(--muted); font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
|
|
.chart-tooltip-body { max-height: calc(100vh - 76px); overflow: auto; columns: 220px auto; column-gap: 1.5rem; }
|
|
.chart-tooltip-row { display: flex; align-items: center; gap: .45rem; break-inside: avoid; white-space: nowrap; }
|
|
.chart-tooltip-swatch { width: .55rem; height: .55rem; border-radius: 50%; flex: 0 0 auto; }
|
|
.empty { padding: 2rem 0; color: var(--muted); }
|
|
</style>
|
|
<script src="data.js"></script>
|
|
<script src="https://unpkg.com/chart.js@latest/dist/chart.umd.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="header-row">
|
|
<h1>Fastfetch Benchmark</h1>
|
|
<button id="theme-toggle" class="theme-toggle" type="button"></button>
|
|
</div>
|
|
<p class="intro">Performance history grouped by CPU family. Each chart contains all modules and the total process time for one operating system and architecture. All metric series are shown by default except total process time, module Weather, and module PublicIp — toggle any series from the legend below the chart.</p>
|
|
<p class="links"><a href="data.js">Download raw benchmark data</a></p>
|
|
</header>
|
|
<nav id="tabs" class="tabs" aria-label="CPU architecture"></nav>
|
|
<main id="panels"><p class="empty">Loading charts…</p></main>
|
|
<script>
|
|
(() => {
|
|
const benchmarkData = window.BENCHMARK_DATA || {};
|
|
const entries = Object.values(benchmarkData.entries || {}).flat();
|
|
const groups = new Map();
|
|
|
|
function architectureFamily(arch) {
|
|
const normalized = arch.toLowerCase();
|
|
if (/^(amd64|x86_64|x86|i[3-6]86)$/.test(normalized)) return 'Intel';
|
|
if (/^(aarch64|arm64|armv[5-8].*|arm)$/.test(normalized)) return 'ARM';
|
|
return 'Other';
|
|
}
|
|
|
|
function parseName(name) {
|
|
let match = name.match(/^fastfetch (linux|macos|windows) ([^ ]+) (module .+|total process time)$/i);
|
|
if (match) {
|
|
return { platform: match[1].toLowerCase(), arch: match[2], metric: match[3] };
|
|
}
|
|
|
|
// Keep older macOS records visible while new records use the platform field.
|
|
match = name.match(/^fastfetch ([^ ]+) (module .+|total process time)$/i);
|
|
return match ? { platform: 'macos', arch: match[1], metric: match[2] } : null;
|
|
}
|
|
|
|
function entryMetadata(entry) {
|
|
const raw = entry.commit?.timestamp ?? entry.date;
|
|
const date = new Date(raw);
|
|
return {
|
|
date: Number.isNaN(date.valueOf()) ? String(entry.date ?? '') : date.toISOString(),
|
|
sha: entry.commit?.id ?? 'unknown',
|
|
message: entry.commit?.message?.trim() ?? ''
|
|
};
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const metricsByGroup = new Map();
|
|
for (const bench of entry.benches || []) {
|
|
const parsed = parseName(bench.name);
|
|
if (!parsed) continue;
|
|
const key = `${parsed.platform} ${parsed.arch}`;
|
|
if (!metricsByGroup.has(key)) metricsByGroup.set(key, new Map());
|
|
metricsByGroup.get(key).set(parsed.metric, bench.value);
|
|
}
|
|
|
|
for (const [key, metrics] of metricsByGroup) {
|
|
if (!groups.has(key)) groups.set(key, []);
|
|
groups.get(key).push({ ...entryMetadata(entry), metrics });
|
|
}
|
|
}
|
|
|
|
const colors = [
|
|
'#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2',
|
|
'#db2777', '#65a30d', '#4f46e5', '#ca8a04', '#0f766e', '#be123c'
|
|
];
|
|
// Series hidden on first render; every other metric is shown by default.
|
|
const hiddenByDefault = new Set(['total process time', 'module Weather', 'module PublicIp']);
|
|
const tabs = document.getElementById('tabs');
|
|
const panels = document.getElementById('panels');
|
|
const root = document.documentElement;
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const savedTheme = localStorage.getItem('fastfetch-benchmark-theme');
|
|
let theme = savedTheme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
const chartsByInstance = [];
|
|
|
|
if (!window.Chart) {
|
|
tabs.hidden = true;
|
|
panels.innerHTML = '<p class="empty">Failed to load Chart.js.</p>';
|
|
return;
|
|
}
|
|
panels.replaceChildren();
|
|
|
|
function chartColors() {
|
|
const styles = getComputedStyle(root);
|
|
return {
|
|
text: styles.getPropertyValue('--chart-text').trim(),
|
|
grid: styles.getPropertyValue('--chart-grid').trim()
|
|
};
|
|
}
|
|
|
|
function externalTooltipHandler(context) {
|
|
const { chart, tooltip } = context;
|
|
let tooltipElement = chart.canvas.parentNode.querySelector('.chart-tooltip');
|
|
if (!tooltipElement) {
|
|
tooltipElement = document.createElement('div');
|
|
tooltipElement.className = 'chart-tooltip';
|
|
chart.canvas.parentNode.appendChild(tooltipElement);
|
|
}
|
|
|
|
if (tooltip.opacity === 0) {
|
|
tooltipElement.style.opacity = '0';
|
|
return;
|
|
}
|
|
|
|
tooltipElement.replaceChildren();
|
|
const title = document.createElement('div');
|
|
title.className = 'chart-tooltip-title';
|
|
const row = chart._benchmarkRows[tooltip.dataPoints[0].dataIndex];
|
|
title.textContent = `${new Date(row.date).toLocaleString()} · ${row.sha}`;
|
|
tooltipElement.appendChild(title);
|
|
if (row.message) {
|
|
const message = document.createElement('div');
|
|
message.className = 'chart-tooltip-message';
|
|
message.textContent = row.message;
|
|
tooltipElement.appendChild(message);
|
|
}
|
|
const bodyElement = document.createElement('div');
|
|
bodyElement.className = 'chart-tooltip-body';
|
|
for (const body of [...tooltip.dataPoints].sort((a, b) => b.parsed.y - a.parsed.y)) {
|
|
const item = document.createElement('div');
|
|
item.className = 'chart-tooltip-row';
|
|
const swatch = document.createElement('span');
|
|
swatch.className = 'chart-tooltip-swatch';
|
|
swatch.style.background = body.dataset.borderColor;
|
|
const value = document.createElement('span');
|
|
value.textContent = `${body.dataset.label}: ${body.formattedValue} ms`;
|
|
item.append(swatch, value);
|
|
bodyElement.appendChild(item);
|
|
}
|
|
tooltipElement.appendChild(bodyElement);
|
|
|
|
const canvasRect = chart.canvas.getBoundingClientRect();
|
|
const margin = 12;
|
|
let left = canvasRect.left + tooltip.caretX + margin;
|
|
let top = canvasRect.top + tooltip.caretY + margin;
|
|
tooltipElement.style.left = `${left}px`;
|
|
tooltipElement.style.top = `${top}px`;
|
|
tooltipElement.style.opacity = '0';
|
|
const tooltipRect = tooltipElement.getBoundingClientRect();
|
|
if (left + tooltipRect.width > window.innerWidth - margin) left = canvasRect.left + tooltip.caretX - tooltipRect.width - margin;
|
|
if (top + tooltipRect.height > window.innerHeight - margin) top = canvasRect.top + tooltip.caretY - tooltipRect.height - margin;
|
|
tooltipElement.style.left = `${Math.max(margin, left)}px`;
|
|
tooltipElement.style.top = `${Math.max(margin, top)}px`;
|
|
tooltipElement.style.opacity = '1';
|
|
}
|
|
|
|
function updateTheme() {
|
|
root.dataset.theme = theme;
|
|
themeToggle.textContent = theme === 'dark' ? '☀ Light mode' : '☾ Dark mode';
|
|
themeToggle.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`);
|
|
const colors = chartColors();
|
|
Chart.defaults.color = colors.text;
|
|
Chart.defaults.borderColor = colors.grid;
|
|
for (const chart of chartsByInstance) {
|
|
chart.options.scales.x.title.color = colors.text;
|
|
chart.options.scales.x.ticks.color = colors.text;
|
|
chart.options.scales.x.grid.color = colors.grid;
|
|
chart.options.scales.y.title.color = colors.text;
|
|
chart.options.scales.y.ticks.color = colors.text;
|
|
chart.options.scales.y.grid.color = colors.grid;
|
|
chart.update();
|
|
}
|
|
}
|
|
themeToggle.addEventListener('click', () => {
|
|
theme = theme === 'dark' ? 'light' : 'dark';
|
|
localStorage.setItem('fastfetch-benchmark-theme', theme);
|
|
updateTheme();
|
|
});
|
|
updateTheme();
|
|
const families = new Map();
|
|
|
|
for (const [key, rows] of groups) {
|
|
const arch = key.slice(key.lastIndexOf(' ') + 1);
|
|
const family = architectureFamily(arch);
|
|
if (!families.has(family)) families.set(family, []);
|
|
families.get(family).push({ key, rows });
|
|
}
|
|
|
|
// 'Other' is intentionally not rendered: it only catches stray series (e.g. the retired
|
|
// 'macos universal' records) that should not show up as their own tab
|
|
const familyOrder = ['Intel', 'ARM'];
|
|
const availableFamilies = familyOrder.filter(family => families.has(family));
|
|
|
|
for (const [index, family] of availableFamilies.entries()) {
|
|
const tab = document.createElement('button');
|
|
const panelId = `panel-${family.toLowerCase()}`;
|
|
tab.className = 'tab';
|
|
tab.type = 'button';
|
|
tab.id = `tab-${family.toLowerCase()}`;
|
|
tab.setAttribute('role', 'tab');
|
|
tab.textContent = family;
|
|
tab.setAttribute('aria-controls', panelId);
|
|
tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
|
|
tabs.appendChild(tab);
|
|
|
|
const panel = document.createElement('section');
|
|
panel.className = 'tab-panel';
|
|
panel.id = panelId;
|
|
panel.setAttribute('role', 'tabpanel');
|
|
panel.hidden = index !== 0;
|
|
panel.setAttribute('aria-labelledby', tab.id);
|
|
const title = document.createElement('h2');
|
|
title.className = 'platform-title';
|
|
title.textContent = `${family} processors`;
|
|
panel.appendChild(title);
|
|
const chartGrid = document.createElement('div');
|
|
chartGrid.className = 'charts';
|
|
panel.appendChild(chartGrid);
|
|
panels.appendChild(panel);
|
|
|
|
tab.addEventListener('click', () => {
|
|
for (const otherTab of tabs.children) otherTab.setAttribute('aria-selected', otherTab === tab ? 'true' : 'false');
|
|
for (const otherPanel of panels.children) otherPanel.hidden = otherPanel !== panel;
|
|
});
|
|
|
|
for (const { key, rows } of families.get(family).sort((a, b) => a.key.localeCompare(b.key))) {
|
|
rows.sort((a, b) => a.date.localeCompare(b.date));
|
|
const metrics = [...new Set(rows.flatMap(row => [...row.metrics.keys()]))].sort((a, b) => {
|
|
if (a === 'total process time') return -1;
|
|
if (b === 'total process time') return 1;
|
|
return a.localeCompare(b);
|
|
});
|
|
const section = document.createElement('article');
|
|
section.className = 'chart';
|
|
const chartHeader = document.createElement('div');
|
|
chartHeader.className = 'chart-header';
|
|
const chartTitle = document.createElement('h2');
|
|
chartTitle.textContent = key;
|
|
const chartActions = document.createElement('div');
|
|
chartActions.className = 'chart-actions';
|
|
const chartHint = document.createElement('span');
|
|
chartHint.textContent = `${rows.length} run${rows.length === 1 ? '' : 's'}`;
|
|
const fullscreen = document.createElement('button');
|
|
fullscreen.className = 'fullscreen';
|
|
fullscreen.type = 'button';
|
|
fullscreen.textContent = '⛶ Fullscreen';
|
|
fullscreen.addEventListener('click', () => {
|
|
if (document.fullscreenElement) document.exitFullscreen();
|
|
else section.requestFullscreen();
|
|
});
|
|
chartActions.append(chartHint, fullscreen);
|
|
chartHeader.append(chartTitle, chartActions);
|
|
const canvasWrap = document.createElement('div');
|
|
canvasWrap.className = 'canvas-wrap';
|
|
const canvas = document.createElement('canvas');
|
|
canvasWrap.appendChild(canvas);
|
|
section.append(chartHeader, canvasWrap);
|
|
chartGrid.appendChild(section);
|
|
|
|
const colorsForChart = chartColors();
|
|
const chart = new Chart(canvas, {
|
|
type: 'line',
|
|
data: {
|
|
labels: rows.map(row => row.sha.slice(0, 7)),
|
|
datasets: metrics.map((metric, index) => ({
|
|
label: metric,
|
|
data: rows.map(row => row.metrics.has(metric) ? row.metrics.get(metric) : null),
|
|
borderColor: colors[index % colors.length],
|
|
backgroundColor: colors[index % colors.length],
|
|
pointRadius: 3,
|
|
tension: 0.2,
|
|
spanGaps: true,
|
|
hidden: hiddenByDefault.has(metric)
|
|
}))
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: { mode: 'index', intersect: false },
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
enabled: false,
|
|
external: externalTooltipHandler,
|
|
callbacks: {
|
|
title: tooltipItems => {
|
|
const row = rows[tooltipItems[0].dataIndex];
|
|
return `${row.date} · ${row.sha}`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: { title: { display: true, text: 'Date', color: colorsForChart.text }, ticks: { color: colorsForChart.text }, grid: { color: colorsForChart.grid } },
|
|
y: { beginAtZero: true, title: { display: true, text: 'Milliseconds', color: colorsForChart.text }, ticks: { color: colorsForChart.text }, grid: { color: colorsForChart.grid } }
|
|
}
|
|
}
|
|
});
|
|
chart._benchmarkRows = rows;
|
|
chartsByInstance.push(chart);
|
|
const legendActions = document.createElement('div');
|
|
legendActions.className = 'legend-actions';
|
|
const legend = document.createElement('div');
|
|
legend.className = 'legend';
|
|
const legendItems = [];
|
|
|
|
function refreshLegend() {
|
|
legendItems.forEach((item, metricIndex) => {
|
|
item.classList.toggle('is-hidden', !chart.isDatasetVisible(metricIndex));
|
|
});
|
|
}
|
|
|
|
function setAllLegends(visible) {
|
|
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, visible));
|
|
refreshLegend();
|
|
chart.update();
|
|
}
|
|
|
|
function addLegendAction(label, action) {
|
|
const button = document.createElement('button');
|
|
button.className = 'legend-action';
|
|
button.type = 'button';
|
|
button.textContent = label;
|
|
button.addEventListener('click', action);
|
|
legendActions.appendChild(button);
|
|
}
|
|
|
|
addLegendAction('Show all', () => setAllLegends(true));
|
|
addLegendAction('Toggle all', () => {
|
|
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, !chart.isDatasetVisible(metricIndex)));
|
|
refreshLegend();
|
|
chart.update();
|
|
});
|
|
addLegendAction('Reset', () => {
|
|
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, !hiddenByDefault.has(metric)));
|
|
refreshLegend();
|
|
chart.update();
|
|
});
|
|
|
|
metrics.forEach((metric, metricIndex) => {
|
|
const item = document.createElement('button');
|
|
item.className = `legend-item${chart.isDatasetVisible(metricIndex) ? '' : ' is-hidden'}`;
|
|
item.type = 'button';
|
|
item.innerHTML = `<span class="legend-swatch" style="background:${colors[metricIndex % colors.length]}"></span>${metric}`;
|
|
item.addEventListener('click', () => {
|
|
chart.setDatasetVisibility(metricIndex, !chart.isDatasetVisible(metricIndex));
|
|
item.classList.toggle('is-hidden', !chart.isDatasetVisible(metricIndex));
|
|
chart.update();
|
|
});
|
|
legendItems.push(item);
|
|
legend.appendChild(item);
|
|
});
|
|
section.append(legendActions, legend);
|
|
}
|
|
}
|
|
|
|
if (!availableFamilies.length) {
|
|
tabs.hidden = true;
|
|
panels.innerHTML = '<p class="empty">No benchmark data available.</p>';
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|