packages feed

ghc-bench-0.3.3: src/Benchmark/Type.hs

module Benchmark.Type (
  Label(..)
, Seconds(..)
, Benchmark
, dependencies
, dryRun
, prepare
, run

, withLabel
, setEnv
, cd
, download
, call
, measure
) where

import Imports

import GHC.Clock (getMonotonicTimeNSec)
import Data.Char (isSpace)
import Data.Text qualified as T
import Data.Text.IO.Utf8 qualified as Utf8
import System.FilePath (splitPath)
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Writer.CPS hiding (tell)
import Control.Monad.Trans.Writer.CPS qualified as Writer

import Blob (Blob)
import Blob qualified
import Command (Env(..), callWith)

newtype Label = Label Text
  deriving newtype (Eq, Show, Ord, IsString)

newtype Seconds = Seconds Int
  deriving newtype (Eq, Show, Num, Ord, Bounded)

data Command =
    SetEnv String String [Command]
  | ChangeDirectory FilePath [Command]
  | Download Blob
  | Call FilePath [FilePath]
  | Measure Label [Command]
  deriving (Show)

type Benchmark = ReaderT [String] (Writer [Command])

dependencies :: Benchmark () -> [FilePath]
dependencies = collectDependencies . toForest

dryRun :: Benchmark () -> IO [(Label, Seconds)]
dryRun action = do
  let (times, output) = dryRunForest $ toForest action
  Utf8.putStrLn output
  return times

prepare :: Benchmark () -> IO [(Label, Seconds)]
prepare = execForest . dropMeasuredCommands . toForest

run :: Benchmark () -> IO [(Label, Seconds)]
run = execForest . toForest

toForest :: Benchmark () -> [Command]
toForest = toForestWith []

toForestWith :: [String] -> Benchmark () -> [Command]
toForestWith labels action = execWriter $ runReaderT action labels

tell :: Command -> Benchmark ()
tell = lift . Writer.tell . return

withLabel :: String -> Benchmark () -> Benchmark ()
withLabel = local . (:)

setEnv :: String -> String -> Benchmark () -> Benchmark ()
setEnv key value action = do
  labels <- ask
  let commands = toForestWith labels action
  tell $ SetEnv key value commands

cd :: FilePath -> Benchmark () -> Benchmark ()
cd dir action = do
  labels <- ask
  let commands = toForestWith labels action
  tell $ ChangeDirectory dir commands

download :: Blob -> Benchmark ()
download = tell . Download

call :: FilePath -> [FilePath] -> Benchmark ()
call command = tell . Call command

measure :: String -> Benchmark () -> Benchmark ()
measure label action = withLabel label do
  labels <- ask
  let commands = toForestWith labels action
  tell $ Measure (toLabel labels) commands

collectDependencies :: [Command] -> [FilePath]
collectDependencies = concatMap \ case
  Download _ -> []
  Call path _ -> case splitPath path of
    [name] -> [name]
    _ -> []
  SetEnv _ _ commands -> collectDependencies commands
  ChangeDirectory _ commands -> collectDependencies commands
  Measure _ commands -> collectDependencies commands

dryRunForest :: [Command] -> ([(Label, Seconds)], Text)
dryRunForest = fmap unlines . runWriter . \ commands -> do
  writeLine ""
  writeLine "############################################"
  writeLine "# With a fresh temporary working directory #"
  writeLine "############################################"
  writeLine ""
  go commands
  where
    writeLine :: Text -> Writer [Text] ()
    writeLine = Writer.tell . (: [])

    go :: [Command] -> Writer [Text] [(Label, Seconds)]
    go = fmap concat . traverse \ case
      ChangeDirectory dir commands -> do
        writeLine $ "cd " <> pack dir
        go commands

      SetEnv name value commands -> do
        writeLine $ "export " <> pack name <> "=" <> pack value
        go commands

      Download blob -> do
        writeLine ""
        writeLine $ "# Ensure that " <> pack blob.path <> " exists; download if necessary."
        writeLine $ "# url: " <> pack blob.url
        writeLine $ "# hash: " <> blob.hash
        writeLine ""
        return []

      Call command args -> do
        writeLine $ showCommand command args
        return []

      Measure (Label label) commands -> do
        writeLine ""
        writeLine $ "# MEASURE " <> label
        times <- go commands
        return $ (Label label, 0) : times

showCommand :: String -> [String] -> Text
showCommand command = unwords . map showArg . (:) command

showArg :: String -> Text
showArg arg
  | any isSpace arg = show arg
  | otherwise = pack arg

dropMeasuredCommands :: [Command] -> [Command]
dropMeasuredCommands = map \ case
  SetEnv name value commands -> SetEnv name value (dropMeasuredCommands commands)
  ChangeDirectory dir commands -> ChangeDirectory dir (dropMeasuredCommands commands)
  Download blob -> Download blob
  Call command args -> Call command args
  Measure label _ -> Measure label []

execForest :: [Command] -> IO [(Label, Seconds)]
execForest = go mempty
  where
    go env = fmap concat . traverse \ case
      SetEnv name value commands -> do
        go env { extend = (name, value) : env.extend } commands

      ChangeDirectory dir commands -> do
        go env { dir = env.dir </> dir } commands

      Download blob -> do
        Blob.download blob $> []

      Call command args -> do
        Command.callWith env command args $> []

      Measure label cs -> do
        start <- getMonotonicTimeNSec
        times <- go env cs
        end <- getMonotonicTimeNSec
        let
          time :: Double
          time = fromIntegral (end - start) / 1e9
        return $ (label, Seconds (round time)) : times

toLabel :: [String] -> Label
toLabel = Label . T.intercalate "-" . map pack . reverse