packages feed

ghc-bench-0.3.0: test/README.hs

module README (
  update
, ensureFile
) where

import Helper

import Data.List qualified as List
import Data.Tuple (swap)
import Control.Exception
import System.Directory (createDirectoryIfMissing)
import System.FilePath (takeDirectory)
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.Text qualified as T
import Data.Text.IO.Utf8 qualified as Utf8
import Data.Set qualified as Set
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map

import Result
import SystemInfo

update :: [Result] -> IO ()
update results = do
      Utf8.readFile "README.md"
  >>= (updateResults results >>> encodeUtf8 >>> ensureFile "README.md")

updateResults :: [Result] -> Text -> Text
updateResults (resultTable -> results) = splitOutResultTable >>> \ case
  (prefix, (_results, suffix)) -> mconcat [prefix, "\n", results, "\n", suffix]
  where
    splitOutResultTable = T.breakOnEnd "## Benchmark results\n" >>> (<&> T.breakOn "## ")

type Configuration = (Cpu, Concurrency)

aggregateResults :: [Result] -> Map Configuration (Map Label [Seconds])
aggregateResults = map resultTimes >>> Map.fromListWith (Map.unionWith (++))
  where
    resultTimes :: Result -> (Configuration, Map Label [Seconds])
    resultTimes result = (configuration, return <$> Map.fromList result.times)
      where
        configuration :: Configuration
        configuration = (result.system.cpu, result.concurrency)

resultTable :: [Result] -> Text
resultTable results = unlines $ map joinColumns table
  where
    table :: [[Text]]
    table = header : replicate (length header) "---" : map formatRow rows

    joinColumns :: [Text] -> Text
    joinColumns columns = mconcat ["| ", T.intercalate " | " columns, " |"]

    header :: [Text]
    header = "CPU" : map formatLabel labels

    rows :: [(Configuration, [Maybe Seconds])]
    rows = sortByTimes . map toRow $ Map.toList aggregated
      where
        sortByTimes :: [(c, [Maybe Seconds])] -> [(c, [Maybe Seconds])]
        sortByTimes = List.sortOn sortKey
          where
            sortKey :: (c, [Maybe Seconds]) -> [Seconds]
            sortKey = map (fromMaybe maxBound) . snd

        toRow :: (Configuration, Map Label [Seconds]) -> (Configuration, [Maybe Seconds])
        toRow (configuration, times) = (configuration, columns)
          where
            medians :: Map Label Seconds
            medians = Map.mapMaybe median times

            columns :: [Maybe Seconds]
            columns = map (`Map.lookup` medians) labels

    formatRow :: (Configuration, [Maybe Seconds]) -> [Text]
    formatRow ((cpu, _), times) = formatCpu cpu : map formatColumn times
      where
        formatColumn :: Maybe Seconds -> Text
        formatColumn = \ case
          Nothing -> "-"
          Just t -> formatTime t

    aggregated :: Map Configuration (Map Label [Seconds])
    aggregated = aggregateResults results

    labels :: [Label]
    labels = sortLabels . Set.toList . mconcat . map Map.keysSet $ Map.elems aggregated
      where
        sortLabels :: [Label] -> [Label]
        sortLabels = List.sortOn sortKey

        sortKey :: Label -> (Text, Int)
        sortKey (Label label) = fromMaybe (label, maxBound) key
          where
            key :: Maybe (Text, Int)
            key = swap <$> (listToMaybe (mapMaybe toKey labelOrder))

            toKey :: (Int, Text) -> Maybe (Int, Text)
            toKey = traverse (`T.stripSuffix` label)

        labelOrder :: [(Int, Text)]
        labelOrder = zip [0..] [
            "ghc"
          , "-dependencies"
          , "-build"
          ]

formatLabel :: Label -> Text
formatLabel = \ case
  "ghc-9.12.4-build" -> "[GHC 9.12.4 build](src/Benchmark/BuildGhc.hs)"
  "hedgehog-1.7-dependencies" -> "[hedgehog-1.7-dependencies](src/Benchmark/BuildCabalPackage.hs)"
  "hedgehog-1.7-build" -> "[hedgehog-1.7-build](src/Benchmark/BuildCabalPackage.hs)"
  Label label -> label

formatCpu :: Cpu -> Text
formatCpu cpu = mconcat ["[", cpuName cpu, "](", pack $ basePath cpu,  ")"]

median :: [Seconds] -> Maybe Seconds
median = List.sort >>> \ case
  [] -> Nothing
  values -> Just $ values !! (length values `div` 2)

ensureFile :: FilePath -> ByteString -> IO ()
ensureFile file new = do
  old <- try @IOException $ B.readFile file
  unless (old == Right new) do
    createDirectoryIfMissing True (takeDirectory file)
    B.writeFile file new