packages feed

wrecker-0.1.1.0: src/Wrecker/Runner.hs

{-# LANGUAGE RecordWildCards, ScopedTypeVariables, LambdaCase, BangPatterns  #-}
{-# LANGUAGE TupleSections #-}
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
import qualified Data.HashMap.Strict as H
import Data.HashMap.Strict (HashMap)
import qualified Control.Immortal as Immortal
-- TODO configure whether errors are used in times or not

newStandaloneRecorder :: IO (NextRef AllStats, Immortal.Thread, Recorder)
newStandaloneRecorder = do
  recorder      <- newRecorder 10000
  logger        <- newStdErrLogger 1000 LevelError 
  (ref, thread) <- sinkRecorder logger recorder
  return (ref, thread, recorder)

sinkRecorder :: Logger -> Recorder -> IO (NextRef AllStats, Immortal.Thread)
sinkRecorder logger recorder = do 
  ref <- newNextRef emptyAllStats
  
  immortal <- Immortal.createWithLabel "collectEvent" 
            $ \_ -> collectEvent logger ref recorder
  
  return (ref, immortal)

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 AllStats
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
    )
  readLast sampler
-------------------------------------------------------------------------------
---   Non-interactive Rendering
-------------------------------------------------------------------------------
printLastSamples :: Options -> NextRef AllStats -> IO ()
printLastSamples options sampler = printStats options =<< readLast sampler

runNonInteractive :: Options -> (Recorder -> IO ()) -> IO AllStats
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 AllStats
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 (HashMap String AllStats)
run options actions = do
  hSetBuffering stderr LineBuffering

  fmap H.fromList . forM actions $ \(groupName, action) -> do
    if (match options `isInfixOf` groupName) then do
      putStrLn groupName
      (groupName, ) <$> case displayMode options of  
        NonInteractive -> runNonInteractive options action
        Interactive    -> runInteractive    options action
    else
      return (groupName, emptyAllStats)