packages feed

streaming-benchmarks-0.1.0: Charts.hs

import Control.Arrow (second)
import Data.Char (isSpace)
import Data.List.Split (splitOn)
import Data.Maybe (fromMaybe, catMaybes, fromJust)
import Debug.Trace (trace)
import System.Directory (createDirectoryIfMissing)
import System.Environment (getArgs)
import System.Process.Typed (readProcess_)
import Text.CSV (CSV, parseCSVFromFile)

import qualified Data.Text.Lazy.Encoding as T
import qualified Data.Text.Lazy as T

import Graphics.Rendering.Chart.Easy
import Graphics.Rendering.Chart.Backend.Diagrams

-------------------------------------------------------------------------------
-- Configurable stuff
-------------------------------------------------------------------------------

outputDir :: String
outputDir = "charts"

packages :: [String]
packages = ["streamly", "streaming", "pipes", "conduit", "machines", "vector"]

-- pairs of benchmark group titles and corresponding list of benchmark
-- prefixes i.e. without the package name at the end.
bmGroups :: [(String, [String])]
bmGroups =
    [
      -- Operations are listed in increasing cost order
      ( "All Operations at a Glance (Shorter is Faster)"
      , [
        -- "filtering/take-one"
          "elimination/toNull"
        , "filtering/drop-all"
        , "elimination/last"
        , "elimination/fold"

        , "filtering/filter-all-out"
        , "filtering/dropWhile-true"
        , "filtering/take-all"
        , "filtering/takeWhile-true"
        , "transformation/map"
        , "filtering/filter-all-in"
        , "filtering/filter-even"

        , "elimination/scan"
        , "transformation/mapM"
        ,  "zip"

        , "elimination/toList"
        , "elimination/concat"
        ]
      )
    , ( "Discarding and Folding (Shorter is Faster)"
      , [
        -- "filtering/take-one"
          "elimination/toNull"
        , "filtering/drop-all"
        , "elimination/last"
        , "elimination/fold"
        ]
      )
    , ( "Pure Transformation and Filtering (Shorter is Faster)"
      , [
          "filtering/filter-all-out"
        , "filtering/dropWhile-true"
        , "filtering/take-all"
        , "filtering/takeWhile-true"
        , "transformation/map"
        , "filtering/filter-all-in"
        , "filtering/filter-even"
        , "elimination/scan"
        ]
      )
    , ( "Monadic Transformation (Shorter is Faster)"
      , [
          "transformation/mapM"
        ]
      )
    , ( "Folding to List (Shorter is Faster)"
      , [
          "elimination/toList"
        ]
      )
    , ( "Zipping and Concating Streams (Shorter is Faster)"
      , [ "zip"
        , "elimination/concat"
        ]
      )
    , ( "Composing Pipeline Stages (Shorter is Faster)"
      , [
          "compose/all-out-filters"
        , "compose/all-in-filters"
        , "compose/map-with-all-in-filter"
        , "compose/mapM"
        ]
      )
    ]

-------------------------------------------------------------------------------

-- "values" has results for each package for each title in bmTitles
genGroupGraph :: String -> [String] -> [(String, [Maybe Double])] -> IO ()
genGroupGraph bmGroupName bmTitles values =
    toFile def (outputDir
                ++ "/"
                -- links in README.rst eat up the space so we match the same
                ++ (filter (not . isSpace) (takeWhile (/= '(') bmGroupName))
                ++ ".svg") $ do
        layout_title .= bmGroupName
        layout_title_style . font_size .= 25
        layout_x_axis . laxis_generate .= autoIndexAxis (map fst values)
        layout_x_axis . laxis_style . axis_label_style . font_size .= 12

        -- layout_y_axis . laxis_override .= axisGridAtTicks
        let modifyLabels ad = ad {
                _axis_labels = map (map (second (++ " ms"))) (_axis_labels ad)
            }
        layout_y_axis . laxis_override .= modifyLabels
        -- XXX We are mapping a missing value to 0, can we label it missing
        -- instead?
        let modifyVal x = map ((*1000) . fromMaybe 0) (snd x)
        plot $ fmap plotBars $ bars bmTitles (addIndexes (map modifyVal values))

-- Given a package name (e.g. streaming) and benchmark prefixes (e.g.
-- [elimination/null, elimination/toList]) get the corresponding results e.g.
-- [8.1 ms, 5.4 ms]. The corresponding result file entries will have
-- elimination/null/streaming etc. as the names of the entries.
getResultsForPackage :: CSV -> String -> [String] -> [Maybe Double]
getResultsForPackage csvData pkgname bmPrefixes =
      map (getBenchmarkMean csvData)
    $ map (++ "/" ++ pkgname) bmPrefixes

    where

    getBenchmarkMean entries bmname =
        case filter ((== bmname) .  head) entries of
            [] -> trace
                ("Warning! Benchmark [" ++ bmname ++"] not found in csv data")
                Nothing
            xs -> Just (read ((last xs) !! 1))

genOneGraph :: CSV -> [(String, String)] -> (String, [String]) -> IO ()
genOneGraph csvData pkginfo (bmGroupTitle, prefixes) =
    genGroupGraph bmGroupTitle bmTitles bmResults

    where

    bmTitles = map (last . splitOn "/" ) prefixes

    pkgName = fst
    pkgVersion = snd
    pkgNameWithVersion pkgInfo = pkgName pkgInfo ++ "-" ++ pkgVersion pkgInfo
    pkgGetResults pkgInfo =
        let vals = getResultsForPackage csvData (pkgName pkgInfo) prefixes
        in (pkgNameWithVersion pkgInfo, vals)

    -- this produces results for all packages for all prefixes
    -- [(packagenamewithversion, [Maybe Double])]
    bmResults = map pkgGetResults pkginfo

genGraphs :: CSV -> [(String, String)] -> IO ()
genGraphs csvData pkginfo = mapM_ (genOneGraph csvData pkginfo) bmGroups

-- XXX display GHC version as well
-- XXX display the OS/arch
-- XXX fix the y axis labels
-- XXX fix the legend position
main :: IO ()
main = do
    args <- getArgs

    createDirectoryIfMissing True outputDir

    (out, _) <- readProcess_ "stack --system-ghc list-dependencies --bench"

    -- Get our streaming packages and their versions
    let match [] = Nothing
        match (_ : []) = Nothing
        match (x : y : _) =
            case elem x packages of
                False -> Nothing
                True -> Just (x, y)
        pkginfo =
              catMaybes
            $ map match
            $ map words (lines (T.unpack $ T.decodeUtf8 out))

    -- order them in the order specified in packages so that the order is
    -- can be controlled by the user.
    let pkginfo' = map (\x -> (x, fromJust $ lookup x pkginfo)) packages

    csvData <- parseCSVFromFile (head args)
    case csvData of
        Left e -> error $ show e
        Right dat -> genGraphs dat pkginfo'
    return ()