packages feed

ghc-stack-profiler-0.5.0.0: src/GHC/Stack/Profiler/Internal/Sampler.hs

module GHC.Stack.Profiler.Internal.Sampler (
  Interval (..),
  SamplerDescr (..),
  withSampler,
  startSampler,
  stopSampler,
) where

import Control.Concurrent (ThreadId, myThreadId, threadCapability, threadDelay)
import Control.Concurrent.Async (async)
import Control.Concurrent.Chan (writeChan)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
import Control.Exception (bracket, finally)
import Control.Monad.STM (atomically)
import qualified Control.Monad.STM as STM
import qualified Data.ByteString.Lazy as BSL
import Data.Coerce (coerce)
import Data.Foldable (for_)
import Data.Word (Word32)
import GHC.Conc (BlockReason (..), ThreadStatus (..), labelThread, threadStatus)
import GHC.Conc.Sync (fromThreadId)
import GHC.Internal.Control.Monad (forever)
import GHC.Stack.CloneStack (cloneThreadStack)
import qualified GHC.Stack.Profiler.Core as GSPC
import GHC.Stack.Profiler.Internal.Decode (
  CallStackSample (..),
  decodeToCallStack,
  serializeCallStack,
  serializeMessages,
 )
import GHC.Stack.Profiler.Internal.Manager (
  ControlMessage (..),
  Manager (..),
  Sampler (..),
  cancelSampler,
  registerSamplerThread,
  shouldProfile,
  unregisterSamplerThread,
 )

-- NOTE: Part of the public API.

-- | The sampling interval.
--
--   @since 0.5.0.0
newtype Interval
  = MkIntervalMillis {intervalMillis :: Int}
  deriving stock (Eq, Show)

-- | @`fromInteger` n@ constructs an interval of @n@ milliseconds.
instance Num Interval where
  (+) :: Interval -> Interval -> Interval
  (+) = coerce @(Int -> Int -> Int) (+)

  (-) :: Interval -> Interval -> Interval
  (-) = coerce @(Int -> Int -> Int) (+)

  (*) :: Interval -> Interval -> Interval
  (*) = coerce @(Int -> Int -> Int) (*)

  abs :: Interval -> Interval
  abs = coerce @(Int -> Int) abs

  signum :: Interval -> Interval
  signum = coerce @(Int -> Int) signum

  fromInteger :: Integer -> Interval
  fromInteger = coerce @(Integer -> Int) fromInteger

-- | Get the interval in microseconds.
intervalMicros :: Interval -> Int
intervalMicros = (* 1_000) . intervalMillis
{-# INLINE intervalMicros #-}

-- | A description used to construct a `Sampler` thread.
data SamplerDescr = MkSamplerDescr
  { samplerManager :: Manager
  , samplerThreads :: IO [ThreadId]
  , sampleInterval :: !Interval
  }

withSampler :: SamplerDescr -> (Sampler -> IO a) -> IO a
withSampler sampler action =
  bracket
    (startSampler sampler)
    (stopSampler (samplerManager sampler))
    action

-- | Run a `SamplerDescr`.
startSampler :: SamplerDescr -> IO Sampler
startSampler sampler@MkSamplerDescr{samplerManager, sampleInterval} = do
  barrier <- newEmptyMVar
  samplerAsync <- async $ do
    () <- takeMVar barrier
    samplerThreadId <- myThreadId
    labelThread samplerThreadId $
      "Stack Sampler " <> show (fromThreadId samplerThreadId)
    forever $ do
      sampleThreads sampler
      -- TODO: Measure the delay at each step and subtract that from the next tick.
      threadDelay (intervalMicros sampleInterval)

  let
    samplerThread = MkSampler{samplerAsync}

  -- Register this sampler thread to avoid sampling it
  registerSamplerThread samplerManager samplerThread
  putMVar barrier ()
  pure samplerThread

-- NOTE: `stopSampler` is part of the public API.

-- | Stop a `Sampler` thread.
--
--   @since 0.5.0.0
stopSampler :: Manager -> Sampler -> IO ()
stopSampler manager samplerThread = do
  cancelSampler samplerThread
    `finally` unregisterSamplerThread manager samplerThread

-- | Take one `CallStackSample` for every thread sampled by the `Sampler`.
sampleThreads :: SamplerDescr -> IO ()
sampleThreads MkSamplerDescr{samplerManager, samplerThreads} = do
  -- Wait until the manager signals to start profiling.
  atomically (STM.check =<< shouldProfile samplerManager)
  -- List all threads that should be sampled.
  threadIds <- samplerThreads
  -- Sample all threads.
  for_ threadIds $ \threadId ->
    sampleThread samplerManager threadId

-- | Take one `CallStackSample` for the given `ThreadId` and send it to the given `Manager`.
sampleThread :: Manager -> ThreadId -> IO ()
sampleThread manager threadId =
  sampleCallStackFor threadId
    >>= maybe (pure ()) (sendCallStackSample manager)

-- | Send a `CallStackSample` to the given `Manager`.
sendCallStackSample :: Manager -> CallStackSample -> IO ()
sendCallStackSample manager callStackSample = do
  callStack <- decodeToCallStack callStackSample
  binaryMessages <-
    atomically $ do
      -- TODO: Should these two STM calls be put in a single transaction?
      messages <- serializeCallStack (symbolTableRef manager) callStack
      STM.check =<< shouldProfile manager
      pure $! serializeMessages messages
  writeChan (messageChan manager) $!
    WriteProfileSample $
      BSL.toStrict <$> binaryMessages

-- | Take a `CallStackSample` for the given `ThreadId`.
sampleCallStackFor :: ThreadId -> IO (Maybe CallStackSample)
sampleCallStackFor threadId = do
  status <- threadStatus threadId
  (capNo, _lockedToCap) <- threadCapability threadId
  if canTakeCallStackSample status
    then do
      cloneThreadStack threadId >>= \stackSnapshot ->
        pure $
          Just $
            CallStackSample
              { callStackSampleThreadId = GSPC.MkThreadId . fromThreadId $ threadId
              , callStackSampleCapabilityId = GSPC.MkCapabilityId . fromIntegral @Int @Word32 $ capNo
              , callStackSampleStackSnapshot = stackSnapshot
              }
    else pure Nothing

-- | Can a `CallStackSample` be taken for the given `ThreadId`?
canTakeCallStackSample :: ThreadStatus -> Bool
canTakeCallStackSample = \case
  ThreadRunning -> True
  ThreadBlocked BlockedOnMVar -> True
  _ -> False
{-# INLINE canTakeCallStackSample #-}