packages feed

criterion 1.3.0.0 → 1.4.0.0

raw patch · 11 files changed

+188/−86 lines, 11 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Criterion.Analysis: [anOverhead] :: SampleAnalysis -> Double
- Criterion.Monad: getOverhead :: Criterion Double
- Criterion.Types: [anOverhead] :: SampleAnalysis -> Double
- Criterion.Analysis: SampleAnalysis :: [Regression] -> Double -> Estimate ConfInt Double -> Estimate ConfInt Double -> OutlierVariance -> SampleAnalysis
+ Criterion.Analysis: SampleAnalysis :: [Regression] -> Estimate ConfInt Double -> Estimate ConfInt Double -> OutlierVariance -> SampleAnalysis
- Criterion.Measurement: applyGCStatistics :: Maybe GCStatistics -> Maybe GCStatistics -> Measured -> Measured
+ Criterion.Measurement: applyGCStatistics :: Maybe GCStatistics -> Maybe GCStatistics -> Maybe GCStatistics -> Measured -> Measured
- Criterion.Types: SampleAnalysis :: [Regression] -> Double -> Estimate ConfInt Double -> Estimate ConfInt Double -> OutlierVariance -> SampleAnalysis
+ Criterion.Types: SampleAnalysis :: [Regression] -> Estimate ConfInt Double -> Estimate ConfInt Double -> OutlierVariance -> SampleAnalysis

Files

