Skip to content

Dashboard

The k4Bench dashboard is a Streamlit app that browses benchmark history collected by the nightly CI. It's deployed at k4bench-dashboard.app.cern.ch and also runs locally against your own logs/ directory.

Its internals are documented under Architecture → dashboard; this page is the user guide.

Purpose

Looking at one run tells you today's cost. The dashboard answers the questions that need history and comparison:

  • How does each detector's cost compare in the latest run?
  • Has a Key4hep release made simulation slower or hungrier?
  • How does per-event time/memory drift across releases?

Where the data comes from

Every night, CI runs a curated set of benchmarks (defined in .github/benchmarks/*.yml), uploads each result to CERN EOS, and the dashboard reads them over HTTPS from a WebEOS endpoint. The EOS layout is hierarchical:

{detector}/{platform}/key4hep-{release}/{sample}/{YYYY-MM-DD}/
    run_info.json  machine_info.json
    {config}_results.csv  {config}_events.json  {config}_regions.json  {config}.log

The sidebar lets you drill down through that hierarchy: detector → platform → physics sample → Key4hep release. Single-run tabs use the newest uploaded run for that selection; the trend window controls the multi-run views.

Each level is rebuilt from the one above it, so changing the detector can leave the selected platform or sample with no match — in which case it re-defaults to a valid one and says so right under the selector. That notice matters because the Overview tab is scoped by platform and sample across all detectors: without it, a sample that quietly moved would look like detectors vanishing from its charts.

Not every tab honours the whole hierarchy, so a single muted line directly under the section bar names the slice the tab in front of you actually uses — for example Showing CLD — AlmaLinux 9 · GCC 14.2.0 (optimized) — Single e⁻ · 10GeV — 2026-07-10. Each level is written the way the tabs and the nightly mail write it, so a sample reads as its physics rather than as its EOS directory. Where a tab deliberately ignores a level it says so instead of quietly dropping it: Overview reads all detectors, Run Trends reads all releases in the trend window, and Stack Changes says its package diff is platform-wide, because a Key4hep release is the same stack whatever benchmarked it. The line also follows the controls inside a tab that change its scope: switching to a Historical Trends view replaces the release with all releases in the trend window, and switching on Stack Changes' Whole platform toggle replaces the detector and sample with all detectors and all samples. The line never repeats a date range, report night or run date — each tab prints its own, in its own words. In local mode there is no hierarchy to show, so it names the run directory instead.

Example detectors

A couple of detectors (currently: SiD) come from a simulation toolkit's own reference/tutorial geometry rather than a maintained FCC/Key4hep experiment design. Picking one of these shows a note in the sidebar linking back to its source, so it doesn't read as "just another experiment" (see EXAMPLE_DETECTORS in dashboard/ui_chrome.py).

The tabs

Tab What it shows Backed by
Config Impact baseline-relative configuration impact in the selected run *_results.csv
Event timing per-event wall time distributions *_events.json
Event memory per-event RSS and growth *_events.json
Region timing per-subdetector stepping time, at_location vs by_birth, step counts, attribution analysis *_regions.json
Trends metrics over time across releases many runs, windowed
Regressions the nightly regression report for the selected detector/platform/sample _reports/{date}/report.json
Stack Changes which Key4hep packages moved between two nightly releases run_info.json (k4h_packages)
Overview cross-detector comparison: performance trends, landscape, regression status & the nightly mail _reports/{date}/report.json
Machine info the host the benchmark ran on (CPU, RAM, governor, throttling) machine_info.json
Logs the raw ddsim log for the selected run *.log

Display options

Anything that changes how a figure is drawn — colour palette, opacity, style cycling, smoothing, histogram bins, error bars, mean lines, how many detectors a ranking keeps — lives in a 👁️ Display options popover at the right-hand end of the view's control row. It looks and behaves the same in every view that has one (Event Timing, Event Memory, Region Timing's four views and Run Trends), and each popover ends with Reset to defaults, which restores every control inside it in one click.

What stays on the page is everything that changes what you are looking at: baseline, configuration, attribution, metric, view, report night, the reliability scope and the regression flag pills. Overview's Log/Linear/Relative % scale stays out on the page for the same reason — it changes what the numbers mean, not how they are painted. Whenever unreliable runs are present, the shared Runs · ⚠ N unreliable selector occupies the right-hand end of that view's existing controls line (beside Display options where present); it never inserts a separate warning or controls row.

Tabs with multiple views use the same lightweight selector: horizontal radio buttons under a bold View label. The segmented bar is reserved for the dashboard's top-level section navigation and for compact value choices such as Scale; it is not repeated as tab-local navigation.

Palette defaults follow the data: a figure picks the smallest Matplotlib tab-N palette that colours every series without repeating a colour, and re-picks it when the number of series crosses a boundary. An explicit choice sticks until that happens.

Config Impact tab

The four summary cards name the alternative configuration with the largest estimated impact on wall time, peak memory, user CPU and output size. Pick a metric to open its horizontal ranking: the selected baseline is the zero line, better-than-baseline changes extend right in blue, and worse changes extend left in orange. Positive has the same meaning for every metric — less time/memory/CPU/output, or more throughput. The default Full detector (baseline_all) comparison is the usual subdetector-removal study; the wording remains baseline-relative if you select another reference. Throughput remains available in the metric selector as the rate-oriented view of wall time. Hover a bar for rounded raw measurements and their absolute change. The compact ranking keeps the 12 largest absolute impacts, so it retains both major gains and major regressions. When more than 12 configurations are comparable, switch on All configs beside it to expand the chart.

With the full-detector baseline, the removal impacts are ablation estimates rather than additive accounting. Removing material changes particle transport, so the individual percentages need not add up to the cost of the full detector. For a direct view of where simulation time is spent, Region Timing attributes per-subdetector Geant4 stepping time both to where tracks run and where they were created.

Region timing tab

The richest tab. It mirrors the region plugin's two attribution views (at_location and by_birth) and adds:

  • Current run — per-detector time for the selected run; Top N detectors (in Display options) sets how many of the most expensive regions it ranks.
  • Attribution analysis — the gap between the two views (intrinsic vs imported cost).
  • Step analysis — timer-interval counts per detector.
  • Historical — the same detector's time across runs.

Plots a metric over time. The x-axis is anchored on the Key4hep release date (falling back to the run date), so you see regressions aligned with releases. A sidebar look-back window (Last 7 days, 14 — the default — 30, 90, 6 months, All, or a custom range) controls how much history is downloaded and shown. A preset counts back from the latest available nightly, not today, so it always shows data even if the nightly hasn't run recently. That anchor is deliberately detector-independent: it is the newest nightly report night, which every detector shares, so the same preset covers the same dates whichever detector the sidebar has selected — including while a batch is still uploading and one detector's run is already newer than the last report. (The window's end does stretch to that run, so it stays visible in Run Trends; a window can therefore be a day or two longer than its label.) This matters most for the cross-detector Overview tab, which draws every detector over this one window: an anchor that followed the selected detector's last run would pull the window back whenever a lagging detector was picked, and detectors that had run more recently would silently vanish from its charts. If the selected detector has no runs in the window, the sidebar caption says so and names the night it last ran (see trend_window — pure logic, unit-tested).

Nights the nightly regression detector confirmed a step are ringed in red on the lines, and nights it flagged but hasn't confirmed are ringed as ⚠️ watch points — both on by default, each behind its own toggle. It's the same Confirmed/Watch toggle and marker language as the Overview tab, so a flag here means exactly what it means there. Flags appear on the metrics the engine judges (wall time, user CPU, peak RSS) plus throughput, which borrows the wall-time verdict since throughput is exactly n_events / wall_time_s (the same regression, inverted); CPU efficiency and context switches carry none. Runs that failed the host-reliability check are excluded by default with the same Runs · ⚠ N unreliable selector as every other historical view: Reliable only (the default) or All runs. Palette, style cycling, opacity and line smoothing sit in this tab's Display options popover, alongside the pills on the same row.

Warmup is excluded

Trend and summary statistics drop event 0 (warmup), matching the analysis convention.

Regressions tab

Where the Trends tab lets you look for regressions, this tab shows what the nightly detector found — for the sidebar-selected detector, platform and sample, the same scoping as every trend view (the cross-detector picture lives in the Overview tab). Every night, CI (the regression-report job in nightly.yml) walks the full EOS history and judges every (detector, platform, sample, config, metric) series with a conservative step-change detector (k4bench/regression/):

  • Baseline. Each night is compared against the median and spread (scaled MAD, not mean/stddev, so one bad night can't skew it) of the trailing 14 reliable runs before it. Nights flagged by the machine-info reliability check are excluded outright and never judged themselves. With fewer than 7 reliable runs to compare against, the metric is left unknown rather than flagged.
  • What trips a flag. The value has to clear two gates at once: a robust z-score above 3.5 (a statistical outlier) and a practical-effect floor — at least 5 % for time and memory, 3 percentage points for CPU efficiency, and a wider floor for the noisier region metrics. Requiring both keeps a very steady metric (tiny MAD) from flagging on a change too small to care about.
  • The workload is pinned and recorded. Timing measures software only if both nights simulated the same events, so the benchmarks fix the ddsim seed and each run records the seed it used in run_info.json. Without one, ddsim draws a fresh seed per run and every night re-rolls the shower physics. When the recorded seed changes — the migration to a pinned seed, a later change of the value — that release is not judged against the old baseline: a fixed workload sits at one particular offset and reproduces it every night, so the two-strike rule would confirm the changeover rather than protect against it. The changed-over values re-anchor the baseline instead, and judging resumes at the new level with the next release. It costs one release of sensitivity, which is the honest price of no longer measuring the same thing; the report also says so in its notes.
  • Shared moves are reported once. When every config of a run group moves together — a host or stack effect rather than a detector one — that is one event, not one per config. The group-wide shift is judged as its own series, reported under all configs (common mode), and divided out of each config's series so what remains is that config's own movement. Nothing is discarded: averaging over the group makes a stack-wide change easier to catch, not harder. The decomposition only happens when the group really did move together — a single config still sitting at its historical level vetoes it, because on a removal sweep a regression inside one detector moves the baseline and every without_X except the one that removed it, and dividing that majority out would leave the one configuration that identifies the cause looking like the only thing that moved. That row's value is a ratio, not a measurement — 1.00 is the run group at its usual level, 1.20 is every config 20 % above it — so it is shown as ×1.20 and plotted on a × baseline axis, never in seconds or MB. A config's own row reports its residual after the shift is removed, with the measurement it came from and the shift kept alongside it in report.json (raw_value, common_mode_shift).
  • Trimmed alongside total. Timing is judged on the mean, the median and an upper-trimmed mean that drops the slowest 5 % of events — one-sided, so only the slow tail goes. It reports the typical event; the untrimmed totals stay judged because the tail is exactly where a tail-confined regression would appear.
  • One measurement, one test. user_cpu_s tracks wall_time_s closely enough that judging both would count the same event twice, so it is recorded and plottable but never flagged.
  • Watch, then regression. The first night to clear both gates is a ⚠️ Watch. It only becomes a confirmed 🔴 Regression once the next reliable night moves the same way again — a two-strike rule that is the main defence against false alarms. The confirming night may re-benchmark the same release as the watch night: a second run of the same binary tripping the same way is exactly the independent evidence the rule wants.
  • Releases, not nights. The unit of change is the Key4hep release; nights that re-benchmark one release are repeat measurements of the same binary. Every night of a release is judged against the same frozen baseline, and once a step is confirmed for a release, every later night of that release that trips the same way is confirmed too, carrying the same onset window — so all of a release's reports agree on its regressions, modulo each night's own measured value. A repeat confirmation is labelled as such in the report and email ("repeat — first confirmed {night}"), so consecutive alerts read as "still present", not fresh news.
  • Confirmations follow the balance of evidence. A single quiet night inside a confirmed release cannot outvote the two nights that confirmed — it is reported OK with a note that the release's median still sits beyond the baseline ("tonight's value looks like noise"). But when quiet nights drag the median of the release's nights back inside the gates, the confirmation is revoked ("confirmation revised") — the confirming nights were more likely noise — and the baseline is never re-anchored onto a fluke level.
  • Re-anchoring. A confirmed regression that still stands at the next release boundary is treated as a change-point: the baseline resets to the new level when the next release arrives, so a deliberate step (say, a physics change) is flagged on the release that introduced it — on every night that measured that release — and falls quiet from the next release on, instead of alerting every night until the window rolls over. A second change right afterwards is still caught.
  • Direction (faster/slower, more/less memory) is shown but not treated as good or bad — a regression is simply any confirmed step beyond the baseline in either direction.
  • Failures. A config exiting non-zero, or a whole run missing, is a ❌ Failure and skips the confirmation step (it alerts immediately).
  • Region timing (sub-detector) is not flagged — it is the noisiest series in the system, dominated by timer-granularity wobble at microsecond scale, so the regression report judges only top-level run and per-event metrics.

A report is written per run, so several nights routinely re-benchmark one fixed release. Their reports mostly agree — every night of a release is judged against the same baseline — but can still differ: the first strike is only a watch, a marginal night can come out OK, and reports built before the release-grouped engine confirmed on a single night. The sidebar's release selects which of that release's report nights are on offer; the tab defaults to the most attention-worthy one (a confirmed regression or failure outranks a watch outranks a quiet night, newest breaking ties), so the most alarming night is never hidden behind a quieter one. A Report night pill always appears above the report — one option carrying that night's glance badge (❌ / 🔴 / ⚠️ / ✅) even when the release was only benchmarked once, and one pill per night to step through the rest when it was benchmarked several times. The newest release (the sidebar default) still surfaces the latest report even when it is newer than the release's last run, so a "no run uploaded" failure stays visible. The banner, flagged metrics, and trend preview follow the selected night's historical evidence. Package and pull-request attribution is instead release-level: it is fixed from the first report night that confirmed a change window for the release and reused on every rerun, because no upstream package changes between repeat measurements of the same binary.

A benchmark job is dated when it starts, so a nightly batch beginning near midnight splits across two dates and some detectors are stamped a day behind the report night. Those are still tonight's measurements and are reported as such, with a note saying so — they are recognised by the CI run they came from, which every detector of one nightly shares, so a detector whose job crashed and uploaded nothing is still reported as a missing run rather than passing as a straddling batch — once the report night names a CI run, only a detector naming that same run is kept, whatever the gap between the two dates. Nights recorded before that CI run existed fall back to accepting a single night's lag, and their note says that is what happened. The Overview's charts follow each run by the date it actually ran, so a straddling job is still covered by the unreliable-run selector — it is one of tonight's runs, and the date it is stamped with should not decide whether the filter can see it.

A ?report=YYYY-MM-DD query parameter pins one report night directly and is authoritative when valid — this is the stable deep link the nightly email and the Overview roster generate, so a link to a confirmed regression keeps pointing at the exact report the email described. (The Run Trends range= window is unrelated and does not select a regression report.)

The selected run group renders flat: an at-a-glance banner (regressed / watch / failures / within-baseline counts) and a trend preview that plots a flagged metric's recent history with the baseline band it was judged against (the flagged night marked 🔴, the night it was first watched marked ⚠️), a few releases further out when the release has since moved on. The window contains up to 14 distinct Key4hep releases through the flagged release plus up to 7 later releases when available; repeat measurements of one release stay visible without consuming another release slot. Its dropdown lists every flagged metric worst first, each option carrying its own severity badge and Δ-vs-baseline — so scanning the list alone shows the size of every flag, without a separate ledger table. The preview opens on the most severe flag automatically; switch it to any other flagged metric, or to "—" to hide it. Confirmed regressions and failures — and only those — are also emailed to the team's e-group by the same CI job, with per-group links landing directly on this scoped view, pinned (via stack= and report=) to the exact report night they describe.

For a confirmed regression the drill-down also shades the release window the step entered in. Because confirmation is a two-strike rule, the reported night is one reliable night after the step appeared, so the cause is upstream of the ⚠️ onset, not at the 🔴 report — the amber band spans from the last release seen at the accepted level up to the onset release. The upstream-changes card below the preview (see below) is where to follow that window into the Stack Changes tab. When both ends of the window are the same release the band gives way to a "nothing upstream changed" note on that card: the stack did not move across the step, so the cause is the host, the sample, or noise rather than an upstream commit.

Below the drill-down, the release gets an upstream-changes card naming the packages that moved in the blame window established by its first confirmed report, each linking to its commit range, plus an Open in Stack Changes → link that seeds that tab with the exact release range. That same card is shown on every report-night view of a re-benchmarked release: a later run may provide the second strike for another metric, but it cannot create a new package-change story for unchanged software. When the canonical report contains several distinct windows, each retains its own card. When the blame sidecar carries a ranking, the card also lists suggested candidate pull requests: each PR in the window with a 0–100% Likelihood it is the cause and a one-line Why. The ranking is produced offline by a language model that read the metric that moved and each PR's actual code diff (configured in CI via K4BENCH_LLM_*; see the sidecar format) — the dashboard only displays the stored result. Several PRs can land in one package's range, so each is scored on its own. All ranked leads are shown in one sortable table. The group is judged once per detector/platform/sample/window: when several metrics (or several benchmark-config labels, e.g. a removal sweep's baseline and without_<detector> runs) stepped across the same release boundary in the same run group, they share one diff and one candidate set, so the card shows a single verdict rather than a table per metric. A different detector or sample sharing the same release dates never shares a verdict. This is a ranked lead for a human, not a verdict — a suggestion, not proof of cause, in keeping with the detector's no evidence ⇒ no verdict rule; the nightly email surfaces the same top candidates under each regression. Most nights carry no blame.json at all, and a night whose candidates are not yet ranked says that no AI ranking is stored rather than leaving an ambiguous blank below the package diff.

Stack Changes tab

Answers "what came in last night?" — and, when a metric has stepped, "what upstream change could that be?". Pick two nightly tags and it lists the Key4hep packages whose commit differs between them, each linking to the range on GitHub. When you open the tab, To release defaults to the stack selected in the sidebar and From release to the release immediately before it, when one is available. A compact line below the range reports how many tracked packages changed and remained unchanged without turning those counts into a second dashboard banner.

The package diff is cross-detector: a Key4hep release is one stack, sourced identically by every detector benchmarked against it, so only the platform scopes it.

Below the diff, Regressions in this range lists the confirmed metrics whose onset falls inside the selected release interval. It is scoped to the sidebar's detector and sample. A Whole platform (+N metrics) toggle appears only when widening would reveal confirmed metrics in other detector/sample scopes; a package change can affect any detector that sources it.

The worst metric opens in a compact selector rather than a ledger. Every option shows its severity, metric (including its region), config, Δ-vs-baseline and exact blame window; in the all-detectors view it also names the detector and sample. If distinct steps otherwise have the same label, their onset run is shown too. The selected item uses the same trend component as the Regressions tab: up to 14 releases of history and 7 later releases, the accepted-baseline band, first-watch and confirmation markers, unreliable-run filtering, and the shaded blame window. This makes a multi-release cumulative diff safe to inspect without implying that every package above belongs to every metric below. When that selected metric's blame window is narrower than the package comparison, Focus package diff on … resets From/To to the exact interval in one click. If package provenance is unavailable, or the selected endpoint stacks are identical, the section says so instead of describing the metric changes as effects of a package diff that was not observed. For a cumulative range with identical endpoints it also warns that an intermediate release may still have moved.

When the selected regression has a ranked blame sidecar, its AI-generated PR ranking appears directly below the trend. The same complete ledger and "suggestion, not proof" framing are reused from the Regressions tab; Stack Changes does not recompute or reinterpret the model output. When no ranking was stored, the tab says so explicitly instead of leaving an ambiguous blank.

The selected regression is part of the URL (reg_* parameters), including its detector/sample identity, config, metric, region, exact onset run and whether Whole platform is enabled. Sharing, reopening, or navigating back to the URL therefore restores the exact investigation rather than the range's default metric. An explicitly linked metric remains selectable even when it falls below the normal worst-30 display cap. Historical report JSONs for a cold range are fetched as one parallel batch; the selected trend still downloads run data only when it is shown.

It compares releases, not run dates. The nightly build does not publish every day; a benchmark then re-uses the newest release available, so several consecutive run dates routinely share one identical stack — most run dates on EOS are not release dates at all, and only dates that have a release are selectable.

Picking two releases that are far apart gives the cumulative change across every release in between, which the header states explicitly — a month-wide diff looks no different from one night's in the table.

An empty diff is a result, not a blank: if two releases sit at the same commit for every tracked package, nothing upstream changed between them, so a metric that moved did so for another reason — the host, the sample, or noise.

The data comes from k4h_packages in each run's run_info.json, recorded from CVMFS as the benchmark runs (see file formats). Releases benchmarked before provenance capture — or whose stack had already aged off CVMFS when the history was backfilled — cannot be compared, and the tab says so rather than showing an empty diff.

Overview tab

Where every other metric tab compares configs within the selected detector, this one compares the detectors against each other — always on their baseline config, for the sidebar-selected platform and sample, over the sidebar's trend window (the same scoping as Run Trends, minus the detector — and the window is anchored detector-independently, so switching the sidebar detector never changes which detectors this tab shows). It reads the same nightly _reports/{date}/report.json as the Regressions tab — whose verdicts carry the raw nightly value of every run and per-event metric for all detectors — so the whole comparison loads from one small JSON per night, with no per-detector run downloads.

Four views, dispatched by the same View switcher as the other multi-view tabs (one colour per detector, consistent across the figures). The selected view rides in the URL as ?view=, so a copied link reopens the view it came from along with the parameters only that view reads:

Like every tab-local view selector in the dashboard, the Overview switcher uses simple horizontal radio buttons under a bold View label. It is visually separate from a single bordered comparison toolbar. Time, Memory and Scale form its left-hand group; Regressions and the explicit Runs · ⚠ N unreliable choice form its right-hand group on the same row. When unreliable runs are present, that choice offers: Reliable only (the default) or All runs; affected dates and the Machine Info pointer live in the control's help instead of a separate banner. The rows wrap naturally on narrow screens, with no CSS override. Each detector family has its own vertical legend below the figure: the family is the heading and its versions sit directly beneath it, while every version remains individually toggleable. Legend columns are centre-anchored and move into additional rows when there are many families or unusually long labels, keeping the plot usable at narrower dashboard widths.

  • Performance Trends — the two selected metrics side by side (CPU, Memory), one line per detector across every nightly tag in the sidebar's trend window (x-axis: the Key4hep nightly tag, like every other trend view; a nightly benchmarked twice collapses to one point, newest run wins), so cross-detector gaps can be tracked over time. Nights the regression detector confirmed a step are ringed in red on the lines, and nights it flagged but hasn't confirmed are ringed as ⚠️ watch points — both on by default, each behind its own toggle. The Scale switcher's Relative % option rescales each line to its first night = 100 %, making drift comparable across detectors of very different absolute cost; it sits on the page rather than in Display options because it changes what the numbers mean. Any scoped detector the chart has no line for is named with the reason — every run excluded as unreliable · no value for the selected metrics · ran but produced no comparable metrics (a hard-failed config) · no run in the window, with the night it last ran · not benchmarked with this sample/platform — so a missing detector doesn't read as a quiet one. The roster comes from the nightly reports' run groups, not from their metric values, which is what lets a detector whose config failed outright be named at all. It is stated whether or not there is a chart, since a window with nothing to draw is when it matters most. One gap remains: a detector retired past the engine's seven-day missing-run grace period is dropped from the reports entirely and cannot be named — widen the window to a range it still ran in to see it;
  • Performance Landscape — the selected time metric against the selected memory metric, one point per detector at its most recent run — closer to the origin is faster and leaner. It follows the runs rather than the reports, so it is unaffected by the Regression Status night on screen, and a detector that missed the newest night keeps its last measured point (the caption and the hover name its nightly tag) instead of dropping off the chart; the same fallback applies to a run excluded by the reliability toggle; a detector that ran but produced no usable point — a hard-failed config, or one whose every run the toggle dropped — is named in the caption rather than left out. That last run is found even when it falls outside the trend window — a detector that has been missing for a few nights is shown at the night it really last ran, not at wherever the window happens to end. Both coordinates always come from one run, reruns of a nightly tag included. The metric selectors offer mean/median event time, wall time or user CPU for time; mean event RSS or peak RSS for memory (shown in GB), are shared with the trends view, and the selection is shareable via ?tmetric=/?mmetric=;
  • Regression Status — since the Regressions tab is scoped to one detector, the cross-detector regression picture lives here: a report night picker (every fetched night that covers the scope, newest first, each badged with its worst cross-detector state; defaults to the newest, pinnable via ?report= — the same parameter the roster links and alert emails carry, and the trend window sets how far back it reaches; changing the sidebar platform or sample returns it to the newest night, since two scopes rarely flag their regression on the same one), a banner with that night's verdict counts across all scoped detectors (checked / 🔴 regressed / ⚠️ watch / ❌ failures), a per-detector status roster (badge, flag counts, worst flagged metric, and a link that opens the Regressions tab scoped to that detector and pinned to the selected night), and a flagged-metric trend that opens on the night's worst flag — the metric's history over the trend window (plus the selected night itself when it lies outside) with the baseline band its verdict was judged against and, for a confirmed step, the shaded blame window, all built from the cached reports with no run downloads. The view renders even on a night whose configs all hard-failed (when there are no values to plot), so a failure is never hidden behind an empty chart;
  • Nightly Report — one night's regression report in the form the Key4hep e-group received it. Nothing is archived for it: the mail body is rebuilt from the already-fetched report (plus that night's blame sidecar) by the same renderer the nightly mail uses, so it is a reconstruction rather than a stored copy — an old night is redrawn by today's renderer, and the mail's CI run button is absent because that workflow-run URL is not stored in the report. Its night picker reaches as far back as the trend window and is deep-linkable via ?report=, the same parameter the Regressions tab and the alert emails already speak. Unlike every other view it is not scoped to the sidebar's platform/sample — the mail covers every detector, platform and sample the night benchmarked — which is why it stays reachable even when the selected scope has no data. Links open in a new tab.

Colours follow the detector family (ALLEGRO, CLD, …), with versions of one family distinguished by dash pattern and marker symbol, so experiment-level comparisons read at a glance. Runs that failed the host-reliability check are excluded by default with the same Reliable only / All runs selector as every historical view (the nightly report carries each night's per-detector verdict); their raw values are still recorded — as unjudged points, never flagged — so choosing All runs plots them like Run Trends does. One selector covers all three views, the Regression Status trend preview included, and it reaches every report in view — including nights outside the trend window, which the landscape and the night picker both read, and any the landscape reaches back to for a detector that has not run in a while. Exclusion drops the run, not the nightly tag: a tag benchmarked twice keeps the point measured by whichever rerun passed, and a 🔴/⚠️ marker leaves the chart with the run that earned it rather than ringing on a rerun that was never flagged.

A point on these charts is a Key4hep nightly tag, not a run — the unit the regression engine judges on — so where a tag was benchmarked more than once it carries the newest run's value and the worst verdict among its runs. A release that regressed is therefore still marked even when a later rerun of the same stack came out quiet. The per-run view is the Regressions tab's drill-down, where every run gets its own point and a flag sits only on the run that earned it; if that run was excluded or falls outside the window, the drill-down hides the marker and says why rather than moving it.

The landscape is the one chart that is a run, not a tag: its point comes from a single run of the detector's newest tag, so the two coordinates are always the same measurement even when a rerun re-measured only one of them. Value axes are logarithmic by default (the Scale switcher offers Linear as well) — the detectors span more than a decade in both time and memory, so a linear scale squashes the small ones into an unreadable cluster.

Detectors are only compared like-for-like: anything not benchmarked with the selected sample/platform is listed as excluded instead of silently plotted against a different workload. The values shown are the raw nightly measurements the regression engine judged — baselines and Δ-verdicts stay in the Regressions tab.

Running the dashboard locally

You can point the dashboard at either a local directory or the remote WebEOS endpoint, via environment variables read by dashboard/config.py:

Variable Default Meaning
K4BENCH_DATA_DIR logs local directory to read runs from
K4BENCH_DATA_URL (unset) WebEOS base URL; when set, overrides local reads
K4BENCH_CACHE_DIR $TMPDIR/k4bench_cache on-disk cache for downloaded runs
K4BENCH_DASHBOARD_URL https://k4bench-dashboard.app.cern.ch this deployment's own public URL; the Overview tab's Nightly Report view needs it because the embedded mail's deep links must be absolute
pip install -r dashboard/requirements.txt
cd dashboard
K4BENCH_DATA_DIR=../logs/ALLEGRO_o1_v03 streamlit run app.py
cd dashboard
K4BENCH_DATA_URL=https://k4bench-data.web.cern.ch streamlit run app.py

The dashboard imports k4bench.analysis for its loaders, so a working k4bench install is required.

How downloads are cached

Historical runs are immutable, so the dashboard downloads each run at most once into K4BENCH_CACHE_DIR and reuses it across reruns. Downloads are staged in a temp dir and published with a single atomic rename, so a reader never sees a half-written run and an interrupted download leaves no partial run behind. Filenames from the directory listing are validated (decoded, then rejected if they aren't a single plain path component) to prevent path-traversal. Details: Architecture → data flow.

Deployment

The dashboard is containerised (dashboard/Dockerfile), pushed to ghcr.io, and deployed to CERN's OpenShift PaaS via openshift/ manifests (Deployment/Service/Route) by the deploy-dashboard.yml workflow. The full deployment path is in Architecture → data flow.