shake-bench 0.1.1.0 → 0.2.0.0
raw patch · 2 files changed
+122/−62 lines, 2 filesdep +bytestring
Dependencies added: bytestring
Files
- shake-bench.cabal +8/−4
- src/Development/Benchmark/Rules.hs +114/−58
shake-bench.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: shake-bench-version: 0.1.1.0+version: 0.2.0.0 synopsis: Build rules for historical benchmarking license: Apache-2.0 license-file: LICENSE@@ -8,16 +8,20 @@ maintainer: pepeiborra@gmail.com category: Development build-type: Simple-description:- A library Shake rules to build and run benchmarks for multiple revisions of a project.- An example of usage can be found in the ghcide benchmark suite+-- description is a single line so that implicit-hie can parse it+description: A library Shake rules to build and run benchmarks for multiple revisions of a project. An example of usage can be found in the ghcide benchmark suite +source-repository head+ type: git+ location: https://github.com/haskell/haskell-language-server.git+ library exposed-modules: Development.Benchmark.Rules hs-source-dirs: src build-depends: aeson, base == 4.*,+ bytestring, Chart, Chart-diagrams, diagrams-contrib,
src/Development/Benchmark/Rules.hs view
@@ -26,20 +26,22 @@ ├── binaries │ └── <git-reference> │ ├── ghc.path - path to ghc used to build the executable- │ └── <executable> - binary for this version+ │ ├── <executable> - binary for this version │ └── commitid - Git commit id for this reference ├─ <example>- │ ├── results.csv - aggregated results for all the versions- │ └── <git-reference>- │ ├── <experiment>.gcStats.log - RTS -s output- │ ├── <experiment>.csv - stats for the experiment- │ ├── <experiment>.svg - Graph of bytes over elapsed time- │ ├── <experiment>.diff.svg - idem, including the previous version- │ ├── <experiment>.heap.svg - Heap profile- │ ├── <experiment>.log - bench stdout- │ └── results.csv - results of all the experiments for the example- ├── results.csv - aggregated results of all the experiments and versions- └── <experiment>.svg - graph of bytes over elapsed time, for all the included versions+ │ ├── results.csv - aggregated results for all the versions and configurations+ │ ├── <experiment>.svg - graph of bytes over elapsed time, for all the versions and configurations+ | └── <git-reference>+ │ └── <configuration>+ │ ├── <experiment>.gcStats.log - RTS -s output+ │ ├── <experiment>.csv - stats for the experiment+ │ ├── <experiment>.svg - Graph of bytes over elapsed time+ │ ├── <experiment>.diff.svg - idem, including the previous version+ │ ├── <experiment>.heap.svg - Heap profile+ │ ├── <experiment>.log - bench stdout+ │ └── results.csv - results of all the experiments for the example+ ├── results.csv - aggregated results of all the examples, experiments, versions and configurations+ └── <experiment>.svg - graph of bytes over elapsed time, for all the examples, experiments, versions and configurations For diff graphs, the "previous version" is the preceding entry in the list of versions in the config file. A possible improvement is to obtain this info via `git rev-list`.@@ -47,7 +49,7 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module Development.Benchmark.Rules (- buildRules, MkBuildRules(..),+ buildRules, MkBuildRules(..), OutputFolder, ProjectRoot, benchRules, MkBenchRules(..), BenchProject(..), ProfilingMode(..), csvRules, svgRules,@@ -60,6 +62,7 @@ GetVersions(..), GetCommitId(..), GetBuildSystem(..),+ GetConfigurations(..), Configuration(..), BuildSystem(..), findGhcForBuildSystem, Escaped(..), Unescaped(..), escapeExperiment, unescapeExperiment, GitCommit@@ -67,14 +70,16 @@ ) where import Control.Applicative-import Control.Lens ((^.))+import Control.Lens (preview, view, (^.)) import Control.Monad import qualified Control.Monad.State as S import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, (.!=), (.:?), (.=))-import Data.Aeson.Lens (_Object)+import Data.Aeson.Lens (AsJSON (_JSON),+ _Object, _String)+import Data.ByteString.Lazy (ByteString) import Data.Char (isDigit) import Data.List (find, isInfixOf, stripPrefix,@@ -93,6 +98,7 @@ import GHC.Stack (HasCallStack) import qualified Graphics.Rendering.Chart.Backend.Diagrams as E import qualified Graphics.Rendering.Chart.Easy as E+import Numeric.Natural import System.Directory (createDirectoryIfMissing, findExecutable, renameFile)@@ -111,6 +117,7 @@ newtype GetBuildSystem = GetBuildSystem () deriving newtype (Binary, Eq, Hashable, NFData, Show) newtype GetExample = GetExample String deriving newtype (Binary, Eq, Hashable, NFData, Show) newtype GetExamples = GetExamples () deriving newtype (Binary, Eq, Hashable, NFData, Show)+newtype GetConfigurations = GetConfigurations () deriving newtype (Binary, Eq, Hashable, NFData, Show) type instance RuleResult GetExperiments = [Unescaped String] type instance RuleResult GetVersions = [GitCommit]@@ -123,6 +130,10 @@ , RuleResult GetExamples ~ [e] , IsExample e) +data Configuration = Configuration {confName :: String, confValue :: ByteString}+ deriving (Binary, Eq, Generic, Hashable, NFData, Show, Typeable)+type instance RuleResult GetConfigurations = [Configuration]+ -- | Knowledge needed to run an example class (Binary e, Eq e, Hashable e, NFData e, Show e, Typeable e) => IsExample e where getExampleName :: e -> String@@ -133,6 +144,7 @@ allTargetsForExample prof baseFolder ex = do experiments <- askOracle $ GetExperiments () versions <- askOracle $ GetVersions ()+ configurations <- askOracle $ GetConfigurations () let buildFolder = baseFolder </> profilingPath prof return $ [buildFolder </> getExampleName ex </> "results.csv"]@@ -142,9 +154,12 @@ ++ [ buildFolder </> getExampleName ex </> T.unpack (humanName ver) </>- escaped (escapeExperiment e) <.> mode+ confName </>+ escaped (escapeExperiment e) <.>+ mode | e <- experiments, ver <- versions,+ Configuration{confName} <- configurations, mode <- ["svg", "diff.svg"] ++ ["heap.svg" | prof /= NoProfiling] ] @@ -178,6 +193,7 @@ phony (prefix <> "all-binaries") $ need =<< allBinaries buildFolder executableName -------------------------------------------------------------------------------- type OutputFolder = FilePath+type ProjectRoot = FilePath data MkBuildRules buildSystem = MkBuildRules { -- | Return the path to the GHC executable to use for the project found in the cwd@@ -186,9 +202,9 @@ , executableName :: String -- | An action that captures the source dependencies, used for the HEAD build , projectDepends :: Action ()- -- | Build the project found in the cwd and save the build artifacts in the output folder+ -- | Build the project found in the given path and save the build artifacts in the output folder , buildProject :: buildSystem- -> [CmdOption]+ -> ProjectRoot -> OutputFolder -> Action () }@@ -216,7 +232,7 @@ projectDepends liftIO $ createDirectoryIfMissing True $ dropFileName out buildSystem <- askOracle $ GetBuildSystem ()- buildProject buildSystem [Cwd "."] (takeDirectory out)+ buildProject buildSystem "." (takeDirectory out) ghcLoc <- liftIO $ findGhc buildSystem "." writeFile' ghcpath ghcLoc @@ -231,7 +247,7 @@ buildSystem <- askOracle $ GetBuildSystem () flip actionFinally (cmd_ ("git worktree remove bench-temp-" <> ver <> " --force" :: String)) $ do ghcLoc <- liftIO $ findGhc buildSystem ver- buildProject buildSystem [Cwd $ "bench-temp-" <> ver] (".." </> takeDirectory out)+ buildProject buildSystem ("bench-temp-" <> ver) (".." </> takeDirectory out) writeFile' ghcPath ghcLoc --------------------------------------------------------------------------------@@ -245,14 +261,17 @@ , warmupProject :: buildSystem -> FilePath -> [CmdOption] -> example -> Action () -- | Name of the executable to benchmark. Should match the one used to 'MkBuildRules' , executableName :: String+ -- | Number of concurrent benchmarks to run+ , parallelism :: Natural } data BenchProject example = BenchProject- { outcsv :: FilePath -- ^ where to save the CSV output- , exePath :: FilePath -- ^ where to find the executable for benchmarking- , exeExtraArgs :: [String] -- ^ extra args for the executable- , example :: example -- ^ example to benchmark- , experiment :: Escaped String -- ^ experiment to run+ { outcsv :: FilePath -- ^ where to save the CSV output+ , exePath :: FilePath -- ^ where to find the executable for benchmarking+ , exeExtraArgs :: [String] -- ^ extra args for the executable+ , example :: example -- ^ example to benchmark+ , experiment :: Escaped String -- ^ experiment to run+ , configuration :: ByteString -- ^ configuration to use } data ProfilingMode = NoProfiling | CheapHeapProfiling Seconds@@ -271,7 +290,7 @@ benchRules :: RuleResultForExample example => FilePattern -> MkBenchRules BuildSystem example -> Rules () benchRules build MkBenchRules{..} = do - benchResource <- newResource "ghcide-bench" 1+ benchResource <- newResource "ghcide-bench" (fromIntegral parallelism) -- warmup an example build -/- "binaries/*/*.warmup" %> \out -> do let [_, _, ver, exampleName] = splitDirectories (dropExtension out)@@ -294,33 +313,38 @@ example -- run an experiment priority 0 $- [ build -/- "*/*/*/*.csv",- build -/- "*/*/*/*.gcStats.log",- build -/- "*/*/*/*.output.log",- build -/- "*/*/*/*.eventlog",- build -/- "*/*/*/*.hp"+ [ build -/- "*/*/*/*/*.csv",+ build -/- "*/*/*/*/*.gcStats.log",+ build -/- "*/*/*/*/*.output.log",+ build -/- "*/*/*/*/*.eventlog",+ build -/- "*/*/*/*/*.hp" ] &%> \[outcsv, outGc, outLog, outEventlog, outHp] -> do- let [_, flavour, exampleName, ver, exp] = splitDirectories outcsv+ let [_, flavour, exampleName, ver, conf, exp] = splitDirectories outcsv prof = fromMaybe (error $ "Not a valid profiling mode: " <> flavour) $ profilingP flavour example <- fromMaybe (error $ "Unknown example " <> exampleName) <$> askOracle (GetExample exampleName) buildSystem <- askOracle $ GetBuildSystem ()+ configurations <- askOracle $ GetConfigurations () setupRes <- setupProject liftIO $ createDirectoryIfMissing True $ dropFileName outcsv let exePath = build </> "binaries" </> ver </> executableName exeExtraArgs = [ "+RTS" , "-l"+ , "-ol" <> outEventlog , "-S" <> outGc] ++ concat [[ "-h" , "-i" <> show i+ , "-po" <> outHp , "-qg"] | CheapHeapProfiling i <- [prof]] ++ ["-RTS"] ghcPath = build </> "binaries" </> ver </> "ghc.path" warmupPath = build </> "binaries" </> ver </> exampleName <.> "warmup" experiment = Escaped $ dropExtension exp+ Just Configuration{..} = find (\Configuration{confName} -> confName == conf) configurations+ configuration = confValue need [exePath, ghcPath, warmupPath] ghcPath <- readFile' ghcPath withResource benchResource 1 $ do@@ -332,10 +356,9 @@ AddPath [takeDirectory ghcPath, "."] [] ] BenchProject {..}- liftIO $ renameFile "ghcide.eventlog" outEventlog liftIO $ case prof of- CheapHeapProfiling{} -> renameFile "ghcide.hp" outHp- NoProfiling -> writeFile outHp dummyHp+ NoProfiling -> writeFile outHp dummyHp+ _ -> return () -- extend csv output with allocation data csvContents <- liftIO $ lines <$> readFile outcsv@@ -369,7 +392,7 @@ csvRules :: forall example . RuleResultForExample example => FilePattern -> Rules () csvRules build = do -- build results for every experiment*example- build -/- "*/*/*/results.csv" %> \out -> do+ build -/- "*/*/*/*/results.csv" %> \out -> do experiments <- askOracle $ GetExperiments () let allResultFiles = [takeDirectory out </> escaped (escapeExperiment e) <.> "csv" | e <- experiments]@@ -379,6 +402,20 @@ results = map tail allResults writeFileChanged out $ unlines $ header : concat results + -- aggregate all configurations for an experiment+ build -/- "*/*/*/results.csv" %> \out -> do+ configurations <- map confName <$> askOracle (GetConfigurations ())+ let allResultFiles = [takeDirectory out </> c </> "results.csv" | c <- configurations ]++ allResults <- traverse readFileLines allResultFiles++ let header = head $ head allResults+ results = map tail allResults+ header' = "configuration, " <> header+ results' = zipWith (\v -> map (\l -> v <> ", " <> l)) configurations results++ writeFileChanged out $ unlines $ header' : interleave results'+ -- aggregate all experiments for an example build -/- "*/*/results.csv" %> \out -> do versions <- map (T.unpack . humanName) <$> askOracle (GetVersions ())@@ -415,44 +452,60 @@ void $ addOracle $ \(GetParent name) -> findPrev name <$> askOracle (GetVersions ()) -- chart GC stats for an experiment on a given revision priority 1 $- build -/- "*/*/*/*.svg" %> \out -> do- let [_, _, _example, ver, _exp] = splitDirectories out- runLog <- loadRunLog (Escaped $ replaceExtension out "csv") ver+ build -/- "*/*/*/*/*.svg" %> \out -> do+ let [_, _, _example, ver, conf, _exp] = splitDirectories out+ runLog <- loadRunLog (Escaped $ replaceExtension out "csv") ver conf let diagram = Diagram Live [runLog] title title = ver <> " live bytes over time" plotDiagram True diagram out -- chart of GC stats for an experiment on this and the previous revision priority 2 $- build -/- "*/*/*/*.diff.svg" %> \out -> do- let [b, flav, example, ver, exp_] = splitDirectories out+ build -/- "*/*/*/*/*.diff.svg" %> \out -> do+ let [b, flav, example, ver, conf, exp_] = splitDirectories out exp = Escaped $ dropExtension2 exp_ prev <- fmap T.unpack $ askOracle $ GetParent $ T.pack ver - runLog <- loadRunLog (Escaped $ replaceExtension (dropExtension out) "csv") ver- runLogPrev <- loadRunLog (Escaped $ joinPath [b,flav, example, prev, replaceExtension (dropExtension exp_) "csv"]) prev+ runLog <- loadRunLog (Escaped $ replaceExtension (dropExtension out) "csv") ver conf+ runLogPrev <- loadRunLog (Escaped $ joinPath [b,flav, example, prev, conf, replaceExtension (dropExtension exp_) "csv"]) prev conf let diagram = Diagram Live [runLog, runLogPrev] title title = show (unescapeExperiment exp) <> " - live bytes over time compared" plotDiagram True diagram out + -- aggregated chart of GC stats for all the configurations+ build -/- "*/*/*/*.svg" %> \out -> do+ let exp = Escaped $ dropExtension $ takeFileName out+ [b, flav, example, ver] = splitDirectories out+ versions <- askOracle $ GetVersions ()+ configurations <- askOracle $ GetConfigurations ()++ runLogs <- forM configurations $ \Configuration{confName} -> do+ loadRunLog (Escaped $ takeDirectory out </> confName </> replaceExtension (takeFileName out) "csv") ver confName++ let diagram = Diagram Live runLogs title+ title = show (unescapeExperiment exp) <> " - live bytes over time"+ plotDiagram False diagram out+ -- aggregated chart of GC stats for all the revisions build -/- "*/*/*.svg" %> \out -> do let exp = Escaped $ dropExtension $ takeFileName out versions <- askOracle $ GetVersions ()+ configurations <- askOracle $ GetConfigurations () - runLogs <- forM (filter include versions) $ \v -> do+ runLogs <- forM (filter include versions) $ \v ->+ forM configurations $ \Configuration{confName} -> do let v' = T.unpack (humanName v)- loadRunLog (Escaped $ takeDirectory out </> v' </> replaceExtension (takeFileName out) "csv") v'+ loadRunLog (Escaped $ takeDirectory out </> v' </> confName </> replaceExtension (takeFileName out) "csv") v' confName - let diagram = Diagram Live runLogs title+ let diagram = Diagram Live (concat runLogs) title title = show (unescapeExperiment exp) <> " - live bytes over time" plotDiagram False diagram out heapProfileRules :: FilePattern -> Rules () heapProfileRules build = do priority 3 $- build -/- "*/*/*/*.heap.svg" %> \out -> do+ build -/- "*/*/*/*/*.heap.svg" %> \out -> do let hpFile = dropExtension2 out <.> "hp" need [hpFile] cmd_ ("hp2pretty" :: String) [hpFile]@@ -504,8 +557,10 @@ parseJSON o@(Object _) = do let keymap = o ^. _Object case toList keymap of- [(name, String gitName)] -> pure $ GitCommit gitName (Just name) Nothing True- [(name, Object props)] ->+ -- excuse the aeson 2.0 compatibility hack+ [(preview _String . toJSON -> Just name, String gitName)] ->+ pure $ GitCommit gitName (Just name) Nothing True+ [(preview _String . toJSON -> Just name, Object props)] -> GitCommit <$> props .:? "git" .!= name <*> pure (Just name)@@ -560,14 +615,15 @@ -- | A file path containing the output of -S for a given run data RunLog = RunLog- { runVersion :: !String,- runFrames :: ![Frame],- runSuccess :: !Bool,- runFirstReponse :: !(Maybe Seconds)+ { runVersion :: !String,+ runConfiguration :: !String,+ runFrames :: ![Frame],+ runSuccess :: !Bool,+ runFirstReponse :: !(Maybe Seconds) } -loadRunLog :: HasCallStack => Escaped FilePath -> String -> Action RunLog-loadRunLog (Escaped csv_fp) ver = do+loadRunLog :: HasCallStack => Escaped FilePath -> String -> String -> Action RunLog+loadRunLog (Escaped csv_fp) ver conf = do let log_fp = replaceExtension csv_fp "gcStats.log" log <- readFileLines log_fp csv <- readFileLines csv_fp@@ -588,7 +644,7 @@ , Just s <- readMaybe (T.unpack s) -> (s,timeForFirstResponse) _ -> error $ "Cannot parse: " <> csv_fp- return $ RunLog ver frames success firstResponse+ return $ RunLog ver conf frames success firstResponse -------------------------------------------------------------------------------- @@ -628,13 +684,13 @@ ~(c:_) <- E.liftCState $ S.gets (E.view E.colors) E.plot $ do lplot <- E.line- (runVersion rl ++ if runSuccess rl then "" else " (FAILED)")+ (runVersion rl ++ " " ++ runConfiguration rl ++ if runSuccess rl then "" else " (FAILED)") [ [ (totElapsed f, extract f) | f <- runFrames rl ] ] return (lplot E.& E.plot_lines_style . E.line_width E.*~ 2)- case (runFirstReponse rl) of+ case runFirstReponse rl of Just t -> E.plot $ pure $ E.vlinePlot ("First build: " ++ runVersion rl) (E.defaultPlotLineStyle E.& E.line_color E..~ c) t _ -> pure ()