# dusk:perf_compare
Judge one `dusk:perf_run` file against another: a verdict per metric and one overall, on counts per painted frame first and timing-mode milliseconds second.
---
## Table of contents
- [Synopsis](#synopsis)
- [What is compared](#what-is-compared)
- [Returns](#returns)
- [See also](#see-also)
---
## Synopsis
```
dart run fluttersdk_dusk dusk:perf_compare [--json]
```
`dusk:perf_compare` reads two files and needs no running app (`CommandBoot.none`). `a` is the baseline, `b` the candidate; `--a` and `--b` name them too, which is how the MCP tool passes them.
---
## What is compared
- **Counts per painted frame**, from `summary.perFrame`, are the gate. Raw counts are never compared: a run that drew 10% fewer frames reports 10% fewer of every count, which reads as an improvement and is not one. The painted-frame counts are shown as info.
- **Milliseconds**, only from `summary.timing.ms`, the medians of the `--timing` repeats. Attribution milliseconds are inflated by build profiling and never gated; without a timing pass on both sides the result says so in `timing.note`.
- **Emulator raster milliseconds** (`env.emulator`) are listed with severity `info` and never gated: an emulator's raster thread is the host GPU's, not a device's.
Every metric is one where higher is worse. Per metric:
| Change (B against A) | Verdict |
|---|---|
| inside either run's own repeat-to-repeat range (`summary.spread`) | `unchanged`: the repeats produce that much by themselves |
| at or above `error` percent | `regressed`, severity `error` |
| at or above `warn` percent | `regressed`, severity `warn` |
| at or below minus `warn` percent | `improved` |
| a metric A never recorded | `regressed`, severity `warn`, no percentage |
| otherwise | `unchanged` |
Thresholds default to warn 10% and error 25%, and come from B's scenario `thresholds` (else A's). The overall verdict is `regressed` when any gated row regressed, else `improved` when any improved, else `unchanged`.
---
## Returns
The default output is a compact table: the verdict and thresholds, the painted frames of each side, then the changed rows, worst first (the first 20; `--json` has all).
```json
{
"verdict": "regressed",
"thresholds": {"warn": 10, "error": 25},
"frames": {"painted": {"a": 100, "b": 90}},
"rows": [
{"metric": "blocks.MonitorRow", "a": 2.0, "b": 2.6, "deltaPct": 30.0,
"verdict": "regressed", "severity": "error"},
{"metric": "timing.rasterMs.p50", "a": 3.0, "b": 9.0, "deltaPct": 200.0,
"verdict": "regressed", "severity": "info"}
],
"unchanged": 41,
"timing": {"gated": true}
}
```
| Exit code | Meaning |
|-----------|---------|
| `0` | No error-level regression (a warn is reported, not failed). |
| `1` | An error-level regression, a missing argument or file, or a run with no measured repeat (every one refused). |
---
## See also
- [dusk:perf_run](dusk-perf-run.md): the files this reads.