# Performance regression checks

**GPU policy revised 2026-09-26; runtime migration pending.**
[GPU-01](/docs/plans/gpu-resident-processing) prioritizes resident fields, complete device
reductions and removal of bulk-field CPU round trips. Host setup and explicit bounded
control/status/layout metadata are allowed; record their payload, count, cadence,
synchronization and memory cost separately from requested output. Audit direct and
indirect transfers across setup, steps, rebuilds and failures, exposing instrumentation
blind spots. Zero copies alone is not a performance result: excessive inactive launches
or speculative buffers can be worse than measured status polling. AMR-02 does not
require full GPU-01 closure, but must introduce no new bulk numerical readbacks and
meet the unchanged [architecture acceptance](#architecture-change-acceptance) gates.
This revision changes none of the timing/reference rules or limits below.

All three `verify-all` selections (`quick`, `multigrid`, `full`) measure the same nine mandatory cases: two planar
Taylor–Green schemes, four exact periodic controls, two short moving fans and the
accepted backward step. Numerical acceptance and timing acceptance remain separate.
See [complete validation](/docs/COMPLETE_VALIDATION) for the full catalog and resume contract.

```sh
dotnet build PolyCfd.sln -c Release
# Intentionally accept a new reference only after all quick checks pass.
dotnet run --no-build -c Release --project src/PolyCfd.Cli -- verify-all --suite quick --backend cuda --output output/validation/record-timing --record-performance --performance-baseline output/references/performance-raw.json
# Compare on the same idle device, without rewriting the reference.
dotnet run --no-build -c Release --project src/PolyCfd.Cli -- verify-all --suite quick --backend cuda --output output/validation/check-timing --check-performance --performance-baseline output/references/performance-raw.json --max-slowdown-percent 20
```

Supply the reference explicitly. Missing clock/coverage/reference compatibility is
incomplete or failed coverage, never a measured performance pass. Recording requires
a new reference path and every selected check to pass. Timings and comparisons live
inside the `performance/attempt-NNN/` directory under the suite output.
The old stopwatch `Baselines/performance.json` remains historical evidence; do not
rescale it or compare it directly with the new raw-clock boundary.

## What is measured

The suite installs a validation observer; production time integration is unchanged.
On Linux it uses **CLOCK_MONOTONIC_RAW**, required by this WSL workstation's observed
clock-rate changes. Other platforms use `Stopwatch.GetTimestamp` and record that clock.
The clock and timing boundary must match the reference exactly.

- **Whole verification call:** setup, geometry/cache construction, integration,
  diagnostics, output and scientific acceptance. Excludes process startup, builds,
  suite bookkeeping and the later fan time-series audit.
- **Median solver step:** synchronized integration, including adaptive selection and
  moving geometry. Host output/diagnostics after the step are excluded. The first
  two steps are warm-up; at least three remaining samples are required.
- **Device and environment:** actual requested accelerator, machine, runtime,
  configuration, processor/thread and cuBLAS settings. No silent backend fallback.

Internal `ValidationResultBase.Performance` still describes its existing stopwatch
boundary and remains in saved verification records. The suite's separate performance
artifact is authoritative for raw-clock comparisons; do not mix those measurements.
The preserved incremental-projection benchmark uses this same raw boundary and nine
case keys, permitting a direct before/after check without replacing its reference.

A metric fails only when **both** conditions hold:

```text
current > reference × (1 + maxSlowdownPercent / 100)
current − reference > absolute noise floor
```

The noise floor is 1,000 ms for elapsed time and 1 ms for median solver-step time.
For example, with a 20% allowance, an elapsed increase from 10 to 13 seconds fails;
1 to 1.3 seconds does not. Equality at the allowance or noise floor passes.

## Reference lifecycle and comparable workloads

`PerformanceSnapshot` records a format version, UTC timestamp, environment,
correctness result, and case measurements. Environment matching covers machine
name, OS, process architecture, logical processor count, .NET runtime, build
configuration, and `POLYCFD_MAX_THREADS` / `POLYCFD_ENABLE_CUBLAS` settings. Device
names must match per case. An environment or accelerator mismatch fails with an
explanation; it is not reported as a measured slowdown.

The nine mandatory cases have stable keys. Saved-case checks are separate from
this timing catalog. Adding or removing a case
requires deliberately recording an updated reference; missing coverage cannot
silently make the suite pass. Saved-baseline dispatch requires canonical scenario and verification records, checks resolved inputs
and reference policy, and permits current-build comparison. Complete older references need explicit
lossless conversion; missing inputs are rejected. Archived CSV-only evidence is reported separately.
See the [runtime contracts](/docs/RUNTIME_CONTRACTS) and [validation guide](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/validation/PolyCfd.Validation/README.md).

Only explicit `--record-performance` creates a new reference, and only if all
selected checks pass and measurements are valid. Existing references are preserved. A correctness failure or a comparison failure
does not update the reference.

Every completed mandatory case stores its measurement in `report.json`, including
when a later case fails. Once all nine mandatory cases pass, the `performance`
check writes `performance.json` and, when a compatible reference is supplied,
`comparison.json` inside its attempt directory. `ValidationPassed` describes
numerical correctness; the comparison and suite report give the separate timing
verdict. The reference path is never used for a latest-report side effect.

Timings remain sensitive to concurrent CPU/GPU work, clock/power settings, thermal
throttling, and driver changes. Recheck an unexpected slowdown on an idle machine.
Do not refresh a reference merely to suppress a regression. Dated numbers in
[timing.md](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/docs/timing.md) are historical logs, not portable thresholds.

The [backward-step OpenFOAM comparison](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/docs/BACKWARD_STEP_PERFORMANCE.md) is a separate
cross-code benchmark with external process wall times. It does not replace these
local regression references or use OpenFOAM timings as a `verify-all` threshold.

## Initial reference — 2026-09-16, before backward-step

The initial local Release run of `verify-all --record-performance` passed all 11 checks
on the NVIDIA GeForce RTX 3080: both mandatory fan advection schemes and nine
saved cases. It exited 0 and saved `Baselines/performance.json` and
`Baselines/performance.latest.json`. Each case has valid elapsed and median-step
measurements; the reference records the machine, runtime, configuration, and device.

The complete log, reference copies, per-case timing table, build/test results,
and cylinder-reference migration evidence are saved locally under
`output/validation/performance-record-2026-09-16/`. The original cylinder references
were preserved before adding measured force histories, and both replacements
matched their original wake tolerances. The fine cylinder also passed its
canonical force checks during the full suite. No numerical tolerances were relaxed.

This run established the first timing reference. Recording does not itself test
for a slowdown against an older reference. Adding the backward-step case requires
a new recording because performance case coverage must match.

## Source and regression coverage

- [SimulationPerformance.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/validation/PolyCfd.Validation/Performance/SimulationPerformance.cs): timing result and warm-up/median recorder.
- [PerformanceBaseline.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/validation/PolyCfd.Validation/Performance/PerformanceBaseline.cs): snapshot format, validation, atomic recording, and comparison.
- [VerificationRecord.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/validation/PolyCfd.Validation/Scenarios/VerificationRecord.cs) and shared scenario records: explicit numerical workload and instrumentation settings.
- [ScenarioCaseRunner.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/validation/PolyCfd.Validation/Framework/ScenarioCaseRunner.cs): synchronized timing capture around the shared executor.
- [SuiteCommand.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/src/PolyCfd.Cli/SuiteCommand.cs): CLI coverage, performance comparison and exit status.
- [SuiteRunner.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/validation/PolyCfd.Validation/Suites/SuiteRunner.cs): durable attempts, integrity checks and resume.
- [SuiteCases.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/validation/PolyCfd.Validation/Suites/SuiteCases.cs): scientific cases and raw-clock observer.
- [PerformanceBaselineTests.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/test/PolyCfd.Tests/Unit/PerformanceBaselineTests.cs): thresholds, noise floors, invalid data, environment/device mismatches, coverage changes, and reference preservation.
- [MovingFanValidationTests.cs](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/test/PolyCfd.Tests/Unit/MovingFanValidationTests.cs): real fan stability and populated timing measurements.

The automated unit tests use deterministic synthetic timing values for pass/fail
rules. `dotnet test` does not impose machine-dependent speed limits. CLI checks
also exercised real fan/Taylor–Green recording and comparison, a deliberately
faster reference that forced exit 1, and refusal to refresh after a failed case.

The mandatory `backward-step-regression` now contributes a 384x32x32, Re_h=100
MacCormack run at dt=0.05 through t=120. It checks physical accuracy against an
embedded OpenFOAM reference before performance can be recorded. Existing timing
references need an explicit `verify-all --record-performance` refresh to include
this new case; case-coverage changes are never silently ignored. See
[Backward-step validation](/docs/BACKWARD_STEP_VALIDATION).

## Expanded reference — 2026-09-16

The completed Release `verify-all --record-performance` run passed **12/12**:
two moving-fan checks, the new backward-step check, and nine saved baselines.
It saved `Baselines/performance.json` and `Baselines/performance.latest.json`.
The step case took **202.61 s**, with **82.76 ms** median solver-step time
(2,398 timed steps after two warm-up steps). Summed simulation elapsed time for
the suite was 2,021.50 s, about 33 minutes 42 seconds.

An audit against the previous reference passed the existing 20% gate for all
11 common cases. The largest elapsed-time increase was 8.14%, and the largest
median-step increase was 5.86%; both were the cavity-with-sphere case. The new
step is additional coverage, not an existing-case slowdown.

The log, previous/new reference copies, and timing audit are preserved under
`output/validation/backward-step/`. The accepted performance file SHA-256 is
`4f16c2f9c9bae049b004e6a3e491d8fcfbc6a3b8b8102b47961773aa1bc084fc`.

## Simplification acceptance and instrumentation

The [implementation evidence](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/docs/ARCHITECTURE_IMPLEMENTATION_EVIDENCE.md) records cumulative CUDA acceptance, rejected intermediate measurements and deferred optimizations. Run GPU comparisons sequentially. Stage profiling is enabled by default; match that policy across comparisons (`--no-stage-profiling` disables it). Memory-audit counters describe logical application transfers/allocations, not physical PCIe traffic or hidden ILGPU work. Keep audit settings identical and disabled for strict timing comparisons.

## Architecture change acceptance

The architecture refactor used a stricter criterion than the CLI allowance: no repeatable regression outside measured run-to-run noise for each affected representative workload. Aggregate improvements do not excuse a slower affected case. Compare repeated pairs on an otherwise idle machine against the original and immediately preceding accepted revisions; three pairs are a starting point, not automatic proof. Gather more evidence for noisy or close results. A zero-percent CLI allowance still retains the absolute noise floors above, so inspect raw elapsed and median-step measurements as well as the exit code.

Match grid, geometry, solver, tolerances, timestep policy, final time, required output fields/cadence, diagnostics, profiling mode and accelerator. Record revision, runtime, driver, build, device and relevant environment settings. Separate setup, stepping, geometry rebuild and output costs; audit H2D/D2H/D2D bytes and calls, allocations, peak retention and synchronization independently of final uninstrumented timings. Viewer changes also need comparable latency, decode, upload and retained-memory measurements. Logical payloads and CPU render submission times are not physical bus traffic or GPU completion timings.

Preserve the [transfer and memory budgets](https://github.com/hankbeasley/polycfd/blob/452441867295de2160cb7631bd023662cc75fb50/docs/ARCHITECTURE_IMPLEMENTATION_EVIDENCE.md#transfer-and-memory-budgets): no extra steady-state uploads of unchanged geometry/BCs, duplicate same-state captures, or full-volume readbacks for scalar-only consumers. Keep required numerical checks and useful output. Revise or defer a change that fails its budget or timing comparison; ambiguous evidence leaves acceptance pending. Do not silently refresh references or infer local regression acceptance from an external OpenFOAM comparison. When suite coverage changes, compare common workloads separately before explicitly accepting a new reference.

## WSL raw-clock method comparison

On this workstation, ordinary monotonic/calendar clocks have shown rate changes.
The [incremental-pressure timing harness] (unavailable local reference: `../../openfoam/compare/projection_benchmark/README.md`)
uses `CLOCK_MONOTONIC_RAW` for both synchronized steps and whole verification calls,
with explicit CUDA and identical instrumentation on preserved Release builds. It
exercises the same nine mandatory workloads and their existing physical gates; a
separate cavity key adds the 128³ short trajectory. Its unchanged 20% comparison
uses the existing 1 ms step / 1,000 ms elapsed noise floors.

These measurements are separate from the verifier's existing Stopwatch timings,
which remain in their original artifacts. Whole-call timing includes setup, output
and acceptance but excludes process startup/build; standalone enclosure process
timing also includes startup. Do not mix these boundaries or overwrite an accepted
baseline. See [the method validation report] (unavailable local reference: `../../openfoam/compare/INCREMENTAL_PROJECTION.md`).

## Chebyshev diffusion quick comparison — 2026-09-26

The final SOL-02a build (frozen as `output/builds/sol02a` until SOL-02c removes it; assembly hashes
in the report's provenance) was compared with the pre-change quick reference
`output/validation/sol04a-quick/performance-reference.json` in
`output/validation/sol02a-fix-quick/performance/attempt-001/comparison.json`, on an idle machine (GPU
0% before and 1% after, no other PolyCFD or test processes; `output/validation/sol02a-fix-idle-checks/`).
The check passes with no issues. Median solver step: fan-sl 296.77 → 111.16 ms (−62.5%), fan-mc
310.23 → 119.36 ms (−61.5%), backward step 59.63 → 57.86 ms (−3.0%), periodic and planar cases +0.2%
to −5.9%. Elapsed: fans −9.1% and −9.5%, backward step −2.7%; beltrami +44.9%, planar-sl +19.2% and
planar-mc +12.8% are small absolute changes (under a second) inside their noise floors. The run on the
build before the review fix pass (`output/validation/sol02a-quick/`) passed the same way. The multigrid
and full-suite comparisons against the WSL reference follow in SOL-02c.

## Diffusion fallback measurements — 2026-09-24

The pre-fix `eee86ac` build and the diffusion fallback candidate were frozen
separately before measuring the same nine scientific workloads on CUDA. Both
complete pairs pass scientific acceptance. Verification-call totals are
276.466 → 294.795 s (+6.63%) and, in reversed order, 263.718 → 264.555 s
(+0.32%). Combined change is +3.55%, with substantial run-to-run variability.
The first pair's shear-Y median misses the unchanged 20% allowance; the reversed
pair passes. Neither that failure nor the earlier unchanged-build historical
shear-Y failure (+22.2%) was replaced or used to refresh a reference.

Two-order isolated diffusion tests (50 warmups, 200 samples, independent final
residual checks) show mean changes of -0.76% at 64³ and -0.84% at 128³. These
are fast-path inputs; extra work needed by formerly failed enclosure solves is
not covered. The initial full suite stopped on the historical gate (backward-step call +25.1%, median +23.7%; shear-Z +27.7%; planar-SL +21.2%). It passed all 1,519 selected tests and nine scientific cases first. A subsequent small ordinary-path cleanup passes 92 focused tests, four startups and a complete matched timing pair (process wall time 316.324 → 288.697 s). Balanced isolated diffusion samples change by about -1.6% at 64³ and +1.0% at 128³. These variable observations do not establish a speedup. Full validation completed in `output/validation/diffusion-full-verified-20260924`: all scientific checks pass, while the historical gate remains failed after one fresh timing retry (backward-step call +32.9%, median +25.8%; shear-Z and planar-SL medians +25.8%). A control using all 26 unchanged historical assemblies and matching scientific identities also fails the old gate (backward-step call +41.8%, median +24.9%). Its verification calls total 312.693 s versus 293.330 s for the current retry. This contemporaneous comparison passes the unchanged comparison limits, but includes intervening code changes. A five-second monitor captured short-lived Windows PolyCFD workers during both sequences; these are not isolated timings. Latest-sample checks had missed those bursts. Coordinate a quiet test session before another deliberate phase; do not rebaseline or repeat until a favorable sample appears. Commands, identities, raw
clock boundaries and every timing attempt are linked in the
[maintained report] (unavailable local reference: `../../openfoam/compare/ENCLOSURE_N128_DIAGNOSTICS.md`) and
[JSON] (unavailable local reference: `../../openfoam/compare/enclosure_diffusion_fix.json`).

A deliberate `verify-all --resume --retry-failed` performance retry now reruns
all nine scientific workloads in a fresh attempt, preserving the initial timings,
reference and completed long checks. The unchanged allowance and scientific
acceptance apply to every fresh measurement. See
[restart semantics](/docs/COMPLETE_VALIDATION#results-and-restart).
