ghc-debug-client-0.8.0.0: src/GHC/Debug/ParTrace.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE NoApplicativeDo #-}
-- | Functions to support the constant space traversal of a heap.
-- This module is like the Trace module but performs the tracing in
-- parellel. The speed-up is quite modest but hopefully can be improved in
-- future.
--
-- The tracing functions create a thread for each MBlock which we
-- traverse, closures are then sent to the relevant threads to be
-- dereferenced and thread-local storage is accumulated.
module GHC.Debug.ParTrace ( traceParFromM
, tracePar
, traceParFromWithState
, asyncTraceParFromWithState
, justClosuresPar
, WorkerId(..)
, TraceFunctionsIO(..)
, ClosurePtrWithInfo(..)
) where
import GHC.Debug.Async
import GHC.Debug.Client.Monad.Class
import GHC.Debug.Client.Monad.Simple
import GHC.Debug.Client.Query
import GHC.Debug.Types
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception.Base
import Control.Monad
import Control.Monad.Reader
import Data.Array.BitArray.IO hiding (map)
import Data.Bitraversable
import Data.Coerce (coerce)
import Data.IORef
import qualified Data.IntMap as IM
import qualified Data.Map as Map
import Data.Word
import GHC.Conc (numCapabilities)
threads :: Int
threads = numCapabilities
type InChan = TChan
type OutChan = TChan
-- | State local to a thread, there are $threads spawned, each which deals
-- with (address `div` 8) % threads. Each thread therefore:
--
-- * Outer map, segmented by MBlock
-- * Inner map, blocks for that MBlock
-- * Inner IOBitArray, visited information for that block
data ThreadState = ThreadState
{ visitedPtrs :: IM.IntMap (IM.IntMap (IOBitArray Word16))
}
newtype ThreadInfo a = ThreadInfo (InChan (ClosurePtrWithInfo a))
-- | A 'ClosurePtr' with some additional information which needs to be
-- communicated across to another thread.
data ClosurePtrWithInfo a = ClosurePtrWithInfo !a !ClosurePtr
| CCSPtrWithInfo CCSPtr
-- | Map from Thread -> Information about the thread
type ThreadMap a = IM.IntMap (ThreadInfo a)
newtype TraceState a = TraceState { visited :: (ThreadMap a) }
getKeyTriple :: ClosurePtr -> (Int, Int, Word16)
getKeyTriple cp =
let BlockPtr raw_bk = applyBlockMask cp
bk = fromIntegral raw_bk `div` 8
offset = getBlockOffset cp `div` 8
BlockPtr raw_mbk = applyMBlockMask cp
mbk = fromIntegral raw_mbk `div` 8
in (mbk, bk, fromIntegral offset)
getMBlockKey :: ClosurePtr -> WorkerId
getMBlockKey cp =
let BlockPtr raw_bk = applyMBlockMask cp
-- Not sure why I had to divide this by 4, but I did.
in WorkerId ((fromIntegral raw_bk `div` fromIntegral mblockMask `div` 4) `mod` threads)
sendToChan :: TraceState a -> ClosurePtrWithInfo a -> DebugM ()
sendToChan ts cpi@(ClosurePtrWithInfo _ cp) = DebugM $ liftIO $ do
let st = visited ts
WorkerId mkey = getMBlockKey cp
case IM.lookup mkey st of
Nothing -> error $ "Not enough chans:" ++ show mkey ++ show threads
Just (ThreadInfo ic) -> atomically $ writeTChan ic cpi
sendToChan ts cpi@(CCSPtrWithInfo ccsPtr) = DebugM $ liftIO $ do
let st = visited ts
WorkerId mkey = getMBlockKey (coerce ccsPtr)
case IM.lookup mkey st of
Nothing -> error $ "Not enough chans:" ++ show mkey ++ show threads
Just (ThreadInfo ic) -> atomically $ writeTChan ic cpi
initThread :: WorkerId
-> TraceFunctionsIO a ()
-> DebugM (ThreadInfo a, STM Bool, (ClosurePtrWithInfo a -> DebugM ()) -> IO ())
initThread n k = DebugM $ do
e <- ask
ic <- liftIO $ newTChanIO
let oc = ic
worker_active <- liftIO $ newTVarIO True
let start go = runSimple e $ workerThread n k worker_active go oc
finished = do
active <- not <$> readTVar worker_active
empty <- isEmptyTChan ic
return (active && empty)
return (ThreadInfo ic, finished, start)
workerThread :: forall a . WorkerId -> TraceFunctionsIO a () -> TVar Bool -> (ClosurePtrWithInfo a -> DebugM ()) -> OutChan (ClosurePtrWithInfo a) -> DebugM ()
workerThread n k worker_active go oc = DebugM $ do
d <- ask
r <- liftIO $ newIORef $
ThreadState
{ visitedPtrs = IM.empty
}
liftIO $ runSimple d (loop r)
where
loop r = do
mcp <- unsafeLiftIO $ try $ do
atomically $ writeTVar worker_active False
atomically $ do
v <- readTChan oc
writeTVar worker_active True
return v
case mcp of
-- The thread gets blocked on readChan when the work is finished so
-- when this happens, catch the exception and return the accumulated
-- state for the thread. Each thread has a reference to all over
-- threads, so the exception is only raised when ALL threads are
-- waiting for work.
Left AsyncCancelled -> return ()
Right cpi -> deref r cpi >> loop r
deref r (ClosurePtrWithInfo a cp) = do
m <- unsafeLiftIO $ readIORef r
do
(m', b) <- unsafeLiftIO $ checkVisit cp m
unsafeLiftIO $ writeIORef r m'
if b
then do
visitedClosVal k n cp a
else do
sc <- dereferenceClosure cp
(a', (), cont) <- closTrace k n cp sc a
cont (() <$ hextraverse (goCCS r) (gosrt r a') (gop r a') gocd (gos r a') (goc r . ClosurePtrWithInfo a') sc)
deref r (CCSPtrWithInfo cp) = do
m <- unsafeLiftIO $ readIORef r
do
(m', b) <- unsafeLiftIO $ checkVisit (coerce cp) m
unsafeLiftIO $ writeIORef r m'
if b
then do
visitedCcsVal k n cp
else do
ccs' <- dereferenceCCS cp
ccsTrace k n cp ccs'
() <$ bitraverse (goCCS r) goCC ccs'
goc r c@(ClosurePtrWithInfo _i cp) =
let mkey = getMBlockKey cp
in if (mkey == n)
then deref r c
else go c
-- Just do the other dereferencing in the same thread for other closure
-- types as they are not as common.
gos r a st = do
st' <- dereferenceStack st
stackTrace k n st'
() <$ bitraverse (gosrt r a) (goc r . ClosurePtrWithInfo a) st'
gocd d = do
cd <- dereferenceConDesc d
conDescTrace k n cd
goCCS r cp =
let mkey = getMBlockKey (coerce cp)
in if (mkey == n)
then deref r (CCSPtrWithInfo cp)
else go (CCSPtrWithInfo cp)
goCC p = do
() <$ dereferenceCC p
gop r a p = do
p' <- dereferencePapPayload p
papTrace k n p'
() <$ traverse (goc r . ClosurePtrWithInfo a) p'
gosrt r a p = do
p' <- dereferenceSRT p
srtTrace k n p'
() <$ traverse (goc r . ClosurePtrWithInfo a) p'
handleBlockLevel :: IM.Key
-> Word16
-> IM.IntMap (IOBitArray Word16)
-> IO (IM.IntMap (IOBitArray Word16), Bool)
handleBlockLevel bk offset m = do
case IM.lookup bk m of
Nothing -> do
na <- newArray (0, fromIntegral (blockMask `div` 8)) False
writeArray na offset True
return (IM.insert bk na m, False)
Just bm -> do
res <- readArray bm offset
unless res (writeArray bm offset True)
return (m, res)
checkVisit :: ClosurePtr -> ThreadState -> IO (ThreadState, Bool)
checkVisit cp st = do
let (mbk, bk, offset) = getKeyTriple cp
ThreadState v = st
case IM.lookup mbk v of
Nothing -> do
(st', res) <- handleBlockLevel bk offset IM.empty
return (ThreadState (IM.insert mbk st' v) , res)
Just bm -> do
(st', res) <- handleBlockLevel bk offset bm
return (ThreadState (IM.insert mbk st' v) , res)
newtype WorkerId = WorkerId Int deriving (Eq, Ord)
data TraceFunctionsIO a s =
TraceFunctionsIO { papTrace :: !(WorkerId -> GenPapPayload ClosurePtr -> DebugM ())
, srtTrace :: !(WorkerId -> GenSrtPayload ClosurePtr -> DebugM ())
, stackTrace :: !(WorkerId -> GenStackFrames SrtCont ClosurePtr -> DebugM ())
, closTrace :: !(WorkerId -> ClosurePtr -> SizedClosure -> a -> DebugM (a, s, DebugM () -> DebugM ()))
, visitedClosVal :: !(WorkerId -> ClosurePtr -> a -> DebugM s)
, visitedCcsVal :: !(WorkerId -> CCSPtr -> DebugM s)
, conDescTrace :: !(WorkerId -> ConstrDesc -> DebugM ())
, ccsTrace :: !(WorkerId -> CCSPtr -> CCSPayload -> DebugM s)
}
justClosuresPar :: Monoid s =>
(WorkerId -> ClosurePtr -> SizedClosure -> a -> DebugM (a, s, DebugM () -> DebugM ()))
-> TraceFunctionsIO a s
justClosuresPar clos = TraceFunctionsIO
{ papTrace = nop
, srtTrace = nop
, stackTrace = nop
, closTrace = clos
, visitedClosVal = const (const (const (return mempty)))
, visitedCcsVal = const (const (return mempty))
, conDescTrace = nop
, ccsTrace = const (const (const (return mempty)))
}
where
nop = const (const (return ()))
-- | Accumulate state from each node
stateAccumulator :: Semigroup s => Map.Map WorkerId (IORef s)
-> TraceFunctionsIO a s
-> TraceFunctionsIO a ()
stateAccumulator refs f =
TraceFunctionsIO
{ papTrace = papTrace f
, srtTrace = srtTrace f
, stackTrace = stackTrace f
, closTrace = \wid cp sc a -> do
let ref = refs Map.! wid
closTrace f wid cp sc a >>= \ (a', s, cont) -> do
unsafeLiftIO $ modifyIORef' ref (s <>)
return (a', (), cont)
, visitedClosVal = \wid cp a -> do
let ref = refs Map.! wid
visitedClosVal f wid cp a >>= \ s -> do
unsafeLiftIO $ modifyIORef' ref (s <>)
return ()
, visitedCcsVal = \wid ccs -> do
let ref = refs Map.! wid
visitedCcsVal f wid ccs >>= \ s -> do
unsafeLiftIO $ modifyIORef' ref (s <>)
return ()
, conDescTrace = conDescTrace f
, ccsTrace = \wid ccs ccs' -> do
let ref = refs Map.! wid
ccsTrace f wid ccs ccs' >>= \ s -> do
unsafeLiftIO $ modifyIORef' ref (s <>)
return ()
}
-- | Perform a parallel trace, accumulating state as we go, which can be queried whilst
-- the traversal is running.
asyncTraceParFromWithState :: Monoid s
=> TraceFunctionsIO a s
-- ^ The tracing function for dealing with closures.
-> [ClosurePtrWithInfo a]
-> DebugM (AsyncTrace s)
-- An IO action which can be used to query the current result of the trace, and an action to run the trace.
asyncTraceParFromWithState f cps = do
refs <- Map.fromList <$> mapM (\i -> (WorkerId i,) <$> unsafeLiftIO (newIORef mempty)) [0 .. threads - 1]
-- Collect the results
let read_results = mconcat <$> mapM (\(_wid, ref) -> readIORef ref) (Map.toList refs)
withAsyncTrace read_results (traceParFromM (stateAccumulator refs f) cps)
-- | A parallel traversal which accumulates state when visiting closures.
traceParFromWithState :: Monoid s => TraceFunctionsIO a s
-> [ClosurePtrWithInfo a]
-> DebugM s
traceParFromWithState f cps = do
asyncTrace <- asyncTraceParFromWithState f cps
waitForAsyncTraceResultM asyncTrace
-- | A generic heap traversal function which will use a small amount of
-- memory linear in the heap size. Using this function with appropriate
-- accumulation functions you should be able to traverse quite big heaps in
-- not a huge amount of memory.
--
-- The performance of this parallel version depends on how much contention
-- the functions given in 'TraceFunctionsIO' content for the handle
-- connecting for the debuggee (which is protected by an 'MVar'). With no
-- contention, and precached blocks, the workload can be very evenly
-- distributed leading to high core utilisation.
--
-- As performance depends highly on contention, snapshot mode is much more
-- amenable to parallelisation where the time taken for requests is much
-- lower.
traceParFromM :: TraceFunctionsIO a () -> [ClosurePtrWithInfo a] -> DebugM ()
traceParFromM k cps = do
traceMsg ("SPAWNING: " ++ show threads)
-- Set up the worker threads.
let workerThreadIds = [0 .. threads - 1]
setupWorker wid = do
(ti, working, start) <- initThread (WorkerId wid) k
return ((fromIntegral wid, ti), working, start)
(init_mblocks, work_actives, start) <- unzip3 <$> mapM setupWorker workerThreadIds
let ts_map = IM.fromList init_mblocks
go = sendToChan (TraceState ts_map)
-- Start the work!
-- First, we start populating the main queue with tasks.
-- Then, we start the worker threads concurrenly, which ensures they all get cancelled
-- if one of the worker threads crash in any way.
-- At last, we wait for completion in a coordinated fashion.
dbg <- DebugM ask
unsafeLiftIO $
withAsync (mapConcurrently_ ($ go) start) $ \ workerPool -> do
runSimple dbg $ mapM_ go cps
-- We have to wait at least for
withAsync (waitFinish work_actives) $ \ allWorkersFinished -> do
result <- waitEitherCancel allWorkersFinished workerPool
case result of
Left () -> pure ()
Right () -> throwIO $ ErrorCall $ "traceParFromM: Worker threads terminated"
waitFinish :: [STM Bool] -> IO ()
waitFinish working = atomically (checkDone working)
where
checkDone [] = return ()
checkDone (x:xs) = do
b <- x
-- The variable tracks whether the thread thinks it's finished (no
-- active work and empty chan)
if b then checkDone xs else retry
-- | A parallel tracing function.
tracePar :: [ClosurePtr] -> DebugM ()
tracePar = traceParFromM funcs . map (ClosurePtrWithInfo ())
where
nop = const (const (return ()))
funcs = TraceFunctionsIO
{ papTrace = nop
, srtTrace = nop
, stackTrace = stack
, closTrace = clos
, visitedClosVal = const (const (const (return ())))
, visitedCcsVal = nop
, conDescTrace = nop
, ccsTrace = const (const (const (return ())))
}
stack :: WorkerId -> GenStackFrames SrtCont ClosurePtr -> DebugM ()
stack _ fs =
let stack_frames = getFrames fs
in mapM_ (getSourceInfo . tableId . frame_info) stack_frames
clos :: WorkerId -> ClosurePtr -> SizedClosure -> ()
-> DebugM ((), (), DebugM () -> DebugM ())
clos _ _cp sc _ = do
let itb = info (noSize sc)
_traced <- getSourceInfo (tableId itb)
return ((), (), id)