packages feed

deltaq-1.0.0.0: benchmark/Benchmark/Plot.hs

{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

{-|
Copyright   : Predictable Network Solutions Ltd., 2003-2024
License     : BSD-3-Clause
Description : Plotting utilities for benchmark results.
-}
module Benchmark.Plot where

import Data.String
    ( fromString
    )

import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as C
import qualified Data.Vector as V
import qualified Graphics.Vega.VegaLite as G

{-----------------------------------------------------------------------------
    Data
------------------------------------------------------------------------------}
type Time = Double -- in seconds

data Measurement = Measurement
    { mName :: String
        -- ^ Name used for grouping the expression
    , mTime :: Time
        -- ^ Time required to evaluate the expression to Normal Form.
    , mExpressionSize :: Int
        -- ^ Size of the expression as we write it down.
    , mValueComplexity :: Int
        -- ^ Complexity of the value represented by the expression.
    }
    deriving (Eq, Show, Read)

{-----------------------------------------------------------------------------
    Read
------------------------------------------------------------------------------}
readCsv :: FilePath -> String -> IO [Measurement]
readCsv fpath op = do
    file <- BL.readFile fpath
    let Right (_, measurements) = C.decodeByName file
    pure $ filter ((op ==) . mName) $ V.toList measurements

instance C.FromNamedRecord Measurement where
    parseNamedRecord r =
        mkMeasurement <$> r C..: "Name" <*> r C..: "Mean"
      where
        mkMeasurement name time =
            Measurement
                { mName = take 4 name
                , mTime = time
                , mExpressionSize = size
                , mValueComplexity = size -- FIXME: Record complexities.
                }
          where
            size = read $ drop (length prefix) $ name
            prefix = ".>>./m = " :: String

{-----------------------------------------------------------------------------
    Plot
------------------------------------------------------------------------------}
-- | Plot all operations in the current directory.
plotAllOperations :: FilePath -> [Measurement] -> IO ()
plotAllOperations dir xs = do
    plotOp "sequentially" ".>>."
    plotOp "lastToFinish" "./\\."
    plotOp "firstToFinish" ".\\/."
  where
    plotOp name ticker = do
        let ys = filter ((ticker == ) . mName) xs
        plotExprToHtmlFile (dir <> "/expr-" <> name <> ".html") ys
        plotComplexityToHtmlFile (dir <> "/complexity-" <> name <> ".html") ys

-- | Time against expression size.
plotExprToHtmlFile :: FilePath -> [Measurement] -> IO ()
plotExprToHtmlFile fpath measurements =
    G.toHtmlFile fpath . G.toVegaLite $
        [ enc []
        , G.title ("Operation " <> fromString name) []
        , G.layer [ values, points ]
        , G.height 300
        , G.width 400
        ]
  where
    enc = G.encoding
        . G.position G.X
            [ G.PName "M"
            , G.PmType G.Quantitative
            , G.PAxis [ G.AxTickMinStep 1 ]
            ]
        . G.position G.Y [ G.PName "Time / ms", G.PmType G.Quantitative ]
    mkData xfs = G.dataFromColumns []
        . G.dataColumn "M" (G.Numbers xs)
        . G.dataColumn "Time / ms" (G.Numbers fs)
        $ []
      where (xs, fs) = unzip xfs

    name = mName $ head measurements
    xys = map (\m -> (fromIntegral $ mExpressionSize m, 1000 * mTime m)) measurements

    values = G.asSpec
        [ mkData xys
        , G.mark G.Line []
        ]
    points = G.asSpec
        [ mkData xys
        , G.mark G.Circle []
        ]

-- | Time against complexity.
plotComplexityToHtmlFile :: FilePath -> [Measurement] -> IO ()
plotComplexityToHtmlFile fpath measurements =
    G.toHtmlFile fpath . G.toVegaLite $
        [ enc []
        , G.title ("Operation " <> fromString name) []
        , G.layer [ values, points ]
        , G.height 300
        , G.width 400
        ]
  where
    enc = G.encoding
        . G.position G.X
            [ G.PName "Complexity"
            , G.PmType G.Quantitative
            , G.PAxis [ G.AxTickMinStep 1 ]
            ]
        . G.position G.Y [ G.PName "Time / ms", G.PmType G.Quantitative ]
    mkData xfs = G.dataFromColumns []
        . G.dataColumn "Complexity" (G.Numbers xs)
        . G.dataColumn "Time / ms" (G.Numbers fs)
        $ []
      where (xs, fs) = unzip xfs

    name = mName $ head measurements
    xys = map (\m -> (fromIntegral $ mValueComplexity m, 1000 * mTime m)) measurements

    values = G.asSpec
        [ mkData xys
        , G.mark G.Line []
        ]
    points = G.asSpec
        [ mkData xys
        , G.mark G.Circle []
        ]