From a767beaf45023e4b90033494e7655d17f7364a77 Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:46:48 -0700 Subject: [PATCH] Refine runtime selection and runner details Scope bulk selection to search matches and show runtime assignments beneath hardware rows. Use solid MSHV lines and dashed KVM lines. Keep ignored Pages events outside the deployment queue and document website previews. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- .github/workflows/pages.yml | 10 +++++- docs/README.md | 11 ++++++ src/main.ts | 68 ++++++++++++++++++++++++++++--------- src/style.css | 1 + 4 files changed, 73 insertions(+), 17 deletions(-) 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}