diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 143109f..456b965 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -20,7 +20,15 @@ permissions: issues: read concurrency: - group: pages-deployment + group: >- + ${{ ( + inputs.publication || github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' || + github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' || + github.event_name == 'pull_request_target' && github.event.pull_request.user.login != 'dependabot[bot]' && + (github.event.action != 'closed' || !github.event.pull_request.merged) && + (!(github.event.action == 'labeled' || github.event.action == 'unlabeled') || github.event.label.name == 'benchmarks: skip') && + (github.event.action != 'edited' || github.event.changes.base) + ) && 'pages-deployment' || format('pages-ignored-{0}-{1}', github.run_id, github.run_attempt) }} queue: max jobs: diff --git a/docs/README.md b/docs/README.md index 1683500..f23d62b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -82,6 +82,17 @@ flowchart TB ResultPath -->|Read stored results| After ``` +## Website Previews + +One GitHub Pages deployment contains separate HTML, JavaScript, and CSS builds: + +* `/` serves website code from `main` with published benchmark history. +* `/previews/pr-N/` serves website code from PR N with published history and matching validated pending results when available. + +Previews show UI changes before merge, even when pending benchmark results are unavailable. +After a PR is merged or closed, the next successful Pages deployment removes its preview. +The preview URL then returns 404. + ## CI Rules * `benchmarks: skip` skips measurement and result publication. Dependabot applies it automatically. diff --git a/src/main.ts b/src/main.ts index 063291b..6f98ed2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import '@fontsource/ibm-plex-sans/600.css' import '@fontsource/ibm-plex-mono/400.css' import './style.css' import { Chart, registerables } from 'chart.js' -import { createIcons, Download, Link, ArrowUpRight, AlertTriangle, RotateCcw } from 'lucide' +import { createIcons, Download, Link, ArrowUpRight, AlertTriangle, RotateCcw, ChevronRight } from 'lucide' import { loadDataset } from './data' import { catalog } from '../shared/catalog.ts' import { runSelectionSchema } from '../shared/results.ts' @@ -15,7 +15,7 @@ Chart.defaults.font.family = 'IBM Plex Sans' Chart.defaults.color = '#626b66' const palette = ['#65736a', '#93663c', '#2f66cb', '#e06b2f', '#968526', '#148c87', '#a34764', '#7159b5', '#438333', '#bf4961', '#427992'] -const icons = { Download, Link, ArrowUpRight, AlertTriangle, RotateCcw } +const icons = { Download, Link, ArrowUpRight, AlertTriangle, RotateCcw, ChevronRight } const strategyDescriptions: Record = { reload: 'Restore the sandbox for each request.', reuse: 'Keep the sandbox as-is across requests.', @@ -106,7 +106,7 @@ function renderDashboard(data: Dataset) {
COMMIT SNAPSHOT

RankRuntimePlatform% of largest selected value
-
Runner specifications
PlatformCPUvCPUsCoresAzure VM SKUOperating systemMemory (GiB)Runner poolRegionRunners
+
Runner specifications
PlatformCPUvCPUsCoresAzure VM SKUOperating systemMemory (GiB)Runner poolRegionRunners
@@ -165,25 +165,51 @@ function renderDashboard(data: Dataset) { if (pr) { prLink.href = `https://github.com/${pr.repository}/pull/${pr.number}` } - const groups = new Map }>() + const rows = rowsFor(selectedRunId) + const groups = new Map, runtimes: Set }>() + const groupCounts = new Map() for (const platform of data.platforms.filter(platform => selectedPlatforms.has(platform.id))) { for (const runner of run.runners.filter(runner => runner.platformId === platform.id)) { + const measurements = rows.filter(row => row.runnerId === runner.id) + if (!measurements.length) continue const cells = [platform.label, runner.cpu.model, runner.cpu.logicalProcessors, runner.cpu.cores, runner.sku, [runner.os.name, runner.os.version, runner.os.architecture].filter(Boolean).join(' '), (runner.memoryBytes / 1024 ** 3).toFixed(1), runner.pool, runner.region] const key = JSON.stringify([platform.id, runner.os.name, runner.os.version, runner.os.architecture, ...cells]) const group = groups.get(key) - if (group) group.names.add(runner.name) - else groups.set(key, { - cells, - names: new Set([runner.name]), - }) + if (group) { + group.names.add(runner.name) + for (const measurement of measurements) group.runtimes.add(measurement.runtimeId) + } + else { + groups.set(key, { + platformId: platform.id, + cells, + names: new Set([runner.name]), + runtimes: new Set(measurements.map(measurement => measurement.runtimeId)), + }) + groupCounts.set(platform.id, (groupCounts.get(platform.id) ?? 0) + 1) + } } } - element('#runner-details').innerHTML = groups.size ? [...groups.values()].map(group => { + const showRuntimeAssignments = [...groupCounts.values()].some(count => count > 1) + element('#runner-expand-heading').hidden = !showRuntimeAssignments + element('#runner-details').innerHTML = groups.size ? [...groups.values()].map((group, index) => { const cells = [...group.cells, group.names.size] - return `${cells.map(value => `${escapeHtml(String(value ?? '-'))}`).join('')}` - }).join('') : 'No platforms selected.' + const expandable = (groupCounts.get(group.platformId) ?? 0) > 1 + const detailId = `runner-assignments-${index}` + const label = escapeHtml(`Runtimes for ${group.cells[0]}, ${group.cells[1] || 'Unknown CPU'}`) + const toggle = expandable ? `` : '' + const assignments = expandable ? `
Runtimes (${group.runtimes.size})
    ${[...group.runtimes].sort().map(runtime => `
  • ${escapeHtml(runtime)}
  • `).join('')}
