packages feed

ghc-bench-0.3.2: src/Result.hs

{-# LANGUAGE CPP #-}
module Result (
  submit
, Result(..)
, Label(..)
, Seconds(..)
, Concurrency(..)
, parseFromIssueBody
, resultPath
, formatTime
, cpuName
, basePath

#ifdef TEST
, issueTitle
#endif
) where

import Imports hiding (product)
import Prelude qualified

import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.ByteString.Char8 (ByteString, putStrLn)
import System.FilePath (joinPath)
import Network.HTTP.Types.URI (renderSimpleQuery)

import Benchmark.Type (Label(..), Seconds(..))
import SystemInfo

base :: ByteString
base = "https://github.com/sol/ghc-bench/issues/new"

data Result = Result {
  times :: [(Label, Seconds)]
, concurrency :: Concurrency
, system :: SystemInfo
} deriving (Eq, Show, Generic)

submit :: Result -> (Maybe (FilePath -> IO ())) -> IO ()
submit (issueUrl -> url) = \ case
  Nothing -> do
    putStrLn "Open this URL to submit your result:\n"
    putStrLn $ "  " <> url
  Just qrencode -> do
    putStrLn "Open this URL to submit your result:\n"
    qrencode (unpack $ T.decodeUtf8Lenient url)

issueUrl :: Result -> ByteString
issueUrl result = base <> renderQuery [
    ("template", "benchmark-result.yml")
  , ("title", issueTitle (sum $ map snd result.times) system)

  , ("times", formatTimes result.times)
  , ("concurrency", show result.concurrency)

  , ("os", system.os)
  , ("arch", system.arch)

  , ("system_vendor", system.vendor)

  , ("product_category", system.product.category)
  , ("product_chassis_type", system.product.chassis_type)
  , ("product_family", system.product.family)
  , ("product_name", system.product.name)
  , ("product_version", system.product.version)

  , ("board", unwords [system.board.vendor, system.board.name])

  , ("cpu_name", system.cpu.name)
  , ("cpu_cores", show system.cpu.cores)
  , ("cpu_threads", show system.cpu.threads)

  , ("cpuid", cpuid)

  , ("ram", show system.ram)
  ]
  where
    system :: SystemInfo
    system = result.system

    cpuid :: Text
    cpuid = T.intercalate " / " [
        fromMaybe "unknown" system.cpu.vendor
      , fromMaybe "unknown" system.cpu.family
      , fromMaybe "unknown" system.cpu.model
      , fromMaybe "unknown" system.cpu.stepping
      ]

issueTitle :: Seconds -> SystemInfo -> Text
issueTitle seconds system = unwords ["[result]", formatTime seconds, "-", description, "-", cpuName system.cpu]
  where
    description :: Text
    description = unwords case (system.vendor, system.product.name) of
      ("LENOVO", _) -> [system.vendor, system.product.version]
      (unknown -> True, _) -> [system.product.category, "computer"]
      (vendor, unknown -> True) -> [vendor, system.product.category]
      (vendor, name) -> [vendor, name]

cpuName :: Cpu -> Text
cpuName cpu = case cpu.vendor of
  Just "GenuineIntel" -> case T.breakOn " @ " cpu.name of
    (name, _) ->
        unwords
      . filter (/= "CPU")
      . words
      . tryStripPrefix "11th Gen "
      . T.replace "(R)" ""
      . T.replace "(TM)" " "
      $ name
  _ -> cpu.name

formatTime :: Seconds -> Text
formatTime (Seconds seconds) = case seconds `divMod` 60 of
  (0, s) -> show s <> "s"
  (m, s) -> mconcat [show seconds, "s (", show m, "m ", show s, "s)"]

unknown :: Text -> Bool
unknown = (== "To Be Filled By O.E.M.")

renderQuery :: [(ByteString, Text)] -> ByteString
renderQuery = renderSimpleQuery True . map (fmap encodeUtf8)

parseFromIssueBody :: Text -> Result
parseFromIssueBody markdown = Result {
    times = parseTimes $ get "Build times (seconds)"
  , concurrency = Concurrency $ int "Used number of threads"
  , system
  }
  where
    system :: SystemInfo
    system = SystemInfo {
        os = get "Operating System"
      , arch = get "Architecture"
      , vendor = get "System Vendor"
      , product
      , board
      , cpu
      , ram = int "RAM Size (GB)"
      }

    product :: Product
    product = Product {
        category = get "Product Category"
      , chassis_type = get "Chassis Type"
      , family = get "Product Family"
      , name = get "Product Name"
      , version = get "Product Version"
      }

    board :: Board
    board = case T.breakOnEnd " " $ get "Motherboard" of
      (strip -> vendor, name) -> Board {..}

    cpuid :: Text
    cpuid = get "CPUID (vendor / family / model / stepping)"

    cpu :: Cpu
    cpu = case map unknownToNothing $ T.splitOn " / " cpuid of
      [vendor, family, model, stepping] -> Cpu {
          name = get "CPU Name"
        , cores = int "Cores"
        , threads = int "Threads"
        , ..
        }
      _ -> Prelude.error $ "Invalid CPUID: " <> unpack cpuid

    unknownToNothing :: Text -> Maybe Text
    unknownToNothing "unknown" = Nothing
    unknownToNothing value = Just value

    get :: Text -> Text
    get key = case lookup key sections of
      Just value -> value
      Nothing -> Prelude.error $ "Missing field: " <> unpack key

    int :: Text -> Int
    int = read . get

    sections :: [(Text, Text)]
    sections = parseSections markdown

    parseSections :: Text -> [(Text, Text)]
    parseSections = map parseSection . reverse . drop 1 . T.splitOn "\n### "
      where
        parseSection :: Text -> (Text, Text)
        parseSection = bimap strip strip . T.break (== '\n') >>> \ case
          (key, "_No response_") -> (key, "")
          (key, value) -> (key, value)

formatTimes :: [(Label, Seconds)] -> Text
formatTimes = unwords . map \ case
  (Label name, time) -> mconcat [name, ":", show time]

parseTimes :: Text -> [(Label, Seconds)]
parseTimes = words >>> map parseTime
  where
    parseTime :: Text -> (Label, Seconds)
    parseTime = T.breakOn ":" >>> bimap Label (Seconds . read . T.drop 1)

newtype Timestamp = Timestamp String
  deriving newtype (Eq, Show, Read, IsString)

resultPath :: Timestamp -> SystemInfo -> FilePath
resultPath (Timestamp timestamp) system = joinPathComponents path
  where
    path :: [Text]
    path = basePathComponents system.cpu ++  [file]

    file :: Text
    file = model <> "_" <> pack timestamp <> ".yaml"

    model :: Text
    model = T.intercalate "-"  case (system.vendor, system.product.name) of
      ("LENOVO", _) -> [tryStripPrefix "ThinkPad " system.product.version, system.product.name]
      (_, unknown -> True) -> [system.board.vendor, system.board.name]
      (_, name) -> [name]

basePath :: Cpu -> String
basePath = joinPathComponents . basePathComponents

basePathComponents :: Cpu -> [Text]
basePathComponents cpu = "results" : vendor : cpuToPathComponents cpu
  where
    vendor :: Text
    vendor = case cpu.vendor of
      Just "GenuineIntel" -> "intel"
      Just "AuthenticAMD" -> "amd"
      Just name -> name
      Nothing -> "unknown"

cpuToPathComponents :: Cpu -> [Text]
cpuToPathComponents cpu = case (cpu.vendor, cpu.family, cpu.model, cpu.stepping) of
  (Just "GenuineIntel", Just "6", Just "198", Just "2") -> ["core_ultra_series_2", T.drop 26 cpu.name]
  (Just "GenuineIntel", Just "6", Just "165", Just "5") -> ["10th", T.take 9 $ T.drop 18 cpu.name]
  (Just "GenuineIntel", Just "6", Just "140", Just "1") -> ["11th", T.take 9 $ T.drop 27 cpu.name]
  (Just "GenuineIntel", Just "6", Just "23", Just "10") -> ["core_2", T.take 5 $ T.drop 31 cpu.name]
  _ -> ["unknown", T.replace " " "_" cpu.name]

joinPathComponents :: [Text] -> String
joinPathComponents = joinPath . sanitizePathComponents

sanitizePathComponent :: Text -> FilePath
sanitizePathComponent component = case sanitize component of
  "" -> "unknown"
  name -> name
  where
    sanitize = unpack . T.filter (/= '\0') . T.replace "/" "-" . T.intercalate "-" . T.words

sanitizePathComponents :: [Text] -> [FilePath]
sanitizePathComponents = map sanitizePathComponent

tryStripPrefix :: Text -> Text -> Text
tryStripPrefix prefix value = fromMaybe value $ T.stripPrefix prefix value