packages feed

fibon-0.2.0: tools/fibon-run/Fibon/Run/BenchmarkRunner.hs

module Fibon.Run.BenchmarkRunner (
    RunResult(..)
  , RunFailure(..)
  , Fibon.Run.BenchmarkRunner.run
)
where

import Control.Concurrent
import Control.Monad
import Control.Exception
import qualified Data.ByteString as B
import Data.Maybe
import Data.Time.Clock
import qualified Data.Vector.Unboxed as Vector
import Fibon.BenchmarkInstance
import Fibon.Result
import Fibon.Run.BenchmarkBundle
import Fibon.Run.Log as Log
import qualified Fibon.Run.SysTools as SysTools
import Statistics.Sample
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import System.Process
import Text.Printf

data RunResult =
    Success {runSummary :: RunSummary, runDetails :: [RunDetail]}
  | Failure [RunFailure]
  deriving (Read, Show)

data RunFailure =
    MissingOutput FilePath
  | DiffError     String
  | Timeout
  deriving (Read, Show)

run :: BenchmarkBundle -> IO RunResult
run bb = do
  let bmk = (bundleName bb)
      pwd = (pathToExeBuildDir bb)
      cmd = (prettyRunCommand bb)
  Log.info $ "Running Benchmark "
  Log.info $ "   BMK: " ++ bmk
  Log.info $ "   PWD: " ++ pwd
  Log.info $ "   CMD: " ++ cmd
  Log.info $ printf "\n@%s|%s|%s" bmk pwd cmd
  runDirect bb

{-
-- Move this to analysis time
analyze :: Sample -> ExtraStats -> Int -> Double -> IO RunSummary
analyze times ghcStats numResamples ci = do
  let ests = [mean, stdDev]
  res   <- withSystemRandom $ \gen ->
            resample gen ests numResamples times :: IO [Resample]
  let [em,es] = bootstrapBCA ci times ests res
  let runData = RunSummary {
                timeSummary =
                  TimeMeasurement {
                      meanTime     = estPoint em
                    , meanTimeLB   = estLowerBound em
                    , meanTimeUB   = estUpperBound em
                    , meanStddev   = estPoint es
                    , meanStddevUB = estLowerBound es
                    , meanStddevLB = estUpperBound es
                    , confidence   = ci
                  }
              , statsSummary = ghcStats
  }
  return runData
-}

checkResult :: BenchmarkBundle -> IO (Maybe [RunFailure])
checkResult bb = do
  rs <- mapM (checkOutput bb) (output . benchDetails $ bb)
  let errs = filter isJust rs
  case errs of
    [] -> return $ Nothing
    es -> return $ Just (catMaybes es)

checkOutput :: BenchmarkBundle -> OutputDescription -> IO (Maybe RunFailure)
checkOutput bb (o, Exists) = do
  let f = (destinationToRealFile bb o)
  e <- doesFileExist f
  if e then return   Nothing
       else return $ Just $ MissingOutput ("File "++f++" does not exist")
checkOutput bb (o, Diff diffFile) = do
  e1 <- checkOutput bb (o, Exists)
  e2 <- checkOutput bb (d, Exists)
  e3 <- runDiff f1 f2
  return $ msum [e1, e2, e3]
  where
  d  = OutputFile diffFile
  f1 = (destinationToRealFile bb o)
  f2 = (destinationToRealFile bb d)

runDiff :: FilePath -> FilePath -> IO (Maybe RunFailure)
runDiff f1 f2 = do
  Log.info $ "Diffing files: "++f1++" "++f2
  (r, o, _) <- readProcessWithExitCode (SysTools.diff) [f1, f2] ""
  if r == ExitSuccess then Log.info "No diff error" >>
                           return   Nothing
                      else Log.info "Diff error" >>
                           (return $ Just $ DiffError o)

destinationToRealFile :: BenchmarkBundle -> OutputDestination -> FilePath
destinationToRealFile bb (OutputFile f) = (pathToExeRunDir bb)  </> f
destinationToRealFile bb  Stdout        = (pathToStdoutFile bb)
destinationToRealFile bb  Stderr        = (pathToStderrFile bb)

