live-stats-0.1.0.0: src/LiveStats.hs
module LiveStats
( StatState (..)
, Stats
, newStats
, addCounter
, readStats
, incStats
, obtainReport
, reportStats
, reportStatsForever
) where
import Relude
import Relude.Extra
import Data.Time.Clock.POSIX
import Control.Concurrent
import Numeric
newtype Stats = Stats (IORef (Map Text StatState))
data StatState = StatState
{ statStart :: !Double
-- ^ Origin start of run
, statChkPnt :: !Double
-- ^ Last time checkpoint
, statGen :: !Int
-- ^ Hits since last checkpoint
, statTotal :: !Int
-- ^ Total hits since original start
}
newStats :: (MonadIO m) => m Stats
newStats = Stats <$> newIORef mempty
-- | Each named counter needs to be added (initialized) before it can be used.
addCounter :: (MonadIO m) => Stats -> Text -> m ()
addCounter (Stats s) nm = do
now <- getTime
atomicModifyIORef' s $ \m -> (insert nm (StatState now now 0 0) m, ())
readStats :: (MonadIO m) => Stats -> m (Map Text StatState)
readStats (Stats s) = readIORef s
-- | Increment given counter by n.
incStats :: (MonadIO m) => Stats -> Text -> Int -> m ()
incStats (Stats stats) nm n = atomicModifyIORef' stats $ \m -> (alter f nm m, ())
where
f = \case
Nothing -> error "Counter should be added before usage"
Just old@StatState {statGen, statTotal} ->
let !x = Just $! old { statGen = statGen + n, statTotal = statTotal + n }
in x
-- | Produce stats messages - one for each registered counter.
obtainReport :: (MonadIO m) => Stats -> m [Text]
obtainReport (Stats stats) = do
now <- liftIO getTime
olds <- atomicModifyIORef' stats $ \m -> do
let
olds = toPairs m
reset (nm, old) = (nm, old { statGen = 0, statChkPnt = now })
news = fromList $ reset <$> olds
(news, olds)
pure $ go now <$> olds
where
go now (nm, StatState {statGen, statTotal, statStart, statChkPnt}) = do
let
chk = fromIntegral statGen / (now - statChkPnt)
tot = fromIntegral statTotal / (now - statStart)
toText $ "[" <> toString nm <> "]" <>
" Processed " <> showInt statGen
" items at " <> showFFloat (Just 2) chk
" items/sec for a total of " <> showInt statTotal
" items at " <> showFFloat (Just 2) tot " items/sec."
-- | Obtain report and call function for each line.
reportStats
:: (MonadIO m)
=> (Text -> m ())
-- ^ How to render each line
-> Stats
-> m ()
reportStats f stats = obtainReport stats >>= mapM_ f
-- | Report stats every n seconds. Blocks forever.
reportStatsForever :: (MonadIO m) => (Text -> m ()) -> Stats -> Int -> m ()
reportStatsForever f stats n = forever $ do
reportStats f stats
liftIO $ threadDelay $ n*1000*1000
getTime :: (MonadIO m) => m Double
getTime = realToFrac <$> liftIO getPOSIXTime