wrecker-0.1.0.0: src/Wrecker/Runner.hs
{-# LANGUAGE RecordWildCards, ScopedTypeVariables, LambdaCase, BangPatterns #-}
module Wrecker.Runner where
import Wrecker.Logger
import System.Posix.Signals
import Data.Function
import qualified Control.Concurrent.Thread.BoundedThreadGroup as BoundedThreadGroup
import Control.Concurrent
import Control.Exception
import Wrecker.Recorder
import Wrecker.Statistics
import Control.Monad
import Data.Foldable (for_)
import System.Timeout
import qualified Graphics.Vty as VTY
import Control.Concurrent.NextRef
import Wrecker.Options
import Data.IORef
import System.IO
import Control.Concurrent.STM.TChan
import Control.Concurrent.STM
import Data.List (isInfixOf)
import Data.Aeson (encode)
import qualified Data.ByteString.Lazy as BSL
-- TODO configure whether errors are used in times or not
updateSampler :: NextRef AllStats -> Event -> IO AllStats
updateSampler !ref !event = modifyNextRef ref
$ \x -> let !new = stepAllStats x
(eRunIndex event)
(name $ result event)
(result event)
in (new, new)
collectEvent :: Logger -> NextRef AllStats -> Recorder -> IO ()
collectEvent logger ref recorder = fix $ \next -> do
mevent <- readEvent recorder
for_ mevent $ \event -> do
sampler <- updateSampler ref event
logDebug logger $ show sampler
next
runAction :: Logger
-> Int
-> Int
-> RunType
-> (Recorder -> IO ())
-> Recorder -> IO ()
runAction logger timeoutTime concurrency runStyle action recorder = do
threadLimit <- BoundedThreadGroup.new concurrency
recorderRef <- newIORef recorder
let takeRecorder = atomicModifyIORef' recorderRef $ \x -> (split x, x)
actionThread = void
$ BoundedThreadGroup.forkIO threadLimit $ do
rec <- takeRecorder
handle (\(e :: SomeException) -> do
logWarn logger $ show e
addEvent recorder $ Error
{ resultTime = 0
, exception = e
, name = "__UNKNOWN__"
}
addEvent rec End
)
$ do
action rec
addEvent rec End
case runStyle of
RunCount count -> replicateM_ (count * concurrency) actionThread
RunTimed time -> void $ timeout time $ forever actionThread
mtimeout <- timeout timeoutTime $ BoundedThreadGroup.wait threadLimit
case mtimeout of
Nothing -> void
$ logError logger
$ "Timed out waiting for all "
++ "threads to complete"
Just () -> return ()
------------------------------------------------------------------------------
--- Generic Run Function
-------------------------------------------------------------------------------
runWithNextVar :: Options
-> (NextRef AllStats -> IO ())
-> (NextRef AllStats -> IO ())
-> (Recorder -> IO ())
-> IO ()
runWithNextVar (Options {..}) consumer final action = do
recorder <- newRecorder 100000
sampler <- newNextRef emptyAllStats
logger <- newStdErrLogger 100000 logLevel
-- Collect events and
forkIO $ handle (\(e :: SomeException) -> void $ logError logger $ show e)
$ collectEvent logger sampler recorder
consumer sampler
logDebug logger "Starting Runs"
runAction logger timeoutTime concurrency runStyle action recorder `finally`
(do logDebug logger "Shutting Down"
stopRecorder recorder
shutdownLogger 1000000 logger
final sampler
)
-------------------------------------------------------------------------------
--- Non-interactive Rendering
-------------------------------------------------------------------------------
printLastSamples :: Options -> NextRef AllStats -> IO ()
printLastSamples options sampler = printStats options =<< readLast sampler
runNonInteractive :: Options -> (Recorder -> IO ()) -> IO ()
runNonInteractive options action = do
let shutdown sampler = do
putStrLn ""
hFlush stdout
hSetBuffering stdout (BlockBuffering (Just 100000000))
printLastSamples options sampler
hFlush stdout
for_ (outputFilePath options) $ \filePath ->
BSL.writeFile filePath . encode =<< readLast sampler
runWithNextVar options (const $ return ()) shutdown action
-------------------------------------------------------------------------------
--- Interactive Rendering
-------------------------------------------------------------------------------
printLoop :: Options
-> VTY.DisplayContext
-> VTY.Vty
-> NextRef AllStats
-> IO ()
printLoop options context vty sampler
= fix $ \next -> takeNextRef sampler >>= \case
Nothing -> return ()
Just allStats -> do
updateUI (requestNameColumnSize options) context allStats
VTY.refresh vty
next
processInputForCtrlC :: TChan VTY.Event -> IO ThreadId
processInputForCtrlC chan = forkIO $ forever $ do
event <- atomically $ readTChan chan
case event of
VTY.EvKey (VTY.KChar 'c') [VTY.MCtrl] -> raiseSignal sigINT
_ -> return ()
updateUI :: Maybe Int -> VTY.DisplayContext -> AllStats -> IO ()
updateUI nameSize displayContext stats
= VTY.outputPicture displayContext
$ VTY.picForImage
$ VTY.vertCat
$ map (VTY.string VTY.defAttr)
$ lines
$ pprStats nameSize stats
runInteractive :: Options -> (Recorder -> IO ()) -> IO ()
runInteractive options action = do
vtyConfig <- VTY.standardIOConfig
vty <- VTY.mkVty vtyConfig
let output = VTY.outputIface vty
(width, height) <- VTY.displayBounds output
displayContext <- VTY.displayContext output (width, height)
inputThread <- processInputForCtrlC $ VTY._eventChannel $ VTY.inputIface vty
let shutdown sampler = do
killThread inputThread
VTY.shutdown vty
putStrLn ""
hSetBuffering stdout (BlockBuffering (Just 100000000))
printLastSamples options sampler
hFlush stdout
for_ (outputFilePath options) $ \filePath ->
BSL.writeFile filePath . encode =<< readLast sampler
runWithNextVar options (\sampler -> void
$ forkIO (printLoop options
displayContext
vty
sampler
)
)
shutdown
action
-------------------------------------------------------------------------------
--- Main Entry Point
-------------------------------------------------------------------------------
-- | 'run' is the a lower level entry point, compared to 'defaultMain'. Unlike
-- 'defaultMain' no command line argument parsing is performed. Instead,
-- 'Options' are directly passed in. 'defaultOptions' can be used as a
-- default argument for 'run'.
--
-- Like 'defaultMain', 'run' creates a 'Recorder' and passes it each
-- benchmark.
run :: Options -> [(String, Recorder -> IO ())] -> IO ()
run options actions = do
hSetBuffering stderr LineBuffering
forM_ actions $ \(groupName, action) -> do
when (match options `isInfixOf` groupName) $ do
putStrLn groupName
case displayMode options of
NonInteractive -> runNonInteractive options action
Interactive -> runInteractive options action