streamly-0.10.0: benchmark/lib/Streamly/Benchmark/Common.hs
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wno-orphans #-}
-- |
-- Module : Streamly.Benchmark.Common
-- Copyright : (c) 2019 Composewell Technologies
--
-- License : BSD3
-- Maintainer : streamly@composewell.com
module Streamly.Benchmark.Common
( o_1_space_prefix
, o_n_space_prefix
, o_n_heap_prefix
, o_n_stack_prefix
, runWithCLIOptsEnv
, runWithCLIOpts
, benchIOSink1
, benchPure
, benchPureSink1
, benchFold
, benchIOSrc1
, benchPureSrc
, mkString
, mkList
, mkListString
, defaultStreamSize
)
where
#ifdef MIN_VERSION_gauge
import Control.Exception (evaluate)
import Control.Monad (when)
import Text.Read (readMaybe)
import Data.List (scanl')
import Data.Maybe (mapMaybe)
import System.Console.GetOpt
(OptDescr(..), ArgDescr(..), ArgOrder(..), getOpt')
import System.Environment (getArgs, lookupEnv, setEnv)
#else
import Data.Proxy (Proxy(..))
import Test.Tasty.Ingredients.Basic (includingOptions)
import Test.Tasty.Options
(IsOption(..), OptionDescription(..), lookupOption, safeRead)
import Test.Tasty.Runners (Ingredient, defaultMainWithIngredients, parseOptions)
#endif
import Control.DeepSeq (NFData(..))
import Data.Functor.Identity (Identity, runIdentity)
import System.Random (randomRIO)
import qualified Streamly.Internal.Data.Fold as Fold
import qualified Streamly.Internal.Data.Stream as S
import Test.Tasty.Bench
-------------------------------------------------------------------------------
-- Benchmark Prefixes
-------------------------------------------------------------------------------
o_1_space_prefix :: String -> String
o_1_space_prefix name = name ++ "/o-1-space"
o_n_space_prefix :: String -> String
o_n_space_prefix name = name ++ "/o-n-space"
o_n_heap_prefix :: String -> String
o_n_heap_prefix name = name ++ "/o-n-heap"
o_n_stack_prefix :: String -> String
o_n_stack_prefix name = name ++ "/o-n-stack"
-------------------------------------------------------------------------------
-- Benchmarking utilities
-------------------------------------------------------------------------------
-- XXX once we convert all the functions to use this we can rename this to
-- benchIOSink
{-# INLINE benchIOSink1 #-}
benchIOSink1 :: NFData b => String -> (Int -> IO b) -> Benchmark
benchIOSink1 name f = bench name $ nfIO $ randomRIO (1,1) >>= f
{-# INLINE benchIOSrc1 #-}
benchIOSrc1 :: String -> (Int -> IO ()) -> Benchmark
benchIOSrc1 name f = bench name $ nfIO $ randomRIO (1,1) >>= f
-- We need a monadic bind here to make sure that the function f does not get
-- completely optimized out by the compiler in some cases.
{-# INLINE benchFold #-}
benchFold :: NFData b
=> String -> (t IO Int -> IO b) -> (Int -> t IO Int) -> Benchmark
benchFold name f src = bench name $ nfIO $ randomRIO (1,1) >>= f . src
{-# INLINE benchPure #-}
benchPure :: NFData b => String -> (Int -> a) -> (a -> b) -> Benchmark
benchPure name src f = bench name $ nfIO $ randomRIO (1,1) >>= return . f . src
-- XXX once we convert all the functions to use this we can rename this to
-- benchPureSink
{-# INLINE benchPureSink1 #-}
benchPureSink1 :: NFData b => String -> (Int -> Identity b) -> Benchmark
benchPureSink1 name f =
bench name $ nfIO $ randomRIO (1,1) >>= return . runIdentity . f
{-# INLINE benchPureSrc #-}
benchPureSrc :: String -> (Int -> S.Stream Identity a) -> Benchmark
benchPureSrc name src = benchPure name src (runIdentity . drain)
where
drain = S.fold Fold.drain
-------------------------------------------------------------------------------
-- String/List generation for read instances
-------------------------------------------------------------------------------
{-# INLINABLE mkString #-}
mkString :: Int -> String
mkString value = "fromList [1" ++ concat (replicate value ",1") ++ "]"
{-# INLINABLE mkListString #-}
mkListString :: Int -> String
mkListString value = "[1" ++ concat (replicate value ",1") ++ "]"
{-# INLINABLE mkList #-}
mkList :: Int -> [Int]
mkList value = [1..value]
-------------------------------------------------------------------------------
-- Stream size
-------------------------------------------------------------------------------
defaultStreamSize :: Int
defaultStreamSize = 100000
-------------------------------------------------------------------------------
-- Parse custom CLI options
-------------------------------------------------------------------------------
newtype BenchOpts = StreamSize Int deriving Show
#ifdef MIN_VERSION_gauge
getStreamSize :: String -> Int
getStreamSize size =
case (readMaybe size :: Maybe Int) of
Just x -> x
Nothing -> error "Stream size must be numeric"
options :: [OptDescr BenchOpts]
options =
[
System.Console.GetOpt.Option [] ["stream-size"] (ReqArg getSize "COUNT") "Stream element count"
]
where
getSize = StreamSize . getStreamSize
deleteOptArgs
:: (Maybe String, Maybe String) -- (prev, yielded)
-> String
-> (Maybe String, Maybe String)
deleteOptArgs (Nothing, _) opt =
if opt == "--stream-size"
then (Just opt, Nothing)
else (Just opt, Just opt)
deleteOptArgs (Just prev, _) opt =
if opt == "--stream-size" || prev == "--stream-size"
then (Just opt, Nothing)
else (Just opt, Just opt)
parseCLIOpts :: Int -> IO (Int, Config, [String])
parseCLIOpts defStreamSize = do
args <- getArgs
-- Parse custom options
let (opts, _, _, errs) = getOpt' Permute options args
when (not $ null errs) $ error $ concat errs
(streamSize, args') <-
case opts of
StreamSize x : _ -> do
putStrLn $ "Stream size: " ++ show x
-- When using the gauge "--measure-with" option we need to make
-- sure that we pass the stream size to child process forked by
-- gauge. So we use this env var for that purpose.
setEnv "STREAM_SIZE" (show x)
-- Hack! remove the option and its argument from args
-- getOpt should have a way to return the unconsumed args in
-- correct order.
newArgs <-
evaluate
$ mapMaybe snd
$ scanl' deleteOptArgs (Nothing, Nothing) args
return (x, newArgs)
_ -> do
r <- lookupEnv "STREAM_SIZE"
case r of
Just x -> do
s <- evaluate $ getStreamSize x
return (s, args)
Nothing -> do
setEnv "STREAM_SIZE" (show defStreamSize)
return (defStreamSize, args)
-- Parse gauge options
let config = defaultConfig
{ timeLimit = Just 1
, minDuration = 0
, includeFirstIter = streamSize > defStreamSize
}
let (cfg, benches) = parseWith config args'
streamSize `seq` return (streamSize, cfg, benches)
#else
instance IsOption BenchOpts where
defaultValue = StreamSize defaultStreamSize
parseValue = fmap StreamSize . safeRead
optionName = pure "stream-size"
optionHelp = pure "Size of the stream to be used in benchmarks"
parseCLIOpts :: Int -> Benchmark -> IO (Int, [Ingredient])
parseCLIOpts defStreamSize benches = do
let customOpts = [Test.Tasty.Options.Option (Proxy :: Proxy BenchOpts)]
ingredients = includingOptions customOpts : benchIngredients
opts <- parseOptions ingredients benches -- (TestGroup "" [])
let StreamSize size = lookupOption opts
-- putStrLn $ "Stream size: " ++ show size
if size == defaultStreamSize
then return (defStreamSize, ingredients)
else return (size, ingredients)
#endif
runWithCLIOptsEnv :: Int -> (Int -> IO a) -> (a -> Int -> [Benchmark]) -> IO ()
runWithCLIOptsEnv defStreamSize alloc mkBench = do
#ifdef MIN_VERSION_gauge
(value, cfg, benches) <- parseCLIOpts defStreamSize
r <- alloc value
value `seq` runMode (mode cfg) cfg benches (mkBench r value)
#else
r0 <- alloc 0
(value, ingredients) <-
parseCLIOpts defStreamSize $ bgroup "All" (mkBench r0 0)
r <- alloc value
value `seq` defaultMainWithIngredients ingredients
$ bgroup "All" (mkBench r value)
#endif
runWithCLIOpts :: Int -> (Int -> [Benchmark]) -> IO ()
runWithCLIOpts defStreamSize f =
runWithCLIOptsEnv defStreamSize
(const $ return undefined)
(\_ v -> f v)