golds-gym-0.4.0.0: README.md
# golds-gym 🏋️
[](https://github.com/ocramz/golds-gym/actions/workflows/ci.yml) [](https://hackage.haskell.org/package/golds-gym)
Golden testing for performance benchmarks. Save timing baselines on first run, compare against them on subsequent runs.
**Key Features:**
- Architecture-specific baselines (different hardware = different golden files)
- Hybrid tolerance (handles both fast <1ms and slow operations)
- Robust statistics mode (outlier detection, trimmed mean)
- Lens-based custom expectations (assert "must be faster", compare by median, etc.)
## Quick Start
```haskell
import Test.Hspec
import Test.Hspec.BenchGolden
import Data.List (sort)
main :: IO ()
main = hspec $ do
describe "Performance" $ do
-- Pure function with normal form evaluation (deep, full evaluation)
benchGolden "list append" $
nf (\xs -> xs ++ xs) [1..1000]
-- Weak head normal form (shallow, outermost constructor only)
benchGolden "replicate" $
whnf (replicate 1000) 42
-- Custom configuration
benchGoldenWith defaultBenchConfig
{ iterations = 500
, tolerancePercent = 10.0
}
"sorting" $
nf sort [1000, 999..1]
```
**Evaluation strategies** (required - specify how values are forced):
- `nf f x` - Force result of `f x` to **normal form** (deep, full evaluation)
- `whnf f x` - Force result of `f x` to **weak head normal form** (shallow, outermost constructor only)
- `nfIO action` - Execute IO action and force result to normal form
- `whnfIO action` - Execute IO action and force result to WHNF
- `nfAppIO f x` - Apply function, execute resulting IO, force result to normal form
- `whnfAppIO f x` - Apply function, execute resulting IO, force result to WHNF
- `io action` - Plain IO action without additional forcing
**Why evaluation strategies matter**: Without forcing, GHC may optimize away computations or share results across iterations, making benchmarks meaningless. Use `nf` for most cases unless you specifically want lazy evaluation (`whnf`).
**First run** creates `.golden/<arch>/list-append.golden` with baseline stats.
**Subsequent runs** compare against baseline. Test fails if mean time changes beyond tolerance (default: ±15% OR ±0.01ms).
**Output format** :
```
Metric Baseline Actual Diff
------ -------- ------ ----
Mean 0.150 ms 0.170 ms +13.3%
```
**Update baselines** after intentional changes:
```bash
GOLDS_GYM_ACCEPT=1 stack test
```
## How It Works
Golden files store timing statistics per architecture (e.g., `.golden/aarch64-darwin-Apple_M1/`):
```json
{
"mean": 1.234,
"stddev": 0.056,
"median": 1.201,
"architecture": "aarch64-darwin-Apple_M1",
"timestamp": "2026-01-30T12:00:00Z"
}
```
**Hybrid tolerance** (default) prevents false failures: benchmarks pass if within **±15% OR ±0.01ms**. This handles measurement noise for fast operations (<1ms) while catching real regressions for slower code.
## Configuration
Key `BenchConfig` options:
| Field | Default | Description |
|-------|---------|-------------|
| `iterations` | 100 | Number of benchmark iterations |
| `tolerancePercent` | 15.0 | Allowed mean time deviation (%) |
| `absoluteToleranceMs` | Just 0.01 | Absolute tolerance (ms) - enables hybrid mode |
| `useRobustStatistics` | False | Use trimmed mean/MAD instead of mean/stddev |
| `warmupIterations` | 5 | Warm-up runs before measurement |
See `BenchConfig` type for all options.
**Environment variables:**
- `GOLDS_GYM_ACCEPT=1` - Regenerate all golden files
- `GOLDS_GYM_SKIP=1` - Skip benchmarks entirely (useful in CI)
## Advanced: Robust Statistics
Standard mean/stddev are sensitive to outliers (GC pauses, OS scheduling). Robust statistics provide outlier-resistant comparisons:
```haskell
benchGoldenWith defaultBenchConfig
{ useRobustStatistics = True -- Use trimmed mean + MAD
, trimPercent = 10.0 -- Remove top/bottom 10%
, outlierThreshold = 3.0 -- Flag outliers >3 MADs from median
}
"noisy benchmark" $
nf computation input
```
**When to use:**
- Benchmarking in noisy environments (shared CI, development machines)
- Operations with occasional GC pauses or system interruptions
- Fast operations (<1ms) with high variance
- You see outliers in test output warnings
## Advanced: Lens-Based Expectations
For fine-grained control, use lens-based expectations to assert custom performance requirements:
```haskell
import Test.Hspec.BenchGolden.Lenses
-- Compare by median instead of mean (more robust)
benchGoldenWithExpectation "median comparison" defaultBenchConfig
[expect _statsMedian (Percent 10.0)]
(nf myAlgorithm input)
-- Compose multiple requirements (both must pass)
benchGoldenWithExpectation "strict requirements" defaultBenchConfig
[ expect _statsMean (Percent 15.0) &&~
expect _statsIQR (Absolute 0.1) -- Low variance required
]
(nf criticalFunction data)
```
**Available lenses:** `_statsMean`, `_statsMedian`, `_statsTrimmedMean`, `_statsStddev`, `_statsMAD`, `_statsIQR`, `_statsMin`, `_statsMax`
**Tolerance types:**
- `Percent 15.0` - Within ±15%
- `Absolute 0.01` - Within ±0.01ms
- `Hybrid 15.0 0.01` - Within ±15% OR ±0.01ms
- `MustImprove 10.0` - Must be ≥10% faster (for testing optimizations)
- `MustRegress 5.0` - Must be ≥5% slower (for accepting controlled regressions)
**Composition:** `(&&~)` for AND, `(||~)` for OR
## Documentation
- [API documentation](https://hackage.haskell.org/package/golds-gym) - Full Haddock docs
- [Example benchmarks](example/Spec.hs) - Comprehensive usage examples
- [CHANGELOG](CHANGELOG.md) - Version history and migration guides
## Related Work
* [tasty-bench](https://hackage.haskell.org/package/tasty-bench)
## License
MIT