packages feed

hyperion (empty) → 0.1.0.0

raw patch · 19 files changed

+1284/−0 lines, 19 filesdep +QuickCheckdep +aesondep +ansi-wl-pprintsetup-changed

Dependencies added: QuickCheck, aeson, ansi-wl-pprint, base, bytestring, clock, containers, deepseq, directory, exceptions, filepath, generic-deriving, hashable, hspec, hyperion, lens, mtl, optparse-applicative, process, random, random-shuffle, statistics, text, time, unordered-containers, vector

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright EURL Tweag (c) 2016-2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,72 @@+# Hyperion: Haskell-based systems benchmarking++[![Build Status](https://travis-ci.org/tweag/hyperion.svg?branch=master)](https://travis-ci.org/tweag/hyperion)++Hyperion is a DSL for writing benchmarks to measure and analyze+software performance. It is a lab for future [Criterion][criterion]+features.++## Getting started++### Build++You can build the [`micro benchmark example`](examples/micro-benchmarks.hs)+using stack:++``` shell+$ stack build+$ stack exec hyperion-micro-benchmark-example+```++### Example usage++The Hyperion DSL is a backwards compatible extension+to [Criterion][criterion]'s DSL (except for the rarely used `env`+combinator, which has a safer type). Here is an example:++``` haskell+benchmarks :: [Benchmark]+benchmarks =+    [ bench "id" (nf id ())+    , series [0,5..20] $ \n ->+        bgroup "pure-functions"+          [ bench "fact" (nf fact n)+          , bench "fib" (nf fib n)+          ]+    , series [1..4] $ \n ->+        series [1..n] $ \k ->+          bench "n choose k" $ nf (uncurry choose) (n, k)+    ]++main :: IO ()+main = defaultMain "hyperion-example-micro-benchmarks" benchmarks+```++By default Hyperion runs your benchmarks and pretty prints the+results. There are several command-line options that you can pass to+the executable, like printing the results to a JSON file or including+individual raw measurements. To see the full set of options run the+executable with `--help`:++``` shell+$ stack exec hyperion-micro-benchmark-example -- --help+Usage: hyperion-micro-benchmark-example ([--pretty] | [-j|--json PATH] |+                                        [-f|--flat PATH]) ([-l|--list] | [--run]+                                        | [--no-analyze]) [--raw]+                                        [--arg KEY:VAL] [NAME...]++Available options:+  -h,--help                Show this help text+  --pretty                 Pretty prints the measurements on stdout.+  -j,--json PATH           Where to write the json benchmarks output. Can be a+                           file name, a directory name or '-' for stdout.+  -f,--flat PATH           Where to write the json benchmarks output. Can be a+                           file name, a directory name or '-' for stdout.+  --version                Display version information+  -l,--list                List benchmark names+  --run                    Run benchmarks and analyze them (default)+  --no-analyze             Only run the benchmarks+  --raw                    Include raw measurement data in report.+  --arg KEY:VAL            Extra metadata to include in the report, in the+                           format key:value.+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/end-to-end-benchmarks.hs view
@@ -0,0 +1,25 @@+#!/usr/bin/env stack+-- stack --no-nix-pure runghc --package hyperion++{-# LANGUAGE OverloadedLists #-}++module Main where++import Hyperion.Benchmark+import Hyperion.Run+import Hyperion.Main+import System.Process (system)++benchmarks :: [Benchmark]+benchmarks =+    [ bgroup "roundrip"+        [ bench "ping" (nfIO (system "ping -c1 8.8.8.8 > /dev/null")) ]+    ]++main :: IO ()+main = defaultMainWith config "hyperion-example-end-to-end" benchmarks+  where+    config = defaultConfig+      { configMonoidSamplingStrategy = return $ timeBound (5 * secs) (repeat 10)+      }+    secs = 10^(9::Int) * nanos where nanos = 1
+ examples/micro-benchmarks.hs view
@@ -0,0 +1,33 @@+#!/usr/bin/env stack+-- stack --no-nix-pure runghc --package hyperion++{-# LANGUAGE OverloadedLists #-}++module Main where++import Hyperion.Benchmark+import Hyperion.Main++fact, fib :: Int -> Int+fact n = if n == 0 then 1 else n * fact (n - 1)+fib n = case n of 0 -> 0; 1 -> 1; _ -> fib (n - 1) + fib (n - 2)++-- | Binomial coefficient+choose :: Int -> Int -> Int+choose n k = fact n `div` (fact k * fact (n - k))++benchmarks :: [Benchmark]+benchmarks =+    [ bench "id" (nf id ())+    , series [0,5..20] $ \n ->+        bgroup "pure-functions"+          [ bench "fact" (nf fact n)+          , bench "fib" (nf fib n)+          ]+    , series [1..4] $ \n ->+        series [1..n] $ \k ->+          bench "n choose k" $ nf (uncurry choose) (n, k)+    ]++main :: IO ()+main = defaultMain "hyperion-example-micro-benchmarks" benchmarks
+ hyperion.cabal view
@@ -0,0 +1,96 @@+name:                hyperion+version:             0.1.0.0+synopsis:            Reliable performance measurement with robust data export.+description:         Please see README.md+homepage:            https://github.com/tweag/hyperion#readme+author:              Tweag I/O+maintainer:          nicolas.mattia@tweag.io+license:             BSD3+license-file:        LICENSE+category:            Benchmarking+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++source-repository head+  type:     git+  location: https://github.com/tweag/hyperion++library+  hs-source-dirs: src+  exposed-modules:+    Hyperion+    Hyperion.Analysis+    Hyperion.Benchmark+    Hyperion.Internal+    Hyperion.Main+    Hyperion.Measurement+    Hyperion.PrintReport+    Hyperion.Report+    Hyperion.Run+  other-modules:+    Paths_hyperion+  build-depends:+    aeson >= 0.11,+    ansi-wl-pprint,+    base >= 4.9 && < 5,+    bytestring >= 0.10,+    containers >= 0.5,+    clock >= 0.7.2,+    deepseq >= 1.4,+    directory,+    exceptions >= 0.8,+    filepath,+    generic-deriving >= 1.11,+    hashable,+    lens >= 4.0,+    mtl >= 2.2,+    optparse-applicative >= 0.12,+    random >= 1.1,+    random-shuffle >= 0.0.4,+    statistics >= 0.13,+    text >= 1.2,+    time >= 1.0,+    unordered-containers >= 0.2,+    vector >= 0.11+  default-language: Haskell2010+  ghc-options:         -Wall++executable hyperion-micro-benchmark-example+  hs-source-dirs:      examples+  main-is:             micro-benchmarks.hs+  build-depends:+    base,+    hyperion,+    process+  default-language:    Haskell2010+  ghc-options:         -Wall++executable hyperion-end-to-end-benchmark-example+  hs-source-dirs:      examples+  main-is:             end-to-end-benchmarks.hs+  build-depends:+    base,+    hyperion,+    process+  default-language:    Haskell2010+  ghc-options:         -Wall++test-suite spec+  type:+    exitcode-stdio-1.0+  hs-source-dirs: tests+  main-is: Main.hs+  other-modules:+    Hyperion.BenchmarkSpec+    Hyperion.MainSpec+    Spec+  build-depends:+    base,+    hspec >= 2.2,+    hyperion,+    lens,+    QuickCheck >= 2.8,+    text,+    unordered-containers >= 0.2+  default-language: Haskell2010
+ src/Hyperion.hs view
@@ -0,0 +1,5 @@+module Hyperion+  ( module Hyperion.Benchmark+  ) where++import Hyperion.Benchmark
+ src/Hyperion/Analysis.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}++module Hyperion.Analysis+  ( Parameter(..)+  , identifiers+  , analyze+  ) where++import Control.Lens+  ( Contravariant(..)+  , Fold+  , ala+  , foldMapOf+  , folded+  , to+  )++import Control.Lens.Each+import Data.Monoid+import Data.Traversable (for)+import Hyperion.Benchmark+import Hyperion.Internal+import Hyperion.Measurement+import Hyperion.Report++identifiers :: Fold Benchmark BenchmarkId+identifiers = go []+  where+    go :: [Component] -> Fold Benchmark BenchmarkId+    go comps f (Bench name _) = coerce $ f $ BenchmarkId $ comps <> [BenchC name]+    go comps f (Group name bks) = coerce $ (folded.go (comps <> [GroupC name])) f bks+    go comps f (Bracket _ _ g) = go comps f (g Empty)+    go comps f (Series xs g) =+      coerce $ for xs $ \x ->+        go (comps <> [SeriesC (Parameter x)]) f (g x)++    coerce :: (Contravariant f, Applicative f) => f a -> f b+    coerce = contramap (const ()) . fmap (const ())++analyze+  :: BenchmarkId -- ^ Benchmark identifier.+  -> Sample -- ^ Measurements.+  -> Report+analyze ident samp = Report+    { _reportBenchName = renderBenchmarkId ident+    , _reportBenchParams =+        map (\(Parameter x) -> fromEnum x) $ benchmarkParameters ident+    , _reportTimeInNanos =+        totalDuration / trueNumIterations+    , _reportCycles = Nothing+    , _reportAlloc = Nothing+    , _reportGarbageCollections = Nothing+    , _reportMeasurements = Just samp+    }+  where+    totalDuration =+      ala+        Sum+        (foldMapOf (measurements.each.duration.to realToFrac))+        samp+    trueNumIterations =+      ala+        Sum+        (foldMapOf (measurements.each.batchSize.to realToFrac))+        samp
+ src/Hyperion/Benchmark.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE GADTSyntax #-}+{-# LANGUAGE ExistentialQuantification #-}++module Hyperion.Benchmark+  ( -- * Benchmarks+    Benchmark(..)+  , bench+  , bgroup+  , env+  , series+  -- * Batches+  , Batch+  , runBatch+  , nf+  , nfIO+  , whnf+  , whnfIO+  -- * Environments+  , Env+  , use+  ) where++import Control.Exception (evaluate)+import Control.Monad.State.Strict (modify')+import Data.Monoid+import Data.Vector (Vector)+import Data.Int (Int64)+import Data.Text (Text)+import qualified Data.Text as Text+import Control.DeepSeq+import Hyperion.Internal++data Benchmark where+  Bench :: Text -> Batch () -> Benchmark+  Group :: Text -> [Benchmark] -> Benchmark+  Bracket :: NFData r => IO r -> (r -> IO ()) -> (Env r -> Benchmark) -> Benchmark+  Series :: (Show a, Enum a) => Vector a -> (a -> Benchmark) -> Benchmark++sp :: ShowS+sp = showChar ' '++instance Show Benchmark where+  showsPrec _ (Bench name _) =+      showString "Bench" . sp . shows name . sp . showString "_"+  showsPrec x (Group name bks) =+      showString "Group" . sp . shows name . sp . showsPrec x bks+  showsPrec x (Bracket _ _ f) =+      showString "Bracket" . sp . showString "(\\_ -> " . showsPrec x (f Empty) . showString ")"+  showsPrec x (Series xs f) =+      showString "Series" . sp . shows xs . sp . showsPrec x (f <$> xs)++bench :: String -> Batch () -> Benchmark+bench name batch = Bench (Text.pack name) batch++bgroup :: String -> [Benchmark] -> Benchmark+bgroup name bks = Group (Text.pack name) bks++series :: (Show a, Enum a) => Vector a -> (a -> Benchmark) -> Benchmark+series = Series++env+  :: NFData r+  => IO r -- ^ Acquire resource+  -> (r -> IO ()) -- ^ Finalize resource+  -> (Env r -> Benchmark)+  -> Benchmark+env = Bracket++-- | Apply an argument to a function, and evaluate the result to weak+-- head normal form (WHNF).+whnf :: (a -> b) -> a -> Batch ()+{-# INLINE whnf #-}+whnf f x = Batch $ modify' (<> pureFunc id f x)++-- | Apply an argument to a function, and evaluate the result to head+-- normal form (NF).+nf :: NFData b => (a -> b) -> a -> Batch ()+{-# INLINE nf #-}+nf f x = Batch $ modify' (<> pureFunc rnf f x)++pureFunc :: (b -> c) -> (a -> b) -> a -> Int64 -> IO ()+{-# INLINE pureFunc #-}+pureFunc reduce f0 x0 = go f0 x0+  where go f x n+          | n <= 0    = return ()+          | otherwise = evaluate (reduce (f x)) >> go f x (n-1)++-- | Perform an action, then evaluate its result to head normal form.+-- This is particularly useful for forcing a lazy 'IO' action to be+-- completely performed.+nfIO :: NFData a => IO a -> Batch ()+{-# INLINE nfIO #-}+nfIO m = Batch $ modify' (<> impure rnf m)++-- | 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 -> Batch ()+{-# INLINE whnfIO #-}+whnfIO m = Batch $ modify' (<> impure id m)++impure :: (a -> b) -> IO a -> Int64 -> IO ()+{-# INLINE impure #-}+impure strategy a = go+  where go n+          | n <= 0    = return ()+          | otherwise = a >>= (evaluate . strategy) >> go (n-1)
+ src/Hyperion/Internal.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Hyperion.Internal where++import Control.Monad.State.Strict (State, execState)+import Data.Function (on)+import Data.Hashable (Hashable(..))+import Data.Int+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as Text++newtype Batch a = Batch { unBatch :: State (Int64 -> IO ()) a }+  deriving (Functor, Applicative, Monad)++-- | Run a batch of the given size.+runBatch :: Batch () -> Int64 -> IO ()+{-# INLINE runBatch #-}+runBatch bk = execState (unBatch bk) mempty++data Env r = Empty | Resource r++use :: Env r -> Batch r+use Empty = error "use called on empty environment."+use (Resource x) = return x++data Parameter = forall a. (Show a, Enum a) => Parameter a++instance Eq Parameter where+  (==) = (==) `on` \(Parameter x) -> fromEnum x++instance Ord Parameter where+  compare = compare `on` \(Parameter x) -> fromEnum x++data Component+  = BenchC Text+  | GroupC Text+  | SeriesC Parameter+  deriving (Eq, Ord)++newtype BenchmarkId = BenchmarkId [Component]+  deriving (Eq, Ord)+++instance Hashable BenchmarkId where+  hashWithSalt s = hashWithSalt s . renderBenchmarkId++instance Show BenchmarkId where+  show = Text.unpack . renderBenchmarkId++renderBenchmarkId :: BenchmarkId -> Text+renderBenchmarkId (BenchmarkId comps0) = go "" comps0+  where+    go index [BenchC txt] = txt <> index+    go index (GroupC txt : comps) = txt <> index <> "/" <> go "" comps+    go index (SeriesC (Parameter x) : comps) =+        go (index <> ":" <> Text.pack (show x)) comps+    go _ _ = error "renderBenchmarkId: Impossible"++benchmarkParameters :: BenchmarkId -> [Parameter]+benchmarkParameters (BenchmarkId comps) =+    concatMap (\case SeriesC x -> [x]; _ -> []) comps
+ src/Hyperion/Main.hs view
@@ -0,0 +1,313 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Hyperion.Main+  ( defaultMain+  , Mode(..)+  , ConfigMonoid(..)+  , ReportOutput(..)+  , nullOutputPath+  , defaultConfig+  , defaultMainWith+  ) where++import Control.Applicative+import Control.Exception (Exception, throwIO, bracket)+import Control.Lens ((&), (.~), (%~), (%@~), (^..), folded, imapped, mapped, to)+import Control.Monad (unless, mzero, void)+import qualified Data.Aeson as JSON+import qualified Data.ByteString.Lazy.Char8 as BS+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.List (group, sort)+import Data.Maybe (fromMaybe)+import Data.Monoid+import Data.Set (Set)+import qualified Data.Set as Set+import Data.Text (pack, Text, unpack)+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import Data.Time (getCurrentTime)+import Data.Typeable (Typeable)+import Data.Version (showVersion)+import GHC.Generics (Generic)+import Generics.Deriving.Monoid (memptydefault, mappenddefault)+import Hyperion.Analysis+import Hyperion.Benchmark+import Hyperion.Internal+import Hyperion.Measurement+import Hyperion.PrintReport+import Hyperion.Report+import Hyperion.Run+import qualified Options.Applicative as Options+import Paths_hyperion (version)+import System.Directory (createDirectoryIfMissing, doesDirectoryExist)+import System.Environment (getProgName)+import System.FilePath ((</>), (<.>))+import System.FilePath.Posix (hasTrailingPathSeparator)+import qualified System.IO as IO++data Mode = Version | List | Run | Analyze+  deriving (Eq, Ord, Show)++-- | Specify a particular way of reporting the benchmark results.+data ReportOutput a = ReportPretty | ReportJson a | ReportJsonFlat a+  deriving (Functor, Eq, Ord, Show)++-- | Context information about the benchmark.+data ContextInfo = ContextInfo+  { contextPackageName :: Text+  , contextExecutableName :: Text+  }++data ConfigMonoid = ConfigMonoid+  { configMonoidReportOutputs :: [ReportOutput FilePath]+  , configMonoidMode :: First Mode+  , configMonoidRaw :: First Bool+  , configMonoidSamplingStrategy :: First SamplingStrategy+  , configMonoidUserMetadata :: JSON.Object+  , configMonoidSelectorPatterns :: [Text]+  } deriving (Generic, Show)++instance Monoid ConfigMonoid where+  mempty = memptydefault+  mappend = mappenddefault++data Config = Config+  { configReportOutputs :: Set (ReportOutput FilePath)+  , configMode :: Mode+  , configRaw :: Bool+  , configSamplingStrategy :: SamplingStrategy+  , configUserMetadata :: JSON.Object+  , configSelectorPatterns :: [Text]+  } deriving (Generic, Show)++fromFirst :: a -> First a -> a+fromFirst x = fromMaybe x . getFirst++configFromMonoid :: ConfigMonoid -> Config+configFromMonoid ConfigMonoid{..} = Config+    { configReportOutputs =+        if null configMonoidReportOutputs+        then Set.singleton ReportPretty+        else Set.fromList configMonoidReportOutputs+    , configMode = fromFirst Analyze configMonoidMode+    , configRaw = fromFirst False configMonoidRaw+    , configSamplingStrategy = fromFirst defaultStrategy configMonoidSamplingStrategy+    , configUserMetadata = configMonoidUserMetadata+    , configSelectorPatterns = configMonoidSelectorPatterns+    }++options :: Options.Parser ConfigMonoid+options = do+     configMonoidReportOutputs <- many reportOutputParse+     configMonoidMode <-+       First <$> optional+         (Options.flag' Version+            (Options.long "version" <>+             Options.hidden <>+             Options.help "Display version information") <|>+          Options.flag' List+            (Options.long "list" <>+             Options.short 'l' <>+             Options.help "List benchmark names") <|>+          Options.flag' Analyze+            (Options.long "run" <>+             Options.help "Run benchmarks and analyze them (default)") <|>+          Options.flag' Run+            (Options.long "no-analyze" <>+             Options.help "Only run the benchmarks"))+     configMonoidRaw <-+       First <$> optional+         (Options.switch+            (Options.long "raw" <>+             Options.help "Include raw measurement data in report."))+     configMonoidUserMetadata <-+       HashMap.fromList <$> many+         (Options.option+            parseKV+            (Options.long "arg" <>+             Options.metavar "KEY:VAL" <>+             Options.help "Extra metadata to include in the report, in the format key:value."))+     configMonoidSelectorPatterns <-+       many+         (pack <$> Options.argument Options.str+            (Options.metavar "NAME..." ))+     pure ConfigMonoid{..}+  where+     -- TODO allow setting this from CLI.+    configMonoidSamplingStrategy = First Nothing+    parseKV = do+      txt <- Text.pack <$> Options.str+      case Text.splitOn ":" txt of+        [x,y] -> pure (x, JSON.String y)+        _ -> mzero++reportOutputParse :: Options.Parser (ReportOutput FilePath)+reportOutputParse =+    (ReportPretty <$ Options.flag' ()+       (Options.long "pretty" <>+        Options.help "Pretty prints the measurements on stdout.")) <|>+    (ReportJson <$> Options.strOption+      (Options.long "json" <>+       Options.short 'j' <>+       Options.help (unwords+          ["Where to write the json benchmarks output."+          ,"Can be a file name, a directory name or '-' for stdout."+          ]) <>+       Options.metavar "PATH")) <|>+    (ReportJsonFlat <$> Options.strOption+      (Options.long "flat" <>+       Options.short 'f' <>+       Options.help (unwords+          ["Where to write the json benchmarks output."+          ,"Can be a file name, a directory name or '-' for stdout."+          ]) <>+       Options.metavar "PATH"))++-- | The path to the null output file. This is @"nul"@ on Windows and+-- @"/dev/null"@ elsewhere.+nullOutputPath :: FilePath+#ifdef mingw32_HOST_OS+nullOutputPath = "nul"+#else+nullOutputPath = "/dev/null"+#endif++defaultConfig :: ConfigMonoid+defaultConfig = mempty++data DuplicateIdentifiers a = DuplicateIdentifiers [a]+instance (Show a, Typeable a) => Exception (DuplicateIdentifiers a)+instance Show a => Show (DuplicateIdentifiers a) where+  show (DuplicateIdentifiers ids) = "Duplicate identifiers: " <> show ids++doList :: [Benchmark] -> IO ()+doList bks =+    mapM_ Text.putStrLn $ bks^..folded.identifiers.to renderBenchmarkId++-- | Derive a 'SamplingStrategy' indexed by 'BenchmarkId' from the current+-- configuration.+indexedStrategy :: Config -> (BenchmarkId -> Maybe SamplingStrategy)+indexedStrategy Config{..} = case configSelectorPatterns of+    [] -> uniform configSamplingStrategy+    patts -> filtered f configSamplingStrategy+      where+        f bid = any (`Text.isPrefixOf` renderBenchmarkId bid) patts++doRun+  :: (BenchmarkId -> Maybe SamplingStrategy)+  -> [Benchmark]+  -> IO (HashMap BenchmarkId Sample)+doRun strategy bks = do+    let ids = bks^..folded.identifiers+    -- Better asymptotics than nub.+    unless (length (group (sort ids)) == length ids) $+      throwIO $ DuplicateIdentifiers [ n | n:_:_ <- group (sort ids) ]+    foldMap (runBenchmark strategy) bks++-- | Print the report.+printReport+  :: ReportOutput IO.Handle+  -> JSON.Object -- ^ Metadata+  -> HashMap BenchmarkId Report+  -> IO ()+-- XXX: should we print user metadata in pretty mode as well?+printReport ReportPretty _ report = printReports report+printReport (ReportJson h) metadata report =+    BS.hPutStrLn h $ JSON.encode $+      json metadata report+printReport (ReportJsonFlat h) metadata report =+    BS.hPutStrLn h $ JSON.encode $+      jsonFlat metadata report++-- | Open a 'Handle' for given report (if needed).+openReportHandle+  :: ContextInfo+  -> ReportOutput FilePath -> IO (ReportOutput IO.Handle)+openReportHandle _ ReportPretty = pure ReportPretty+openReportHandle cinfo (ReportJson path) =+    ReportJson <$> openReportFileHandle cinfo path+openReportHandle cinfo (ReportJsonFlat path) =+    ReportJsonFlat <$> openReportFileHandle cinfo path++openReportFileHandle :: ContextInfo -> FilePath -> IO IO.Handle+openReportFileHandle _ "-" = pure IO.stdout+openReportFileHandle cinfo path = do+    let packageName = unpack $ contextPackageName cinfo+        executableName = unpack $ contextExecutableName cinfo+    dirExists <- doesDirectoryExist path+    if dirExists ||+       hasTrailingPathSeparator path+    then do+      let filename = packageName <.> executableName <.> "json"+      createDirectoryIfMissing True path -- Creates the directory if needed.+      IO.openFile (path </> filename) IO.WriteMode+    else+      IO.openFile path IO.WriteMode++closeReportHandle :: ReportOutput IO.Handle -> IO ()+closeReportHandle ReportPretty = return ()+closeReportHandle (ReportJson h) = IO.hClose h+closeReportHandle (ReportJsonFlat h) = IO.hClose h++doAnalyze+  :: Config -- ^ Hyperion config.+  -> ContextInfo -- ^ Benchmark context information.+  -> [Benchmark] -- ^ Benchmarks to be run.+  -> IO ()+doAnalyze Config{..} cinfo bks = do+    results <- doRun (indexedStrategy Config{..}) bks+    let strip+          | configRaw = id+          | otherwise = reportMeasurements .~ Nothing+        report = results & imapped %@~ analyze & mapped %~ strip+    now <- getCurrentTime+    let -- TODO Use output of hostname(1) as reasonable default.+        hostId = Nothing :: Maybe Text+        metadata =+          configUserMetadata+          -- Prepend user metadata so that the user can rewrite @timestamp@,+          -- for instance.+            <> HashMap.fromList [ "timestamp" JSON..= now, "location" JSON..= hostId ]+    void $ bracket+      (mapM (openReportHandle cinfo)+        $ Set.toList configReportOutputs)+      (mapM_ closeReportHandle)+      (mapM (\h -> printReport h metadata report))++defaultMainWith+  :: ConfigMonoid -- ^ Preset Hyperion config.+  -> String -- ^ Package name, user provided.+  -> [Benchmark] -- ^ Benchmarks to be run.+  -> IO ()+defaultMainWith presetConfig packageName bks = do+    executableName <- getProgName -- Name of the executable that launched the benches.+    cmdlineConfig <-+      Options.execParser+        (Options.info+          (Options.helper <*> options)+          Options.fullDesc)+    let config = configFromMonoid (cmdlineConfig <> presetConfig)+        cinfo = ContextInfo+          { contextPackageName = pack packageName+          , contextExecutableName = pack executableName+          }+    case config of+      Config{..} -> case configMode of+        Version -> putStrLn $ "Hyperion " <> showVersion version+        List -> doList bks+        Run -> do+          _ <- doRun (indexedStrategy config) bks+          return ()+        Analyze -> doAnalyze config cinfo bks++defaultMain+  :: String -- ^ Package name, user provided.+  -> [Benchmark] -- ^ Benchmarks to be run.+  -> IO ()+defaultMain = defaultMainWith defaultConfig
+ src/Hyperion/Measurement.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module Hyperion.Measurement+  ( Measurement(..)+  , batchSize+  , duration+  , Sample(..)+  , measurements+  ) where++import Data.Aeson (FromJSON, ToJSON)+import Data.Aeson.TH+import Data.Int+import Control.Lens.TH (makeLenses)+import Control.Monad (liftM)+import qualified Data.Vector.Generic as GV+import qualified Data.Vector.Generic.Mutable as GMV+import qualified Data.Vector.Unboxed as UV++data Measurement = Measurement+  { _batchSize :: {-# UNPACK #-} !Int64+  , _duration :: {-# UNPACK #-} !Int64+  }+  deriving (Eq, Ord, Show)+makeLenses ''Measurement+deriveJSON defaultOptions{ fieldLabelModifier = drop 1 } ''Measurement++instance UV.Unbox Measurement++newtype Sample = Sample { _measurements :: UV.Vector Measurement }+  deriving (Eq, Monoid, Ord, Show)+makeLenses ''Sample++deriving instance FromJSON Sample+deriving instance ToJSON Sample++newtype instance UV.MVector s Measurement = MV_Measurement (UV.MVector s (Int64, Int64))+newtype instance UV.Vector Measurement = V_Measurement  (UV.Vector (Int64, Int64))++instance GMV.MVector UV.MVector Measurement where+  {-# INLINE basicLength #-}+  {-# INLINE basicUnsafeSlice #-}+  {-# INLINE basicOverlaps #-}+  {-# INLINE basicUnsafeNew #-}+  {-# INLINE basicInitialize #-}+  {-# INLINE basicUnsafeReplicate #-}+  {-# INLINE basicUnsafeRead #-}+  {-# INLINE basicUnsafeWrite #-}+  {-# INLINE basicClear #-}+  {-# INLINE basicSet #-}+  {-# INLINE basicUnsafeCopy #-}+  {-# INLINE basicUnsafeGrow #-}+  basicLength (MV_Measurement v) = GMV.basicLength v+  basicUnsafeSlice i n (MV_Measurement v) = MV_Measurement $ GMV.basicUnsafeSlice i n v+  basicOverlaps (MV_Measurement v1) (MV_Measurement v2) = GMV.basicOverlaps v1 v2+  basicUnsafeNew n = MV_Measurement `liftM` GMV.basicUnsafeNew n+  basicInitialize (MV_Measurement v) = GMV.basicInitialize v+  basicUnsafeReplicate n (Measurement x y) = MV_Measurement `liftM` GMV.basicUnsafeReplicate n (x,y)+  basicUnsafeRead (MV_Measurement v) i = uncurry Measurement `liftM` GMV.basicUnsafeRead v i+  basicUnsafeWrite (MV_Measurement v) i (Measurement x y) = GMV.basicUnsafeWrite v i (x,y)+  basicClear (MV_Measurement v) = GMV.basicClear v+  basicSet (MV_Measurement v) (Measurement x y) = GMV.basicSet v (x,y)+  basicUnsafeCopy (MV_Measurement v1) (MV_Measurement v2) = GMV.basicUnsafeCopy v1 v2+  basicUnsafeMove (MV_Measurement v1) (MV_Measurement v2) = GMV.basicUnsafeMove v1 v2+  basicUnsafeGrow (MV_Measurement v) n = MV_Measurement `liftM` GMV.basicUnsafeGrow v n++instance GV.Vector UV.Vector Measurement where+  {-# INLINE basicUnsafeFreeze #-}+  {-# INLINE basicUnsafeThaw #-}+  {-# INLINE basicLength #-}+  {-# INLINE basicUnsafeSlice #-}+  {-# INLINE basicUnsafeIndexM #-}+  {-# INLINE elemseq #-}+  basicUnsafeFreeze (MV_Measurement v) = V_Measurement `liftM` GV.basicUnsafeFreeze v+  basicUnsafeThaw (V_Measurement v) = MV_Measurement `liftM` GV.basicUnsafeThaw v+  basicLength (V_Measurement v) = GV.basicLength v+  basicUnsafeSlice i n (V_Measurement v) = V_Measurement $ GV.basicUnsafeSlice i n v+  basicUnsafeIndexM (V_Measurement v) i = uncurry Measurement `liftM` GV.basicUnsafeIndexM v i+  basicUnsafeCopy (MV_Measurement mv) (V_Measurement v) = GV.basicUnsafeCopy mv v+  elemseq _ (Measurement x y) z =+    GV.elemseq (undefined :: UV.Vector a) x $ GV.elemseq (undefined :: UV.Vector a) y z
+ src/Hyperion/PrintReport.hs view
@@ -0,0 +1,35 @@+{- This module is used for pretty printing the hyperion report.+ - This is mostly put in a separate module to avoid conflicts in+ - the imports of (<>) from Monoid an ansi-wl-pprint+ -}++module Hyperion.PrintReport (printReports) where++import Numeric+import Control.Lens (view)+import Data.Text (unpack)+import Data.HashMap.Strict (HashMap)+import Hyperion.Internal+import Hyperion.Report+import Text.PrettyPrint.ANSI.Leijen++formatReport :: Report -> Doc+formatReport report =+           green (bold (text (unpack (view reportBenchName report)))) <> line+           <> (indent 2 $ text ("Bench time: " ++ prettyNanos (view reportTimeInNanos report)))+           <> line+  where+    show2decs x= showFFloat (Just 2) x ""+    prettyNanos :: Double -> String+    prettyNanos x+      -- seconds+      | x > 1e9 = show2decs (x/1e9) ++ "s"+      -- milliseconds+      | x > 1e6 = show2decs (x/1e6) ++ "ms"+      -- microseconds+      | x > 1e3 = show2decs (x/1e3) ++ "us"+      | otherwise = show2decs x ++ "ns"++printReports :: HashMap BenchmarkId Report -> IO ()+printReports report = do+  putDoc $ foldMap formatReport report
+ src/Hyperion/Report.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}++module Hyperion.Report where++import Control.Lens.TH (makeLenses)+import Data.Monoid+import GHC.Generics (Generic)+import qualified Data.Aeson as JSON+import Data.Aeson ((.=))+import Data.Aeson.TH+import Data.Aeson.Types (camelTo2)+import Data.Bifunctor (first)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.Int+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Vector as V+import Hyperion.Measurement (Sample)+import Hyperion.Internal++data Report = Report+  { _reportBenchName :: !Text+  , _reportBenchParams :: [Int]+  , _reportTimeInNanos :: !Double+  , _reportCycles :: !(Maybe Double)+  , _reportAlloc :: !(Maybe Int64)+  , _reportGarbageCollections :: !(Maybe Int64)+  , _reportMeasurements :: !(Maybe Sample)+  } deriving (Generic)+makeLenses ''Report+deriveJSON defaultOptions{ fieldLabelModifier = camelTo2 '_' . drop (length @[] "_report") } ''Report++json+  :: JSON.Object -- ^ Metadata+  -> HashMap BenchmarkId Report -- ^ Report to encode+  -> JSON.Value+json md report =+    JSON.object+      [ "metadata" .= md+      , "results" .= HashMap.elems report+      ]++jsonFlat+  :: JSON.Object -- ^ Metadata+  -> HashMap BenchmarkId Report+  -- ^ Report to encode+  -> JSON.Value+jsonFlat md report = jsonList $ flip fmap (HashMap.elems report) $ \b ->+    JSON.object $+      HashMap.toList md <> (flatten $ JSON.toJSON b)+  where+    flatten (JSON.Object b) = concatMap splitParams $ HashMap.toList b+    flatten x = [("result", x)] -- XXX should never happen+    jsonList = JSON.Array . V.fromList+    -- flatten the benchmark params+    splitParams ("bench_params", JSON.Array xs) =+      fmap (first (mappend "x_" . Text.pack . show)) (zip [(1::Int)..] $ V.toList xs)+    splitParams x = [x]
+ src/Hyperion/Run.hs view
@@ -0,0 +1,202 @@+-- | Run a hierarchical benchmark suite, collecting results.++{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE RecordWildCards #-}++module Hyperion.Run+  ( -- * Run benchmarks+    runBenchmark+    -- * Benchmark transformations+  , shuffle+  , reorder+    -- * Sampling strategy selectors+  , filtered+  , uniform+    -- * Sampling strategies+  , SamplingStrategy(..)+  , defaultStrategy+  , fixed+  , sample+  , geometric+  , timeBound+    -- * Strategy helpers+  , geometricSeries+  ) where++import Control.DeepSeq+import Control.Exception (evaluate)+import Control.Lens (foldMapOf)+import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, bracket)+import Control.Monad.State.Class (MonadState)+import Control.Monad.State.Strict (StateT, evalStateT, get, put)+import Control.Monad.Trans (MonadTrans(..))+import Data.Int+import Data.List (mapAccumR)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.Sequence (ViewL((:<)), viewl)+import qualified Data.Vector.Unboxed as Unboxed+import Hyperion.Analysis (identifiers)+import Hyperion.Benchmark+import Hyperion.Internal+import Hyperion.Measurement+import qualified System.Clock as Clock+import System.Random (RandomGen(..))+import qualified System.Random.Shuffle as SRS+import Text.Show.Functions ()++-- | Local private copy of 'StateT' to hang our otherwise orphan 'Monoid'+-- instance to. This instance is missing from transformers.+newtype StateT' s m a = StateT' { unStateT' :: StateT s m a }+  deriving (Functor, Applicative, Monad, MonadCatch, MonadMask, MonadThrow, MonadState s, MonadTrans)++instance (Monad m, Monoid a) => Monoid (StateT' s m a) where+  mempty = lift (return mempty)+  mappend m1 m2 = mappend <$> m1 <*> m2++-- | Sampling strategy.+newtype SamplingStrategy = SamplingStrategy (Batch () -> IO Sample)+  deriving (Monoid, Show)++-- | Provided a sampling strategy (which can be keyed on the 'BenchmarkId'),+-- sample the runtime of all the benchmark cases in the given benchmark tree.+runBenchmark+  :: (BenchmarkId -> Maybe SamplingStrategy)+  -- ^ Name indexed batch sampling strategy.+  -> Benchmark+  -- ^ Benchmark to be run.+  -> IO (HashMap BenchmarkId Sample)+runBenchmark istrategy bk0 =+  -- Ignore the identifiers we find. Use fully qualified identifiers+  -- accumulated from the lens defined above. The order is DFS in both cases.+  evalStateT (unStateT' (go bk0)) (foldMapOf identifiers return bk0)+  where+    go (Bench _ batch) = do+      ident <- pop+      case (istrategy ident) of+        Nothing -> return HashMap.empty+        Just (SamplingStrategy f) ->+          HashMap.singleton ident <$> lift (f batch)+    go (Group _ bks) = foldMap (go) bks+    go (Bracket ini fini g) =+      bracket (lift (ini >>= evaluate . force)) (lift . fini) (go . g . Resource)+    go (Series xs g) = foldMap (go . g) xs++    pop = do+      x :< xs <- viewl <$> get+      put xs+      return x++-- | Time an action.+chrono :: IO () -> IO Int64+chrono act = do+    start <- Clock.getTime Clock.Monotonic+    act+    end <- Clock.getTime Clock.Monotonic+    return $ fromIntegral $ Clock.toNanoSecs $ Clock.diffTimeSpec start end++-- | Sample once a batch of fixed size.+fixed :: Int64 -> SamplingStrategy+fixed _batchSize = SamplingStrategy $ \batch -> do+    _duration <- chrono $ runBatch batch _batchSize+    return $ Sample $ Unboxed.singleton Measurement{..}++-- | Run a sampling strategy @n@ times.+sample :: Int64 -> SamplingStrategy -> SamplingStrategy+sample n strategy = mconcat $ replicate (fromIntegral n) strategy++-- | Sampling strategy that creates samples of the specified sizes with a time+-- bound. Sampling stops when either a sample has been sampled for each size or+-- when the total benchmark time is greater than the specified time bound.+--+-- The actual amount of time spent may be longer since hyperion will always+-- wait for a 'Sample' of a given size to complete.+timeBound+  :: Clock.TimeSpec -- ^ Time bound+  -> [Int64] -- ^ Sample sizes; may be infinite+  -> SamplingStrategy+timeBound maxTime batchSizes = SamplingStrategy $ \batch -> do+    start <- Clock.getTime Clock.Monotonic+    go batch start batchSizes mempty+  where+    go batch start (_batchSize:bss) smpl = do+      _duration <- chrono $ runBatch batch _batchSize+      let smpl' = smpl `mappend` (Sample $ Unboxed.singleton Measurement{..})+      now <- Clock.getTime Clock.Monotonic+      if Clock.diffTimeSpec start now > maxTime+        then return smpl'+        else go batch start bss smpl'+    go _ _ _ s = return s++-- | Sampling strategies that ignore the name index, i.e. are uniform across all+-- benchmarks.+uniform :: SamplingStrategy -> (BenchmarkId -> Maybe SamplingStrategy)+uniform = const . Just++-- | Sampling strategies that filters the benchmarks based on a predicate: a+-- benchmark is included iff the predicate is 'True'.+filtered+  :: (BenchmarkId -> Bool)+  -> SamplingStrategy+  -> (BenchmarkId -> Maybe SamplingStrategy)+filtered p ss bid =+    if p bid then Just ss else Nothing++-- | Default to 100 samples, for each batch size from 1 to 20 with a geometric+-- progression of 1.2.+defaultStrategy :: SamplingStrategy+defaultStrategy = geometric 100 20 1.2++-- | Batching strategy, following a geometric progression from 1+-- to the provided limit, with the given ratio.+geometric+  :: Int64 -- ^ Sample size.+  -> Int64 -- ^ Max batch size.+  -> Double -- ^ Ratio of geometric progression.+  -> SamplingStrategy+geometric nSamples limit ratio =+    foldMap (\size -> sample nSamples (fixed size)) (geometricSeries ratio limit)++geometricSeries+  :: Double -- ^ Geometric progress.+  -> Int64 -- ^ End of the series.+  -> [Int64]+geometricSeries ratio limit =+    if ratio > 1+    then+      takeWhile (<= limit) $+      squish $+      map truncate $+      map (ratio^) ([1..] :: [Int])+    else+      error "Geometric ratio must be bigger than 1"++-- | Our series starts its growth very slowly when we begin at 1, so we+-- eliminate repeated values.+-- NOTE: taken from Criterion.+squish :: (Eq a) => [a] -> [a]+squish ys = foldr go [] ys+  where go x xs = x : dropWhile (==x) xs++-- | Convenience wrapper around 'SRS.shuffle'.+shuffle :: RandomGen g => g -> [a] -> [a]+shuffle gen xs = SRS.shuffle' xs (length xs) gen++splitn :: RandomGen g => Int -> g -> [g]+splitn n gen = snd $ mapAccumR (flip (const split)) gen [1..n]++reorder+  :: RandomGen g+  => (g -> [Benchmark] -> [Benchmark])+  -> (g -> [Benchmark] -> [Benchmark])+reorder shuf gen0 bks0 =+    shuf gen0 (zipWith go (splitn (length bks0) gen0) bks0)+  where+    go _ bk@(Bench _ _) = bk+    go gen (Group name bks) =+        Group name (shuf gen (zipWith go (splitn (length bks) gen) bks))+    go gen (Bracket ini fini f) =+        Bracket ini fini (\x -> go gen (f x))+    go gen (Series xs f) =+        Series xs (\x -> go gen (f x))
+ tests/Hyperion/BenchmarkSpec.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Hyperion.BenchmarkSpec where++import Control.Lens ((^..), foldOf, to)+import Data.List (nub)+import Hyperion.Analysis+import Hyperion.Benchmark+import Hyperion.Run+import Test.Hspec+import Test.QuickCheck+  ( Arbitrary(..)+  , (==>)+  , elements+  , oneof+  , property+  , scale+  )+import Test.QuickCheck.Monadic (assert, monadicIO, run)++benchmarkNames, groupNames :: [String]+benchmarkNames = ["foo", "bar", "baz"]+groupNames = ["one", "two"]++instance Arbitrary Benchmark where+  arbitrary = scale (min 4) $ oneof+    [ bench <$>+        elements benchmarkNames <*>+        pure (nf id ())+    , bgroup <$>+        elements groupNames <*>+        arbitrary+    , env <$>+        pure (return ()) <*>+        pure (\_ -> return ()) <*>+        const <$> scale (max 0 . pred) arbitrary+    , series <$>+        pure [1,2,3 :: Int] <*>+        const <$> scale (max 0 . pred) arbitrary+    ]++hasSeries :: Benchmark -> Bool+hasSeries b = any (==':') (foldOf (identifiers.to show) b)++spec :: Spec+spec = do+    describe "runBenchmark" $ do+      it "returns as many samples as there are benchmarks" $ property $ \b ->+        not (hasSeries b) ==>+        monadicIO $ do+          results <- run $ runBenchmark (uniform (fixed 1)) b+          assert (length results == length (nub (b^..identifiers)))
+ tests/Hyperion/MainSpec.hs view
@@ -0,0 +1,22 @@+module Hyperion.MainSpec where++import Control.Lens ((^..))+import Data.List (group, sort)+import Hyperion.Analysis+import Hyperion.BenchmarkSpec ()+import Hyperion.Main+import Test.Hspec+import Test.QuickCheck ((==>), expectFailure, property)+import Test.QuickCheck.Monadic (monadicIO, run)++spec :: Spec+spec = do+    describe "defaultMain" $ do+      it "checks for duplicate identifiers" $ property $ \b ->+        length (b^..identifiers) /= length (group (sort (b^..identifiers))) ==>+        expectFailure $ monadicIO $ run $+          defaultMainWith defaultConfig{configMonoidMode = return Run} "spec" [b]+      it "Analyzes uniquely identified benchmarks" $ property $ \b ->+        length (b^..identifiers) == length (group (sort (b^..identifiers))) ==>+        monadicIO $ run $+          defaultMainWith defaultConfig{configMonoidReportOutputs = [ReportJson nullOutputPath]} "specs" [b]
+ tests/Main.hs view
@@ -0,0 +1,7 @@+module Main where++import qualified Spec+import Test.Hspec++main :: IO ()+main = hspec Spec.spec
+ tests/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}