sandwich-0.3.1.0: src/Test/Sandwich/Instrumentation.hs
module Test.Sandwich.Instrumentation (
streamLogsToFile
, streamEventsToFile
, streamRtsStatsToFile
, streamManagedAsyncEventsToFile
, writeTreeFile
, formatRtsStats
) where
import Control.Concurrent.STM
import Control.Monad
import Control.Monad.Logger
import qualified Data.ByteString.Char8 as BS8
import Data.IORef
import qualified Data.Map.Strict as M
import Data.String.Interpolate
import Data.Text (Text)
import qualified Data.Text.IO as T
import Data.Time
import Data.Word
import Debug.Trace (traceMarkerIO)
import GHC.Stats
import System.IO (IOMode(..), hFlush, hPutStr, hSetBuffering, BufferMode(..), withFile)
import Test.Sandwich.ManagedAsync (AsyncEvent(..), AsyncInfo(..), getManagedAsyncInfos)
import Test.Sandwich.Types.RunTree
import Test.Sandwich.Types.Spec
import UnliftIO.Concurrent (threadDelay)
import UnliftIO.Exception
-- | Stream all log entries from a broadcast channel to a file,
-- including the recursive heap size of each LogEntry.
-- When cancelled, writes a summary line with total bytes.
streamLogsToFile :: FilePath -> TChan (Int, String, LogEntry) -> IO ()
streamLogsToFile path broadcastChan = do
chan <- atomically $ dupTChan broadcastChan
totalRef <- newIORef (0 :: Word64)
countRef <- newIORef (0 :: Int)
withFile path AppendMode $ \h -> do
hSetBuffering h LineBuffering
let loop = forever $ do
(nodeId, nodeLabel, LogEntry {..}) <- atomically $ readTChan chan
let entrySize = fromIntegral (BS8.length logEntryStr) :: Word64
modifyIORef' totalRef (+ entrySize)
modifyIORef' countRef (+ 1)
let levelStr :: String
levelStr = case logEntryLevel of
LevelDebug -> "DEBUG"
LevelInfo -> "INFO"
LevelWarn -> "WARN"
LevelError -> "ERROR"
LevelOther t -> show t
msgStr = BS8.unpack logEntryStr
formatted = [i|#{show logEntryTime} [#{levelStr}] [#{nodeId}] #{nodeLabel}: #{msgStr} (#{entrySize} bytes)\n|]
hPutStr h formatted
hFlush h
loop `finally` do
total <- readIORef totalRef
count <- readIORef countRef
hPutStr h [i|\nTotal: #{count} log entries, #{formatBytes total} total log bytes\n|]
hFlush h
-- | Stream node lifecycle events from a broadcast channel to a file.
-- Exits cleanly when it receives an 'EventEndOfStream' event.
streamEventsToFile :: FilePath -> TChan NodeEvent -> IO ()
streamEventsToFile path broadcastChan = do
chan <- atomically $ dupTChan broadcastChan
withFile path AppendMode $ \h -> do
hSetBuffering h LineBuffering
let loop = do
NodeEvent {..} <- atomically $ readTChan chan
case nodeEventType of
EventEndOfStream -> do
hPutStr h [i|#{show nodeEventTime} [#{nodeEventId}] #{nodeEventLabel}: END_OF_STREAM\n|]
hFlush h
_ -> do
traceMarkerIO [i|[#{nodeEventId}] #{nodeEventLabel}: #{typeStr nodeEventType}|]
hPutStr h [i|#{show nodeEventTime} [#{nodeEventId}] #{nodeEventLabel}: #{typeStr nodeEventType}\n|]
hFlush h
loop
loop
where
typeStr :: NodeEventType -> String
typeStr typ = case typ of
EventStarted -> "STARTED"
EventDone Success -> "DONE:OK"
EventDone (Failure (Pending {})) -> "DONE:PENDING"
EventDone (Failure reason) -> [i|DONE:FAIL: #{showFailureReasonBrief reason}|]
EventDone DryRun -> "DONE:DRYRUN"
EventDone Cancelled -> "DONE:CANCELLED"
EventSetupStarted -> "SETUP:STARTED"
EventSetupFinished -> "SETUP:FINISHED"
EventTeardownStarted -> "TEARDOWN:STARTED"
EventTeardownFinished -> "TEARDOWN:FINISHED"
EventMilestone msg -> [i|MILESTONE: #{msg}|]
EventEndOfStream -> "END_OF_STREAM" -- unreachable, handled above
-- | Poll RTS stats every second and append to a file.
-- Requires the program to be run with +RTS -T for stats to be available.
streamRtsStatsToFile :: FilePath -> IO ()
streamRtsStatsToFile path = do
enabled <- getRTSStatsEnabled
when enabled $ do
withFile path AppendMode $ \h -> do
hSetBuffering h LineBuffering
forever $ do
now <- getCurrentTime
stats <- getRTSStats
let gc' = gc stats
T.hPutStr h "\n\n"
T.hPutStr h $ formatRtsStats now stats gc'
hFlush h
threadDelay 1000000
showFailureReasonBrief :: FailureReason -> String
showFailureReasonBrief (Reason {failureReason}) = failureReason
showFailureReasonBrief (ChildrenFailed {failureNumChildren}) = [i|#{failureNumChildren} children failed|]
showFailureReasonBrief _ = "(see node detail)"
formatRtsStats :: UTCTime -> RTSStats -> GCDetails -> Text
formatRtsStats now stats gc' = [__i|
#{now}
live_bytes: #{formatBytes (gcdetails_live_bytes gc')}
heap_size: #{formatBytes (gcdetails_mem_in_use_bytes gc')}
allocated_bytes: #{formatBytes (allocated_bytes stats)}
max_live_bytes: #{formatBytes (max_live_bytes stats)}
large_objects: #{formatBytes (gcdetails_large_objects_bytes gc')}
compact_bytes: #{formatBytes (gcdetails_compact_bytes gc')}
slop_bytes: #{formatBytes (gcdetails_slop_bytes gc')}
gcs: #{gcs stats}
major_gcs: #{major_gcs stats}
gc_cpu: #{nsToMs (gc_cpu_ns stats)}ms
mutator_cpu: #{nsToMs (mutator_cpu_ns stats)}ms
|]
where
nsToMs :: RtsTime -> RtsTime
nsToMs ns = ns `div` 1000000
formatBytes :: Word64 -> String
formatBytes b
| b < 1024 = [i|#{b} B|]
| b < 1024 * 1024 = [i|#{b `div` 1024} KiB (#{b})|]
| b < 1024 * 1024 * 1024 = [i|#{b `div` (1024 * 1024)} MiB (#{b})|]
| otherwise = [i|#{b `div` (1024 * 1024 * 1024)} GiB (#{b})|]
-- | Stream managed async lifecycle events (started/finished) from a broadcast channel to a file.
-- When cancelled, writes a summary of all asyncs still alive at that point.
streamManagedAsyncEventsToFile :: FilePath -> TChan AsyncEvent -> IO ()
streamManagedAsyncEventsToFile path broadcastChan = do
chan <- atomically $ dupTChan broadcastChan
withFile path AppendMode $ \h -> do
hSetBuffering h LineBuffering
let loop = forever $ do
event <- atomically $ readTChan chan
now <- getCurrentTime
let line :: String
line = case event of
AsyncStarted info -> [i|#{show now} STARTED (#{asyncInfoThreadId info}, parent #{asyncInfoParentThreadId info}, #{asyncInfoRunId info}) "#{asyncInfoName info}" |]
AsyncFinished info -> [i|#{show now} FINISHED (#{asyncInfoThreadId info}, #{asyncInfoRunId info}) "#{asyncInfoName info}"|]
hPutStr h (line <> "\n")
hFlush h
loop `finally` do
now <- getCurrentTime
remaining <- getManagedAsyncInfos
hPutStr h [i|\n#{show now} === Remaining managed asyncs: #{M.size remaining} ===\n|]
forM_ (M.toList remaining) $ \(tid, info) ->
hPutStr h [i| #{tid}: #{asyncInfoName info} (runId: #{asyncInfoRunId info})\n|]
hFlush h
-- | Write a tree of node IDs and labels to a file for cross-referencing with events.
writeTreeFile :: FilePath -> [RunNodeWithStatus context s l t] -> IO ()
writeTreeFile path rts =
writeFile path $ unlines $ concatMap (renderTree 0) rts
renderTree :: Int -> RunNodeWithStatus context s l t -> [String]
renderTree depth node = line : children
where
c = runNodeCommon node
indent = replicate (depth * 2) ' '
label = runTreeLabel c
nid = runTreeId c
line = [i|#{indent}[#{nid}] #{label}|]
children = case node of
RunNodeIt {} -> []
RunNodeIntroduce {runNodeChildrenAugmented} -> concatMap (renderTree (depth + 1)) runNodeChildrenAugmented
RunNodeIntroduceWith {runNodeChildrenAugmented} -> concatMap (renderTree (depth + 1)) runNodeChildrenAugmented
_ -> concatMap (renderTree (depth + 1)) (runNodeChildren node)