tilia-0.1.0.0: bench/Tilia/Bench/Record.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | What the benchmarks cost when they were last recorded, kept in the
-- repository so that a change to it shows in review like any other.
module Tilia.Bench.Record
( Record (..),
Entry (..),
readRecord,
writeRecord,
)
where
import Control.Exception (SomeException, try)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Data.Word (Word64)
import Text.Read (readMaybe)
-- | The bytes each benchmark allocated and the instructions it retired, by
-- stage and module.
data Record = Record
{ -- | The compiler the record was made with, which what is allocated
-- depends on.
recordCompiler :: Text,
-- | The processor the instructions were counted on, which how many
-- there are depends on, or @none@.
recordProcessor :: Text,
-- | A digest of the names and sizes of the interfaces decoded, which
-- what decoding them costs depends on.
recordInterfaces :: Text,
-- | One entry for every benchmark.
recordEntries :: Map (Text, Text) Entry
}
-- | What one benchmark cost.
data Entry = Entry
{ -- | The bytes it allocated.
entryAllocated :: !Word64,
-- | The instructions it retired, where they were counted.
entryInstructions :: !(Maybe Word64)
}
-- | Read a record. A missing one is an empty one, so that benchmarks added
-- before it is regenerated each say they are not in it.
readRecord :: FilePath -> IO Record
readRecord path =
try (T.readFile path) >>= \case
Left (_ :: SomeException) -> pure (Record "" "none" "" Map.empty)
Right text ->
pure
Record
{ recordCompiler = field "compiler " text,
recordProcessor = field "processor " text,
recordInterfaces = field "interfaces " text,
recordEntries = Map.fromList (concatMap entry (T.lines text))
}
where
field key = mconcat . mapMaybe (T.stripPrefix key) . T.lines
entry line = case T.words line of
[stage, allocated, instructions, name]
| Just a <- readMaybe (T.unpack allocated) ->
[((stage, name), Entry a (readMaybe (T.unpack instructions)))]
_ -> []
-- | Write a record, sorted by stage and module so that a regeneration diff
-- shows what changed rather than what moved.
writeRecord :: FilePath -> Record -> IO ()
writeRecord path record =
T.writeFile path . T.unlines $
[ "# What each benchmark costs, as far as that does not depend on how",
"# busy the machine is. `cabal bench` checks it, and",
"# `TILIA_BENCH_ACCEPT=1 cabal bench` writes it again.",
"#",
"# compiler The compiler the benchmarks were built with, which what",
"# they allocate depends on.",
"# processor The processor the instructions were counted on, which",
"# how many there are depends on, or none.",
"# interfaces A digest of the names and sizes of the interfaces the",
"# decode stage reads, which what decoding them costs",
"# depends on.",
"#",
"# Every other line is a benchmark:",
"#",
"# stage What it measures: formatting a module (format),",
"# checking what that printed as --check-ast does",
"# (check), reading what every module of a package",
"# establishes out of its tarball with no cache",
"# (resolve), the same out of a cache an earlier reading",
"# filled (recall), or decoding every interface of a",
"# package the compiler ships with (decode).",
"# allocated The bytes it allocates.",
"# instructions The instructions it retires in user space, or - where",
"# they were not counted.",
"# benchmark The module or the package it measures that on.",
"",
"compiler " <> recordCompiler record,
"processor " <> recordProcessor record,
"interfaces " <> recordInterfaces record,
"",
row "# stage" ["allocated", "instructions"] "benchmark"
]
<> fmap line (Map.toAscList (recordEntries record))
where
line ((stage, name), Entry allocated instructions) =
row
stage
[shown allocated, maybe "-" shown instructions]
name
row stage costs name =
T.justifyLeft 8 ' ' stage
<> foldMap (T.justifyRight 14 ' ') costs
<> " "
<> name
shown = T.pack . show