` : '' + return `${showRuntimeAssignments ? `${toggle}` : ''}${cells.map(value => `${escapeHtml(String(value ?? '-'))}`).join('')}${assignments}` + }).join('') : 'No measurements selected.' + element('#runner-details').querySelectorAll('.runner-toggle').forEach(button => { + button.addEventListener('click', () => { + const expanded = button.getAttribute('aria-expanded') !== 'true' + button.setAttribute('aria-expanded', String(expanded)) + element(`#${button.getAttribute('aria-controls')}`).hidden = !expanded + }) + }) + createIcons({ icons, attrs: { 'stroke-width': 1.7, 'aria-hidden': 'true' } }) element('#value-heading').textContent = `${metric().label} (${metric().unit})` - const rows = rowsFor(selectedRunId) const maximum = Math.max(...rows.map(entry => entry.values[metricId]), 0) element('#results-body').innerHTML = rows.length ? rows.map((entry, index) => { const percentage = maximum > 0 ? entry.values[metricId] / maximum * 100 : 0 @@ -215,7 +241,7 @@ function renderDashboard(data: Dataset) { label: `${runtime.id}${selectedPlatforms.size > 1 ? ` / ${platform.label}` : ''}`, data: runs.map(run => data.measurements.find(entry => entry.runId === run.id && entry.runtimeId === runtime.id && entry.platformId === platform.id && entry.strategy === strategy)?.values[metricId] ?? null), borderColor: color(runtime.id), backgroundColor: color(runtime.id), borderWidth: 2, - borderDash: platform.id === 'mshv3' ? [6, 4] : [], + borderDash: platform.id === 'kvm' ? [6, 4] : [], tension: 0, pointRadius: 2.5, pointHoverRadius: 5, pointBorderWidth: 1, pointBackgroundColor: '#fff', }))) element('#series-count').textContent = `${series.length} series` @@ -281,12 +307,20 @@ function renderDashboard(data: Dataset) { if (!row.hidden) visible++ }) element('#search-empty').hidden = visible !== 0 + element('#select-all').textContent = query ? 'Select matches' : 'Select all' + element('#select-none').textContent = query ? 'Clear matches' : 'Clear' }) function syncRuntimeInputs() { app.querySelectorAll('#runtime-list input').forEach(input => input.checked = selectedRuntimes.has(input.value)) } - element('#select-all').onclick = () => { data.runtimes.forEach(runtime => selectedRuntimes.add(runtime.id)); syncRuntimeInputs(); update() } - element('#select-none').onclick = () => { selectedRuntimes.clear(); syncRuntimeInputs(); update() } + element('#select-all').onclick = () => { + app.querySelectorAll('[data-runtime]:not([hidden]) input').forEach(input => selectedRuntimes.add(input.value)) + syncRuntimeInputs(); update() + } + element('#select-none').onclick = () => { + app.querySelectorAll('[data-runtime]:not([hidden]) input').forEach(input => selectedRuntimes.delete(input.value)) + syncRuntimeInputs(); update() + } element('#reset').onclick = () => { selectedRuntimes.clear(); defaultRuntimes.forEach(id => selectedRuntimes.add(id)) selectedPlatforms.clear(); selectedPlatforms.add(defaultPlatform) @@ -296,6 +330,8 @@ function renderDashboard(data: Dataset) { element('#runtime-search').value = '' app.querySelectorAll('[data-runtime]').forEach(row => row.hidden = false) element('#search-empty').hidden = true + element('#select-all').textContent = 'Select all' + element('#select-none').textContent = 'Clear' update() } app.querySelectorAll('[data-strategy]').forEach(button => button.onclick = () => { strategy = button.dataset.strategy as Strategy; update() }) diff --git a/src/style.css b/src/style.css index 4467a16..ad702bb 100644 --- a/src/style.css +++ b/src/style.css @@ -4,6 +4,7 @@ @media(max-width:720px){.methodology th:last-child{display:table-cell}} .site-heading{margin:0;font-size:14px;font-weight:500;line-height:1.2}.site-title{font-size:14px;font-weight:500}.site-header .repository-links{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px 16px}.site-header{gap:16px;height:58px} .dashboard-main{padding-top:0}.dashboard-main>.workspace{border-top:0} +.methodology .runner-expand-cell{width:42px;padding:4px}.runner-toggle[aria-expanded=true] svg{transform:rotate(90deg)}.methodology .runner-assignments td{white-space:normal;background:#edf3ee;padding:14px 16px}.runner-assignment-content strong{font-size:12px;font-weight:500}.runner-assignment-content ul{display:flex;flex-wrap:wrap;gap:8px 24px;list-style:none;margin:10px 0 0;padding:0}.runner-assignment-content li{font:11px 'IBM Plex Mono',monospace;overflow-wrap:anywhere;min-width:0} .preview-status{padding:12px 0;color:var(--green);font-size:13px;overflow-wrap:anywhere;border-bottom:1px solid var(--border)} .eyebrow,.count,.control-label,legend,.lifecycle-control p,.platform-options small,.metrics button:not([aria-selected=true]),.chart-heading p,.chart-caption,.chart-legend,.snapshot-heading p,th,.rank,.platform-badge,.methodology,footer{color:var(--muted)} input[type=search]::placeholder{color:var(--muted);opacity:1}