packages feed

chronos-bench 0.1.0.2 → 0.2.0.1

raw patch · 7 files changed

+423/−416 lines, 7 filesdep +chronosdep −timePVP ok

version bump matches the API change (PVP)

Dependencies added: chronos

Dependencies removed: time

API changes (from Hackage documentation)

- Chronos: Analysis :: Natural -> Natural -> Rational -> Rational -> Analysis
- Chronos: Benchmark :: String -> Analysis -> (Analysis -> IO Analysis) -> Benchmark
- Chronos: Config :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Double -> Maybe Double -> Maybe Double -> Config
- Chronos: [analysis] :: Benchmark -> Analysis
- Chronos: [confidence] :: Config -> Double
- Chronos: [hideBar] :: Config -> Bool
- Chronos: [hideDetails] :: Config -> Bool
- Chronos: [mean] :: Analysis -> Rational
- Chronos: [name] :: Benchmark -> String
- Chronos: [printOnce] :: Config -> Bool
- Chronos: [qFactor] :: Analysis -> Rational
- Chronos: [relativeError] :: Config -> Maybe Double
- Chronos: [runner] :: Benchmark -> Analysis -> IO Analysis
- Chronos: [sameLine] :: Config -> Bool
- Chronos: [samples] :: Analysis -> Natural
- Chronos: [simple] :: Config -> Bool
- Chronos: [sortByMean] :: Config -> Bool
- Chronos: [squaredWeights] :: Analysis -> Natural
- Chronos: [timeout] :: Config -> Maybe Double
- Chronos: bench :: NFData b => String -> (a -> b) -> a -> Benchmark
- Chronos: benchIO :: String -> IO a -> Benchmark
- Chronos: benchShell :: String -> String -> Benchmark
- Chronos: data Analysis
- Chronos: data Benchmark
- Chronos: data Config
- Chronos: defaultConfig :: Config
- Chronos: defaultMain :: [Benchmark] -> IO ()
- Chronos: defaultMainWith :: Config -> [Benchmark] -> IO ()
- Chronos: instance GHC.Classes.Eq Chronos.Analysis
- Chronos: instance GHC.Classes.Eq Chronos.BenchmarkMeta
- Chronos: instance GHC.Classes.Eq Chronos.Config
- Chronos: instance GHC.Classes.Ord Chronos.Analysis
- Chronos: instance GHC.Classes.Ord Chronos.BenchmarkMeta
- Chronos: instance GHC.Classes.Ord Chronos.Config
- Chronos: instance GHC.Read.Read Chronos.Analysis
- Chronos: instance GHC.Read.Read Chronos.Config
- Chronos: instance GHC.Show.Show Chronos.Analysis
- Chronos: instance GHC.Show.Show Chronos.Config
- Chronos: isEqualTo :: Benchmark -> Benchmark -> IO Bool
- Chronos: isFasterThan :: Benchmark -> Benchmark -> IO Bool
- Chronos: standardDeviation :: Analysis -> Double
- Chronos: standardError :: Analysis -> Double
- Chronos: step :: Benchmark -> IO Benchmark
- Chronos: variance :: Analysis -> Rational
+ Chronos.Bench: Analysis :: Natural -> Natural -> Rational -> Rational -> Analysis
+ Chronos.Bench: Benchmark :: String -> Analysis -> (Analysis -> IO Analysis) -> Benchmark
+ Chronos.Bench: Config :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Double -> Maybe Double -> Maybe Double -> Config
+ Chronos.Bench: [analysis] :: Benchmark -> Analysis
+ Chronos.Bench: [confidence] :: Config -> Double
+ Chronos.Bench: [hideBar] :: Config -> Bool
+ Chronos.Bench: [hideDetails] :: Config -> Bool
+ Chronos.Bench: [mean] :: Analysis -> Rational
+ Chronos.Bench: [name] :: Benchmark -> String
+ Chronos.Bench: [printOnce] :: Config -> Bool
+ Chronos.Bench: [qFactor] :: Analysis -> Rational
+ Chronos.Bench: [relativeError] :: Config -> Maybe Double
+ Chronos.Bench: [runner] :: Benchmark -> Analysis -> IO Analysis
+ Chronos.Bench: [sameLine] :: Config -> Bool
+ Chronos.Bench: [samples] :: Analysis -> Natural
+ Chronos.Bench: [simple] :: Config -> Bool
+ Chronos.Bench: [sortByMean] :: Config -> Bool
+ Chronos.Bench: [squaredWeights] :: Analysis -> Natural
+ Chronos.Bench: [timeout] :: Config -> Maybe Double
+ Chronos.Bench: bench :: NFData b => String -> (a -> b) -> a -> Benchmark
+ Chronos.Bench: benchIO :: String -> IO a -> Benchmark
+ Chronos.Bench: benchShell :: String -> String -> Benchmark
+ Chronos.Bench: data Analysis
+ Chronos.Bench: data Benchmark
+ Chronos.Bench: data Config
+ Chronos.Bench: defaultConfig :: Config
+ Chronos.Bench: defaultMain :: [Benchmark] -> IO ()
+ Chronos.Bench: defaultMainWith :: Config -> [Benchmark] -> IO ()
+ Chronos.Bench: instance GHC.Classes.Eq Chronos.Bench.Analysis
+ Chronos.Bench: instance GHC.Classes.Eq Chronos.Bench.BenchmarkMeta
+ Chronos.Bench: instance GHC.Classes.Eq Chronos.Bench.Config
+ Chronos.Bench: instance GHC.Classes.Ord Chronos.Bench.Analysis
+ Chronos.Bench: instance GHC.Classes.Ord Chronos.Bench.BenchmarkMeta
+ Chronos.Bench: instance GHC.Classes.Ord Chronos.Bench.Config
+ Chronos.Bench: instance GHC.Read.Read Chronos.Bench.Analysis
+ Chronos.Bench: instance GHC.Read.Read Chronos.Bench.Config
+ Chronos.Bench: instance GHC.Show.Show Chronos.Bench.Analysis
+ Chronos.Bench: instance GHC.Show.Show Chronos.Bench.Config
+ Chronos.Bench: isEqualTo :: Benchmark -> Benchmark -> IO Bool
+ Chronos.Bench: isFasterThan :: Benchmark -> Benchmark -> IO Bool
+ Chronos.Bench: standardDeviation :: Analysis -> Double
+ Chronos.Bench: standardError :: Analysis -> Double
+ Chronos.Bench: step :: Benchmark -> IO Benchmark
+ Chronos.Bench: variance :: Analysis -> Rational

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for chronos +## 0.2.0.0 -- 2019-03-10++* use chronos instead of time+* performance improvements+* change module hierarchy+ ## 0.1.0.0 -- 2019-03-09  * First version.
README.md view
@@ -37,7 +37,7 @@ ```haskell module Main where -import Chronos+import Chronos.Bench  main :: IO () main = defaultMain@@ -56,7 +56,7 @@ ```haskell module Main where -import Chronos+import Chronos.Bench  import Control.Concurrent import Data.IORef@@ -73,7 +73,7 @@ ```haskell module Main where -import Chronos+import Chronos.Bench  main :: IO () main = defaultMain
bench/Main.hs view
@@ -1,6 +1,6 @@ module Main where -import Chronos+import Chronos.Bench  main :: IO () main = defaultMain $ map (\n -> bench ("fib " ++ show n) fib n) [1..10]
bin/Main.hs view
@@ -1,6 +1,6 @@ module Main where -import Chronos+import Chronos.Bench import Parser  import Control.Applicative
chronos-bench.cabal view
@@ -1,7 +1,7 @@ cabal-version:       2.2  name:                chronos-bench-version:             0.1.0.2+version:             0.2.0.1 synopsis:            Benchmarking tool with focus on comparing results. description:         This tool performs lazy benchmarking of functions and shell commands with continuous feedback and improving precision. license:             BSD-3-Clause@@ -21,10 +21,9 @@    Location: https://github.com/knupfer/chronos  library-  exposed-modules:     Chronos+  exposed-modules:     Chronos.Bench   other-modules:       Parser   build-depends:       base >= 4 && < 5-                     , time                      , ansi-terminal                      , process                      , deepseq@@ -32,6 +31,7 @@                      , terminal-size                      , bytestring                      , optparse-applicative+                     , chronos   hs-source-dirs:      src                      , common   default-language:    Haskell2010
− src/Chronos.hs
@@ -1,408 +0,0 @@-{-# LANGUAGE RecordWildCards #-}---- | Library to comparatively benchmark pure functions, impure--- functions and shell commands with lazy precision.--module Chronos-  (-    -- * Benchmark-    defaultMain-  , bench-  , benchIO-  , benchShell-    -- * Configuration-  , defaultMainWith-  , defaultConfig-  , Config(..)-    -- * Testing-  , isEqualTo-  , isFasterThan-    -- * Analysis-  , standardDeviation-  , standardError-  , variance-  , step-  , Benchmark(..)-  , Analysis(..)-  ) where--import Parser--import Control.Arrow-import Control.Applicative-import Control.Concurrent-import Control.DeepSeq-import Control.Exception-import Control.Monad-import Data.Function-import Data.IORef-import Data.List-import Data.String-import Data.Time.Clock.System-import Numeric-import Numeric.Natural-import Options.Applicative-import System.Console.ANSI-import System.Console.ANSI.Codes-import System.Console.Terminal.Size-import System.IO-import System.Mem-import System.Process--import qualified Data.ByteString.Builder as B-import qualified Data.Set as S--data BenchmarkMeta-  = BenchmarkMeta-  { information :: Double-  , maxDuration :: Rational-  , position :: Int-  , benchmark :: Benchmark-  }--instance Eq BenchmarkMeta where-  (==) = (==) `on` position &&& analysis . benchmark--instance Ord BenchmarkMeta where-  compare = compare `on` information &&& negate . position &&& analysis . benchmark---- | Options wich can be specified on the command line or with defaultMainWith.-data Config-  = Config-  { hideBar :: Bool -- ^ Hide the bar indicating relative performance.-  , sameLine :: Bool -- ^ Print the analysis on the same line as the command.-  , hideDetails :: Bool -- ^ Hide standard deviation and number of samples.-  , printOnce :: Bool -- ^ Print only once the analysis.  This is will print the analysis on timeout, maximal relative error or ctrl-c.-  , sortByMean :: Bool -- ^ Sort benchmarks by mean duration.-  , simple :: Bool -- ^ Don't colorize output and don't use unicode.-  , confidence :: Double -- ^ Factor by which the standard error will be multiplied for calculating confidence intervals (default is 6).-  , timeout :: Maybe Double -- ^ Timeout after which the program is terminated. It finishes the currently running benchmark.-  , relativeError :: Maybe Double -- ^ After every benchmark has got a relative error (calculated via confidence interval) below DOUBLE the program is terminated.-  } deriving (Show, Read, Eq, Ord)---- | Name, current analysis and function of a benchmark.-data Benchmark-  = Benchmark-  { name :: String-  , analysis :: Analysis-  , runner :: Analysis -> IO Analysis-  }---- | Collected data from benchmark runs.-data Analysis-  = Analysis-  { samples :: Natural-  , squaredWeights :: Natural-  , mean :: Rational-  , qFactor :: Rational-  } deriving (Eq, Ord, Show, Read)---- | Main function for running a list of benchmarks.  It also allows--- to specify options via commandline.------ > defaultMain [bench "not True" not True, bench "id True" id True]-defaultMain :: [Benchmark] -> IO ()-defaultMain bs = flip defaultMainWith bs =<< execParser opts-  where-    opts = info (configParser Config <**> helper) fullDesc---- | Construct a benchmark of a name, a pure function and an argument.------ > bench "reverse abc" reverse "abc"-bench :: NFData b => String -> (a -> b) -> a -> Benchmark-bench label f x = Benchmark label (Analysis 0 0 0 0) $ \ana -> newIORef x >>= \io -> measure (\n -> replicateM_ n $ (return$!) . force . f =<< readIORef io) ana---- | Construct a benchmark of a name and an impure function.------ > benchIO "ioref" (newIORef () >>= readIORef)-benchIO :: String -> IO a -> Benchmark-benchIO label io = Benchmark label (Analysis 0 0 0 0) (measure (`replicateM_` io))---- | Construct a benchmark of a name and a shell command.------ > benchShell "sleep is slow" "sleep 0"-benchShell :: String -> String -> Benchmark-benchShell label cmd = Benchmark label (Analysis 0 0 0 0) $ measure go-  where go n = uncurry (>>) $ ((`replicateM_` f 10000) *** f) (n `divMod` 10000)-        f x = withCreateProcess (shell (intercalate ";" $ replicate x cmd)) {std_out = CreatePipe, std_err = CreatePipe} $ \_ _ _ p ->-          waitForProcess p >> threadDelay 0 -- this is needed to let UserInterrupt be handled---- | Configurable main function for running a list of benchmarks.------ > defaultMainWith defaultConfig {hideBar = True} [bench "id ()" id ()]-defaultMainWith :: Config -> [Benchmark] -> IO ()-defaultMainWith _ [] = pure ()-defaultMainWith cfg bs | printOnce cfg = go (pure ())-                       | otherwise = bracket_ hideCursor showCursor-                         . go . B.hPutBuilder stdout . fromString $ replicate (printHeight cfg*length bs) '\n'-  where go mkSpace = hSetEcho stdin False *> mkSpace *> warmup *> runMain cfg (S.fromList . zipWith (BenchmarkMeta 0 0) [1..] $ reverse pad)-        pad | sameLine cfg = let len = maximum (map (length . name) bs) in map (\x -> x{name = take len $ name x ++ repeat ' '}) bs-            | otherwise = bs---- | Default configuration.  Use this combined with record updates to--- ensure compatibility with future releases.-defaultConfig :: Config-defaultConfig = Config-  { hideBar = False-  , sameLine = False-  , hideDetails = False-  , printOnce = False-  , sortByMean = False-  , simple = False-  , confidence = 6-  , timeout = Nothing-  , relativeError = Nothing-  }---- | Determine whether two benchmarks have got the same performance.--- It runs each benchmark until their confidence intervals don't--- overlap - in which case False is returned - or are no bigger than--- 1% of the mean - in which case True is returned.------ This function is meant to be used in test suites as infix function.------ > benchShell "echo" "echo" `isEqualTo` benchShell "sleep 0" "sleep 0"-isEqualTo :: Benchmark -> Benchmark -> IO Bool-isEqualTo b1 b2 = (EQ==) <$> compareBench defaultConfig 0.01 b1 b2---- | Determine whether a benchmark is faster than another. It runs--- each benchmark until their confidence intervals don't overlap or--- are no bigger than 1% of the mean. If the confidence intervals--- don't overlap and the mean of the first is lower True will be--- returned.  Otherwise False.------ This function is meant to be used in test suites as infix function.------ > benchShell "echo" "echo" `isFasterThan` benchShell "sleep 0" "sleep 0"-isFasterThan :: Benchmark -> Benchmark -> IO Bool-isFasterThan b1 b2 = (LT==) <$> compareBench defaultConfig 0.01 b1 b2---- | Calculate the standard deviation of an Analysis.-standardDeviation :: Analysis -> Double-standardDeviation a = sqrt (fromRational $ variance a) / biasCorrection-  where biasCorrection-          = 1-          - 1/(4*fromIntegral (samples a))-          - 7/(32*fromIntegral (samples a)**2)-          - 19/(128*fromIntegral (samples a)**3)---- | Calculate the standard error of an Analysis.-standardError :: Analysis -> Double-standardError a | samples a == 1 = fromRational (mean a)-           | otherwise = standardDeviation a * sqrt (fromIntegral $ squaredWeights a) / fromIntegral (samples a)---- | Calculate the variance of an Analysis.-variance :: Analysis -> Rational-variance a | samples a > 1 = qFactor a / fromIntegral (samples a - 1)-           | otherwise = 0---- | Run the benchmark once and update its analysis.  For functions--- with very low runtimes multiple runs will be executed.-step :: Benchmark -> IO Benchmark-step (Benchmark n a f) = flip (Benchmark n) f <$> f a---- * Internal functions.--printBenchmark :: Config -> BenchmarkMeta -> IO ()-printBenchmark cfg b = do-  w <- maybe 60 width <$> size-  B.hPutBuilder stdout . mv $ renderBenchmark cfg w (maxDuration b) (benchmark b)-  where mv x | sortByMean cfg || printOnce cfg = x-             | otherwise = linesUp (printHeight cfg*position b) <> x <> linesDown (printHeight cfg*(position b-1))--linesUp :: Int -> B.Builder-linesUp n | n > 0 = csi' [n] 'F'-          | n < 0 = csi' [abs n] 'E'-          | otherwise = mempty--linesDown :: Int -> B.Builder-linesDown = linesUp . negate--clear :: Config -> B.Builder-clear cfg | printOnce cfg = mempty-          | otherwise = csi' [0] 'K'--mUnless :: Monoid m => Bool -> m -> m-mUnless t = mWhen (not t)--mWhen :: Monoid m => Bool -> m -> m-mWhen t x = if t then x else mempty--renderBenchmark :: Config -> Int -> Rational -> Benchmark -> B.Builder-renderBenchmark cfg w maxDuration Benchmark{..}-  = mUnless (simple cfg) (sgrBuilder $ SetColor Foreground Vivid Cyan)-  <> fromString name-  <> mUnless (simple cfg) (sgrBuilder Reset)-  <> mUnless (sameLine cfg) (clear cfg <> B.char7 '\n' <> B.char7 ' ')-  <> B.char7 ' '-  <> renderAnalysis cfg analysis-  <> clear cfg-  <> B.char7 '\n'-  <> mUnless (hideBar cfg)-  ( mUnless (samples analysis <= 1)-    ( barBuilder cfg w (mean analysis / maxDuration) (min 1 $ confidence cfg * standardError analysis / fromRational (mean analysis)) (min 1 $ standardDeviation analysis / fromRational (mean analysis))-      <> clear cfg-    ) <> B.char7 '\n'-  )--printHeight :: Config -> Int-printHeight cfg = 3 - fromEnum (hideBar cfg) - fromEnum (sameLine cfg)--runMain :: Config -> S.Set BenchmarkMeta -> IO ()-runMain cfg = printAll <=< go . (,) 0-  where-    go (m, s) = handleJust (\e -> if e == UserInterrupt then Just s else Nothing) pure $-        let (BenchmarkMeta{..}, s') = S.deleteFindMin s in do-              ana <- analysis <$> step benchmark-              let newMax | m == mean (analysis benchmark) = mean ana-                         | otherwise = max m $ mean ana-                  new = BenchmarkMeta (informationOf ana) newMax position benchmark{analysis = ana}-                  set = S.insert new s'-              mask_ $ pp new set--              if terminates set-                 then pure set-                 else go (newMax, set)--    f | sortByMean cfg = sortOn (negate . mean . analysis . benchmark)-      | otherwise = sortOn (negate . position)--    printAll set = do-      when (sortByMean cfg && not (printOnce cfg)) . B.hPutBuilder stdout . linesUp $ printHeight cfg*length set-      mapM_ (printBenchmark cfg) . f $ S.toList set--    terminates set = let as = map (analysis . benchmark) $ S.toList set-      in maybe False (<= fromRational (sum $ map (uncurry (*) . (mean &&& fromIntegral . samples)) as)) (timeout cfg)-      || maybe False (>= maximum (map (uncurry (/) . ((confidence cfg*) . standardError &&& fromRational . mean)) as)) (relativeError cfg)--    pp n set-      | printOnce cfg = pure ()-      | sortByMean cfg = printAll set-      | otherwise = printBenchmark cfg n--measure :: (Int -> IO a) -> Analysis -> IO Analysis-measure cmd ana-  = performMinorGC-  >> refineAnalysis ana-  <$> getSystemTime-  <* cmd (fromIntegral $ weightOf ana)-  <*> getSystemTime--renderAnalysis :: Config -> Analysis -> B.Builder-renderAnalysis cfg a@Analysis{..}-  = mUnless (samples == 0) $ B.char7 't' <> B.char7 '='-  <> prettyScientific (simple cfg) (fromRational mean) (Just $ confidence cfg * standardError a)-  <> B.char7 's'-  <> mUnless (hideDetails cfg)-  ( B.char7 ' '-    <> mUnless (samples <= 1)-    ( (if simple cfg then fromString "SD" else B.charUtf8 'σ')-      <> B.char7 '='-      <> prettyScientific (simple cfg) (100 * standardDeviation a / fromRational mean) Nothing-      <> B.char7 '%' <> B.char7 ' '-    )-    <> B.char7 'n' <> B.char7 '='-    <> prettyNatural samples-  )--warmup :: IO ()-warmup = void . foldr1 (>=>) (replicate 10 step) . benchIO "warmup" $ pure ()--compareBench :: Config -> Double -> Benchmark -> Benchmark -> IO Ordering-compareBench cfg d x1 x2 = warmup *> fix go x1 x2-  where go h b1 b2 | oneOf ((<3) . samples) || oneOf ((<1) . informationOf) = next-                   | otherwise = case compareMeans cfg (analysis b1) (analysis b2) of-                       EQ | oneOf (relativeErrorAbove cfg (d/2)) -> next-                       r -> pure r-           where next | ((<=) `on` informationOf . analysis) b1 b2 = (`h` b2) =<< step b1-                      | otherwise = h b1 =<< step b2-                 oneOf f = f (analysis b1) || f (analysis b2)--relativeErrorAbove :: Config -> Double -> Analysis -> Bool-relativeErrorAbove cfg d a = d < confidence cfg * standardError a / fromRational (mean a)--compareMeans :: Config -> Analysis -> Analysis -> Ordering-compareMeans cfg a1 a2-  | f a1 a2 = LT-  | f a2 a1 = GT-  | otherwise = EQ-  where f x y = fromRational (mean x) + confidence cfg*standardError x < fromRational (mean y) - confidence cfg*standardError y--prettyNatural :: Natural -> B.Builder-prettyNatural = go . fromIntegral-  where-    go x = case divMod x 1000 of-             (a,b) | a == 0 -> B.wordDec b-                   | b > 99 -> go a <> B.char7 ',' <> B.wordDec b-                   | b >  9 -> go a <> B.char7 ',' <> B.char7 '0' <> B.wordDec b-                   | otherwise -> go a <> B.char7 ',' <> B.char7 '0' <> B.char7 '0' <> B.wordDec b--prettyScientific :: Bool -> Double -> Maybe Double -> B.Builder-prettyScientific ascii x b = case floatToDigits 10 . min x <$> b of-    Just (errSig,errExpo) | errSig /= [0] && valLen errExpo > 0 -> mantissa (take (valLen errExpo) $ sig ++ repeat 0) <> showError errSig <> f expo-    _ | x == 0 -> B.char7 '0'-    _ -> mantissa (take 2 $ sig ++ repeat 0) <> f expo-  where-    showError err = B.char7 '(' <> foldMap B.intDec (take 2 $ err ++ repeat 0) <> B.char7 ')'-    (sig,expo) = floatToDigits 10 x-    valLen e = expo - e + 2-    mantissa [d] = B.intDec d-    mantissa (d:ds) = B.intDec d <> B.char7 '.' <> foldMap B.intDec ds-    mantissa [] = mempty-    f 1 = mempty-    f e | ascii = B.char7 '*' <> B.intDec 10 <> mWhen (e/=2) (B.char7 '^' <> B.intDec (e-1))-        | otherwise = B.charUtf8 '·' <> B.intDec 10 <> mWhen (e/=2) (showE (e-1))--showE :: Integral a => a -> B.Builder-showE = fix go-  where go f n | n < 0 = B.charUtf8 '⁻' <> f (abs n)-               | n < 10 = B.charUtf8 $ "⁰¹²³⁴⁵⁶⁷⁸⁹" !! fromIntegral n-               | otherwise = uncurry ((<>) `on` f) $ divMod n 10--informationOf :: Analysis -> Double-informationOf Analysis{..} = fromRational mean ** 0.7 * fromIntegral samples--weightOf :: Analysis -> Natural-weightOf Analysis{..} = fromIntegral . max 1 . min samples . round . recip $ (fromRational mean :: Double) ** 0.7--refineAnalysis :: Analysis -> SystemTime -> SystemTime -> Analysis-refineAnalysis ana@Analysis{..} begin end = Analysis newSamples newSquaredWeights newMean newQFactor-  where-    newSamples = samples + weightOf ana-    newSquaredWeights = squaredWeights + weightOf ana*weightOf ana-    newMean = mean + diffWeight / fromIntegral newSamples-    newQFactor = qFactor + diffWeight * (time - newMean)-    diffWeight = fromIntegral (weightOf ana) * (time - mean)-    time = (toSeconds end - toSeconds begin) / fromIntegral (weightOf ana)-    toSeconds t = fromIntegral (systemSeconds t) + fromIntegral (systemNanoseconds t) / 1e9--sgrBuilder :: SGR -> B.Builder-sgrBuilder = (`csi'` 'm') . sgrToCode--csi' :: [Int] -> Char -> B.Builder-csi' (x:xs) b = B.char7 '\ESC' <> B.char7 '[' <> B.intDec x <> foldMap (\n -> B.char7 ';' <> B.intDec n) xs <> B.char7 b-csi' [] b = B.char7 '\ESC' <> B.char7 '[' <> B.char7 b--barBuilder :: Config -> Int -> Rational -> Double -> Double -> B.Builder-barBuilder cfg width m stdErr sd | simple cfg =-  B.char7 ' ' <> B.char7 ' ' <> B.string7 (replicate (pred valueLength) '=')-  <> B.string7 (replicate errorLength '<')-  <> mWhen (len * stdErr >= 0.20) (B.char7 '+')-  <> B.string7 (replicate errorLength '>')-  <> B.string7 (replicate sigmaLength '-')-                                 | otherwise =-  B.char7 ' ' <> B.char7 ' ' <> B.stringUtf8 (replicate (pred valueLength) '▀')-  <> sgrBuilder (SetColor Foreground Dull Magenta)-  <> B.stringUtf8 (replicate errorLength '▀')-  <> mWhen (len * stdErr >= 0.20)-           (sgrBuilder (SetColor Foreground Vivid Magenta) <> B.charUtf8 '▀')-  <> sgrBuilder (SetColor Foreground Dull Magenta)-  <> B.stringUtf8 (replicate errorLength '▀')-  <> sgrBuilder (SetColor Foreground Vivid Black)-  <> B.stringUtf8 (replicate sigmaLength '▔')-  <> sgrBuilder Reset-  where-    len = fromRational m * fromIntegral (width - 6) / 2-    valueLength = round len - errorLength-    errorLength = round $ len * stdErr-    sigmaLength = round (len * sd) - errorLength
+ src/Chronos/Bench.hs view
@@ -0,0 +1,409 @@+{-# LANGUAGE RecordWildCards #-}++-- | Library to comparatively benchmark pure functions, impure+-- functions and shell commands with lazy precision.++module Chronos.Bench+  (+    -- * Benchmark+    defaultMain+  , bench+  , benchIO+  , benchShell+    -- * Configuration+  , defaultMainWith+  , defaultConfig+  , Config(..)+    -- * Testing+  , isEqualTo+  , isFasterThan+    -- * Analysis+  , standardDeviation+  , standardError+  , variance+  , step+  , Benchmark(..)+  , Analysis(..)+  ) where++import Parser++import Chronos (now, Time(..))++import Control.Arrow+import Control.Applicative+import Control.Concurrent+import Control.DeepSeq+import Control.Exception+import Control.Monad+import Data.Function+import Data.IORef+import Data.List+import Data.String+import Numeric+import Numeric.Natural+import Options.Applicative+import System.Console.ANSI+import System.Console.ANSI.Codes+import System.Console.Terminal.Size+import System.IO+import System.Mem+import System.Process++import qualified Data.ByteString.Builder as B+import qualified Data.Set as S++data BenchmarkMeta+  = BenchmarkMeta+  { information :: Double+  , maxDuration :: Rational+  , position :: Int+  , benchmark :: Benchmark+  }++instance Eq BenchmarkMeta where+  (==) = (==) `on` position &&& analysis . benchmark++instance Ord BenchmarkMeta where+  compare = compare `on` information &&& negate . position &&& analysis . benchmark++-- | Options wich can be specified on the command line or with defaultMainWith.+data Config+  = Config+  { hideBar :: Bool -- ^ Hide the bar indicating relative performance.+  , sameLine :: Bool -- ^ Print the analysis on the same line as the command.+  , hideDetails :: Bool -- ^ Hide standard deviation and number of samples.+  , printOnce :: Bool -- ^ Print only once the analysis.  This is will print the analysis on timeout, maximal relative error or ctrl-c.+  , sortByMean :: Bool -- ^ Sort benchmarks by mean duration.+  , simple :: Bool -- ^ Don't colorize output and don't use unicode.+  , confidence :: Double -- ^ Factor by which the standard error will be multiplied for calculating confidence intervals (default is 6).+  , timeout :: Maybe Double -- ^ Timeout after which the program is terminated. It finishes the currently running benchmark.+  , relativeError :: Maybe Double -- ^ After every benchmark has got a relative error (calculated via confidence interval) below DOUBLE the program is terminated.+  } deriving (Show, Read, Eq, Ord)++-- | Name, current analysis and function of a benchmark.+data Benchmark+  = Benchmark+  { name :: String+  , analysis :: Analysis+  , runner :: Analysis -> IO Analysis+  }++-- | Collected data from benchmark runs.+data Analysis+  = Analysis+  { samples :: Natural+  , squaredWeights :: Natural+  , mean :: Rational+  , qFactor :: Rational+  } deriving (Eq, Ord, Show, Read)++-- | Main function for running a list of benchmarks.  It also allows+-- to specify options via commandline.+--+-- > defaultMain [bench "not True" not True, bench "id True" id True]+defaultMain :: [Benchmark] -> IO ()+defaultMain bs = flip defaultMainWith bs =<< execParser opts+  where+    opts = info (configParser Config <**> helper) fullDesc++-- | Construct a benchmark of a name, a pure function and an argument.+--+-- > bench "reverse abc" reverse "abc"+bench :: NFData b => String -> (a -> b) -> a -> Benchmark+bench label f x = Benchmark label (Analysis 0 0 0 0) $ \ana -> newIORef x >>= \io -> measure (\n -> replicateM_ n $ (return$!) . force . f =<< readIORef io) ana++-- | Construct a benchmark of a name and an impure function.+--+-- > benchIO "ioref" (newIORef () >>= readIORef)+benchIO :: String -> IO a -> Benchmark+benchIO label io = Benchmark label (Analysis 0 0 0 0) (measure (`replicateM_` io))++-- | Construct a benchmark of a name and a shell command.+--+-- > benchShell "sleep is slow" "sleep 0"+benchShell :: String -> String -> Benchmark+benchShell label cmd = Benchmark label (Analysis 0 0 0 0) $ measure go+  where go n = uncurry (>>) $ ((`replicateM_` f 10000) *** f) (n `divMod` 10000)+        f x = withCreateProcess (shell (intercalate ";" $ replicate x cmd)) {std_out = CreatePipe, std_err = CreatePipe} $ \_ _ _ p ->+          waitForProcess p >> threadDelay 0 -- this is needed to let UserInterrupt be handled++-- | Configurable main function for running a list of benchmarks.+--+-- > defaultMainWith defaultConfig {hideBar = True} [bench "id ()" id ()]+defaultMainWith :: Config -> [Benchmark] -> IO ()+defaultMainWith _ [] = pure ()+defaultMainWith cfg bs | printOnce cfg = go (pure ())+                       | otherwise = bracket_ hideCursor showCursor+                         . go . B.hPutBuilder stdout . fromString $ replicate (printHeight cfg*length bs) '\n'+  where go mkSpace = hSetEcho stdin False *> mkSpace *> warmup *> runMain cfg (S.fromList . zipWith (BenchmarkMeta 0 0) [1..] $ reverse pad)+        pad | sameLine cfg = let len = maximum (map (length . name) bs) in map (\x -> x{name = take len $ name x ++ repeat ' '}) bs+            | otherwise = bs++-- | Default configuration.  Use this combined with record updates to+-- ensure compatibility with future releases.+defaultConfig :: Config+defaultConfig = Config+  { hideBar = False+  , sameLine = False+  , hideDetails = False+  , printOnce = False+  , sortByMean = False+  , simple = False+  , confidence = 6+  , timeout = Nothing+  , relativeError = Nothing+  }++-- | Determine whether two benchmarks have got the same performance.+-- It runs each benchmark until their confidence intervals don't+-- overlap - in which case False is returned - or are no bigger than+-- 1% of the mean - in which case True is returned.+--+-- This function is meant to be used in test suites as infix function.+--+-- > benchShell "echo" "echo" `isEqualTo` benchShell "sleep 0" "sleep 0"+isEqualTo :: Benchmark -> Benchmark -> IO Bool+isEqualTo b1 b2 = (EQ==) <$> compareBench defaultConfig 0.01 b1 b2++-- | Determine whether a benchmark is faster than another. It runs+-- each benchmark until their confidence intervals don't overlap or+-- are no bigger than 1% of the mean. If the confidence intervals+-- don't overlap and the mean of the first is lower True will be+-- returned.  Otherwise False.+--+-- This function is meant to be used in test suites as infix function.+--+-- > benchShell "echo" "echo" `isFasterThan` benchShell "sleep 0" "sleep 0"+isFasterThan :: Benchmark -> Benchmark -> IO Bool+isFasterThan b1 b2 = (LT==) <$> compareBench defaultConfig 0.01 b1 b2++-- | Calculate the standard deviation of an Analysis.+standardDeviation :: Analysis -> Double+standardDeviation a = sqrt (fromRational $ variance a) / biasCorrection+  where biasCorrection+          = 1+          - 1/(4*fromIntegral (samples a))+          - 7/(32*fromIntegral (samples a)**2)+          - 19/(128*fromIntegral (samples a)**3)++-- | Calculate the standard error of an Analysis.+standardError :: Analysis -> Double+standardError a | samples a == 1 = fromRational (mean a)+           | otherwise = standardDeviation a * sqrt (fromIntegral $ squaredWeights a) / fromIntegral (samples a)++-- | Calculate the variance of an Analysis.+variance :: Analysis -> Rational+variance a | samples a > 1 = qFactor a / fromIntegral (samples a - 1)+           | otherwise = 0++-- | Run the benchmark once and update its analysis.  For functions+-- with very low runtimes multiple runs will be executed.+step :: Benchmark -> IO Benchmark+step (Benchmark n a f) = flip (Benchmark n) f <$> f a++-- * Internal functions.++printBenchmark :: Config -> BenchmarkMeta -> IO ()+printBenchmark cfg b = do+  w <- maybe 60 width <$> size+  B.hPutBuilder stdout . mv $ renderBenchmark cfg w (maxDuration b) (benchmark b)+  where mv x | sortByMean cfg || printOnce cfg = x+             | otherwise = linesUp (printHeight cfg*position b) <> x <> linesDown (printHeight cfg*(position b-1))++linesUp :: Int -> B.Builder+linesUp n | n > 0 = csi' [n] 'F'+          | n < 0 = csi' [abs n] 'E'+          | otherwise = mempty++linesDown :: Int -> B.Builder+linesDown = linesUp . negate++clear :: Config -> B.Builder+clear cfg | printOnce cfg = mempty+          | otherwise = csi' [0] 'K'++mUnless :: Monoid m => Bool -> m -> m+mUnless t = mWhen (not t)++mWhen :: Monoid m => Bool -> m -> m+mWhen t x = if t then x else mempty++renderBenchmark :: Config -> Int -> Rational -> Benchmark -> B.Builder+renderBenchmark cfg w maxDuration Benchmark{..}+  = mUnless (simple cfg) (sgrBuilder $ SetColor Foreground Vivid Cyan)+  <> fromString name+  <> mUnless (simple cfg) (sgrBuilder Reset)+  <> mUnless (sameLine cfg) (clear cfg <> B.char7 '\n' <> B.char7 ' ')+  <> B.char7 ' '+  <> renderAnalysis cfg analysis+  <> clear cfg+  <> B.char7 '\n'+  <> mUnless (hideBar cfg)+  ( mUnless (samples analysis <= 1)+    ( barBuilder cfg w (mean analysis / maxDuration) (min 1 $ confidence cfg * standardError analysis / fromRational (mean analysis)) (min 1 $ standardDeviation analysis / fromRational (mean analysis))+      <> clear cfg+    ) <> B.char7 '\n'+  )++printHeight :: Config -> Int+printHeight cfg = 3 - fromEnum (hideBar cfg) - fromEnum (sameLine cfg)++runMain :: Config -> S.Set BenchmarkMeta -> IO ()+runMain cfg = printAll <=< go . (,) 0+  where+    go (m, s) = handleJust (\e -> if e == UserInterrupt then Just s else Nothing) pure $+        let (BenchmarkMeta{..}, s') = S.deleteFindMin s in do+              ana <- analysis <$> step benchmark+              let newMax | m == mean (analysis benchmark) = mean ana+                         | otherwise = max m $ mean ana+                  new = BenchmarkMeta (informationOf ana) newMax position benchmark{analysis = ana}+                  set = S.insert new s'+              mask_ $ pp new set++              if terminates set+                 then pure set+                 else go (newMax, set)++    f | sortByMean cfg = sortOn (negate . mean . analysis . benchmark)+      | otherwise = sortOn (negate . position)++    printAll set = do+      when (sortByMean cfg && not (printOnce cfg)) . B.hPutBuilder stdout . linesUp $ printHeight cfg*length set+      mapM_ (printBenchmark cfg) . f $ S.toList set++    terminates set = let as = map (analysis . benchmark) $ S.toList set+      in maybe False (<= fromRational (sum $ map (uncurry (*) . (mean &&& fromIntegral . samples)) as)) (timeout cfg)+      || maybe False (>= maximum (map (uncurry (/) . ((confidence cfg*) . standardError &&& fromRational . mean)) as)) (relativeError cfg)++    pp n set+      | printOnce cfg = pure ()+      | sortByMean cfg = printAll set+      | otherwise = printBenchmark cfg n++measure :: (Int -> IO a) -> Analysis -> IO Analysis+measure cmd ana+  = performMinorGC+  >> refineAnalysis ana+  <$> now+  <* cmd (fromIntegral $ weightOf ana)+  <*> now++renderAnalysis :: Config -> Analysis -> B.Builder+renderAnalysis cfg a@Analysis{..}+  = mUnless (samples == 0) $ B.char7 't' <> B.char7 '='+  <> prettyScientific (simple cfg) (fromRational mean) (Just $ confidence cfg * standardError a)+  <> B.char7 's'+  <> mUnless (hideDetails cfg)+  ( B.char7 ' '+    <> mUnless (samples <= 1)+    ( (if simple cfg then fromString "SD" else B.charUtf8 'σ')+      <> B.char7 '='+      <> prettyScientific (simple cfg) (100 * standardDeviation a / fromRational mean) Nothing+      <> B.char7 '%' <> B.char7 ' '+    )+    <> B.char7 'n' <> B.char7 '='+    <> prettyNatural samples+  )++warmup :: IO ()+warmup = void . foldr1 (>=>) (replicate 10 step) . benchIO "warmup" $ pure ()++compareBench :: Config -> Double -> Benchmark -> Benchmark -> IO Ordering+compareBench cfg d x1 x2 = warmup *> fix go x1 x2+  where go h b1 b2 | oneOf ((<3) . samples) || oneOf ((<1) . informationOf) = next+                   | otherwise = case compareMeans cfg (analysis b1) (analysis b2) of+                       EQ | oneOf (relativeErrorAbove cfg (d/2)) -> next+                       r -> pure r+           where next | ((<=) `on` informationOf . analysis) b1 b2 = (`h` b2) =<< step b1+                      | otherwise = h b1 =<< step b2+                 oneOf f = f (analysis b1) || f (analysis b2)++relativeErrorAbove :: Config -> Double -> Analysis -> Bool+relativeErrorAbove cfg d a = d < confidence cfg * standardError a / fromRational (mean a)++compareMeans :: Config -> Analysis -> Analysis -> Ordering+compareMeans cfg a1 a2+  | f a1 a2 = LT+  | f a2 a1 = GT+  | otherwise = EQ+  where f x y = fromRational (mean x) + confidence cfg*standardError x < fromRational (mean y) - confidence cfg*standardError y++prettyNatural :: Natural -> B.Builder+prettyNatural = go . fromIntegral+  where+    go x = case divMod x 1000 of+             (a,b) | a == 0 -> B.wordDec b+                   | b > 99 -> go a <> B.char7 ',' <> B.wordDec b+                   | b >  9 -> go a <> B.char7 ',' <> B.char7 '0' <> B.wordDec b+                   | otherwise -> go a <> B.char7 ',' <> B.char7 '0' <> B.char7 '0' <> B.wordDec b++prettyScientific :: Bool -> Double -> Maybe Double -> B.Builder+prettyScientific ascii x b = case floatToDigits 10 . min x <$> b of+    Just (errSig,errExpo) | errSig /= [0] && valLen errExpo > 0 -> mantissa (take (valLen errExpo) $ sig ++ repeat 0) <> showError errSig <> f expo+    _ | x == 0 -> B.char7 '0'+    _ -> mantissa (take 2 $ sig ++ repeat 0) <> f expo+  where+    showError err = B.char7 '(' <> foldMap B.intDec (take 2 $ err ++ repeat 0) <> B.char7 ')'+    (sig,expo) = floatToDigits 10 x+    valLen e = expo - e + 2+    mantissa [d] = B.intDec d+    mantissa (d:ds) = B.intDec d <> B.char7 '.' <> foldMap B.intDec ds+    mantissa [] = mempty+    f 1 = mempty+    f e | ascii = B.char7 '*' <> B.intDec 10 <> mWhen (e/=2) (B.char7 '^' <> B.intDec (e-1))+        | otherwise = B.charUtf8 '·' <> B.intDec 10 <> mWhen (e/=2) (showE (e-1))++showE :: Integral a => a -> B.Builder+showE = fix go+  where go f n | n < 0 = B.charUtf8 '⁻' <> f (abs n)+               | n < 10 = B.charUtf8 $ "⁰¹²³⁴⁵⁶⁷⁸⁹" !! fromIntegral n+               | otherwise = uncurry ((<>) `on` f) $ divMod n 10++informationOf :: Analysis -> Double+informationOf Analysis{..} = fromRational mean ** 0.7 * fromIntegral samples++weightOf :: Analysis -> Natural+weightOf Analysis{..} = fromIntegral . max 1 . min samples . round . recip $ (fromRational mean :: Double) ** 0.7++refineAnalysis :: Analysis -> Time -> Time -> Analysis+refineAnalysis ana@Analysis{..} (Time begin) (Time end) = Analysis newSamples newSquaredWeights newMean newQFactor+  where+    newSamples = samples + weight+    newSquaredWeights = squaredWeights + weight*weight+    newMean = mean + diffWeight / fromIntegral newSamples+    newQFactor = qFactor + diffWeight * (time - newMean)+    diffWeight = fromIntegral weight * (time - mean)+    time = (fromIntegral (end - begin) / 1e9) / fromIntegral weight+    weight = weightOf ana++sgrBuilder :: SGR -> B.Builder+sgrBuilder = (`csi'` 'm') . sgrToCode++csi' :: [Int] -> Char -> B.Builder+csi' (x:xs) b = B.char7 '\ESC' <> B.char7 '[' <> B.intDec x <> foldMap (\n -> B.char7 ';' <> B.intDec n) xs <> B.char7 b+csi' [] b = B.char7 '\ESC' <> B.char7 '[' <> B.char7 b++barBuilder :: Config -> Int -> Rational -> Double -> Double -> B.Builder+barBuilder cfg width m stdErr sd | simple cfg =+  B.char7 ' ' <> B.char7 ' ' <> B.string7 (replicate (pred valueLength) '=')+  <> B.string7 (replicate errorLength '<')+  <> mWhen (len * stdErr >= 0.20) (B.char7 '+')+  <> B.string7 (replicate errorLength '>')+  <> B.string7 (replicate sigmaLength '-')+                                 | otherwise =+  B.char7 ' ' <> B.char7 ' ' <> B.stringUtf8 (replicate (pred valueLength) '▀')+  <> sgrBuilder (SetColor Foreground Dull Magenta)+  <> B.stringUtf8 (replicate errorLength '▀')+  <> mWhen (len * stdErr >= 0.20)+           (sgrBuilder (SetColor Foreground Vivid Magenta) <> B.charUtf8 '▀')+  <> sgrBuilder (SetColor Foreground Dull Magenta)+  <> B.stringUtf8 (replicate errorLength '▀')+  <> sgrBuilder (SetColor Foreground Vivid Black)+  <> B.stringUtf8 (replicate sigmaLength '▔')+  <> sgrBuilder Reset+  where+    len = fromRational m * fromIntegral (width - 6) / 2+    valueLength = round len - errorLength+    errorLength = round $ len * stdErr+    sigmaLength = round (len * sd) - errorLength