Criterion/Analysis.hs view
@@ -37,7 +37,7 @@ import Control.Monad.Trans.Except import Criterion.IO.Printf (note, prolix) import Criterion.Measurement (secs, threshold)-import Criterion.Monad (Criterion, getGen, getOverhead)+import Criterion.Monad (Criterion, getGen) import Criterion.Types import Data.Int (Int64) import Data.Maybe (fromJust)@@ -142,16 +142,13 @@               -> ExceptT String Criterion Report analyseSample i name meas = do   Config{..} <- ask-  overhead <- lift getOverhead   let ests      = [Mean,StdDev]       -- The use of filter here throws away very-low-quality       -- measurements when bootstrapping the mean and standard       -- deviations.  Without this, the numbers look nonsensical when       -- very brief actions are measured.       stime     = measure (measTime . rescale) .-                  G.filter ((>= threshold) . measTime) . G.map fixTime .-                  G.tail $ meas-      fixTime m = m { measTime = measTime m - overhead / 2 }+                  G.filter ((>= threshold) . measTime) $ meas       n         = G.length meas       s         = G.length stime   _ <- lift $ prolix "bootstrapping with %d of %d samples (%d%%)\n"@@ -164,7 +161,6 @@       ov = outlierVariance estMean estStdDev (fromIntegral n)       an = SampleAnalysis {                anRegress    = rs-             , anOverhead   = overhead              , anMean       = estMean              , anStdDev     = estStdDev              , anOutlierVar = ov
Criterion/Measurement.hs view
@@ -49,12 +49,20 @@ #endif import Prelude () import Prelude.Compat+#if MIN_VERSION_base(4,7,0)+import System.Mem (performGC, performMinorGC)+# else import System.Mem (performGC)+#endif import Text.Printf (printf) import qualified Control.Exception as Exc import qualified Data.Vector as V import qualified GHC.Stats as Stats +#if !(MIN_VERSION_base(4,7,0))+foreign import ccall "performGC" performMinorGC :: IO ()+#endif+ -- | Statistics about memory usage and the garbage collector. Apart from -- 'gcStatsCurrentBytesUsed' and 'gcStatsCurrentBytesSlop' all are cumulative values since -- the program started.@@ -108,6 +116,8 @@ -- | Try to get GC statistics, bearing in mind that the GHC runtime -- will throw an exception if statistics collection was not enabled -- using \"@+RTS -T@\".+-- +-- If you need guaranteed up-to-date stats, call 'performGC' first. getGCStatistics :: IO (Maybe GCStatistics) #if MIN_VERSION_base(4,10,0) -- Use RTSStats/GCDetails to gather GC stats@@ -170,16 +180,29 @@         -> Int64                -- ^ Number of iterations.         -> IO (Measured, Double) measure bm iters = runBenchmarkable bm iters addResults $ \ !n act -> do+  -- Ensure the stats from getGCStatistics are up-to-date+  -- by garbage collecting. performMinorGC does /not/ update all stats, but+  -- it does update the ones we need (see applyGCStatistics for details.+  --+  -- We use performMinorGC instead of performGC to avoid the cost of copying+  -- the live data in the heap potentially hundreds of times in a+  -- single benchmark.+  performMinorGC   startStats <- getGCStatistics   startTime <- getTime   startCpuTime <- getCPUTime   startCycles <- getCycles   act-  endStats <- getGCStatistics   endTime <- getTime   endCpuTime <- getCPUTime   endCycles <- getCycles-  let !m = applyGCStatistics endStats startStats $ measured {+  -- From these we can derive GC-related deltas.+  endStatsPreGC <- getGCStatistics+  performMinorGC+  -- From these we can derive all other deltas, and performGC guarantees they+  -- are up-to-date.+  endStatsPostGC <- getGCStatistics+  let !m = applyGCStatistics endStatsPostGC endStatsPreGC startStats $ measured {              measTime    = max 0 (endTime - startTime)            , measCpuTime = max 0 (endCpuTime - startCpuTime)            , measCycles  = max 0 (fromIntegral (endCycles - startCycles))@@ -187,8 +210,10 @@            }   return (m, endTime)   where+    -- When combining runs, the Measured value is accumulated over many runs,+    -- but the Double value is the most recent absolute measurement of time.     addResults :: (Measured, Double) -> (Measured, Double) -> (Measured, Double)-    addResults (!m1, !d1) (!m2, !d2) = (m3, d1 + d2)+    addResults (!m1, _) (!m2, !d2) = (m3, d2)       where         add f = f m1 + f m2 @@ -239,8 +264,7 @@          clean `seq` run `seq` evaluate $ rnf env -        performGC-        f count run `finally` clean <* performGC+        f count run `finally` clean     {-# INLINE work #-} {-# INLINE runBenchmarkable #-} @@ -313,22 +337,29 @@ -- | Apply the difference between two sets of GC statistics to a -- measurement. applyGCStatistics :: Maybe GCStatistics-                  -- ^ Statistics gathered at the __end__ of a run.+                  -- ^ Statistics gathered at the __end__ of a run, post-GC.                   -> Maybe GCStatistics+                  -- ^ Statistics gathered at the __end__ of a run, pre-GC.+                  -> Maybe GCStatistics                   -- ^ Statistics gathered at the __beginning__ of a run.                   -> Measured                   -- ^ Value to \"modify\".                   -> Measured-applyGCStatistics (Just end) (Just start) m = m {-    measAllocated          = diff gcStatsBytesAllocated-  , measNumGcs             = diff gcStatsNumGcs-  , measBytesCopied        = diff gcStatsBytesCopied-  , measMutatorWallSeconds = diff gcStatsMutatorWallSeconds-  , measMutatorCpuSeconds  = diff gcStatsMutatorCpuSeconds-  , measGcWallSeconds      = diff gcStatsGcWallSeconds-  , measGcCpuSeconds       = diff gcStatsGcCpuSeconds-  } where diff f = f end - f start-applyGCStatistics _ _ m = m+applyGCStatistics (Just endPostGC) (Just endPreGC) (Just start) m = m {+    -- The choice of endPostGC or endPreGC is important.+    -- For bytes allocated/copied, and mutator statistics, we use+    -- endPostGC, because the intermediate performGC ensures they're up-to-date.+    -- The others (num GCs and GC cpu/wall seconds) must be diffed against+    -- endPreGC so that the extra performGC does not taint them.+    measAllocated          = diff endPostGC gcStatsBytesAllocated+  , measNumGcs             = diff endPreGC  gcStatsNumGcs+  , measBytesCopied        = diff endPostGC gcStatsBytesCopied+  , measMutatorWallSeconds = diff endPostGC gcStatsMutatorWallSeconds+  , measMutatorCpuSeconds  = diff endPostGC gcStatsMutatorCpuSeconds+  , measGcWallSeconds      = diff endPreGC  gcStatsGcWallSeconds+  , measGcCpuSeconds       = diff endPreGC  gcStatsGcCpuSeconds+  } where diff a f = f a - f start+applyGCStatistics _ _ _ m = m  -- | Convert a number of seconds to a string.  The string will consist -- of four decimal places, followed by a short description of the time
Criterion/Monad.hs view
@@ -14,26 +14,20 @@       Criterion     , withConfig     , getGen-    , getOverhead     ) where  import Control.Monad.Reader (asks, runReaderT) import Control.Monad.Trans (liftIO)-import Control.Monad (when)-import Criterion.Measurement (measure, runBenchmark, secs) import Criterion.Monad.Internal (Criterion(..), Crit(..)) import Criterion.Types hiding (measure) import Data.IORef (IORef, newIORef, readIORef, writeIORef)-import Statistics.Regression (olsRegress) import System.Random.MWC (GenIO, createSystemRandom)-import qualified Data.Vector.Generic as G  -- | Run a 'Criterion' action with the given 'Config'. withConfig :: Config -> Criterion a -> IO a withConfig cfg (Criterion act) = do   g <- newIORef Nothing-  o <- newIORef Nothing-  runReaderT act (Crit cfg g o)+  runReaderT act (Crit cfg g)  -- | Return a random number generator, creating one if necessary. --@@ -41,19 +35,6 @@ -- call 'createSystemRandom' more than once if multiple threads race). getGen :: Criterion GenIO getGen = memoise gen createSystemRandom---- | Return an estimate of the measurement overhead.-getOverhead :: Criterion Double-getOverhead = do-  verbose <- asks ((== Verbose) . verbosity)-  memoise overhead $ do-    (meas,_) <- runBenchmark (whnfIO $ measure (whnfIO $ return ()) 1) 1-    let metric get = G.convert . G.map get $ meas-    let o = G.head . fst $-            olsRegress [metric (fromIntegral . measIters)] (metric measTime)-    when verbose . liftIO $-      putStrLn $ "measurement overhead " ++ secs o-    return o  -- | Memoise the result of an 'IO' action. --
Criterion/Monad/Internal.hs view
@@ -29,7 +29,6 @@ data Crit = Crit {     config   :: !Config   , gen      :: !(IORef (Maybe GenIO))-  , overhead :: !(IORef (Maybe Double))   }  -- | The monad in which most criterion code executes.
Criterion/Types.hs view
@@ -57,8 +57,8 @@     , addPrefix     , benchNames     -- ** Evaluation control-    , whnf     , nf+    , whnf     , nfIO     , whnfIO     -- * Result types@@ -76,8 +76,7 @@ import Data.Semigroup  import Control.DeepSeq (NFData(rnf))-import Control.Exception (evaluate)-import Criterion.Types.Internal (fakeEnvironment)+import Criterion.Types.Internal (fakeEnvironment, nf', whnf') import Data.Aeson (FromJSON(..), ToJSON(..)) import Data.Binary (Binary(..), putWord8, getWord8) import Data.Data (Data, Typeable)@@ -212,7 +211,7 @@       (measTime, measCpuTime, measCycles, measIters,        i measAllocated, i measNumGcs, i measBytesCopied,        d measMutatorWallSeconds, d measMutatorCpuSeconds,-       d measGcWallSeconds, d measMutatorCpuSeconds)+       d measGcWallSeconds, d measGcCpuSeconds)       where i = fromInt; d = fromDouble  instance NFData Measured where@@ -313,46 +312,61 @@     get = Measured <$> get <*> get <*> get <*> get                    <*> get <*> get <*> get <*> get <*> get <*> get <*> get --- | Apply an argument to a function, and evaluate the result to weak--- head normal form (WHNF).-whnf :: (a -> b) -> a -> Benchmarkable-whnf = pureFunc id-{-# INLINE whnf #-}- -- | Apply an argument to a function, and evaluate the result to -- normal form (NF). nf :: NFData b => (a -> b) -> a -> Benchmarkable-nf = pureFunc rnf-{-# INLINE nf #-}+nf f x = toBenchmarkable (nf' rnf f x) -pureFunc :: (b -> c) -> (a -> b) -> a -> Benchmarkable-pureFunc reduce f0 x0 = toBenchmarkable (go f0 x0)-  where go f x n-          | n <= 0    = return ()-          | otherwise = evaluate (reduce (f x)) >> go f x (n-1)-{-# INLINE pureFunc #-}+-- | Apply an argument to a function, and evaluate the result to weak+-- head normal form (WHNF).+whnf :: (a -> b) -> a -> Benchmarkable+whnf f x = toBenchmarkable (whnf' f x)  -- | Perform an action, then evaluate its result to normal form. -- This is particularly useful for forcing a lazy 'IO' action to be -- completely performed. nfIO :: NFData a => IO a -> Benchmarkable-nfIO = toBenchmarkable . impure rnf-{-# INLINE nfIO #-}+nfIO a = toBenchmarkable (nfIO' rnf a)  -- | Perform an action, then evaluate its result to weak head normal -- form (WHNF).  This is useful for forcing an 'IO' action whose result -- is an expression to be evaluated down to a more useful value. whnfIO :: IO a -> Benchmarkable-whnfIO = toBenchmarkable . impure id-{-# INLINE whnfIO #-}+whnfIO a = toBenchmarkable (whnfIO' a) -impure :: (a -> b) -> IO a -> Int64 -> IO ()-impure strategy a = go+-- Along with nf' and whnf', the following two functions are the core+-- benchmarking loops. They have been carefully constructed to avoid+-- allocation while also evaluating @a@.+--+-- These functions must not be inlined. There are two possible issues that+-- can arise if they are inlined. First, the work is often floated out of+-- the loop, which creates a nonsense benchmark. Second, the benchmark code+-- itself could be changed by the user's optimization level. By marking them+-- @NOINLINE@, the core benchmark code is always the same.+--+-- See #183 and #184 for discussion.++-- | Generate a function that will run an action a given number of times,+-- reducing it to normal form each time.+nfIO' :: (a -> b) -> IO a -> (Int64 -> IO ())+nfIO' reduce a = go   where go n           | n <= 0    = return ()-          | otherwise = a >>= (evaluate . strategy) >> go (n-1)-{-# INLINE impure #-}+          | otherwise = do+              x <- a+              reduce x `seq` go (n-1)+{-# NOINLINE nfIO' #-} +-- | Generate a function that will run an action a given number of times.+whnfIO' :: IO a -> (Int64 -> IO ())+whnfIO' a = go+  where+    go n | n <= 0    = return ()+         | otherwise = do+             x <- a+             x `seq` go (n-1)+{-# NOINLINE whnfIO' #-}+ -- | Specification of a collection of benchmarks and environments. A -- benchmark may consist of: --@@ -523,7 +537,7 @@     -- newly generated environment.     -> Benchmarkable perBatchEnvWithCleanup alloc clean work-    = Benchmarkable alloc clean (impure rnf . work) False+    = Benchmarkable alloc clean (nfIO' rnf . work) False  -- | Create a Benchmarkable where a fresh environment is allocated for every -- run of the operation to benchmark. This is useful for benchmarking mutable@@ -709,9 +723,6 @@ data SampleAnalysis = SampleAnalysis {       anRegress    :: [Regression]       -- ^ Estimates calculated via linear regression.-    , anOverhead   :: Double-      -- ^ Estimated measurement overhead, in seconds.  Estimation is-      -- performed via linear regression.     , anMean       :: St.Estimate St.ConfInt Double       -- ^ Estimated mean.     , anStdDev     :: St.Estimate St.ConfInt Double@@ -726,12 +737,12 @@  instance Binary SampleAnalysis where     put SampleAnalysis{..} = do-      put anRegress; put anOverhead; put anMean; put anStdDev; put anOutlierVar-    get = SampleAnalysis <$> get <*> get <*> get <*> get <*> get+      put anRegress; put anMean; put anStdDev; put anOutlierVar+    get = SampleAnalysis <$> get <*> get <*> get <*> get  instance NFData SampleAnalysis where     rnf SampleAnalysis{..} =-        rnf anRegress `seq` rnf anOverhead `seq` rnf anMean `seq`+        rnf anRegress `seq` rnf anMean `seq`         rnf anStdDev `seq` rnf anOutlierVar  -- | Data for a KDE chart of performance.@@ -760,9 +771,7 @@     , reportKeys     :: [String]       -- ^ See 'measureKeys'.     , reportMeasured :: V.Vector Measured-      -- ^ Raw measurements. These are /not/ corrected for the-      -- estimated measurement overhead that can be found via the-      -- 'anOverhead' field of 'reportAnalysis'.+      -- ^ Raw measurements.     , reportAnalysis :: SampleAnalysis       -- ^ Report analysis.     , reportOutliers :: Outliers
Criterion/Types/Internal.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE BangPatterns #-}+{-# OPTIONS_GHC -fno-full-laziness #-} -- | -- Module      : Criterion.Types.Internal -- Copyright   : (c) 2017 Ryan Scott@@ -8,8 +10,10 @@ -- Portability : GHC -- -- Exports 'fakeEnvironment'.-module Criterion.Types.Internal (fakeEnvironment) where+module Criterion.Types.Internal (fakeEnvironment, nf', whnf') where +import Data.Int (Int64)+ -- | A dummy environment that is passed to functions that create benchmarks -- from environments when no concrete environment is available. fakeEnvironment :: env@@ -19,3 +23,38 @@   , "\tconstructs benchmarks from an environment?"   , "\t(see the documentation for `env` for details)"   ]++-- Along with Criterion.Types.nfIO' and Criterion.Types.whnfIO', the following+-- two functions are the core benchmarking loops. They have been carefully+-- constructed to avoid allocation while also evaluating @f x@.+--+-- Because these functions are pure, GHC is particularly smart about optimizing+-- them. We must turn off @-ffull-laziness@ to prevent the computation from+-- being floated out of the loop.+--+-- For a similar reason, these functions must not be inlined. There are two+-- possible issues that can arise if they are inlined. First, the work is often+-- floated out of the loop, which creates a nonsense benchmark. Second, the+-- benchmark code itself could be changed by the user's optimization level. By+-- marking them @NOINLINE@, the core benchmark code is always the same.+--+-- See #183 and #184 for discussion.++-- | Generate a function which applies an argument to a function a+-- given number of times, reducing the result to normal form.+nf' :: (b -> ()) -> (a -> b) -> a -> (Int64 -> IO ())+nf' reduce f x = go+  where+    go n | n <= 0    = return ()+         | otherwise = let !y = f x+                       in reduce y `seq` go (n-1)+{-# NOINLINE nf' #-}++-- | Generate a function which applies an argument to a function a+-- given number of times.+whnf' :: (a -> b) -> a -> (Int64 -> IO ())+whnf' f x = go+  where+    go n | n <= 0    = return ()+         | otherwise = f x `seq` go (n-1)+{-# NOINLINE whnf' #-}
changelog.md view
@@ -1,3 +1,50 @@+1.4.0.0++* We now do three samples for statistics:++  * `performMinorGC` before the first sample, to ensure it's up to date.+  * Take another sample after the action, without a garbage collection, so we+    can gather legitimate readings on GC-related statistics.+  * Then `performMinorGC` and sample once more, so we can get up-to-date+    readings on other metrics.++  The type of `applyGCStatistics` has changed accordingly. Before, it was:++  ```haskell+     Maybe GCStatistics -- ^ Statistics gathered at the end of a run.+  -> Maybe GCStatistics -- ^ Statistics gathered at the beginning of a run.+  -> Measured -> Measured+  ```++  Now, it is:++  ```haskell+     Maybe GCStatistics -- ^ Statistics gathered at the end of a run, post-GC. +  -> Maybe GCStatistics -- ^ Statistics gathered at the end of a run, pre-GC. +  -> Maybe GCStatistics -- ^ Statistics gathered at the beginning of a run.+  -> Measured -> Measured+  ```++  When diffing `GCStatistics` in `applyGCStatistics`, we carefully choose+  whether to diff against the end stats pre- or post-GC.++* Use `performMinorGC` rather than `performGC` to update garbage collection+  statistics. This improves the benchmark performance of fast functions on large+  objects.++* Fix a bug in the `ToJSON Measured` instance which duplicated the+  mutator CPU seconds where GC CPU seconds should go.++* Fix a bug in sample analysis which incorrectly accounted for overhead+  causing runtime errors and invalid results. Accordingly, the buggy+  `getOverhead` function has been removed.++* Fix a bug in `Measurement.measure` which inflated the reported time taken+  for `perRun` benchmarks.++* Reduce overhead of `nf`, `whnf`, `nfIO`, and `whnfIO` by removing allocation+  from the central loops.+ 1.3.0.0  * `criterion` was previously reporting the following statistics incorrectly
criterion.cabal view
@@ -1,5 +1,5 @@ name:           criterion-version:        1.3.0.0+version:        1.4.0.0 synopsis:       Robust, reliable performance measurement and analysis license:        BSD3 license-file:   LICENSE@@ -99,7 +99,7 @@     containers,     deepseq >= 1.1.0.0,     directory,-    exceptions >= 0.8.2 && < 0.9,+    exceptions >= 0.8.2 && < 0.10,     filepath,     Glob >= 0.7.2,     microstache >= 1.0.1 && < 1.1,
examples/ConduitVsPipes.hs view
@@ -5,7 +5,7 @@ -- by compiling with the -fno-full-laziness option.  import Criterion.Main (bench, bgroup, defaultMain, nfIO, whnf)-import Data.Conduit (($=), ($$))+import Data.Conduit (runConduit, (.|)) import Data.Functor.Identity (Identity(..)) import Pipes ((>->), discard, each, for, runEffect) import qualified Data.Conduit.List as C@@ -28,7 +28,7 @@  pipes, conduit :: (Monad m) => Int -> m () pipes n = runEffect $ for (each [1..n] >-> P.map (+1) >-> P.filter even) discard-conduit n = C.sourceList [1..n] $= C.map (+1) $= C.filter even $$ C.sinkNull+conduit n = runConduit $ C.sourceList [1..n] .| C.map (+1) .| C.filter even .| C.sinkNull  main :: IO () main = criterion 10000
examples/criterion-examples.cabal view
@@ -33,7 +33,7 @@   ghc-options: -Wall -rtsopts   build-depends:     base == 4.*,-    conduit >= 1.1,+    conduit >= 1.2.13.1,     criterion,     pipes >= 4.1,     transformers
tests/Tests.hs view
@@ -15,7 +15,7 @@   m1 = Measured 4.613000783137977e-05 3.500000000000378e-05 31432 1 0 0 0 0.0 0.0 0.0 0.0   v1 = V.fromList [m1]   est1 = estimateFromErr 0.0 (0.0, 0.0) (mkCL 0.0)-  s1 = SampleAnalysis [] 0.0 est1 est1 (OutlierVariance Unaffected "" 0.0)+  s1 = SampleAnalysis [] est1 est1 (OutlierVariance Unaffected "" 0.0)  m2 :: Measured m2 = Measured {measTime = 1.1438998626545072e-5