packages feed

golds-gym 0.5.0.0 → 0.6.0.0

raw patch · 8 files changed

+442/−47 lines, 8 filesdep −benchpressPVP ok

version bump matches the API change (PVP)

Dependencies removed: benchpress

API changes (from Hackage documentation)

+ Test.Hspec.BenchGolden: benchGoldenSweep :: Show a => String -> Text -> [a] -> (a -> BenchAction) -> Spec
+ Test.Hspec.BenchGolden: benchGoldenSweepWith :: Show a => BenchConfig -> String -> Text -> [a] -> (a -> BenchAction) -> Spec
+ Test.Hspec.BenchGolden: instance GHC.Show.Show a => Test.Hspec.Core.Example.Example (Test.Hspec.BenchGolden.BenchGoldenSweep a)
+ Test.Hspec.BenchGolden.CSV: buildCSV :: Text -> [(Text, GoldenStats)] -> Builder
+ Test.Hspec.BenchGolden.CSV: csvHeader :: Text -> Builder
+ Test.Hspec.BenchGolden.CSV: csvRow :: Text -> GoldenStats -> Builder
+ Test.Hspec.BenchGolden.CSV: getSweepCSVPath :: FilePath -> Text -> String -> FilePath
+ Test.Hspec.BenchGolden.CSV: writeSweepCSV :: FilePath -> Text -> String -> Text -> [(Text, GoldenStats)] -> IO ()
+ Test.Hspec.BenchGolden.Runner: runSweep :: Show a => String -> BenchConfig -> Text -> [a] -> (a -> BenchAction) -> IO [(a, BenchResult, GoldenStats)]
+ Test.Hspec.BenchGolden.Runner: runSweepPoint :: Show a => String -> BenchConfig -> Text -> a -> BenchAction -> IO (BenchResult, GoldenStats)

Files

