packages feed

NoSlow-0.1: Table.hs

{-# LANGUAGE PatternGuards #-}
module Main where

import Criterion.Measurement ( secs )
import qualified Data.Map as M
import Data.List ( nub, intersect, sort, groupBy )
import Text.Read
import System.Environment ( getArgs )
import Control.Monad ( liftM, foldM )
import Text.Printf ( printf )

main = do
         (arg : args) <- getArgs
         if arg == "--diff"
           then doDiff  args
           else doTable arg args

doTable file args
  = do
      bs <- readLog file
      table <- foldM procArg (mkTable secs bs) args
      outputHTML table

doDiff (file1 : file2 : args)
  = do
      bs1 <- readLog file1
      bs2 <- readLog file2
      let t = intersectTable ratio cmp (mkTable secs bs1) (mkTable secs bs2)
      table <- foldM procArg t args
      outputHTML table
  where
    ratio d = printf "%.3f" d

    cmp d1 d2 = d1 / d2


dropPrefix :: String -> String -> Maybe String
dropPrefix "" cs  = Just cs
dropPrefix (p : ps) (c : cs)
  | p == c    = dropPrefix ps cs
  | otherwise = Nothing

procArg t arg
  | Just s <- dropPrefix "--type=" arg
     = return $ filterColumns (\cfg -> cfgDatatype cfg == s) t

data Cfg = Cfg {
             cfgLibrary   :: String
           , cfgSubsystem :: String
           , cfgDatatype  :: String
           }
 deriving ( Eq, Ord )

data Library = Library String Int [Subsystem]
data Subsystem = Subsystem String Int [String]

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
on f g x y = f (g x) (g y)

eqOn :: Eq b => (a -> b) -> a -> a -> Bool
eqOn = on (==)

groupCfgs :: [Cfg] -> [Library]
groupCfgs = map mk_library . groupBy (eqOn cfgLibrary)
  where
    mk_library cfgs@(c:_)
      = let subs = map mk_subsystem $ groupBy (eqOn cfgSubsystem) cfgs
        in
        Library (cfgLibrary c) (sum [n | Subsystem _ n _ <- subs]) subs
    mk_subsystem cfgs@(c:_)
      = let cols = map cfgDatatype cfgs
        in
        Subsystem (cfgSubsystem c) (length cols) cols

instance Show Cfg where
  showsPrec _ cfg = showString (cfgLibrary cfg)
                  . sub (cfgSubsystem cfg)
                  . showChar '.' . showString (cfgDatatype cfg)
    where
      sub "" = id
      sub s  = showChar '.' . showString s

data Entry a = Entry {
                 eCfg  :: Cfg
               , eName :: String
               , eData :: a
               }

data Table = Table {
               tRows     :: [String]
             , tColumns  :: [Cfg]
             , tCells    :: M.Map String Row
             , tShowCell :: Double -> String
             }
type Row = M.Map Cfg Double

mkTable :: (Double -> String) -> [Entry Double] -> Table
mkTable show es
  = Table {
      tRows     = nub $ map eName es
    , tColumns  = sort $ nub $ map eCfg es
    , tCells    = M.fromListWith M.union [(eName e, unit e) | e <- es]
    , tShowCell = show
    }
  where
    unit e = M.singleton (eCfg e) (eData e)

structure :: Table -> [Library]
structure = groupCfgs . tColumns

cells :: Table -> [(String, [Maybe Double])]
cells (Table { tRows    = names
             , tColumns = cfgs
             , tCells   = t }) = concatMap row_cells names
  where
    row_cells name = case M.lookup name t of
                       Just row -> [(name, map (cell row) cfgs)]
                       Nothing  -> []

    cell row cfg = M.lookup cfg row

outputHTML :: Table -> IO ()
outputHTML t
  = do
      putStrLn "<html><body>"
      putStrLn $ htmlTable t
      putStrLn "</body></html>"

htmlTable :: Table -> String
htmlTable t = unlines
  [ "<table border=\"1\">"
  , header (structure t) -- tr $ concatMap th ("" : map show (tColumns t))
  , unlines $ map (tr . row) (cells t)
  , "</table>"
  ]
  where
    row (s, cs) = th (concatMap escape s) ++ concatMap cell cs
    cell Nothing = td ""
    cell (Just s) = td (tShowCell t s)

    tr s = "<tr>" ++ s ++ "</tr>"

    th s = "<th>" ++ s ++ "</th>"
    td s = "<td>" ++ s ++ "</td>"

    th' n s = "<th colspan=\"" ++ show n ++ "\">" ++ s ++ "</th>"

    header :: [Library] -> String
    header libs =  unlines [
                    tr $ th "" ++ concat [th' n lib | Library lib n _ <- libs]
                  , tr $ th "" ++ concat [th' n sub | Library _ _ subs <- libs
                                                    , Subsystem sub n _ <- subs]
                  ,               types  [s         | Library _ _ subs <- libs
                                                    , Subsystem _ _ ss <- subs
                                                    , s <- ss]
                  ]

    types (s : ss)
      | all (s==) ss = ""
      | otherwise    = tr $ th "" ++ concatMap th (s:ss)

    escape '<' = "&lt;"
    escape '>' = "&gt;"
    escape '&' = "&amp;"
    escape c   = [c]

filterRows :: (String -> Bool) -> Table -> Table
filterRows f t@(Table { tRows = tRows, tCells = tCells })
  = t { tRows  = filter f tRows
      , tCells = M.filterWithKey (\s _ -> f s) tCells }

filterColumns :: (Cfg -> Bool) -> Table -> Table
filterColumns f t@(Table { tColumns = tColumns, tCells = tCells })
  = t { tColumns = filter f tColumns
      , tCells   = M.map (M.filterWithKey (\cfg _ -> f cfg)) tCells
      }

intersectTable :: (Double -> String) -> (Double -> Double -> Double)
                    -> Table -> Table -> Table
intersectTable show f t1 t2
  = Table { tRows     = intersect (tRows t1) (tRows t2)
          , tColumns  = intersect (tColumns t1) (tColumns t2)
          , tCells    = M.intersectionWith (M.intersectionWith f)
                                           (tCells t1) (tCells t2)
          , tShowCell = show
          }

{-
data State = State {
               stOriginal :: Table Double
             , stResult   :: Table Double
             }

mapOriginal :: (Table Double -> Table Double) -> State -> State
mapOriginal f st@(State { stOriginal = t }) = st { stOriginal = f t }

mapResult :: (Table Double -> Table Double) -> State -> State
mapResult f st@(State { stResult = t }) = st { stResult = f t }
-}


newtype Benchmark = Benchmark { unBenchmark :: Entry Double }

readLog :: FilePath -> IO [Entry Double]
readLog file = (proc_lines . lines) `liftM` readFile file
  where
    proc_lines (h : r)
      | h == header = map (unBenchmark . read) r
      | otherwise = error "Invalid file"

    header = "Name,Mean,MeanLB,MeanUB,Stddev,StddevLB,StddevUB"


instance Read Benchmark where
  readPrec = do
               tag <- readPrec
               comma
               mean <- readPrec
               comma
               readPrec :: ReadPrec Double -- meanlb
               comma
               readPrec :: ReadPrec Double -- meanub
               comma
               readPrec :: ReadPrec Double -- stddev
               comma
               readPrec :: ReadPrec Double -- stddevlb
               comma
               readPrec :: ReadPrec Double -- stddevub
               case split_tag tag of
                 (library, subsystem, datatype, name)
                    -> return $ Benchmark
                              $ Entry {
                                  eCfg = Cfg { cfgLibrary   = library
                                             , cfgSubsystem = subsystem
                                             , cfgDatatype  = datatype
                                             }
                                , eName = name
                                , eData = mean
                                }
    where
      comma = do
                c <- get
                if c == ',' then return () else pfail

      split_tag s = case split '/' s of
                     [library,subsystem,datatype,benchmark]
                        -> (library,subsystem,datatype,benchmark)
                     [library,datatype,benchmark]
                        -> (library,"",datatype,benchmark)
    
      split c xs = case span (/= c) xs of
                     (ys,[]) -> [ys]
                     (ys, _ : zs) -> ys : split c zs