packages feed

pure-borrow-0.0.0.0: app/artifact-runner.hs

{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

module Main (main) where

import Control.Applicative
import Control.Concurrent (getNumCapabilities, setNumCapabilities)
import Control.Exception (throwIO, try)
import Control.Monad (forM_)
import Control.Monad.Trans.Writer.CPS (execWriter, tell)
import Data.ByteString qualified as BS
import Data.ByteString.Builder qualified as BB
import Data.ByteString.Lazy qualified as LBS
import Data.Csv (FromNamedRecord (..), decodeByName, (.:))
import Data.FileEmbed
import Data.Foldable (fold)
import Data.Foldable1 (fold1)
import Data.Functor
import Data.IntMap.Monoidal.Strict (MonoidalIntMap)
import Data.IntMap.Monoidal.Strict qualified as MIM
import Data.IntSet qualified as IS
import Data.List qualified as List
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Monoidal.Strict (MonoidalMap)
import Data.Map.Monoidal.Strict qualified as MonoidalMap
import Data.Monoid (First (..))
import Data.Semigroup qualified as Semi
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import GHC.Generics
import Options.Applicative qualified as Opts
import PureBorrow.Demo.QSort qualified as QS
import PureBorrow.Internal.Bench.QSort (BenchOpts)
import PureBorrow.Internal.Bench.QSort qualified as Bench
import System.Directory (canonicalizePath, findExecutable)
import System.Environment (withArgs)
import System.Exit (ExitCode)
import System.IO (hClose, hFlush)
import System.IO.Temp (withSystemTempFile)
import System.Process (readProcess)
import Text.Read (readEither)

data Cmd = Bench BenchOpts | QuickBench | QSortDemo QS.CLIOpts
  deriving (Show, Eq, Ord, Generic)

optionsP :: Int -> Opts.ParserInfo Cmd
optionsP numCapa =
  Opts.info (p <**> Opts.helper) $
    Opts.fullDesc
      <> Opts.progDesc "Artifact runner for qsort benchmarks and demos"
  where
    p = cmds <|> Bench <$> Bench.rawOptsP
    cmds =
      Opts.hsubparser $
        fold1 $
          Opts.command "bench" (Bench <$> Bench.optionsP)
            :| [ Opts.command "demo" $ QSortDemo <$> QS.optionsP numCapa
               , Opts.command "quick" $
                   Opts.info (pure QuickBench) $
                     Opts.progDesc $
                       "Run quick benchmarks with numcpu = 4 for sizes 0 and " <> show Bench.kMAX_SIZE
               ]

main :: IO ()
main = do
  numCap <- getNumCapabilities
  Opts.customExecParser (Opts.prefs Opts.subparserInline) (optionsP numCap) >>= \case
    Bench benchOpts -> runBench benchOpts
    QuickBench -> runBench Bench.BenchOpts {numThreads = 4, sampleSize = 2}
    QSortDemo cliOpts -> QS.defaultMainWith cliOpts

runBench :: BenchOpts -> IO ()
runBench benchOpts = do
  let rawDest = "qsort-raw.csv"
  void $ try @ExitCode $ withArgs ["--csv", rawDest, "-j1", "--time-mode=wall", "-t", "10s"] do
    setNumCapabilities benchOpts.numThreads
    Bench.defaultMainWith benchOpts

  putStrLn "Processing results..."
  (_, rawRows) <- either (throwIO . userError) pure . decodeByName =<< LBS.readFile rawDest
  let sd = foldMap fromRawRow rawRows
      builder = buildOutput sd
  csvDest <- canonicalizePath "qsort.csv"
  BB.writeFile csvDest builder

  mgp <- findExecutable "gnuplot"
  forM_ mgp \gnuplot -> withSystemTempFile "plot.gp" \tmp h -> do
    putStrLn $ "Gnuplot found: " <> gnuplot
    pngDest <- canonicalizePath "qsort.png"
    BS.hPutStr h gnuplotScript
    hFlush h
    hClose h
    !_ <- readProcess gnuplot ["-e", "input='" <> csvDest <> "'; output='" <> pngDest <> "'", tmp] ""
    putStrLn $ "Plot generated: " <> pngDest

gnuplotScript :: BS.ByteString
gnuplotScript =
  $(embedFile "scripts/genplot.gnuplot")

buildOutput :: Statistics -> BB.Builder
buildOutput sd = execWriter do
  let (hdrs, targets) = toHeaders sd

  putLine $
    fold $
      List.intersperse "," $
        map (BB.byteString . TE.encodeUtf8) hdrs
  forM_ (MIM.toAscList sd) \(size, ps) -> do
    let row =
          BB.intDec size
            : concatMap
              ( \t ->
                  maybe (replicate 5 mempty) (\p -> map BB.doubleDec [p.mean, p.stddev, p.alloc, p.copied, p.peak]) $
                    lookupStat t ps
              )
              targets
    putLine $ fold $ List.intersperse "," row
  where
    crlf = tell "\r\n"
    putLine = (>> crlf) . tell

data RawRow = RawRow
  { size :: !Int
  , name :: !T.Text
  , mean :: !Int
  , stddev :: !Int
  , alloc :: !Int
  , copied :: !Int
  , peak :: !Int
  }
  deriving (Show, Eq, Ord, Generic)

instance FromNamedRecord RawRow where
  parseNamedRecord r = do
    fullName <- r .: "Name"
    let ~(sz : name : _) = drop 2 $ T.splitOn "." fullName
    size <- either fail pure $ readEither $ T.unpack sz
    mean <- r .: "Mean (ps)"
    stddev <- r .: "2*Stdev (ps)" <&> (`quot` 2)
    alloc <- r .: "Allocated"
    copied <- r .: "Copied"
    peak <- r .: "Peak Memory"
    pure RawRow {..}

data Performance = Performance
  { mean :: !Double
  , stddev :: !Double
  , alloc :: !Double
  , copied :: !Double
  , peak :: !Double
  }
  deriving (Show, Eq, Ord, Generic)
  deriving (Semigroup) via Semi.First Performance

toPerformance :: RawRow -> Performance
toPerformance RawRow {..} =
  Performance
    { mean = fromIntegral mean * 1e-9
    , stddev = fromIntegral stddev * 1e-9
    , alloc = fromIntegral alloc * 1e-6
    , copied = fromIntegral copied * 1e-6
    , peak = fromIntegral peak * 1e-6
    }

data PerformanceSet = PerformanceSet
  { intro :: !(First Performance)
  , sequential :: !(First Performance)
  , parallel :: !(MonoidalIntMap Performance)
  , worksteal :: !(MonoidalIntMap Performance)
  , others :: !(MonoidalMap T.Text Performance)
  }
  deriving (Show, Eq, Ord, Generic)
  deriving (Semigroup, Monoid) via Generically PerformanceSet

type Statistics = MonoidalIntMap PerformanceSet

data Target = Intro | Sequential | Parallel Int | Worksteal Int | Other T.Text
  deriving (Show, Eq, Ord, Generic)

lookupStat :: Target -> PerformanceSet -> Maybe Performance
lookupStat t ps =
  case t of
    Intro -> getFirst ps.intro
    Sequential -> getFirst ps.sequential
    Parallel n -> MIM.lookup n ps.parallel
    Worksteal n -> MIM.lookup n ps.worksteal
    Other name -> MonoidalMap.lookup name ps.others

toHeaders :: Statistics -> ([T.Text], [Target])
toHeaders stats =
  (headers, targets)
  where
    headers = "size" : [cat <> metric | cat <- categories, metric <- metrics]
    targets = Intro : Sequential : [Parallel n | n <- IS.toList parallels] ++ [Worksteal n | n <- IS.toList worksteals] ++ [Other name | name <- Set.toList miscs]
    (parallels, worksteals, miscs) =
      foldMap
        ( \ps ->
            ( MIM.keysSet ps.parallel
            , MIM.keysSet ps.worksteal
            , MonoidalMap.keysSet ps.others
            )
        )
        stats
    categories =
      "intro"
        : "sequential"
        : [T.pack $ "parallel" <> show n | n <- IS.toList parallels]
        ++ [T.pack $ "workSteal" <> show n | n <- IS.toList worksteals]
        ++ [name | name <- Set.toList miscs]

    metrics = ["Mean", "Stddev", "Alloc", "Copied", "Peak"]

fromRawRow :: RawRow -> Statistics
fromRawRow r@RawRow {..} = MIM.singleton size $
  case name of
    "intro" -> mempty {intro = First (Just $ toPerformance r)}
    "sequential" -> mempty {sequential = First (Just $ perf)}
    inp
      | Just rest <- T.stripPrefix "parallel (budget =" inp
      , [(n, _)] <- reads (T.unpack rest) ->
          mempty {parallel = MIM.singleton n perf}
      | Just rest <- T.stripPrefix "worksteal (workers =" inp
      , [(n, _)] <- reads (T.unpack rest) ->
          mempty {worksteal = MIM.singleton n perf}
    _ -> mempty {others = MonoidalMap.singleton name perf}
  where
    perf = toPerformance r