packages feed

timestats-0.1.4: src/Debug/TimeStats/Unsafe.hs

-- | A module with time measuring primitives that might not work in all monads
-- that building allows.
--
-- 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.Unsafe
  ( -- * Measuring
    unsafeMeasureM
  ) where

import Debug.TimeStats
         ( TimeStats(..)
         , TimeStatsRef
         , enabled
         , lookupTimeStatsRef
         , updateTimeStatsRef
         )
import GHC.Clock (getMonotonicTimeNSec)
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.
--
-- This function relies on a hack to perform IO in any monad, which does not
-- always work. In particular, we can expect it to fail in monads where
--
-- > (m >>= \_ -> undefined) == undefined -- for some computation m
--
-- An example of such a monad is the list monad
--
-- > ([()] >>= \_ -> undefined) == undefined
--
-- Another example is the @Control.Monad.Free.Free IO@.
--
-- > (Control.Monad.Free.Pure () >>= \_ -> undefined) == undefined
--
-- But it seems to work on @IO@ or @ReaderT IO@.
--
-- > seq (print () >>= \_ -> undefined) () == ()
--
-- Also, monads that run the continuation of bind multiple times might only
-- have accounted the time to run the first time only.
--
{-# INLINE unsafeMeasureM #-}
unsafeMeasureM :: Monad m => String -> m a -> m a
unsafeMeasureM label =
    -- See the documentation of 'enabled'
    if enabled then do
          -- @ref@ is the reference to the stats associated to the label.
          -- See note [Looking up stats with unsafePerformIO]
      let ref = unsafePerformIO $ lookupTimeStatsRef label
       in \action -> measureMWith ref action
    else
      id

-- | 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 tref m = do
    t0 <- intersperseIOinM getMonotonicTimeNSec
    a <- m
    intersperseIOinM $ do
      tf <- getMonotonicTimeNSec
      updateTimeStatsRef tref $ \st ->
        st
          { timeStat = (tf - t0) + timeStat st
          , countStat = 1 + countStat st
          }
    return a

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

-- | Hack to intersperse IO actions into any monad
intersperseIOinM :: Monad m => IO a -> m a
intersperseIOinM m = do
    -- The fictitious state is only used to force @unsafePerformIO@
    -- to run @m@ every time @intersperseIOinM m@ is evaluated.
    s <- getStateM
    case unsafePerformIO $ (,) s <$> m of
      (_, r) -> pure r
  where
    -- We mark this function as NOINLINE to ensure the compiler cannot reason
    -- by unfolding that two calls of @getStateM@ yield the same value.
    {-# NOINLINE getStateM #-}
    getStateM = pure True