packages feed

nri-observability-0.1.0.0: src/Platform/Timer.hs

{-# OPTIONS_GHC -fno-cse #-}

-- | Helper functions for converting the timestamp format used in our `Span`
-- data to the time formats used by the various platforms we report to.
module Platform.Timer
  ( Timer (Timer),
    mkTimer,
    toUTC,
    toLocal,
    toPosix,
    toPosixMicroseconds,
    toISO8601,
    difference,
    durationInUs,
  )
where

import qualified Control.Concurrent.MVar as MVar
import qualified Data.Time.Clock as Clock
import qualified Data.Time.Clock.POSIX as Clock.POSIX
import qualified Data.Time.Format as Format
import qualified Data.Time.LocalTime as LocalTime
import qualified Data.Word as Word
import qualified GHC.Clock
import qualified Platform
import qualified System.IO.Unsafe
import qualified Prelude

-- | Our spans' timestamps are produced by the `GHC.Clock` module and consist of
-- the amount of time passed since some arbitrary (but constant) moment in the
-- past. This is the faster and more accurate way to measure precisely what the
-- running time of spans is. This type helpers convert these times into regular
-- dates.
data Timer = Timer
  { -- | The POSIX time in microseconds that corresponds with t=0 according
    -- to `GHC.Clock`. We can use this to calculate other `GHC.Clock`
    -- values.
    tzero :: Word.Word64,
    -- | The timezone of the machine this code is running on. Useful for
    -- printing local times in development reporters.
    timezone :: LocalTime.TimeZone
  }

-- | Create a timer, then cache it. When asked again return the previously
-- created timer.
--
-- Passing separate timers to multiple reporters could result in those reporters
-- disagreeing very subtly on the exact time when events happen. Having a single
-- timer prevents this from happening.
mkTimer :: Prelude.IO Timer
mkTimer =
  MVar.modifyMVar
    timerVar
    ( \maybeTimer ->
        case maybeTimer of
          Just timer -> Prelude.pure (Just timer, timer)
          Nothing -> do
            -- 'Sync our clocks', to find our how monotonic time and actual time relate.
            nowTime <- Clock.POSIX.getPOSIXTime
            nowClock <- GHC.Clock.getMonotonicTimeNSec
            timezone <- LocalTime.getCurrentTimeZone
            let tzero = Prelude.floor (1e6 * nowTime) - (nowClock `Prelude.div` 1000)
            let timer = Timer {tzero, timezone}
            Prelude.pure (Just timer, timer)
    )

{-# NOINLINE timerVar #-}
timerVar :: MVar.MVar (Maybe Timer)
timerVar = System.IO.Unsafe.unsafePerformIO (MVar.newMVar Nothing)

toUTC :: Timer -> Platform.MonotonicTime -> Clock.UTCTime
toUTC timer clock =
  toPosix timer clock
    |> Clock.POSIX.posixSecondsToUTCTime

toLocal :: Timer -> Platform.MonotonicTime -> LocalTime.LocalTime
toLocal timer clock =
  toUTC timer clock
    |> LocalTime.utcToLocalTime (timezone timer)

toPosixMicroseconds :: Timer -> Platform.MonotonicTime -> Word.Word64
toPosixMicroseconds timer clock = tzero timer + Platform.inMicroseconds clock

toPosix :: Timer -> Platform.MonotonicTime -> Clock.POSIX.POSIXTime
toPosix timer clock =
  toPosixMicroseconds timer clock
    |> Prelude.fromIntegral
    |> (*) 1e-6

toISO8601 :: Timer -> Platform.MonotonicTime -> Text
toISO8601 timer clock =
  toUTC timer clock
    |> Format.formatTime Format.defaultTimeLocale "%FT%T%QZ"
    |> Text.fromList

-- | We have to be careful when calculating the difference between two times.
-- Because they are unsigned (don't allow negative numbers), subtracting times
-- in the wrong order is going to result in very large numbers:
--
--     ghci> import GHC.Word
--     ghci> 5 - 2 :: Word64
--     3
--     ghci> 2 - 5 :: Word64
--     18446744073709551613
--
-- The span data we get from Platform should ensure end times always come
-- before start times. If they're not though one of these extremely long span
-- durations can have a major effect on request duration statistics.
--
-- This function performs some defensive programming to prevent flukes from
-- doing major damage.
difference :: Platform.MonotonicTime -> Platform.MonotonicTime -> Platform.MonotonicTime
difference start end =
  if end > start
    then end - start
    else 0

-- | Get the time covered by a duration in microseconds.
durationInUs :: Platform.TracingSpan -> Word.Word64
durationInUs span =
  difference
    (Platform.started span)
    (Platform.finished span)
    |> Platform.inMicroseconds