futhark-0.16.2: src/Futhark/CLI/Bench.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
-- | @futhark bench@
module Futhark.CLI.Bench ( main ) where
import Control.Monad
import Control.Monad.Except
import qualified Data.ByteString.Char8 as SBS
import qualified Data.ByteString.Lazy.Char8 as LBS
import Data.Either
import Data.IORef
import Data.Maybe
import Data.List (foldl', sortBy)
import Data.Ord
import qualified Data.Text as T
import System.Console.ANSI (clearLine)
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.IO
import System.Exit
import Text.Printf
import Text.Regex.TDFA
import Futhark.Bench
import Futhark.Test
import Futhark.Util (fancyTerminal, maybeNth, maxinum, pmapIO)
import Futhark.Util.Console
import Futhark.Util.Options
data BenchOptions = BenchOptions
{ optBackend :: String
, optFuthark :: Maybe String
, optRunner :: String
, optRuns :: Int
, optExtraOptions :: [String]
, optCompilerOptions :: [String]
, optJSON :: Maybe FilePath
, optTimeout :: Int
, optSkipCompilation :: Bool
, optExcludeCase :: [String]
, optIgnoreFiles :: [Regex]
, optEntryPoint :: Maybe String
, optTuning :: Maybe String
, optConcurrency :: Maybe Int
, optVerbose :: Int
}
initialBenchOptions :: BenchOptions
initialBenchOptions = BenchOptions "c" Nothing "" 10 [] [] Nothing (-1) False
["nobench", "disable"] [] Nothing (Just "tuning") Nothing 0
runBenchmarks :: BenchOptions -> [FilePath] -> IO ()
runBenchmarks opts paths = do
-- We force line buffering to ensure that we produce running output.
-- Otherwise, CI tools and the like may believe we are hung and kill
-- us.
hSetBuffering stdout LineBuffering
benchmarks <- filter (not . ignored . fst) <$> testSpecsFromPathsOrDie paths
-- Try to avoid concurrency at both program and data set level.
let opts' = if length paths /= 1
then opts { optConcurrency = Just 1}
else opts
(skipped_benchmarks, compiled_benchmarks) <-
partitionEithers <$> pmapIO (optConcurrency opts) (compileBenchmark opts') benchmarks
when (anyFailedToCompile skipped_benchmarks) exitFailure
putStrLn $ "Reporting average runtime of " ++ show (optRuns opts) ++ " runs for each dataset."
results <- concat <$> mapM (runBenchmark opts)
(sortBy (comparing fst) compiled_benchmarks)
case optJSON opts of
Nothing -> return ()
Just file -> LBS.writeFile file $ encodeBenchResults results
when (anyFailed results) exitFailure
where ignored f = any (`match` f) $ optIgnoreFiles opts
anyFailed :: [BenchResult] -> Bool
anyFailed = any failedBenchResult
where failedBenchResult (BenchResult _ xs) =
any failedResult xs
failedResult (DataResult _ Left{}) = True
failedResult _ = False
anyFailedToCompile :: [SkipReason] -> Bool
anyFailedToCompile = not . all (==Skipped)
data SkipReason = Skipped | FailedToCompile
deriving (Eq)
compileOptions :: BenchOptions -> IO CompileOptions
compileOptions opts = do
futhark <- maybe getExecutablePath return $ optFuthark opts
return $ CompileOptions { compFuthark = futhark
, compBackend = optBackend opts
, compOptions = optCompilerOptions opts
}
compileBenchmark :: BenchOptions -> (FilePath, ProgramTest)
-> IO (Either SkipReason (FilePath, [InputOutputs]))
compileBenchmark opts (program, spec) =
case testAction spec of
RunCases cases _ _ | "nobench" `notElem` testTags spec,
"disable" `notElem` testTags spec,
any hasRuns cases ->
if optSkipCompilation opts
then do
exists <- doesFileExist $ binaryName program
if exists
then return $ Right (program, cases)
else do putStrLn $ binaryName program ++ " does not exist, but --skip-compilation passed."
return $ Left FailedToCompile
else do
putStr $ "Compiling " ++ program ++ "...\n"
compile_opts <- compileOptions opts
res <- prepareBenchmarkProgram (optConcurrency opts) compile_opts program cases
case res of
Left (err, errstr) -> do
putStrLn $ inRed err
maybe (return ()) SBS.putStrLn errstr
return $ Left FailedToCompile
Right () ->
return $ Right (program, cases)
_ ->
return $ Left Skipped
where hasRuns (InputOutputs _ runs) = not $ null runs
runBenchmark :: BenchOptions -> (FilePath, [InputOutputs]) -> IO [BenchResult]
runBenchmark opts (program, cases) = mapM forInputOutputs $ filter relevant cases
where forInputOutputs (InputOutputs entry_name runs) = do
(tuning_opts, tuning_desc) <- determineTuning (optTuning opts) program
putStr $ inBold $ "\nResults for " ++ program' ++ tuning_desc ++ ":\n"
let opts' = opts { optExtraOptions =
optExtraOptions opts ++ tuning_opts }
BenchResult program' . catMaybes <$>
mapM (runBenchmarkCase opts' program entry_name pad_to) runs
where program' = if entry_name == "main"
then program
else program ++ ":" ++ T.unpack entry_name
relevant = maybe (const True) (==) (optEntryPoint opts) . T.unpack . iosEntryPoint
pad_to = foldl max 0 $ concatMap (map (length . runDescription) . iosTestRuns) cases
runOptions :: (Int -> IO ()) -> BenchOptions -> RunOptions
runOptions f opts =
RunOptions { runRunner = optRunner opts
, runRuns = optRuns opts
, runExtraOptions = optExtraOptions opts
, runTimeout = optTimeout opts
, runVerbose = optVerbose opts
, runResultAction = Just f
}
progressBar :: Int -> Int -> Int -> String
progressBar cur bound steps =
"[" ++ map cell [1..steps] ++ "] " ++ show cur ++ "/" ++ show bound
where step_size :: Double
step_size = fromIntegral bound / fromIntegral steps
cur' = fromIntegral cur
chars = " ▏▎▍▍▌▋▊▉█"
char i = fromMaybe ' ' $ maybeNth (i::Int) chars
num_chars = fromIntegral $ length chars
cell :: Int -> Char
cell i
| i' * step_size <= cur' = char 9
| otherwise = char (floor (((cur' - (i'-1) * step_size) * num_chars)
/ step_size))
where i' = fromIntegral i
descString :: String -> Int -> String
descString desc pad_to = desc ++ ": " ++ replicate (pad_to - length desc) ' '
mkProgressPrompt :: Int -> Int -> String -> IO (Maybe Int -> IO ())
mkProgressPrompt runs pad_to dataset_desc
| fancyTerminal = do
count <- newIORef (0::Int)
return $ \us -> do
putStr "\r" -- Go to start of line.
i <- readIORef count
let i' = if isJust us then i+1 else i
writeIORef count i'
putStr $ descString dataset_desc pad_to ++ progressBar i' runs 10
putStr " " -- Just to move the cursor away from the progress bar.
hFlush stdout
| otherwise = do
putStr $ descString dataset_desc pad_to
hFlush stdout
return $ const $ return ()
reportResult :: [RunResult] -> IO ()
reportResult results = do
let runtimes = map (fromIntegral . runMicroseconds) results
avg = sum runtimes / fromIntegral (length runtimes)
rsd = stddevp runtimes / mean runtimes :: Double
putStrLn $ printf "%10.0fμs (RSD: %.3f; min: %3.0f%%; max: %+3.0f%%)"
avg rsd ((minimum runtimes / avg - 1) * 100) ((maxinum runtimes / avg - 1) * 100)
runBenchmarkCase :: BenchOptions -> FilePath -> T.Text -> Int -> TestRun
-> IO (Maybe DataResult)
runBenchmarkCase _ _ _ _ (TestRun _ _ RunTimeFailure{} _ _) =
return Nothing -- Not our concern, we are not a testing tool.
runBenchmarkCase opts _ _ _ (TestRun tags _ _ _ _)
| any (`elem` tags) $ optExcludeCase opts =
return Nothing
runBenchmarkCase opts program entry pad_to tr@(TestRun _ input_spec (Succeeds expected_spec) _ dataset_desc) = do
prompt <- mkProgressPrompt (optRuns opts) pad_to dataset_desc
-- Report the dataset name before running the program, so that if an
-- error occurs it's easier to see where.
prompt Nothing
res <- benchmarkDataset (runOptions (prompt . Just) opts)
program entry input_spec expected_spec
(testRunReferenceOutput program entry tr)
when fancyTerminal $ do
clearLine
putStr "\r"
case res of
Left err -> do
liftIO $ putStrLn $ descString dataset_desc pad_to
liftIO $ putStrLn $ inRed $ T.unpack err
return $ Just $ DataResult dataset_desc $ Left err
Right (runtimes, errout) -> do
when fancyTerminal $
putStr $ descString dataset_desc pad_to
reportResult runtimes
return $ Just $ DataResult dataset_desc $ Right (runtimes, errout)
commandLineOptions :: [FunOptDescr BenchOptions]
commandLineOptions = [
Option "r" ["runs"]
(ReqArg (\n ->
case reads n of
[(n', "")] | n' > 0 ->
Right $ \config ->
config { optRuns = n'
}
_ ->
Left $ error $ "'" ++ n ++ "' is not a positive integer.")
"RUNS")
"Run each test case this many times."
, Option [] ["backend"]
(ReqArg (\backend -> Right $ \config -> config { optBackend = backend })
"PROGRAM")
"The compiler used (defaults to 'futhark-c')."
, Option [] ["futhark"]
(ReqArg (\prog -> Right $ \config -> config { optFuthark = Just prog })
"PROGRAM")
"The binary used for operations (defaults to same binary as 'futhark bench')."
, Option [] ["runner"]
(ReqArg (\prog -> Right $ \config -> config { optRunner = prog }) "PROGRAM")
"The program used to run the Futhark-generated programs (defaults to nothing)."
, Option "p" ["pass-option"]
(ReqArg (\opt ->
Right $ \config ->
config { optExtraOptions = opt : optExtraOptions config })
"OPT")
"Pass this option to programs being run."
, Option [] ["pass-compiler-option"]
(ReqArg (\opt ->
Right $ \config ->
config { optCompilerOptions = opt : optCompilerOptions config })
"OPT")
"Pass this option to the compiler (or typechecker if in -t mode)."
, Option [] ["json"]
(ReqArg (\file ->
Right $ \config -> config { optJSON = Just file})
"FILE")
"Scatter results in JSON format here."
, Option [] ["timeout"]
(ReqArg (\n ->
case reads n of
[(n', "")]
| n' < max_timeout ->
Right $ \config -> config { optTimeout = fromIntegral n' }
_ ->
Left $ error $ "'" ++ n ++
"' is not an integer smaller than" ++ show max_timeout ++ ".")
"SECONDS")
"Number of seconds before a dataset is aborted."
, Option [] ["skip-compilation"]
(NoArg $ Right $ \config -> config { optSkipCompilation = True })
"Use already compiled program."
, Option [] ["exclude-case"]
(ReqArg (\s -> Right $ \config ->
config { optExcludeCase = s : optExcludeCase config })
"TAG")
"Do not run test cases with this tag."
, Option [] ["ignore-files"]
(ReqArg (\s -> Right $ \config ->
config { optIgnoreFiles = makeRegex s : optIgnoreFiles config })
"REGEX")
"Ignore files matching this regular expression."
, Option "e" ["entry-point"]
(ReqArg (\s -> Right $ \config ->
config { optEntryPoint = Just s })
"NAME")
"Only run this entry point."
, Option [] ["tuning"]
(ReqArg (\s -> Right $ \config -> config { optTuning = Just s })
"EXTENSION")
"Look for tuning files with this extension (defaults to .tuning)."
, Option [] ["no-tuning"]
(NoArg $ Right $ \config -> config { optTuning = Nothing })
"Do not load tuning files."
, Option [] ["concurrency"]
(ReqArg (\n ->
case reads n of
[(n', "")]
| n' > 0 ->
Right $ \config -> config { optConcurrency = Just n' }
_ ->
Left $ error $ "'" ++ n ++ "' is not a positive integer.")
"NUM")
"Number of benchmarks to prepare (not run) concurrently."
, Option "v" ["verbose"]
(NoArg $ Right $ \config -> config { optVerbose = optVerbose config + 1 })
"Enable logging. Pass multiple times for more."
]
where max_timeout :: Int
max_timeout = maxBound `div` 1000000
-- | Run @futhark bench@.
main :: String -> [String] -> IO ()
main = mainWithOptions initialBenchOptions commandLineOptions "options... programs..." $ \progs config ->
Just $ runBenchmarks config progs
--- The following extracted from hstats package by Marshall Beddoe:
--- https://hackage.haskell.org/package/hstats-0.3
-- | Numerically stable mean
mean :: Floating a => [a] -> a
mean x = fst $ foldl' (\(!m, !n) x' -> (m+(x'-m)/(n+1),n+1)) (0,0) x
-- | Standard deviation of population
stddevp :: (Floating a) => [a] -> a
stddevp xs = sqrt $ pvar xs
-- | Population variance
pvar :: (Floating a) => [a] -> a
pvar xs = centralMoment xs (2::Int)
-- | Central moments
centralMoment :: (Floating b, Integral t) => [b] -> t -> b
centralMoment _ 1 = 0
centralMoment xs r = sum (map (\x -> (x-m)^r) xs) / n
where
m = mean xs
n = fromIntegral $ length xs