readExtraStats :: BenchmarkBundle -> IO ExtraStats
readExtraStats bb = do
  let mbStatsFile   = extraStats bb
      statsFile     = fromJust mbStatsFile
      logReadE :: IOException -> IO ExtraStats
      logReadE e =
        Log.warn ("Error reading stats file: "++statsFile++"\n  "++show e)
        >> return B.empty
  case mbStatsFile of
    Nothing -> return B.empty
    Just f  -> do
      handle logReadE $
        bracket (openFile ((pathToExeRunDir bb) </> f) ReadMode)
                (hClose)
                (\h -> B.hGetContents h >>= \s -> B.length s `seq` return s)
                    --stats <- hGetContents h
                    -- drop header line in machine readable stats
                    --let body = (unlines . drop 1 . lines) stats
                    --case reads body of
                    --    [(p, _)] -> return p
                    --    _        -> logParseE)

type RunStepResult = IO (Either [RunFailure] RunDetail)

runDirect :: BenchmarkBundle -> IO RunResult
runDirect bb = do
  mbDetails <- go count []
  case mbDetails of
    Left e   -> return $ Failure e
    Right ds -> return $ Success (summarize ds) ds
  where
  go 0 ds = return $ Right (reverse ds)
  go n ds = do
    res <- runB bb
    case res of
      Right d -> go (n-1) (d:ds)
      Left e  -> return $ Left e
  runB    = maybe runBenchmarkWithoutTimeout runBenchmarkWithTimeout limit
  limit   = timeout bb
  count   = (iters bb)

summarize :: [RunDetail] -> RunSummary
summarize ds = RunSummary {
      meanTime     = mean times
    , stdDevTime   = stdDev times
    , statsSummary = stats
  }
  where
    times = (Vector.fromList $ map runTime ds)
    stats = case ds of (x:_) -> runStats x; _ -> B.empty

type TimeoutLength = Int
runBenchmarkWithTimeout :: TimeoutLength -> BenchmarkBundle -> RunStepResult
runBenchmarkWithTimeout us bb = do
  resMVar <- newEmptyMVar
  pidMVar <- newEmptyMVar
  tid1 <- forkIO $ (putMVar resMVar . Just) =<< timeBenchmarkExe bb (Just pidMVar)
  _    <- forkIO $ threadDelay us >> putMVar resMVar Nothing
  res <- takeMVar resMVar
  case res of
    Nothing -> do
      Log.info $ "benchmark timed out after "++(show us)++" us"
      -- try to kill the subprocess
      pid <- tryTakeMVar pidMVar
      maybe pass terminateProcess pid
      -- kill the haskell thread
      killThread tid1
      return $ Left [Timeout]
    Just runDetail -> do
       maybe (Right runDetail) Left `liftM` checkResult bb

runBenchmarkWithoutTimeout :: BenchmarkBundle -> RunStepResult
runBenchmarkWithoutTimeout bb = do
  runDetail <- timeBenchmarkExe bb Nothing
  maybe (Right runDetail) Left `liftM` checkResult bb
      
timeBenchmarkExe :: BenchmarkBundle            -- benchmark to run
                 -> Maybe (MVar ProcessHandle) -- in case we need to kill it
                 -> IO RunDetail
timeBenchmarkExe bb pidMVar = do
  p     <- bundleProcessSpec bb
  start <- getCurrentTime
  (_, _, _, pid) <- createProcess p
  maybe pass (flip putMVar pid) pidMVar
  _  <- waitForProcess pid
  end   <- getCurrentTime
  mapM_ closeStdIO [std_in  p, std_out p, std_err p]
  stats <- readExtraStats bb
  return $ RunDetail (realToFrac (diffUTCTime end start)) stats

closeStdIO :: StdStream -> IO ()
closeStdIO (UseHandle h) = hClose h
closeStdIO _             = return ()

pass :: IO ()
pass = return()