Skip to content

Thread introspection support

Ben Gamari requested to merge wip/thread-status into master

This adds an interface to list the ThreadIds of existing threads and fetch their labels:

GHC.Conc.Sync.listThreads :: IO [ThreadId]
GHC.Conc.Sync.threadLabel :: ThreadId -> IO (Maybe String)

This also fits nicely with the existing threadStatus operation:

threadStatus :: ThreadId -> IO ThreadStatus

data ThreadStatus
  = ThreadRunning
  | ThreadFinished
  | ThreadBlocked  BlockReason
  | ThreadDied

The implementation of threadLabel required a rather considerable rework of how thread labels are managed. In particular, previously they were tracked as C strings separately from the TSO via a hashtable. However, this complicated reasoning about the lifetime of the string, which could now be exposed to the mutator via threadLabel. Consequently, I moved the label into StgTSO and represented it as a standard UTF-8 encoded ByteArray#. This greatly simplifies reasoning and makes the interface more consistent. However, this then prompted the need for a simple UTF-8 codec over ByteArray# in base. For this I moved the implementation currently living in ghc-boot into base (see !8658 (closed), on which this MR depends).

This is based upon !8658 (closed).

Future work

I have opened #21877 to track this idea.

In principle this could be further generalized by exposing the precise object on which the thread is blocked via BlockReason. This could in principle make it easier to inspect "soft" deadlocks in concurrent programs which are currently extremely painful to debug. However, this would only be truly useful if we had a means of identifying (e.g. perhaps via textual label) MVars and thunks in non-profiled code. Cost centers are one way to achieve this in profiled code. In non-profiled code, IPE information helps for thunks but primitives like MVars are problematic.

Sadly, there is no great way of identifying primitives in pure Haskell with the tools that we have today. However, this could be addressed by introducing a new type of primitive object: a finite map keyed on (weak) object references:

-- | A map keyed on object references. Entries keyed on GC'd objects will
-- be automatically dropped.
data WeakMap v

insertWeakMap :: forall (lev :: Levity) (k :: TYPE (BoxedRep lev)).
                 k -> v -> WeakMap v -> WeakMap v

lookupWeakMap :: forall (lev :: Levity) (k :: TYPE (BoxedRep lev)).
                 k -> WeakMap v -> Maybe v

labelledObjects :: IORef (WeakMap String)

labelObject :: a -> String -> IO ()
labelObject x lbl = do
    atomicModifyIORef' labelledObjects (insertWeakMap w lbl)

getObjectLabel :: a -> Maybe String
getObjectLabel x = lookupWeakMap x labelledObjects

In principle one might be able to do implement something like this (with some parts written in Cmm) as an Array# (Weak# v), although efficiency would likely be marginal.

Perhaps even better than String names would be:

data ObjectName where
    ObjectName :: (Typeable a, Show a) => a -> ObjectName
Edited by Ben Gamari

Merge request reports