CHANGELOG.md view
@@ -1,5 +1,26 @@ # Changelog +## [0.6.0]++### Added++- **Parameter sweep support** for benchmarking scaling behaviour+  - `benchGoldenSweep` combinator for simple parameter sweeps with default config and `benchGoldenSweepWith` combinator for sweeps with custom configuration+  - Individual golden files created per parameter value (e.g., `sort-scaling_n_1000.golden`)+  - Automatic regression detection per sweep point using existing tolerance logic+  - NB: Each timing sample now runs multiple inner iterations (leveraging the SPEC trick in `nf`/`nfIO` combinators) and divides by the iteration count+  - Unified timing path: `runBenchmark` now always uses `runBenchmarkWithRawTimings` for consistent, correct measurements++- **CSV export** for analysis and plotting+  - New `Test.Hspec.BenchGolden.CSV` module+  - Single CSV file per sweep with architecture in filename (e.g., `sort-scaling-aarch64-darwin-Apple_M1.csv`)+  - Columns: timestamp, parameter value, mean, stddev, median, min, max, trimmed_mean, mad, iqr+  - Built with `Text.Builder` for efficient serialization++### Removed++- **benchpress dependency** - timing is now done via `getCPUTime` from base, without `benchpress`+ ## [0.5.0]  ### Added
README.md view
@@ -4,6 +4,8 @@  Golden testing for performance benchmarks. Save timing baselines on first run, compare against them on subsequent runs. +The library was originally inspired by (and dependent on) `benchpress`, but now has independent measurement routines.+ **Key Features:** - Architecture-specific baselines (different hardware = different golden files) - Hybrid tolerance (handles both fast <1ms and slow operations)
example/Spec.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ -- | Example benchmark golden tests demonstrating golds-gym usage. module Main (main) where @@ -140,6 +142,28 @@       (defaultBenchConfig { useRobustStatistics = True, iterations = 1000 })       [expect _statsTrimmedMean (Percent 25.0)]       $ nf fib 26++  -- ============================================================================+  -- Parameter Sweeps+  -- ============================================================================+  +  describe "Parameter Sweeps" $ do+    -- Simple parameter sweep: measure how sort scales with input size+    -- This produces:+    --   - Individual golden files: .golden/<arch>/sort-scaling_n=500.golden, etc.+    --   - Single CSV file: .golden/sort-scaling-<arch>.csv+    benchGoldenSweep "sort-scaling" "n" [500, 2000, 5000] $+      \n -> nf sort [n, n-1..1 :: Int]+    +    -- Sweep with custom configuration+    benchGoldenSweepWith+      defaultBenchConfig+        { iterations = 500+        , tolerancePercent = 20.0+        , warmupIterations = 10+        }+      "fib-scaling" "depth" [10, 20, 30] $+      \depth -> nf fib depth   
golds-gym.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               golds-gym-version:            0.5.0.0+version:            0.6.0.0 synopsis:           Golden testing framework for performance benchmarks description:     A Haskell framework for golden testing of timing benchmarks.@@ -8,7 +8,7 @@     against on subsequent runs. Golden files are architecture-specific     to account for hardware differences.     .-    Based on hspec and benchpress.+    Built on hspec.  license:            MIT license-file:       LICENSE@@ -26,11 +26,11 @@         Test.Hspec.BenchGolden.Arch         Test.Hspec.BenchGolden.Runner         Test.Hspec.BenchGolden.Lenses+        Test.Hspec.BenchGolden.CSV      build-depends:         base >= 4.14 && < 5,         hspec-core >= 2.10 && < 3,-        benchpress >= 0.2 && < 0.3,         aeson >= 2.0 && < 3,         directory >= 1.3 && < 2,         filepath >= 1.4 && < 2,@@ -57,7 +57,8 @@     build-depends:         base >= 4.14 && < 5,         golds-gym,-        hspec >= 2.10 && < 3+        hspec >= 2.10 && < 3,+        text >= 1.2 && < 3  test-suite golds-gym-properties     type:             exitcode-stdio-1.0
src/Test/Hspec/BenchGolden.hs view
@@ -14,13 +14,16 @@ -- = Overview -- -- @golds-gym@ is a framework for golden testing of performance benchmarks.--- It integrates with hspec and uses benchpress for lightweight timing measurements.+-- It integrates with hspec and uses CPU time measurements for benchmarking. -- -- Benchmarks can use robust statistics to mitigate the impact of outliers. -- -- The library can be used both to assert that performance does not regress, and to set expectations -- for improvements across project versions (see `benchGoldenWithExpectation`). --+-- There are also combinators for parameter sweep benchmarks that generate CSV files for analysis and plotting, +-- see `benchGoldenSweep` and `benchGoldenSweepWith`.+-- -- = Quick Start -- -- @@@ -178,6 +181,10 @@   , benchGoldenWith   , benchGoldenWithExpectation +    -- * Parameter Sweeps+  , benchGoldenSweep+  , benchGoldenSweepWith+     -- * Configuration   , BenchConfig(..)   , defaultBenchConfig@@ -218,7 +225,7 @@ import Test.Hspec.BenchGolden.Arch import qualified Test.Hspec.BenchGolden.Lenses as L import Test.Hspec.BenchGolden.Lenses hiding (Expectation)-import Test.Hspec.BenchGolden.Runner (runBenchGolden, setAcceptGoldens, setSkipBenchmarks, nf, nfIO, nfAppIO, io)+import Test.Hspec.BenchGolden.Runner (runBenchGolden, runSweep, setAcceptGoldens, setSkipBenchmarks, nf, nfIO, nfAppIO, io) import Test.Hspec.BenchGolden.Types  -- | Create a benchmark golden test with default configuration.@@ -284,6 +291,125 @@     , benchAction = action     , benchConfig = config     }++-- | Create a parameter sweep benchmark with default configuration.+--+-- This combinator runs the same benchmark with multiple parameter values,+-- saving individual golden files for each point and producing a single CSV+-- file for analysis and plotting.+--+-- Example:+--+-- @+-- describe "Scaling Tests" $ do+--   benchGoldenSweep "sort-scaling" "n" [1000, 5000, 10000, 50000] $+--     \\n -> nf sort [n, n-1..1]+-- @+--+-- This produces:+--+-- * Golden files: @.golden\/\<arch\>\/sort-scaling_n=1000.golden@, etc.+-- * CSV file: @.golden\/sort-scaling-\<arch\>.csv@+benchGoldenSweep ::+     Show a+  => String         -- ^ Sweep name (used for CSV filename and golden file prefix)+  -> T.Text         -- ^ Parameter name (for CSV column header)+  -> [a]            -- ^ Parameter values to sweep over+  -> (a -> BenchAction)  -- ^ Action parameterized by sweep value+  -> Spec+benchGoldenSweep = benchGoldenSweepWith defaultBenchConfig++-- | Create a parameter sweep benchmark with custom configuration.+--+-- Example:+--+-- @+-- describe "Performance Scaling" $ do+--   benchGoldenSweepWith+--     defaultBenchConfig { iterations = 500, tolerancePercent = 10.0 }+--     "algorithm-scaling" "size" [100, 500, 1000, 5000] $+--     \\size -> nf myAlgorithm (generateInput size)+-- @+--+-- The CSV file includes columns for timestamp, parameter value, and all+-- standard statistics (mean, stddev, median, min, max, etc.).+benchGoldenSweepWith ::+     Show a+  => BenchConfig    -- ^ Configuration parameters+  -> String         -- ^ Sweep name+  -> T.Text         -- ^ Parameter name (for CSV column header)+  -> [a]            -- ^ Parameter values to sweep over+  -> (a -> BenchAction)  -- ^ Action parameterized by sweep value+  -> Spec+benchGoldenSweepWith config sweepName paramName paramValues mkAction =+  it (sweepName ++ " [sweep]") $ BenchGoldenSweep sweepName config paramName paramValues mkAction++-- | Internal type for sweep benchmarks.+data BenchGoldenSweep a = BenchGoldenSweep+  !String            -- Sweep name+  !BenchConfig       -- Config+  !T.Text            -- Parameter name+  ![a]               -- Parameter values+  !(a -> BenchAction) -- Action generator++-- | Instance for sweep benchmarks.+instance Show a => Example (BenchGoldenSweep a) where+  type Arg (BenchGoldenSweep a) = ()+  evaluateExample (BenchGoldenSweep sweepName config paramName paramValues mkAction) _params hook _progress = do+    -- Read environment variables to determine accept/skip flags+    acceptEnv <- lookupEnv "GOLDS_GYM_ACCEPT"+    skipEnv <- lookupEnv "GOLDS_GYM_SKIP"+    +    let shouldAccept = case acceptEnv of+          Just "1"    -> True+          Just "true" -> True+          Just "yes"  -> True+          _           -> False+        shouldSkip = case skipEnv of+          Just "1"    -> True+          Just "true" -> True+          Just "yes"  -> True+          _           -> False+    +    -- Store the flags so Runner can access them+    setAcceptGoldens shouldAccept+    setSkipBenchmarks shouldSkip+    +    ref <- newIORef (Result "" Success)+    hook $ \() -> do+      results <- runSweep sweepName config paramName paramValues mkAction+      writeIORef ref (fromSweepResults sweepName paramName paramValues results)+    readIORef ref++-- | Convert sweep results to hspec Result.+fromSweepResults :: Show a => String -> T.Text -> [a] -> [(a, BenchResult, GoldenStats)] -> Result+fromSweepResults sweepName paramName paramValues results =+  let -- Check if any point regressed+      regressions = [(pv, r) | (pv, r@(Regression _ _ _ _ _), _) <- results]+      firstRuns = [pv | (pv, FirstRun _, _) <- results]+      +      nPoints = length paramValues+      +  in case regressions of+     ((pv, Regression golden actual pct tol absTol) : _) ->+       let toleranceDesc :: String+           toleranceDesc = case absTol of+             Nothing -> printf "tolerance: %.1f%%" tol+             Just absMs -> printf "tolerance: %.1f%% or %.3f ms" tol absMs+           message = printf "Sweep '%s': regression at %s=%s\nMean time increased by %.1f%% (%s)\n\n%s"+                       sweepName (T.unpack paramName) (show pv) pct toleranceDesc+                       (formatRegression golden actual)+       in Result message (Failure Nothing (Reason message))+     _ ->+       if not (null firstRuns)+       then+         let info = printf "Sweep '%s': baselines created for %d point(s)\nCSV written with %d row(s)"+                      sweepName (length firstRuns) nPoints+         in Result info Success+       else+         let info = printf "Sweep '%s': all %d point(s) passed\nCSV written with %d row(s)"+                      sweepName nPoints nPoints+         in Result info Success   -- | Create a benchmark golden test with custom lens-based expectations.
+ src/Test/Hspec/BenchGolden/CSV.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Test.Hspec.BenchGolden.CSV+-- Description : CSV export for parameter sweep benchmark results+-- Copyright   : (c) 2026+-- License     : MIT+-- Maintainer  : @ocramz+--+-- This module provides CSV export functionality for parameter sweep benchmarks.+-- CSV files are written alongside golden files for convenient analysis and plotting.+--+-- = CSV Format+--+-- The CSV includes columns for:+--+-- * @timestamp@ - When the benchmark was run (ISO 8601 format)+-- * @param_name@ - The name of the sweep parameter+-- * @param_value@ - The value of the parameter for this row+-- * @mean_ms@ - Mean execution time in milliseconds+-- * @stddev_ms@ - Standard deviation in milliseconds+-- * @median_ms@ - Median execution time in milliseconds+-- * @min_ms@ - Minimum execution time in milliseconds+-- * @max_ms@ - Maximum execution time in milliseconds+-- * @trimmed_mean_ms@ - Trimmed mean (if robust statistics enabled)+-- * @mad_ms@ - Median absolute deviation (if robust statistics enabled)+-- * @iqr_ms@ - Interquartile range (if robust statistics enabled)+--+-- = Usage+--+-- CSV files are automatically generated when using 'benchGoldenSweep' or+-- 'benchGoldenSweepWith'. The file is written to:+--+-- @+-- \<outputDir\>/\<sweep-name\>-\<arch-id\>.csv+-- @+--+-- For example: @.golden/sort-scaling-aarch64-darwin-Apple_M1.csv@++module Test.Hspec.BenchGolden.CSV+  ( -- * CSV Generation+    csvHeader+  , csvRow+  , buildCSV++    -- * File I/O+  , writeSweepCSV+  , getSweepCSVPath+  ) where++import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Lazy.Builder (Builder)+import qualified Data.Text.Lazy.Builder as B+import qualified Data.Text.Lazy.Builder.RealFloat as B+import qualified Data.Text.Lazy.IO as TL+import Data.Time.Format.ISO8601 (iso8601Show)+import System.Directory (createDirectoryIfMissing)+import System.FilePath ((</>), (<.>))++import Test.Hspec.BenchGolden.Arch (sanitizeForFilename)+import Test.Hspec.BenchGolden.Types (GoldenStats(..))++-- | Generate CSV header row.+--+-- The header includes all standard statistics columns plus a parameter column.+csvHeader :: Text -> Builder+csvHeader paramName = mconcat+  [ "timestamp,"+  , B.fromText paramName, ","+  , "mean_ms,stddev_ms,median_ms,min_ms,max_ms,"+  , "trimmed_mean_ms,mad_ms,iqr_ms\n"+  ]++-- | Generate a single CSV data row from benchmark stats.+--+-- The timestamp is taken from the 'GoldenStats', and the parameter value+-- is provided separately as a string representation.+csvRow :: Text -> GoldenStats -> Builder+csvRow paramValue GoldenStats{..} = mconcat+  [ B.fromString (iso8601Show statsTimestamp), ","+  , B.fromText paramValue, ","+  , B.realFloat statsMean, ","+  , B.realFloat statsStddev, ","+  , B.realFloat statsMedian, ","+  , B.realFloat statsMin, ","+  , B.realFloat statsMax, ","+  , B.realFloat statsTrimmedMean, ","+  , B.realFloat statsMAD, ","+  , B.realFloat statsIQR, "\n"+  ]++-- | Build complete CSV content from a list of (param value, stats) pairs.+--+-- Example:+--+-- @+-- let results = [("1000", stats1), ("5000", stats2), ("10000", stats3)]+-- let csv = buildCSV "n" results+-- @+buildCSV :: Text -> [(Text, GoldenStats)] -> Builder+buildCSV paramName rows =+  csvHeader paramName <> mconcat (map (uncurry csvRow) rows)++-- | Get the path for a sweep CSV file.+--+-- The file is placed in the output directory with the architecture ID+-- included in the filename (not as a subdirectory) for easy comparison+-- across architectures.+--+-- Example: @.golden/sort-scaling-aarch64-darwin-Apple_M1.csv@+getSweepCSVPath :: FilePath -> Text -> String -> FilePath+getSweepCSVPath outDir archId sweepName =+  let sanitizedName = T.unpack $ sanitizeForFilename (T.pack sweepName)+      sanitizedArch = T.unpack $ sanitizeForFilename archId+  in outDir </> (sanitizedName <> "-" <> sanitizedArch) <.> "csv"++-- | Write sweep results to a CSV file.+--+-- This creates the output directory if it doesn't exist and writes+-- the complete CSV content atomically.+writeSweepCSV :: FilePath -> Text -> String -> Text -> [(Text, GoldenStats)] -> IO ()+writeSweepCSV outDir archId sweepName paramName rows = do+  createDirectoryIfMissing True outDir+  let path = getSweepCSVPath outDir archId sweepName+      content = B.toLazyText $ buildCSV paramName rows+  TL.writeFile path content
src/Test/Hspec/BenchGolden/Runner.hs view
@@ -38,6 +38,10 @@   , runBenchmark   , runBenchmarkWithRawTimings +    -- * Parameter Sweeps+  , runSweepPoint+  , runSweep+     -- * Golden File Operations   , readGoldenFile   , writeGoldenFile@@ -71,7 +75,7 @@  import Control.DeepSeq (NFData, rnf) import Control.Exception (evaluate)-import Control.Monad (when, replicateM_)+import Control.Monad (when, replicateM_, forM) import Data.Aeson (eitherDecodeFileStrict, encodeFile) import Data.IORef (IORef, newIORef, readIORef, writeIORef) import Data.List (sort)@@ -88,9 +92,8 @@  import Lens.Micro ((^.)) -import qualified Test.BenchPress as BP- import Test.Hspec.BenchGolden.Arch (detectArchitecture, sanitizeForFilename)+import Test.Hspec.BenchGolden.CSV (writeSweepCSV) import Test.Hspec.BenchGolden.Lenses (metricFor, varianceFor) import Test.Hspec.BenchGolden.Types @@ -255,42 +258,35 @@               return result  -- | Run a benchmark and collect statistics.+--+-- Uses raw timing collection with proper inner iteration counts to ensure+-- the SPEC trick in nf/nfIO prevents thunk sharing. runBenchmark :: String -> BenchAction -> BenchConfig -> ArchConfig -> IO GoldenStats-runBenchmark _name action config arch = do-  if useRobustStatistics config-    then runBenchmarkWithRawTimings _name action config arch-    else do-      -- Use benchpress for standard statistics-      (cpuStats, _wallStats) <- BP.benchmark-        (iterations config)-        (pure ())                    -- setup-        (const $ runBenchAction action 1)  -- action: run 1 iteration-        (const $ pure ())            -- teardown--      now <- getCurrentTime--      return GoldenStats-        { statsMean        = BP.mean cpuStats-        , statsStddev      = BP.stddev cpuStats-        , statsMedian      = BP.median cpuStats-        , statsMin         = BP.min cpuStats-        , statsMax         = BP.max cpuStats-        , statsPercentiles = BP.percentiles cpuStats-        , statsArch        = archId arch-        , statsTimestamp   = now-        , statsTrimmedMean = 0.0  -- Not calculated in non-robust mode-        , statsMAD         = 0.0-        , statsIQR         = 0.0-        , statsOutliers    = []-        }+runBenchmark name action config arch =+  -- Always use the raw timing path since it correctly handles SPEC+  runBenchmarkWithRawTimings name action config arch  -- | Run a benchmark with raw timing collection for robust statistics.+--+-- This function times running all iterations in a single batch, then+-- divides to get per-iteration timing. The SPEC trick in nf/nfIO+-- prevents sharing within the batch.+--+-- We collect multiple samples by running the full batch multiple times,+-- ensuring accurate measurements even with GHC's -O2 optimizations. runBenchmarkWithRawTimings :: String -> BenchAction -> BenchConfig -> ArchConfig -> IO GoldenStats runBenchmarkWithRawTimings _name action config arch = do-  -- Collect raw CPU timings-  timings <- mapM (const $ measureCPUTimeForAction action) [1 .. iterations config]+  let iters = fromIntegral (iterations config) :: Word64+      numSamples = 10 :: Int  -- Number of timing samples to collect   -  let sortedTimings = sort timings+  -- Collect raw CPU timings (each sample runs all iterations)+  rawTimings <- forM [1 .. numSamples] $ \_ -> do+    startCpu <- getCPUTime+    runBenchAction action iters  -- SPEC trick prevents sharing within this call+    endCpu <- getCPUTime+    pure $ picosToMillis (endCpu - startCpu) / fromIntegral iters+  +  let sortedTimings = sort rawTimings       vec = V.fromList sortedTimings              -- Standard statistics@@ -303,7 +299,7 @@       min' = V.minimum vec       max' = V.maximum vec       -      -- Percentiles (matching benchpress format)+      -- Percentiles       percentiles' = [(p, quantile p vec) | p <- [50, 66, 75, 80, 90, 95, 98, 99, 100]]              -- Robust statistics@@ -326,13 +322,6 @@     , statsOutliers    = outliers'     }   where-    measureCPUTimeForAction act = do-      startCpu <- getCPUTime-      runBenchAction act 1  -- Run the action once-      endCpu <- getCPUTime-      let cpuTime = picosToMillis (endCpu - startCpu)-      return cpuTime-         picosToMillis :: Integer -> Double     picosToMillis t = realToFrac t / (10^(9 :: Int))     @@ -562,3 +551,105 @@ -- @ shouldSkipBenchmarks :: IO Bool shouldSkipBenchmarks = readIORef skipBenchmarksRef++-- -----------------------------------------------------------------------------+-- Parameter Sweeps+-- -----------------------------------------------------------------------------++-- | Run a single point of a parameter sweep.+--+-- This is similar to 'runBenchGolden' but returns the 'GoldenStats' along+-- with the 'BenchResult', allowing the caller to accumulate stats for CSV export.+--+-- Each point is saved to its own golden file with the parameter value+-- included in the filename (e.g., @sort-scaling_n=1000.golden@).+runSweepPoint ::+     Show a+  => String       -- ^ Base sweep name+  -> BenchConfig+  -> T.Text       -- ^ Parameter name+  -> a            -- ^ Parameter value+  -> BenchAction+  -> IO (BenchResult, GoldenStats)+runSweepPoint sweepName config paramName paramValue action = do+  let pointName = sweepName ++ "_" ++ T.unpack paramName ++ "=" ++ show paramValue++  -- Run the benchmark (this writes golden/actual files via runBenchGolden logic)+  skip <- shouldSkipBenchmarks+  if skip+    then do+      now <- getCurrentTime+      arch <- detectArchitecture+      let dummyStats = GoldenStats 0 0 0 0 0 [] (archId arch) now 0 0 0 []+      return (Pass dummyStats dummyStats [], dummyStats)+    else do+      arch <- detectArchitecture+      let archDir = T.unpack $ archId arch++      -- Create output directory+      let dir = outputDir config </> archDir+      createDirectoryIfMissing True dir++      -- Run warm-up iterations+      when (warmupIterations config > 0) $+        runBenchAction action (fromIntegral $ warmupIterations config)++      -- Run the actual benchmark+      actualStats <- runBenchmark pointName action config arch++      -- Write actual results+      writeActualFile (outputDir config) archDir pointName actualStats++      -- Check if we should force update+      update <- shouldUpdateGolden++      -- Read or create golden file+      let goldenPath = getGoldenPath (outputDir config) archDir pointName+      goldenExists <- doesFileExist goldenPath++      if update || not goldenExists+        then do+          -- First run or forced update: create/update golden+          writeGoldenFile (outputDir config) archDir pointName actualStats+          return (FirstRun actualStats, actualStats)+        else do+          -- Compare against existing golden+          goldenResult <- readGoldenFile goldenPath+          case goldenResult of+            Left err -> error $ "Failed to read golden file: " ++ err+            Right goldenStats -> do+              let result = compareStats config goldenStats actualStats+              return (result, actualStats)++-- | Run a full parameter sweep and write CSV output.+--+-- This runs benchmarks for all parameter values, saves individual golden+-- files, and writes a single CSV file with all results for analysis.+--+-- The CSV file is placed at:+--+-- @+-- \<outputDir\>/\<sweep-name\>-\<arch-id\>.csv+-- @+runSweep ::+     Show a+  => String           -- ^ Sweep name+  -> BenchConfig+  -> T.Text           -- ^ Parameter name (for CSV column header)+  -> [a]              -- ^ Parameter values to sweep over+  -> (a -> BenchAction)  -- ^ Action generator+  -> IO [(a, BenchResult, GoldenStats)]+runSweep sweepName config paramName paramValues mkAction = do+  arch <- detectArchitecture+  +  -- Run each parameter value+  results <- forM paramValues $ \paramVal -> do+    let action = mkAction paramVal+    (result, stats) <- runSweepPoint sweepName config paramName paramVal action+    return (paramVal, result, stats)+  +  -- Write CSV with all results+  let csvRows = [(T.pack (show pv), stats) | (pv, _, stats) <- results]+  writeSweepCSV (outputDir config) (archId arch) sweepName paramName csvRows+  +  return results
src/Test/Hspec/BenchGolden/Types.hs view
@@ -239,3 +239,5 @@     <*> v .: "os"     <*> v .: "cpu"     <*> v .:? "model"++