nano-ui-0.1.0.0: lib/NanoUI/Debug.hs
-- | Debug readout sampling shared by the backends: frame timing and skip
-- counts, RTS statistics, draw counts, and the rows the debug windows show.
module NanoUI.Debug
( debugRefreshSec
, blend
, RtsStatsSnapshot (..)
, readRtsSnapshot
, CoreDebugSnapshot (..)
, emptyCoreDebugSnapshot
, DebugSampler (..)
, DebugSamplerRef
, newDebugSampler
, noteDebugLoop
, noteDebugSkip
, isDebugActive
, debugRefreshDue
, noteDebugPresent
, refreshDebugSnapshot
, formatFpsRows
, formatDrawRows
, formatCoreRtsRows
) where
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef, writeIORef)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word32, Word64)
import GHC.Clock (getMonotonicTime)
import GHC.Conc (getNumCapabilities, getNumProcessors)
import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats, getRTSStatsEnabled)
import Text.Printf (printf)
debugRefreshSec :: Double
debugRefreshSec = 0.25
blend :: Double -> Double -> Double
blend prev sample
| prev <= 0 = sample
| otherwise = prev * 0.85 + sample * 0.15
data RtsStatsSnapshot = RtsStatsSnapshot
{ rtsEnabled :: !Bool
, rtsGcs :: !Word32
, rtsMajorGcs :: !Word32
, rtsAllocMb :: !Double
, rtsLiveMb :: !Double
, rtsMaxMemMb :: !Double
, rtsCopiedMb :: !Double
, rtsGcPct :: !Double
, rtsLastGcGen :: !Word32
, rtsLastGcMs :: !Double
, rtsCaps :: !Int
, rtsCpus :: !Int
}
deriving (Eq, Show)
emptyRtsSnapshot :: RtsStatsSnapshot
emptyRtsSnapshot =
RtsStatsSnapshot
{ rtsEnabled = False
, rtsGcs = 0
, rtsMajorGcs = 0
, rtsAllocMb = 0
, rtsLiveMb = 0
, rtsMaxMemMb = 0
, rtsCopiedMb = 0
, rtsGcPct = 0
, rtsLastGcGen = 0
, rtsLastGcMs = 0
, rtsCaps = 0
, rtsCpus = 0
}
readRtsSnapshot :: IO RtsStatsSnapshot
readRtsSnapshot = do
caps <- getNumCapabilities
cpus <- getNumProcessors
rtsOn <- getRTSStatsEnabled
if not rtsOn
then pure emptyRtsSnapshot {rtsCaps = caps, rtsCpus = cpus}
else do
st <- getRTSStats
let tot = elapsed_ns st
lastGc = gc st
bytesMb n = fromIntegral n / (1024 * 1024)
pure
RtsStatsSnapshot
{ rtsEnabled = True
, rtsGcs = gcs st
, rtsMajorGcs = major_gcs st
, rtsAllocMb = bytesMb (allocated_bytes st)
, rtsLiveMb = bytesMb (gcdetails_live_bytes lastGc)
, rtsMaxMemMb = bytesMb (max_mem_in_use_bytes st)
, rtsCopiedMb = bytesMb (copied_bytes st)
, rtsGcPct =
if tot > 0 then 100 * fromIntegral (gc_elapsed_ns st) / fromIntegral tot else 0
, rtsLastGcGen = gcdetails_gen lastGc
, rtsLastGcMs = fromIntegral (gcdetails_elapsed_ns lastGc) / 1.0e6
, rtsCaps = caps
, rtsCpus = cpus
}
data CoreDebugSnapshot = CoreDebugSnapshot
{ dbgPresentFps :: !Double
, dbgLoopFps :: !Double
, dbgFrameMs :: !Double
, dbgUiMs :: !Double
, dbgRenderMs :: !Double
, dbgPresentMs :: !Double
, dbgPresents :: !Word64
, dbgSkips :: !Word64
, dbgVerts :: !Int
, dbgIndices :: !Int
, dbgCmds :: !Int
, dbgWinW :: !Float
, dbgWinH :: !Float
, dbgMouseX :: !Float
, dbgMouseY :: !Float
, dbgRts :: !RtsStatsSnapshot
}
deriving (Eq, Show)
emptyCoreDebugSnapshot :: CoreDebugSnapshot
emptyCoreDebugSnapshot =
CoreDebugSnapshot
{ dbgPresentFps = 0
, dbgLoopFps = 0
, dbgFrameMs = 0
, dbgUiMs = 0
, dbgRenderMs = 0
, dbgPresentMs = 0
, dbgPresents = 0
, dbgSkips = 0
, dbgVerts = 0
, dbgIndices = 0
, dbgCmds = 0
, dbgWinW = 0
, dbgWinH = 0
, dbgMouseX = 0
, dbgMouseY = 0
, dbgRts = emptyRtsSnapshot
}
data DebugSampler = DebugSampler
{ smPresentEma :: {-# UNPACK #-} !Double
, smLoopEma :: {-# UNPACK #-} !Double
, smLastPresentT :: {-# UNPACK #-} !Double
, smLastDebugT :: {-# UNPACK #-} !Double
, smLastQueryT :: {-# UNPACK #-} !Double
, smPresents :: {-# UNPACK #-} !Word64
, smSkips :: {-# UNPACK #-} !Word64
, smUiMs :: {-# UNPACK #-} !Double
, smRenderMs :: {-# UNPACK #-} !Double
, smPresentMs :: {-# UNPACK #-} !Double
, smFrameMs :: {-# UNPACK #-} !Double
, smVerts :: {-# UNPACK #-} !Int
, smIndices :: {-# UNPACK #-} !Int
, smCmds :: {-# UNPACK #-} !Int
, smRatePresents :: {-# UNPACK #-} !Word64
, smRateT :: {-# UNPACK #-} !Double
}
type DebugSamplerRef = IORef DebugSampler
newDebugSampler :: IO DebugSamplerRef
newDebugSampler = do
now <- getMonotonicTime
newIORef
DebugSampler
{ smPresentEma = 0
, smLoopEma = 0
, smLastPresentT = now
, smLastDebugT = 0
, smLastQueryT = 0
, smPresents = 0
, smSkips = 0
, smUiMs = 0
, smRenderMs = 0
, smPresentMs = 0
, smFrameMs = 0
, smVerts = 0
, smIndices = 0
, smCmds = 0
, smRatePresents = 0
, smRateT = now
}
noteDebugLoop :: DebugSamplerRef -> Float -> IO ()
noteDebugLoop ref dt =
atomicModifyIORef' ref $ \s ->
let dtD = realToFrac dt :: Double
fps = if dtD > 1e-4 && dtD < 0.25 then 1 / dtD else 0
ema' =
if fps > 0
then blend (smLoopEma s) fps
else smLoopEma s
in (s {smLoopEma = ema'}, ())
noteDebugSkip :: DebugSamplerRef -> IO ()
noteDebugSkip ref =
atomicModifyIORef' ref $ \s -> (s {smSkips = smSkips s + 1}, ())
-- | Debug HUD cadence is driven by actual snapshot consumption: a snapshot
-- query ('refreshDebugSnapshot') refreshes 'smLastQueryT', so the 4 Hz refresh
-- loop only runs while a stats window is being built. An open window alone
-- does not count as activity, or the event loop would wake every refresh
-- period while any floating window is open.
isDebugActive :: DebugSamplerRef -> IO Bool
isDebugActive ref = do
now <- getMonotonicTime
s <- readIORef ref
pure (now - smLastQueryT s < 1.0)
-- | Whether the published snapshot is older than 'debugRefreshSec'.
debugRefreshDue :: DebugSamplerRef -> IO Bool
debugRefreshDue ref = do
now <- getMonotonicTime
s <- readIORef ref
pure (snapshotDue now s)
snapshotDue :: Double -> DebugSampler -> Bool
snapshotDue now s = smLastDebugT s <= 0 || now - smLastDebugT s >= debugRefreshSec
noteDebugPresent :: DebugSamplerRef -> Double -> Double -> Double -> Double -> Int -> Int -> Int -> IO ()
noteDebugPresent ref uiMs renderMs presentMs frameMs verts indices cmds = do
now <- getMonotonicTime
atomicModifyIORef' ref $ \s ->
let dt = now - smLastPresentT s
instantFps =
if dt > 1e-4 && dt < 0.25
then 1 / dt
else 0
ema' =
if instantFps > 0
then blend (smPresentEma s) instantFps
else smPresentEma s
in ( s
{ smPresentEma = ema'
, smLastPresentT = now
, smPresents = smPresents s + 1
, smUiMs = uiMs
, smRenderMs = renderMs
, smPresentMs = presentMs
, smFrameMs = frameMs
, smVerts = verts
, smIndices = indices
, smCmds = cmds
}
, ()
)
-- | The published snapshot, rebuilt at most every 'debugRefreshSec' and cached
-- in between. A due query samples the core stats and hands them to @build@,
-- which adds the backend's fields: window size and mouse position are left 0
-- for it to fill. Every query marks the readout active ('isDebugActive').
refreshDebugSnapshot :: DebugSamplerRef -> IORef s -> (CoreDebugSnapshot -> IO s) -> IO s
refreshDebugSnapshot ref cache build = do
now <- getMonotonicTime
due <- atomicModifyIORef' ref $ \cur -> (cur {smLastQueryT = now}, snapshotDue now cur)
if not due
then readIORef cache
else do
rts <- readRtsSnapshot
core <- atomicModifyIORef' ref $ \cur ->
-- Actual presents per second since the previous refresh. Unlike the
-- per-present EMA this stays truthful when presents are sparse (idle
-- app: ~4/s with the HUD open, not the theoretical fps of one fast
-- frame).
let elapsed = now - smRateT cur
rate
| elapsed > 1e-3 = fromIntegral (smPresents cur - smRatePresents cur) / elapsed
| otherwise = 0
cur' = cur {smLastDebugT = now, smRatePresents = smPresents cur, smRateT = now}
in (cur', (coreDebugSnapshot cur' rts) {dbgPresentFps = rate})
snap <- build core
writeIORef cache snap
pure snap
coreDebugSnapshot :: DebugSampler -> RtsStatsSnapshot -> CoreDebugSnapshot
coreDebugSnapshot s rts =
CoreDebugSnapshot
{ dbgPresentFps = smPresentEma s
, dbgLoopFps = smLoopEma s
, dbgFrameMs = smFrameMs s
, dbgUiMs = smUiMs s
, dbgRenderMs = smRenderMs s
, dbgPresentMs = smPresentMs s
, dbgPresents = smPresents s
, dbgSkips = smSkips s
, dbgVerts = smVerts s
, dbgIndices = smIndices s
, dbgCmds = smCmds s
, dbgWinW = 0
, dbgWinH = 0
, dbgMouseX = 0
, dbgMouseY = 0
, dbgRts = rts
}
formatFpsRows :: CoreDebugSnapshot -> [(Text, Text)]
formatFpsRows s =
[ ("fps present", T.pack (printf "%6.1f" (dbgPresentFps s)))
, ("fps loop", T.pack (printf "%6.1f" (dbgLoopFps s)))
, ("frame ms", T.pack (printf "%6.2f" (dbgFrameMs s)))
, ("ui ms", T.pack (printf "%6.2f" (dbgUiMs s)))
, ("render ms", T.pack (printf "%6.2f" (dbgRenderMs s)))
, ("present ms", T.pack (printf "%6.2f" (dbgPresentMs s)))
, ("presents", T.pack (printf "%10d" (dbgPresents s)))
, ("skips", T.pack (printf "%10d" (dbgSkips s)))
]
formatDrawRows :: CoreDebugSnapshot -> [(Text, Text)]
formatDrawRows s =
[ ("vertices", T.pack (printf "%10d" (dbgVerts s)))
, ("indices", T.pack (printf "%10d" (dbgIndices s)))
, ("commands", T.pack (printf "%10d" (dbgCmds s)))
]
formatCoreRtsRows :: CoreDebugSnapshot -> [(Text, Text)]
formatCoreRtsRows core
| not (rtsEnabled s) =
[ ("rts", "stats off (need +RTS -T)")
, ("haskell", T.pack (printf "%2d cap / %2d cpu" (rtsCaps s) (rtsCpus s)))
]
| otherwise =
[ ("haskell", T.pack (printf "%2d cap / %2d cpu" (rtsCaps s) (rtsCpus s)))
, ("gc total", T.pack (printf "%10d" (rtsGcs s)))
, ("gc major", T.pack (printf "%10d" (rtsMajorGcs s)))
, ("last gen", T.pack (printf "%10d" (rtsLastGcGen s)))
, ("last gc", T.pack (printf "%7.2f ms" (rtsLastGcMs s)))
, ("heap live", T.pack (printf "%6.1f MiB" (rtsLiveMb s)))
, ("heap alloc", T.pack (printf "%6.1f MiB" (rtsAllocMb s)))
, ("copied", T.pack (printf "%6.1f MiB" (rtsCopiedMb s)))
, ("rss max", T.pack (printf "%6.1f MiB" (rtsMaxMemMb s)))
, ("gc time", T.pack (printf "%9.1f%%" (rtsGcPct s)))
]
where
s = dbgRts core