module BenchmarkHistory where
import Control.Arrow (second)
import Control.DeepSeq
import Data.Csv
import Data.Function (fix)
import Data.Int(Int64)
import Data.Time
import GHC.Conc (pseq)
import GHC.Generics
import GHC.Stats
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.List as L
import qualified Data.Vector as V
import Statistics.Sample
import System.Directory (doesFileExist)
import System.Exit
import System.Mem
import Text.Printf
newtype TimeStamp = TimeStamp { getTimeStamp :: LocalTime }
deriving (Read,Show,Generic)
instance NFData TimeStamp
instance FromField TimeStamp where
parseField = fmap (TimeStamp . read) . parseField
instance ToField TimeStamp where
toField = toField . show . getTimeStamp
newtype GCStatistics = GCStatistics { getGCStatistics :: GCStats }
deriving (Read,Show,Generic)
instance NFData GCStatistics where
rnf (GCStatistics !x) = ()
instance FromField GCStatistics where
parseField = fmap (GCStatistics . read) . parseField
instance ToField GCStatistics where
toField = toField . show . getGCStatistics
data Stats = Stats
{ timeStamp :: !TimeStamp
, preStats :: !GCStatistics
, postStats :: !GCStatistics
, multiplier :: !Int
}
deriving (Read,Show,Generic)
instance DefaultOrdered Stats
instance FromNamedRecord Stats
instance ToNamedRecord Stats
instance FromRecord Stats
instance ToRecord Stats
instance NFData Stats
gcStatDiff :: Num a => (GCStats -> a) -> GCStats -> GCStats -> a
gcStatDiff f pre post = f post - f pre
{-# Inline gcStatDiff #-}
-- | Benchmark a function. The function should take a /considerable amount
-- of time/ to finish, since the benchmarking system is designed to measure
-- coarse-grained timings.
benchmark
:: (NFData e, NFData a, NFData b)
=> Int -- ^ multiplicity of the benchmark run
-> String -- ^ name of the benchmark file
-> (a -> e) -- ^ environment generator (not benched)
-> (e -> a -> b) -- ^ given environment, input, create output
-> a -- ^ input
-> IO ExitCode -- ^ run everything, return exit code based on performance
benchmark mul' file env fun x = do
let mul = max 1 mul'
dfe <- doesFileExist file
(h,xs) <- if dfe
then do BSL.readFile file >>= (return . either error (second V.toList) . decodeByName)
else do return (V.empty,[]) :: IO (Header, [Stats])
time <- fmap zonedTimeToLocalTime getZonedTime
performGC
preE <- getGCStats
let !e = env x
deepseq e $ performGC
pre <- getGCStats
res <- V.foldM' (\a b -> b >> return ()) () $ V.map (call $ fun e) $ V.replicate mul x
post <- pseq res $ getGCStats
putStrLn ""
putStrLn file
let ys = Stats (TimeStamp time) (GCStatistics pre) (GCStatistics post) mul : xs
exit <- basicStats $ eachBlock cpuSeconds (\m o n -> (n-o) / fromIntegral m) $ toBlocks [1,1 ..] ys
BSL.writeFile file $ encodeDefaultOrderedByName ys
return exit
{-# NoInline benchmark #-}
call :: NFData b => (a -> b) -> a -> IO b
call f x = return $!! f x
{-# NoInline call #-}
-- | Divide data into blocks
toBlocks :: [Int] -> [a] -> [[a]]
toBlocks _ [] = []
toBlocks [] _ = error "not enough block division information"
toBlocks (b:bs) xs = let (ys,zs) = splitAt b xs
in ys : toBlocks bs zs
defbs = replicate 4 1 ++ fix (map (*2) . (1:))
-- | for each block, perform an op and return first time stamp and op
-- result
eachBlock
:: (GCStats -> r)
-> (Int -> r -> r -> Double)
-> [[Stats]]
-> [(TimeStamp, Double)]
eachBlock f cmb = map go . filter (not . null)
where go xs = (timeStamp $ head xs, mean $ V.map oneStat $ V.fromList xs)
oneStat s = cmb (multiplier s) (f . getGCStatistics $ preStats s) (f . getGCStatistics $ postStats s)
-- | Statistics for data. We allow one standard deviation or 5% of the mean
-- as error margin before we flag the running time as being slow enough to
-- raise an @ExitFailure 1@.
basicStats :: [(TimeStamp, Double)] -> IO ExitCode
basicStats [] = return ExitSuccess
basicStats xs' = do
let xs = V.fromList xs'
let x = V.head xs
let μ = mean $ V.map snd xs
let σ = stdDev $ V.map snd xs
let p5 = μ * 0.05 -- 5 % ok-ness
let ok = snd x <= μ + max σ p5
printf "μ %f σ %f current: %f (%s)\n" μ σ (snd x) (if ok then "✓" else "✗" :: String)
return $ if ok
then ExitSuccess
else ExitFailure 1