{-# LANGUAGE ScopedTypeVariables #-}

-- | A module to collect aggregates on how much time is spent in a computation
--
-- Aggregates can be identified with a label that determines where the time of
-- each computation is accounted for.
--
-- Measures are collected only if the environment variable
-- @DEBUG_TIMESTATS_ENABLE@ is set to any value ahead of invoking any function
-- in this module.
--
module Debug.TimeStats
  ( -- * Measuring
    measureM
  , measurePure
    -- * Time stats manipulation
  , printTimeStats
  , hPrintTimeStats
  , reset
  , TimeStats(..)
  , collect
  , asText
  , scope
    -- * Not intended for direct use
    --
    -- | These definitions are not intended for instrumenting applications,
    -- but they can be handy to implement other measuring primitives.
    --
  , TimeStatsRef
  , lookupTimeStatsRef
  , updateTimeStatsRef
  ) where

import Control.Exception (evaluate)
import Control.Monad (forM, forM_, unless)
import Data.IORef
import Data.Map (Map)
import Data.Maybe (isJust)
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import Data.Word (Word64)
import Debug.TimeStats.Internal (formatIntWithSeparator)
import GHC.Clock (getMonotonicTimeNSec)
import Text.Printf (printf)
import System.Environment (lookupEnv)
import System.IO (Handle, stderr)
import System.IO.Unsafe (unsafePerformIO)


-- | Measure the time it takes to run the action.
--
-- Add the time to the stats of the given label and increase its count by one.
--
-- 'measureM' keeps the stats in a globally available store in order to minimize
-- the changes necessary when instrumenting a program. Otherwise a reference to
-- the store would need to be passed to every function that might invoke
-- functions that need this reference.
--
-- A time measure isn't collected if the given action fails with an exception.
-- This is a deliberate choice to demand less of the monad in which measures are
-- taken.
--
-- Time measures aren't collected either if the environment variable
-- @DEBUG_TIMESTATS_ENABLE@ isn't set the first time this function is
-- evaluated.
--
{-# INLINE measureM #-}
measureM :: Monad m => String -> m a -> m a
measureM :: forall (m :: * -> *) a. Monad m => String -> m a -> m a
measureM String
label =
    -- See the documentation of 'enabled'
    if Bool
enabled then do
          -- @ref@ is the reference to the stats associated to the label.
          -- See note [Looking up stats with unsafePerformIO]
      let ref :: TimeStatsRef
ref = IO TimeStatsRef -> TimeStatsRef
forall a. IO a -> a
unsafePerformIO (IO TimeStatsRef -> TimeStatsRef)
-> IO TimeStatsRef -> TimeStatsRef
forall a b. (a -> b) -> a -> b
$ String -> IO TimeStatsRef
lookupTimeStatsRef String
label
       in \m a
action -> TimeStatsRef -> m a -> m a
forall (m :: * -> *) a. Monad m => TimeStatsRef -> m a -> m a
measureMWith TimeStatsRef
ref m a
action
    else
      m a -> m a
forall a. a -> a
id

-- | Pure version of 'measureM'. Measures the time taken to reduce the given
-- value to head normal form.
--
-- 'measurePure' is a bit dangerous to use in contexts where there are monadic
-- computations. If 'measurePure' is applied to a monadic computation it
-- will measure the time of constructing the computation rather than the time
-- of executing it, and the typechecker won't catch the mistake. We try to
-- fence against it with a longer name.
{-# INLINE measurePure #-}
measurePure :: String -> a -> a
measurePure :: forall a. String -> a -> a
measurePure String
label =
    if Bool
enabled then
      IO a -> a
forall a. IO a -> a
unsafePerformIO (IO a -> a) -> (a -> IO a) -> a -> a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> IO a -> IO a
forall (m :: * -> *) a. Monad m => String -> m a -> m a
measureM String
label (IO a -> IO a) -> (a -> IO a) -> a -> IO a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. a -> IO a
forall a. a -> IO a
evaluate
    else
      a -> a
forall a. a -> a
id

-- Note [Looking up stats with unsafePerformIO]
--
-- When calling 'measureM' we would like to save the trouble of looking the
-- stats to update on every invocation. Hence, we use unsafePerformIO, and
-- we ask to inline 'measureM'.
--
-- Most of the time 'measureM' should be called with a statically known label.
-- When inlining, GHC should notice this fact and move the lookup closure to
-- the top-level, thus performing it only once per invocation, and perhaps
-- only once per label for all 'measureM' calls in the same module.


-- | @True@ iff the environment variable @DEBUG_TIMESTATS_ENABLE@ is set to any
-- value
--
-- We assume the value of the environment variable doesn't change during the
-- lifetime of the program.
--
-- The purpose of making this a top-level value is to have all calls to
-- 'measureM' checking it only the first time. Thus we save the trouble of
-- looking up the environment variable repeteadly.
{-# NOINLINE enabled #-}
enabled :: Bool
enabled :: Bool
enabled = IO Bool -> Bool
forall a. IO a -> a
unsafePerformIO (IO Bool -> Bool) -> IO Bool -> Bool
forall a b. (a -> b) -> a -> b
$ Maybe String -> Bool
forall a. Maybe a -> Bool
isJust (Maybe String -> Bool) -> IO (Maybe String) -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> IO (Maybe String)
lookupEnv String
"DEBUG_TIMESTATS_ENABLE"

-- | A unique global reference to the map associating labels to their
-- stats.
{-# NOINLINE labelStatsMapRef #-}
labelStatsMapRef :: IORef (Map String TimeStatsRef)
labelStatsMapRef :: IORef (Map String TimeStatsRef)
labelStatsMapRef = IO (IORef (Map String TimeStatsRef))
-> IORef (Map String TimeStatsRef)
forall a. IO a -> a
unsafePerformIO (IO (IORef (Map String TimeStatsRef))
 -> IORef (Map String TimeStatsRef))
-> IO (IORef (Map String TimeStatsRef))
-> IORef (Map String TimeStatsRef)
forall a b. (a -> b) -> a -> b
$ Map String TimeStatsRef -> IO (IORef (Map String TimeStatsRef))
forall a. a -> IO (IORef a)
newIORef Map String TimeStatsRef
forall k a. Map k a
Map.empty

-- | Set all statistics to initial values.
reset :: Monad m => m ()
reset :: forall (m :: * -> *). Monad m => m ()
reset = IO () -> m ()
forall a (m :: * -> *). Monad m => IO a -> m a
intersperseIOinM (IO () -> m ()) -> IO () -> m ()
forall a b. (a -> b) -> a -> b
$
    if Bool
enabled then do
      Map String TimeStatsRef
m <- IORef (Map String TimeStatsRef) -> IO (Map String TimeStatsRef)
forall a. IORef a -> IO a
readIORef IORef (Map String TimeStatsRef)
labelStatsMapRef
      [TimeStatsRef] -> (TimeStatsRef -> IO ()) -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
t a -> (a -> m b) -> m ()
forM_ (Map String TimeStatsRef -> [TimeStatsRef]
forall k a. Map k a -> [a]
Map.elems Map String TimeStatsRef
m) ((TimeStatsRef -> IO ()) -> IO ())
-> (TimeStatsRef -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \(TimeStatsRef IORef TimeStats
ref) ->
        IORef TimeStats -> TimeStats -> IO ()
forall a. IORef a -> a -> IO ()
writeIORef IORef TimeStats
ref TimeStats
initialTimeStats
     else
      () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()

-- | Run an action by previously reseting all stats to initial values
-- and printing them afterwards.
scope :: Monad m => m a -> m a
scope :: forall (m :: * -> *) a. Monad m => m a -> m a
scope =
    if Bool
enabled then
      \m a
m -> do
        m ()
forall (m :: * -> *). Monad m => m ()
reset
        a
a <- m a
m
        Handle -> m ()
forall (m :: * -> *). Monad m => Handle -> m ()
hPrintTimeStats Handle
stderr
        a -> m a
forall a. a -> m a
forall (m :: * -> *) a. Monad m => a -> m a
return a
a
     else
      m a -> m a
forall a. a -> a
id

-- | Looks up the stats of a label. If no stats are found for the label,
-- a new TimeStatsRef is created with initial values.
--
lookupTimeStatsRef :: String -> IO TimeStatsRef
lookupTimeStatsRef :: String -> IO TimeStatsRef
lookupTimeStatsRef String
label = do
    TimeStatsRef
r0 <- IO TimeStatsRef
forall (m :: * -> *). Monad m => m TimeStatsRef
newTimeStatsRef
    IORef (Map String TimeStatsRef)
-> (Map String TimeStatsRef
    -> (Map String TimeStatsRef, TimeStatsRef))
-> IO TimeStatsRef
forall a b. IORef a -> (a -> (a, b)) -> IO b
atomicModifyIORef IORef (Map String TimeStatsRef)
labelStatsMapRef ((Map String TimeStatsRef
  -> (Map String TimeStatsRef, TimeStatsRef))
 -> IO TimeStatsRef)
-> (Map String TimeStatsRef
    -> (Map String TimeStatsRef, TimeStatsRef))
-> IO TimeStatsRef
forall a b. (a -> b) -> a -> b
$ \Map String TimeStatsRef
m ->
      case String -> Map String TimeStatsRef -> Maybe TimeStatsRef
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup String
label Map String TimeStatsRef
m of
        Maybe TimeStatsRef
Nothing -> (String
-> TimeStatsRef
-> Map String TimeStatsRef
-> Map String TimeStatsRef
forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert String
label TimeStatsRef
r0 Map String TimeStatsRef
m, TimeStatsRef
r0)
        Just TimeStatsRef
r -> (Map String TimeStatsRef
m, TimeStatsRef
r)

-- | Yields the labels and the stats collected thus far.
collect :: Monad m => m [(String, TimeStats)]
collect :: forall (m :: * -> *). Monad m => m [(String, TimeStats)]
collect = IO [(String, TimeStats)] -> m [(String, TimeStats)]
forall a (m :: * -> *). Monad m => IO a -> m a
intersperseIOinM (IO [(String, TimeStats)] -> m [(String, TimeStats)])
-> IO [(String, TimeStats)] -> m [(String, TimeStats)]
forall a b. (a -> b) -> a -> b
$ do
    Map String TimeStatsRef
m <- IORef (Map String TimeStatsRef) -> IO (Map String TimeStatsRef)
forall a. IORef a -> IO a
readIORef IORef (Map String TimeStatsRef)
labelStatsMapRef
    [(String, TimeStatsRef)]
-> ((String, TimeStatsRef) -> IO (String, TimeStats))
-> IO [(String, TimeStats)]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM (Map String TimeStatsRef -> [(String, TimeStatsRef)]
forall k a. Map k a -> [(k, a)]
Map.toList Map String TimeStatsRef
m) (((String, TimeStatsRef) -> IO (String, TimeStats))
 -> IO [(String, TimeStats)])
-> ((String, TimeStatsRef) -> IO (String, TimeStats))
-> IO [(String, TimeStats)]
forall a b. (a -> b) -> a -> b
$ \(String
label, TimeStatsRef IORef TimeStats
ref) ->
      (,) String
label (TimeStats -> (String, TimeStats))
-> IO TimeStats -> IO (String, TimeStats)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IORef TimeStats -> IO TimeStats
forall a. IORef a -> IO a
readIORef IORef TimeStats
ref

-- | Prints the time stats to the given handle.
hPrintTimeStats :: Monad m => Handle -> m ()
hPrintTimeStats :: forall (m :: * -> *). Monad m => Handle -> m ()
hPrintTimeStats Handle
h = IO () -> m ()
forall a (m :: * -> *). Monad m => IO a -> m a
intersperseIOinM (IO () -> m ()) -> IO () -> m ()
forall a b. (a -> b) -> a -> b
$ do
    [(String, TimeStats)]
xs <- IO [(String, TimeStats)]
forall (m :: * -> *). Monad m => m [(String, TimeStats)]
collect
    Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless ([(String, TimeStats)] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(String, TimeStats)]
xs) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$
      Handle -> Text -> IO ()
Text.hPutStrLn Handle
h ([(String, TimeStats)] -> Text
asText [(String, TimeStats)]
xs)

-- | Prints the time stats to stderr.
printTimeStats :: Monad m => m ()
printTimeStats :: forall (m :: * -> *). Monad m => m ()
printTimeStats = Handle -> m ()
forall (m :: * -> *). Monad m => Handle -> m ()
hPrintTimeStats Handle
stderr

-- | Renders the given time stats in a tabular format
asText :: [(String, TimeStats)] -> Text
asText :: [(String, TimeStats)] -> Text
asText [(String, TimeStats)]
stats =
    let ([String]
lbls, [TimeStats]
timestats) = [(String, TimeStats)] -> ([String], [TimeStats])
forall a b. [(a, b)] -> ([a], [b])
unzip [(String, TimeStats)]
stats
        ([String]
times, [String]
counts) = [(String, String)] -> ([String], [String])
forall a b. [(a, b)] -> ([a], [b])
unzip ([(String, String)] -> ([String], [String]))
-> [(String, String)] -> ([String], [String])
forall a b. (a -> b) -> a -> b
$ (TimeStats -> (String, String))
-> [TimeStats] -> [(String, String)]
forall a b. (a -> b) -> [a] -> [b]
map TimeStats -> (String, String)
formatTimeStats [TimeStats]
timestats
        widthLbls :: Int
widthLbls = [Int] -> Int
forall a. Ord a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
maximum ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (String -> Int) -> [String] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map String -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [String]
lbls
        widthTimes :: Int
widthTimes = [Int] -> Int
forall a. Ord a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
maximum ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (String -> Int) -> [String] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map String -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [String]
times
        widthCounts :: Int
widthCounts = [Int] -> Int
forall a. Ord a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
maximum ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (String -> Int) -> [String] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map String -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [String]
counts
     in [Text] -> Text
Text.unlines ([Text] -> Text) -> [Text] -> Text
forall a b. (a -> b) -> a -> b
$
        ((String, String, String) -> Text)
-> [(String, String, String)] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (String -> Text
Text.pack (String -> Text)
-> ((String, String, String) -> String)
-> (String, String, String)
-> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Int -> Int -> (String, String, String) -> String
printStat Int
widthLbls Int
widthTimes Int
widthCounts) ([(String, String, String)] -> [Text])
-> [(String, String, String)] -> [Text]
forall a b. (a -> b) -> a -> b
$
        [String] -> [String] -> [String] -> [(String, String, String)]
forall a b c. [a] -> [b] -> [c] -> [(a, b, c)]
zip3 [String]
lbls [String]
times [String]
counts
  where
    formatTimeStats :: TimeStats -> (String, String)
    formatTimeStats :: TimeStats -> (String, String)
formatTimeStats TimeStats
t =
      ( String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f" (Word64 -> Double
forall a b. (Integral a, Num b) => a -> b
fromIntegral (TimeStats -> Word64
timeStat TimeStats
t) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ Double
1e9 :: Double)
      , Char -> Int -> String -> String
formatIntWithSeparator Char
'_' (TimeStats -> Int
countStat TimeStats
t) String
""
      )

    -- At the time of this writing printf can't render to 'Text'.
    printStat :: Int -> Int -> Int -> (String, String, String) -> String
    printStat :: Int -> Int -> Int -> (String, String, String) -> String
printStat Int
widthLbls Int
widthTimes Int
widthCounts (String
label, String
time, String
count) =
      let fmt :: String
fmt = [String] -> String
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat
            [ String
"%", Int -> String
forall a. Show a => a -> String
show Int
widthLbls
            , String
"s: %", Int -> String
forall a. Show a => a -> String
show Int
widthTimes
            , String
"ss  count: %", Int -> String
forall a. Show a => a -> String
show Int
widthCounts, String
"s"
            ]
       in String -> Text -> String -> String -> String
forall r. PrintfType r => String -> r
printf String
fmt (String -> Text
Text.pack String
label) String
time String
count

---------------------
-- TimeStats
---------------------

-- | A reference to a 'TimeStats' value
newtype TimeStatsRef = TimeStatsRef (IORef TimeStats)

-- | Reports how much time (in nanoseconds) the invocations to 'measureM' took
-- for a given label and how many times it was invoked on a given label.
data TimeStats = TimeStats
    { TimeStats -> Word64
timeStat :: {-# UNPACK #-} !Word64
    , TimeStats -> Int
countStat :: {-# UNPACK #-} !Int
    }
  deriving Int -> TimeStats -> String -> String
[TimeStats] -> String -> String
TimeStats -> String
(Int -> TimeStats -> String -> String)
-> (TimeStats -> String)
-> ([TimeStats] -> String -> String)
-> Show TimeStats
forall a.
(Int -> a -> String -> String)
-> (a -> String) -> ([a] -> String -> String) -> Show a
$cshowsPrec :: Int -> TimeStats -> String -> String
showsPrec :: Int -> TimeStats -> String -> String
$cshow :: TimeStats -> String
show :: TimeStats -> String
$cshowList :: [TimeStats] -> String -> String
showList :: [TimeStats] -> String -> String
Show

-- | Measured time is 0 and call count is 0.
initialTimeStats :: TimeStats
initialTimeStats :: TimeStats
initialTimeStats = Word64 -> Int -> TimeStats
TimeStats Word64
0 Int
0

-- | Creates a reference to time stats with intial values
newTimeStatsRef :: Monad m => m TimeStatsRef
newTimeStatsRef :: forall (m :: * -> *). Monad m => m TimeStatsRef
newTimeStatsRef = IO TimeStatsRef -> m TimeStatsRef
forall a (m :: * -> *). Monad m => IO a -> m a
intersperseIOinM (IO TimeStatsRef -> m TimeStatsRef)
-> IO TimeStatsRef -> m TimeStatsRef
forall a b. (a -> b) -> a -> b
$ IORef TimeStats -> TimeStatsRef
TimeStatsRef (IORef TimeStats -> TimeStatsRef)
-> IO (IORef TimeStats) -> IO TimeStatsRef
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> TimeStats -> IO (IORef TimeStats)
forall a. a -> IO (IORef a)
newIORef TimeStats
initialTimeStats

-- | Measure the time it takes to run the given action and update with it
-- the given reference to time stats.
measureMWith :: Monad m => TimeStatsRef -> m a -> m a
measureMWith :: forall (m :: * -> *) a. Monad m => TimeStatsRef -> m a -> m a
measureMWith TimeStatsRef
tref m a
m = do
    Word64
t0 <- IO Word64 -> m Word64
forall a (m :: * -> *). Monad m => IO a -> m a
intersperseIOinM IO Word64
getMonotonicTimeNSec
    a
a <- m a
m
    IO () -> m ()
forall a (m :: * -> *). Monad m => IO a -> m a
intersperseIOinM (IO () -> m ()) -> IO () -> m ()
forall a b. (a -> b) -> a -> b
$ do
      Word64
tf <- IO Word64
getMonotonicTimeNSec
      TimeStatsRef -> (TimeStats -> TimeStats) -> IO ()
updateTimeStatsRef TimeStatsRef
tref ((TimeStats -> TimeStats) -> IO ())
-> (TimeStats -> TimeStats) -> IO ()
forall a b. (a -> b) -> a -> b
$ \TimeStats
st ->
        TimeStats
st
          { timeStat = (tf - t0) + timeStat st
          , countStat = 1 + countStat st
          }
    a -> m a
forall a. a -> m a
forall (m :: * -> *) a. Monad m => a -> m a
return a
a

-- | Updates the TimeStats in a TimeStatsRef
updateTimeStatsRef :: TimeStatsRef -> (TimeStats -> TimeStats) -> IO ()
updateTimeStatsRef :: TimeStatsRef -> (TimeStats -> TimeStats) -> IO ()
updateTimeStatsRef (TimeStatsRef IORef TimeStats
ref) TimeStats -> TimeStats
f =
    IORef TimeStats -> (TimeStats -> (TimeStats, ())) -> IO ()
forall a b. IORef a -> (a -> (a, b)) -> IO b
atomicModifyIORef' IORef TimeStats
ref ((TimeStats -> (TimeStats, ())) -> IO ())
-> (TimeStats -> (TimeStats, ())) -> IO ()
forall a b. (a -> b) -> a -> b
$ \TimeStats
st -> (TimeStats -> TimeStats
f TimeStats
st, ())

---------------------
-- intersperseIOinM
---------------------

-- | Hack to intersperse IO actions into any monad
intersperseIOinM :: forall a m. Monad m => IO a -> m a
intersperseIOinM :: forall a (m :: * -> *). Monad m => IO a -> m a
intersperseIOinM IO a
m = do
    -- The ficticious state is only used to force unsafePerformIO to run @m@
    -- every time @intersperseIOinM m@ is evaluated.
    Int
s <- m Int
getStateM
    a -> m a
forall a. a -> m a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (a -> m a) -> a -> m a
forall a b. (a -> b) -> a -> b
$! (Int, a) -> a
forall a b. (a, b) -> b
snd ((Int, a) -> a) -> (Int, a) -> a
forall a b. (a -> b) -> a -> b
$ IO (Int, a) -> (Int, a)
forall a. IO a -> a
unsafePerformIO (IO (Int, a) -> (Int, a)) -> IO (Int, a) -> (Int, a)
forall a b. (a -> b) -> a -> b
$ do
      a
r <- IO a
m
      (Int, a) -> IO (Int, a)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Int
s, a
r)
  where
    -- We mark this function as NOINLINE to ensure the compiler cannot reason
    -- that two calls of @getStateM@ might yield the same value.
    {-# NOINLINE getStateM #-}
    getStateM :: m Int
    getStateM :: m Int
getStateM = Int -> m Int
forall a. a -> m a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Int
0