packages feed

wrecker-0.1.2.0: src/Wrecker/Statistics.hs

{-# LANGUAGE RecordWildCards, BangPatterns, LambdaCase, OverloadedStrings #-}
module Wrecker.Statistics where
import qualified Data.HashMap.Strict as H
import Data.HashMap.Strict (HashMap)
import Wrecker.Recorder
import Wrecker.Options
import qualified Text.Tabular.AsciiArt as AsciiArt
import Text.Tabular
import Text.Printf
import System.Console.Ansigraph.Core
import qualified Data.Text as T
import qualified Data.Vector.Unboxed as U
import Data.Aeson (object, ToJSON (..), (.=), Value (..))
import Data.List (sortBy)
import Data.Function
import qualified Network.URI as URI

data Histogram = Histogram
  deriving (Show, Eq, Ord)

data VarianceAndMean = VarianceAndMean
  { var         :: {-# UNPACK #-} !Double
  , varMeanDiff :: {-# UNPACK #-} !Double
  , varMean     :: {-# UNPACK #-} !Double
  , varCount    :: {-# UNPACK #-} !Double
  } deriving (Show, Eq, Ord)

emptyVarianceAndMean :: VarianceAndMean
emptyVarianceAndMean = VarianceAndMean
  { var         = 0
  , varMeanDiff = 0
  , varMean     = 0
  , varCount    = 0
  }

stableVarianceStep :: VarianceAndMean -> Double -> VarianceAndMean
stableVarianceStep (!(VarianceAndMean {..})) !newValue =
  let !newCount     = varCount + 1
      !newMean      = varMean + ((newValue - varMean) / newCount)
      !newMeanDiff  = varMeanDiff + ((newValue - varMean)*(newValue - newMean))
  in VarianceAndMean (newMeanDiff / newCount) newMeanDiff newMean newCount

insertHist :: Histogram -> Double -> Histogram
insertHist h _ = h

-- | These are the
data Statistics = Statistics
  { sVarMean   :: {-# UNPACK #-} !VarianceAndMean
  -- ^ Combined variance and mean. This type contains information useful for
  --   incremental computation of the variance and mean. To get the individual
  --   components use 'variance' and 'mean'.
  , sMax       :: {-# UNPACK #-} !Double
  -- ^ The maximum time
  , sMin       :: {-# UNPACK #-} !Double
  -- ^ The maximum time
  , sHistogram :: {-# UNPACK #-} !Histogram
  -- ^ A histogram of times
  , sTotal     :: {-# UNPACK #-} !Double
  -- ^ The total time
  } deriving (Show, Eq, Ord)

-- | Extract the mean
mean :: Statistics -> Double
mean = varMean . sVarMean

-- | Extract the variance
variance :: Statistics -> Double
variance = var . sVarMean

statsCount :: Statistics -> Int
statsCount = floor . (+ 0.1) . varCount . sVarMean

emptyStatistics :: Statistics
emptyStatistics = Statistics
  { sVarMean   = emptyVarianceAndMean
  , sMax       = 0
  , sMin       = 1e32 -- i don't know ... maxBound won't work
  , sHistogram = Histogram
  , sTotal     = 0
  }

stepStatistics :: Statistics -> Double -> Statistics
stepStatistics !stats !value = stats
     { sVarMean   = stableVarianceStep (sVarMean stats) value
     , sMax       = max (sMax stats) value
     , sMin       = min (sMin stats) value
     , sHistogram = insertHist (sHistogram stats) value
     , sTotal     = sTotal stats + value
     }

urlToPathPieceKey :: String -> String
urlToPathPieceKey url = maybe url URI.uriPath $ URI.parseURI url

{- | This type includes statistics for all of the result values we can detect.
     This type is used by AllStats to compute per key (URL) statistics among
     other uses.
-}
data ResultStatistics = ResultStatistics
  { rs2xx    :: !Statistics
  , rs4xx    :: !Statistics
  , rs5xx    :: !Statistics
  , rsFailed :: !Statistics
  , rsRollup :: !Statistics
  } deriving (Show, Eq, Ord)

emptyResultStatistics :: ResultStatistics
emptyResultStatistics = ResultStatistics
  { rs2xx    = emptyStatistics
  , rs4xx    = emptyStatistics
  , rs5xx    = emptyStatistics
  , rsFailed = emptyStatistics
  , rsRollup = emptyStatistics
  }

stepResultStatistics :: ResultStatistics -> RunResult -> ResultStatistics
stepResultStatistics !stats = \case
  Success      { .. } -> stats { rs2xx    = stepStatistics (rs2xx    stats)
                                                           resultTime
                               , rsRollup = stepStatistics (rsRollup stats)
                                                           resultTime
                               }
  ErrorStatus  { .. }
    | is4xx errorCode -> stats { rs4xx    = stepStatistics (rs4xx    stats)
                                                           resultTime
                               , rsRollup = stepStatistics (rsRollup stats)
                                                           resultTime
                               }
    | otherwise       -> stats { rs5xx    = stepStatistics (rs5xx    stats)
                                                           resultTime
                               , rsRollup = stepStatistics (rsRollup stats)
                                                           resultTime
                               }
  Error        { .. } -> stats { rsFailed = stepStatistics (rsFailed stats)
                                                           resultTime
                               , rsRollup = stepStatistics (rsRollup stats)
                                                           resultTime
                               }
  End                 -> stats

count2xx :: ResultStatistics -> Int
count2xx = statsCount . rs2xx

count4xx :: ResultStatistics -> Int
count4xx = statsCount . rs4xx

count5xx :: ResultStatistics -> Int
count5xx = statsCount . rs5xx

countFailed :: ResultStatistics -> Int
countFailed = statsCount . rsFailed

errorRate :: ResultStatistics -> Double
errorRate x
  = fromIntegral (count4xx x + count5xx x + countFailed x)
  / fromIntegral (count2xx x + count4xx x + count5xx x + countFailed x)

isEntirelySuccessful :: ResultStatistics -> Bool
isEntirelySuccessful x = (count4xx x + count5xx x + countFailed x) == 0

successfulToResult :: Statistics -> ResultStatistics
successfulToResult x = emptyResultStatistics { rs2xx = x }

{- | AllStats has all of the ... stats. This type stores all of the information
     'wrecker' uses to display metrics to the user.
-}
data AllStats = AllStats
  { aRollup       :: !ResultStatistics
  -- ^ The "total" stats. This computes things like total 2xx and average time
  --   Across all requests.
  , aCompleteRuns :: !ResultStatistics
  -- ^ This contains statistic for actions that completed entirely successfully.
  --   Useful for knowing if a complex action is under some desired total time.
  , aRuns         :: !(HashMap Int    ResultStatistics)
  -- ^ This is an intermediate holding spot for scripts that are still executing.
  , aPerUrl       :: !(HashMap String ResultStatistics)
  -- ^ This is the per key (URL) statistics.
  } deriving (Show, Eq)

emptyAllStats :: AllStats
emptyAllStats = AllStats
  { aRollup       = emptyResultStatistics
  , aCompleteRuns = emptyResultStatistics
  , aRuns         = H.empty
  , aPerUrl       = H.empty
  }

is4xx :: Int -> Bool
is4xx x = x > 399 && x < 500

stepAllStats :: AllStats -> Int -> String -> RunResult -> AllStats
stepAllStats allStats index key result =
  case result of
    End ->  let mRunStats = H.lookup index $ aRuns allStats
            in case mRunStats of
              Nothing -> allStats
              Just stats
                 | errorRate stats == 0 ->
                    let runTime = sTotal $ rs2xx stats
                    in allStats { aCompleteRuns = stepResultStatistics
                                                    (aCompleteRuns allStats)
                                                    (Success runTime "")
                                , aRuns         = H.delete index
                                                $ aRuns allStats
                                }
                 | otherwise -> allStats { aRuns = H.delete index
                                                 $ aRuns allStats
                                         }
    _   -> allStats
            { aRollup = stepResultStatistics (aRollup allStats) result
            , aRuns   = H.insertWith (\_ x -> stepResultStatistics x result)
                                     index
                                     (stepResultStatistics
                                       emptyResultStatistics
                                       result
                                     )
                                     $ aRuns allStats
            , aPerUrl = H.insertWith (\_ x -> stepResultStatistics x result)
                                     key
                                     (stepResultStatistics
                                       emptyResultStatistics
                                       result
                                     )
                      $ aPerUrl allStats
            }
-------------------------------------------------------------------------------
-- Rendering
-------------------------------------------------------------------------------
renderHistogram :: U.Vector Int -> String
renderHistogram bins = renderPV $ U.toList powers where
  total  = fromIntegral $ U.sum bins
  powers = U.map (\x -> fromIntegral x / total) bins

statToRow :: ResultStatistics -> [String]
statToRow x
  = [ printf "%.4f" $ mean     $ rs2xx x
    , printf "%.8f" $ variance $ rs2xx x
    , printf "%.4f" $ sMax     $ rs2xx x

    , let theMin = sMin $ rs2xx x
      in if theMin == 1e32 then
        "N/A"
      else
        printf "%.4f" $ sMin   $ rs2xx x

    , show $ count2xx x
    , show $ count4xx x
    , show $ count5xx x
    , show $ countFailed x
    ,  printf "%.4f" $ errorRate x
--    , renderHistogram $ mempty
    ]

pprStats :: Maybe Int -> URLDisplay -> AllStats -> String
pprStats nameSize urlDisplay stats
  = AsciiArt.render id id id
  $ statsTable nameSize urlDisplay stats

adjustKey ::Maybe Int -> URLDisplay -> String -> String
adjustKey keySize urlDisplay key
  = maybe id take keySize
  $ case urlDisplay of
      Path -> urlToPathPieceKey key
      Full -> key

statsTable ::  Maybe Int -> URLDisplay -> AllStats -> Table String String String
statsTable urlSize urlDisp AllStats {..}
  = let sortedPerUrl = sortBy (compare `on` fst) $ H.toList aPerUrl
  in Table (Group SingleLine
          $ map (Header . adjustKey urlSize urlDisp . fst) sortedPerUrl
          )
          (Group SingleLine [ Header "mean"
                            , Header "variance"
                            , Header "max"
                            , Header "min"
                            , Header "Successful Count"
                            , Header "4xx Count"
                            , Header "5xx Count"
                            , Header "Failure Count"
                            , Header "Error Rate"
--                            , Header "Histogram"
                            ]
          )
          (map (statToRow . snd) sortedPerUrl)
  +====+ SemiTable (Group SingleLine [Header "All"]) (statToRow aRollup)
  +====+ SemiTable (Group SingleLine [Header "Successful Runs"])
                   (statToRow aCompleteRuns)

printStats :: Options -> AllStats -> IO ()
printStats options sampler
  = putStrLn $ pprStats (requestNameColumnSize options)
                        (urlDisplay options)
                        sampler
------------------------------------------------------------------------------
-- JSON Serialization
------------------------------------------------------------------------------
instance ToJSON Statistics where
  toJSON x = object
    [ "mean"     .= mean       x
    , "variance" .= variance   x
    , "max"      .= sMax       x
    , "min"      .= sMin       x
    , "total"    .= sTotal     x
    , "count"    .= statsCount x
    ]

instance ToJSON ResultStatistics where
  toJSON ResultStatistics {..} = object
    [ "2xx"    .= rs2xx
    , "4xx"    .= rs4xx
    , "5xx"    .= rs5xx
    , "failed" .= rsFailed
    , "rollup" .= rsRollup
    ]

instance ToJSON AllStats where
  toJSON AllStats {..} = object
    [ "per-request" .= Object
                     ( H.fromList
                     $ map (\(k, v) -> (T.pack k, toJSON v))
                     $ H.toList aPerUrl
                     )
    , "runs"        .= aCompleteRuns
    , "rollup"      .= aRollup
    ]