packages feed

ppad-censor-0.5.1: bench/Main.hs

{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-type-defaults #-}
{-# LANGUAGE BangPatterns #-}

module Main where

import Control.DeepSeq (NFData(..))
import Criterion.Main
import qualified Censor as C
import Censor.Meter (Meter(..))

-- the small ADTs Result and Verdict are fully strict in their fields,
-- so WHNF == NF. orphan instance keeps the library API untouched.
instance NFData C.Result where rnf !_ = ()

-- A constant-returning meter. Still runs the wrapped action so any
-- per-call cost gets charged, but the reading itself is fixed -- this
-- isolates framework driver overhead from real meter / target work and
-- guarantees a deterministic Pass path (both classes report identical
-- readings, so the e-process wealth stays flat and the run goes to
-- budget).
constMeter :: Meter
constMeter = Meter $ \k act -> do
  let go 0 = pure ()
      go i = act >> go (i - 1)
  go k
  pure 100
{-# NOINLINE constMeter #-}

-- noop hypothesis: samplers produce (), target does nothing. Combined
-- with constMeter, this exercises only the runCT driver loop and the
-- ppad-eproc per-step update.
noopHyp :: C.Hypothesis ()
noopHyp = C.Hypothesis
  { C.target  = \_ -> pure ()
  , C.prepare = pure ()
  , C.sampleA = pure ()
  , C.sampleB = pure ()
  }

main :: IO ()
main = defaultMain [
    runCT_budget
  , runCT_batch
  ]

-- runCT driver overhead vs budget. With a noop target + constMeter,
-- the per-pair cost is (sampleA + sampleB + 2 * meter-overhead +
-- EP.update + EP.decide). Scaling should be linear in budget after
-- warmup is amortised.
runCT_budget :: Benchmark
runCT_budget =
  let cfg n = C.defaultConfig
        { C.cfgBudget = n
        , C.cfgWarmup = 50
        , C.cfgBatch  = 1
        }
  in  bgroup "runCT (noop target, const meter, batch=1)" [
          bench "budget=100"    $ nfIO (C.runCT constMeter (cfg    100) noopHyp)
        , bench "budget=1000"   $ nfIO (C.runCT constMeter (cfg   1000) noopHyp)
        , bench "budget=10000"  $ nfIO (C.runCT constMeter (cfg  10000) noopHyp)
        , bench "budget=100000" $ nfIO (C.runCT constMeter (cfg 100000) noopHyp)
        ]

-- runCT driver overhead vs batch size. Total target invocations is
-- budget * batch; for a noop target the relative driver vs runOne cost
-- should be visible here.
runCT_batch :: Benchmark
runCT_batch =
  let cfg b = C.defaultConfig
        { C.cfgBudget = 1000
        , C.cfgWarmup = 50
        , C.cfgBatch  = b
        }
  in  bgroup "runCT (noop target, const meter, budget=1000)" [
          bench "batch=1"    $ nfIO (C.runCT constMeter (cfg    1) noopHyp)
        , bench "batch=10"   $ nfIO (C.runCT constMeter (cfg   10) noopHyp)
        , bench "batch=100"  $ nfIO (C.runCT constMeter (cfg  100) noopHyp)
        , bench "batch=1000" $ nfIO (C.runCT constMeter (cfg 1000) noopHyp)
        ]