packages feed

ghc-stack-profiler-core-0.4.0.0: src/GHC/Stack/Profiler/Core/Eventlog.hs

module GHC.Stack.Profiler.Core.Eventlog (
  -- * Eventlgog Message types
  BinaryEventlogMessage (..),
  BinaryCallStackMessage (..),
  BinaryStringMessage (..),
  BinarySourceLocationMessage (..),
  BinaryStackItem (..),
  CapabilityId (..),
  StringId (..),
  incrementStringLocationId,
  SourceLocationId (..),
  incrementSourceLocationId,
  IpeId (..),

  -- * Eventlog constants
  callStackFinalMessageTag,
  callStackPartialMessageTag,
  callStackStringMessageTag,
  callStackSourceLocationMessageTag,
  callStackMessageTags,
  callStackSizeLimit,
  callStackSizeLimit_,
  byteSizeOf,
  eventlogBufferSize,
  stringLengthLimit,
) where

import Control.Monad (replicateM)
import Data.Binary
import Data.Coerce (coerce)
import qualified Data.List as List
import Data.Text (Text)
import GHC.Generics

import GHC.Stack.Profiler.Core.Util

-- ----------------------------------------------------------------------------
-- Eventlog Messages
-- ----------------------------------------------------------------------------

-- | Efficient serialisation format of the GHC RTS callstack.
--
-- Message format:
--
-- @
-- MESSAGE
--  := FF CA (stack: STACK>
--   | FF CB (prefix: STACK>
--   | FF CC (stringId: 'Word64') (string: CStringLen)
--   | FF CD (srcLocId: 'Word64') (row: 'Word32') (col: 'Word32') (functionId: 'Word64') (filename: 'Word64')
--
-- STACK
--  := (capability: 'Word32') (threadId: 'Word32') (length: 'Int16') (ENTRY)+
--   # check that length < (2^16-8) / 9
--
-- ENTRY
--  := 01 (ipe: 'Word64')
--   | 02 (stringId: 'Word64')
--   | 03 (stringId: 'Word64') (srcLocId: 'Word64')
--
-- CStringLen
--   := (length: 'Int16') (Char)+
--    # check that length < 2^16-8
-- @
data BinaryEventlogMessage
  = CallStackFinal !BinaryCallStackMessage
  | CallStackChunk !BinaryCallStackMessage
  | StringDef !BinaryStringMessage
  | SourceLocationDef !BinarySourceLocationMessage
  deriving (Eq, Ord, Show, Read, Generic)

data BinaryCallStackMessage = MkBinaryCallStackMessage
  { binaryCallThreadId :: !Word64
  , binaryCallCapabilityId :: !CapabilityId
  , binaryCallStack :: ![BinaryStackItem]
  }
  deriving (Eq, Ord, Show, Read, Generic)

data BinaryStringMessage = MkBinaryStringMessage
  { binaryStringMessageId :: !StringId
  , binaryStringMessage :: !Text
  }
  deriving (Eq, Ord, Show, Read, Generic)

data BinarySourceLocationMessage = MkBinarySourceLocationMessage
  { binarySourceLocationMessageId :: {-# UNPACK #-} !SourceLocationId
  , binarySourceLocationRow :: {-# UNPACK #-} !Word32
  , binarySourceLocationColumn :: {-# UNPACK #-} !Word32
  , binarySourceLocationFilename :: {-# UNPACK #-} !StringId
  }
  deriving (Eq, Ord, Show, Read, Generic)

data BinaryStackItem
  = BinaryIpe {-# UNPACK #-} !IpeId
  | BinaryMessage
      {-# UNPACK #-} !StringId
      {-# UNPACK #-} !(Maybe SourceLocationId)
  deriving (Eq, Ord, Show, Read, Generic)

-- | Simple newtype for the ID of a capability.
newtype CapabilityId
  = MkCapabilityId
  { getCapabilityId :: Word64
  }
  deriving (Show, Eq, Ord, Read, Generic)

newtype StringId = MkStringId
  { getStringId :: Word64
  }
  deriving (Eq, Ord, Show, Read, Generic)

incrementStringLocationId :: StringId -> StringId
incrementStringLocationId (MkStringId sid) = MkStringId (sid + 1)

newtype SourceLocationId = MkSourceLocationId
  { getSourceLocationId :: Word64
  }
  deriving (Eq, Ord, Show, Read, Generic)

incrementSourceLocationId :: SourceLocationId -> SourceLocationId
incrementSourceLocationId (MkSourceLocationId slId) = MkSourceLocationId (slId + 1)

newtype IpeId = MkIpeId
  { getIpeId :: Word64
  }
  deriving (Eq, Ord, Show, Read, Generic)

-- ----------------------------------------------------------------------------
-- Binary instances
-- ----------------------------------------------------------------------------

callStackFinalMessageTag :: Word16
callStackFinalMessageTag = 0xFFCA

callStackPartialMessageTag :: Word16
callStackPartialMessageTag = 0xFFCB

callStackStringMessageTag :: Word16
callStackStringMessageTag = 0xFFCC

callStackSourceLocationMessageTag :: Word16
callStackSourceLocationMessageTag = 0xFFCD

callStackMessageTags :: [Word16]
callStackMessageTags =
  [ callStackFinalMessageTag
  , callStackPartialMessageTag
  , callStackStringMessageTag
  , callStackSourceLocationMessageTag
  ]

-- | Each message in the eventlog can be at most 2^16 bytes
eventlogBufferSize :: Word64
eventlogBufferSize = (2 :: Word64) ^ (16 :: Word64)

-- | Size limit of strings that can occur in the eventlog.
stringLengthLimit :: Word16
stringLengthLimit =
  word64ToWord16 $
    eventlogBufferSize
      - 2 {- 0xFFCC -}
      - 8 {- Word64 of 'StringId' -}
      - 2 {- Word16 for the length of the string to serialise -}

-- | The limit of stack items that can go in one eventlog message in bytes.
callStackSizeLimit :: Word16
callStackSizeLimit =
  callStackSizeLimit_ eventlogBufferSize

-- | The limit of stack items that can go in one eventlog message in bytes
-- with configurable the eventlog message size.
callStackSizeLimit_ :: Word64 -> Word16
callStackSizeLimit_ eventlogSize =
  word64ToWord16
    ( eventlogSize
        - 2 {- 0xFFCA or 0xFFCB -}
        - 4 {- Word32 of 'CapabilityId' -}
        - 4 {- Word32 of 'ThreadId' -}
        - 2 {- Word16 for the length of stack entry -}
    )

-- | Size in bytes of the given 'BinaryStackItem'
byteSizeOf :: BinaryStackItem -> Word16
byteSizeOf = \case
  BinaryIpe{} -> 1 + 8 {- 0x1 + Word64 of 'IpeId' -}
  BinaryMessage _ Nothing -> 1 + 8 {- 0x2 + Word64 of 'StringId' -}
  BinaryMessage _ (Just _) -> 1 + 8 + 8 {- 0x3 + Word64 of 'StringId' + Word64 of 'SourceLocationId' -}

instance Binary BinaryEventlogMessage where
  put = \case
    CallStackFinal msg ->
      putWithTag callStackFinalMessageTag msg
    CallStackChunk msg ->
      putWithTag callStackPartialMessageTag msg
    StringDef msg ->
      putWithTag callStackStringMessageTag msg
    SourceLocationDef msg ->
      putWithTag callStackSourceLocationMessageTag msg
   where
    putWithTag t msg = putWord16 t >> put msg

  get = do
    tag <- getWord16
    case tag of
      _
        | tag == callStackFinalMessageTag ->
            CallStackFinal <$> get
        | tag == callStackPartialMessageTag ->
            CallStackChunk <$> get
        | tag == callStackStringMessageTag ->
            StringDef <$> get
        | tag == callStackSourceLocationMessageTag ->
            SourceLocationDef <$> get
        | otherwise ->
            fail $
              "BinaryEventlogMessage.get: Unknown tag expected one of "
                ++ tags
                ++ " but got "
                ++ showAsHex tag
   where
    tags = List.intercalate ", " $ map showAsHex callStackMessageTags

instance Binary BinaryCallStackMessage where
  put msg = do
    putWord32 $ word64ToWord32 $ getCapabilityId $ binaryCallCapabilityId msg
    putWord32 $ word64ToWord32 $ binaryCallThreadId msg
    let
      items = binaryCallStack msg
    putWord16 $ intToWord16 $ length items
    mapM_ put items

  get = do
    capId <- getWord32
    tid <- getWord32
    len <- getWord16
    items <- replicateM (word16ToInt len) get
    pure
      MkBinaryCallStackMessage
        { binaryCallThreadId = word32ToWord64 tid
        , binaryCallCapabilityId = MkCapabilityId $ word32ToWord64 capId
        , binaryCallStack = items
        }

instance Binary BinaryStackItem where
  put = \case
    BinaryIpe ipeId -> do
      putWord8 0x1
      put ipeId
    BinaryMessage sid Nothing -> do
      putWord8 0x2
      put sid
    BinaryMessage sid (Just lid) -> do
      putWord8 0x3
      put sid
      put lid

  get = do
    getWord8 >>= \case
      0x1 -> BinaryIpe <$> get
      0x2 -> BinaryMessage <$> get <*> pure Nothing
      0x3 -> BinaryMessage <$> get <*> (Just <$> get)
      n -> fail $ "StackItem: Unexpected tag byte encounter: " <> show n

instance Binary BinarySourceLocationMessage where
  put msg = do
    put $ binarySourceLocationMessageId msg
    putWord32 (binarySourceLocationRow msg)
    putWord32 (binarySourceLocationColumn msg)
    put (binarySourceLocationFilename msg)

  get = do
    MkBinarySourceLocationMessage
      <$> get
      <*> getWord32
      <*> getWord32
      <*> get

instance Binary BinaryStringMessage where
  put msg = do
    put $ binaryStringMessageId msg
    putTextWord16 stringLengthLimit (binaryStringMessage msg)

  get = do
    MkBinaryStringMessage
      <$> get
      <*> getTextWord16

instance Binary SourceLocationId where
  put = putWord64 . coerce
  get = coerce getWord64

instance Binary StringId where
  put = putWord64 . coerce
  get = coerce getWord64

instance Binary IpeId where
  put = putWord64 . coerce
  get = coerce getWord64