ghc-stack-profiler-speedscope 0.4.0.0 → 0.5.0.0
raw patch · 5 files changed
+59/−56 lines, 5 filesdep ~basedep ~ghc-stack-profiler-core
Dependency ranges changed: base, ghc-stack-profiler-core
Files
- CHANGELOG.md +4/−0
- ghc-stack-profiler-speedscope.cabal +9/−7
- src/GHC/Stack/Profiler/Speedscope.hs +40/−41
- src/GHC/Stack/Profiler/Speedscope/Options.hs +3/−3
- src/GHC/Stack/Profiler/Speedscope/Types.hs +3/−5
CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for ghc-stack-profiler-speedscope +## 0.5.0.0 -- 2026-09-14++Major version number changed to match `ghc-stack-profiler` and `ghc-stack-profiler-core`.+ ## 0.4.0.0 -- 2026-07-14 - Switch to `ipedb` version 0.2.0.0 and the new database format.
ghc-stack-profiler-speedscope.cabal view
@@ -1,16 +1,18 @@ cabal-version: 3.8 name: ghc-stack-profiler-speedscope-version: 0.4.0.0+version: 0.5.0.0 license: BSD-3-Clause author: Hannes Siebenhandl, Wen Kokke, Matthew Pickering maintainer: hannes@well-typed.com build-type: Simple-synopsis: Convert eventlog messages from `ghc-stack-profiler` into a speedscope json.+synopsis:+ Export a ghc-stack-profiler call-stack profile from an eventlog to speedscope+ description:- Convert eventlog messages from `ghc-stack-profiler` into a speedscope json.- Produce a flame graph for the RTS callstack samples obtained from a program instrumented with- 'ghc-stack-profiler'.+ Export a @ghc-stack-profiler@ call-stack profile from an eventlog to speedscope. + For details, see [@ghc-stack-profiler@](https://hackage.haskell.org/package/ghc-stack-profiler).+ extra-doc-files: CHANGELOG.md category: Profiling, Benchmarking, Development tested-with:@@ -51,14 +53,14 @@ build-depends: aeson >=2.2 && <2.3,- base >=4.20 && <4.23,+ base >=4.20 && <5, bytestring >=0.11 && <0.13, containers ^>=0.7 || ^>=0.8, data-default >=0.2 && <0.9, extra ^>=1.8.1, filepath ^>=1.4 || ^>=1.5, ghc-events ^>=0.20,- ghc-stack-profiler-core >=0.3 && <0.5,+ ghc-stack-profiler-core ==0.5.0.0, hs-speedscope ^>=0.3, ipedb ^>=0.2.0.0, machines ^>=0.7.4,
src/GHC/Stack/Profiler/Speedscope.hs view
@@ -29,10 +29,7 @@ import Data.Word (Word64) import qualified GHC.RTS.Events as E import qualified GHC.RTS.Events.Incremental as E-import qualified GHC.Stack.Profiler.Core.Eventlog as GSP-import qualified GHC.Stack.Profiler.Core.SymbolTable as GSP-import qualified GHC.Stack.Profiler.Core.ThreadSample as GSP-import qualified GHC.Stack.Profiler.Core.Util as GSP (word64ToInt)+import qualified GHC.Stack.Profiler.Core as GSPC import GHC.Stack.Profiler.Speedscope.Options import GHC.Stack.Profiler.Speedscope.Types import qualified IpeDB.Database as DB@@ -190,20 +187,22 @@ E.RtsIdentifier{rtsident} -> pure st{maybeRtsVersion = parseIdent rtsident} E.UserBinaryMessage bs ->- case GSP.deserializeEventlogMessage $ BSL.fromStrict bs of+ case GSPC.deserializeEventlogMessage $ BSL.fromStrict bs of Left _err -> pure st Right evMsg -> case evMsg of- GSP.CallStackFinal msg -> do+ GSPC.ProtocolVersion _ver -> do+ pure st+ GSPC.CallStackFinal msg -> do let (callStackMessage, elProf1) = hydrateBinaryEventlog st msg processCallStackMessage infoProvTable elProf1 callStackMessage- GSP.CallStackChunk msg ->+ GSPC.CallStackChunk msg -> pure st{current_callstack_chunks = msg : current_callstack_chunks st}- GSP.StringDef msg ->- pure st{hydration_table = GSP.insertTextMessage msg (hydration_table st)}- GSP.SourceLocationDef msg ->- case GSP.insertSourceLocationMessage msg (hydration_table st) of+ GSPC.StringDef msg ->+ pure st{hydration_table = GSPC.insertTextMessage msg (hydration_table st)}+ GSPC.SourceLocationDef msg ->+ case GSPC.insertSourceLocationMessage msg (hydration_table st) of Left err -> pure $ addDecodingErrorsForStack [fromMissingKeyError err] st Right newTable ->@@ -214,16 +213,16 @@ processCallStackMessage :: DB.Table IP.InfoProvId IP.InfoProv -> EventlogProfileState ->- GSP.CallStackMessage ->+ GSPC.CallStack -> IO EventlogProfileState-processCallStackMessage infoProvTable st0 GSP.MkCallStackMessage{callThreadId, callCapabilityId, callStack} = do+processCallStackMessage infoProvTable st0 GSPC.MkCallStack{callThreadId, callCapabilityId, callStack} = do stackFramesOrErrors <- traverse (toStackFrame infoProvTable) callStack let (processingErrors, stackFrames) = partitionEithers stackFramesOrErrors (st1, stackFrameIds) = mapAccumR processStackFrame st0 stackFrames sample = Sample- { sampleThreadId = callThreadId+ { sampleThreadId = GSPC.getThreadId callThreadId , sampleCapabilityId = callCapabilityId , -- TODO: Don't use an arbitrary cutoff, but reduce cycles within the stack. -- TODO: Perform cycle reduction or cutoff before the database lookups.@@ -232,7 +231,7 @@ st2 = addDecodingErrorsForStack processingErrors st1 pure $ st2{samples = sample : st2.samples} -hydrateBinaryEventlog :: EventlogProfileState -> GSP.BinaryCallStackMessage -> (GSP.CallStackMessage, EventlogProfileState)+hydrateBinaryEventlog :: EventlogProfileState -> GSPC.CallStackChunk -> (GSPC.CallStack, EventlogProfileState) hydrateBinaryEventlog st msg = let chunks = current_callstack_chunks st@@ -253,12 +252,12 @@ -- [2,1] -- 3. When reading the eventlog, we store prepend later messages, resulting in: -- [2,1] [4,3] [6,5]- -- 4. 'catCallStackMessage' reverses the individual callstack chunks to be the inverse of 'chunkCallStackMessage'+ -- 4. 'joinCallStackChunks' reverses the individual callstack chunks to be the inverse of 'chunkCallStack' orderedChunks = NonEmpty.reverse $ msg :| chunks- fullBinaryCallStackMessage = GSP.catCallStackMessage orderedChunks+ fullBinaryCallStackMessage = GSPC.joinCallStackChunks orderedChunks (callStackMessage, errs) =- GSP.hydrateEventlogCallStackMessage- (GSP.mkIntMapSymbolTableReader (hydration_table st))+ GSPC.hydrateEventlogCallStackMessage+ (GSPC.mkIntMapSymbolTableReader (hydration_table st)) fullBinaryCallStackMessage in ( callStackMessage@@ -270,16 +269,16 @@ -- | Convert a `StackItem` into a `StackFrame`. toStackFrame :: DB.Table IP.InfoProvId IP.InfoProv ->- GSP.StackItem ->+ GSPC.StackItem -> IO (Either EventlogError StackFrame) toStackFrame infoProvTable = \case- GSP.IpeId (toInfoProvId -> infoProvId) -> do+ GSPC.IpeId (toInfoProvId -> infoProvId) -> do maybeInfoProv <- DB.lookup infoProvTable infoProvId pure $ case maybeInfoProv of Nothing -> Left $! UnknownInfoProvId infoProvId Just infoProv -> Right $! StackFrameInfoProv infoProv- GSP.UserAnnotation (Text.pack -> message) (toSrcLoc -> srcLoc) ->+ GSPC.UserAnnotation (Text.pack -> message) (toSrcLoc -> srcLoc) -> pure . Right $! StackFrameMessage message srcLoc -- | Ensure a `StackFrame` has a `StackFrameId` in the `EventlogProfileState`.@@ -327,11 +326,11 @@ -- ^ A unique counter for stack frames. , samples :: [Sample] -- ^ All samples in the reverse order of finding them in the eventlog.- , hydration_table :: !GSP.IntMapTable+ , hydration_table :: !GSPC.IntMapTable -- ^ The symbol table storing 'Text' and 'SourceLocation' symbols- -- for hydrating a 'BinaryCallStackMessage' into a 'CallStackMessage'.- , current_callstack_chunks :: [GSP.BinaryCallStackMessage]- -- ^ Chunks of 'BinaryCallStackMessage' we are currently decoding.+ -- for hydrating a 'CallStackChunk' into a 'CallStack'.+ , current_callstack_chunks :: [GSPC.CallStackChunk]+ -- ^ Chunks of 'CallStackChunk' we are currently decoding. -- All chunks are assumed to be from the same callstack and will be decoded once a -- 'CallStackFinal' message is encountered. , processingErrors :: [[EventlogError]]@@ -345,7 +344,7 @@ , stackFrames = Map.empty , stackFrameCounter = 0 , samples = []- , hydration_table = GSP.emptyIntMapTable+ , hydration_table = GSPC.emptyIntMapTable , current_callstack_chunks = [] , processingErrors = [] }@@ -361,19 +360,19 @@ data EventlogError = UnknownInfoProvId IP.InfoProvId- | UnknownStringId GSP.StringId- | UnknownSrcLocId GSP.SourceLocationId- | SourceLocationPartUndefined GSP.SourceLocationId GSP.StringId+ | UnknownStringId GSPC.StringId+ | UnknownSrcLocId GSPC.SourceLocationId+ | SourceLocationPartUndefined GSPC.SourceLocationId GSPC.StringId deriving (Show, Eq, Ord) -fromBinaryError :: GSP.BinaryCallStackDecodeError -> EventlogError+fromBinaryError :: GSPC.BinaryCallStackDecodeError -> EventlogError fromBinaryError = \case- GSP.StringIdNotFound sid -> UnknownStringId sid- GSP.SourceLocationIdNotFound sid -> UnknownSrcLocId sid+ GSPC.StringIdNotFound sid -> UnknownStringId sid+ GSPC.SourceLocationIdNotFound sid -> UnknownSrcLocId sid -fromMissingKeyError :: GSP.MissingKeyError -> EventlogError+fromMissingKeyError :: GSPC.MissingKeyError -> EventlogError fromMissingKeyError = \case- GSP.KeyStringIdNotFound sourceLocId stringId -> SourceLocationPartUndefined sourceLocId stringId+ GSPC.KeyStringIdNotFound sourceLocId stringId -> SourceLocationPartUndefined sourceLocId stringId prettyEventlogError :: EventlogError -> String prettyEventlogError = \case@@ -412,8 +411,8 @@ { unit = Speedscope.Nanoseconds , name = programName <> " " <> Text.show sampleId , startValue = 0- , endValue = length samples- , weights = replicate (length samples) 1+ , endValue = length sampleStack+ , weights = replicate (length sampleStack) 1 , samples = sampleStack } @@ -423,16 +422,16 @@ -- NOTE: groupSort is assumed to be stable PerThread -> groupSort $ toThreadSample <$> reverse samples PerCapability -> groupSort $ toCapabilitySample <$> reverse samples- NoAggregation -> [(1, toSingleProfileSample <$> reverse samples)]+ NoAggregation -> [(0, toSingleProfileSample <$> reverse samples)] where toThreadSample :: Sample -> (Word64, [Int])- toThreadSample sample = (sample.sampleThreadId, toSingleProfileSample sample)+ toThreadSample sample = (fromIntegral . sampleThreadId $ sample, toSingleProfileSample sample) toCapabilitySample :: Sample -> (Word64, [Int])- toCapabilitySample sample = (coerce sample.sampleCapabilityId, toSingleProfileSample sample)+ toCapabilitySample sample = (fromIntegral . GSPC.getCapabilityId $ sample.sampleCapabilityId, toSingleProfileSample sample) toSingleProfileSample :: Sample -> [Int]- toSingleProfileSample sample = map (GSP.word64ToInt . coerce) sample.sampleStack+ toSingleProfileSample sample = map (fromIntegral @Word64 @Int . coerce) sample.sampleStack toSpeedscopeFrame :: StackFrame -> Speedscope.Frame toSpeedscopeFrame = \case
src/GHC/Stack/Profiler/Speedscope/Options.hs view
@@ -142,9 +142,9 @@ aggregationModeParser :: O.Parser AggregationMode aggregationModeParser = asum- [ O.flag' PerThread (O.long "per-thread" <> O.help "Group the profile per thread (default)")- , O.flag' PerCapability (O.long "per-capability" <> O.help "Group the profile per capability")- , O.flag' NoAggregation (O.long "no-aggregation" <> O.help "Perform no grouping, single view")+ [ O.flag' PerThread (O.long "per-thread" <> O.help "Group the profiles by thread. (Default.)")+ , O.flag' PerCapability (O.long "per-capability" <> O.help "Group the profiles by capability")+ , O.flag' NoAggregation (O.long "no-aggregation" <> O.help "Do not aggregate the profiles.") , pure PerThread ]
src/GHC/Stack/Profiler/Speedscope/Types.hs view
@@ -3,9 +3,7 @@ import Data.Text (Text) import qualified Data.Text as Text import Data.Word (Word64)-import qualified GHC.Stack.Profiler.Core.Eventlog as GSP (CapabilityId, IpeId (..))-import qualified GHC.Stack.Profiler.Core.SourceLocation as GSP (SourceLocation (..))-import qualified GHC.Stack.Profiler.Core.Util as GSP+import qualified GHC.Stack.Profiler.Core as GSP (CapabilityId, IpeId (..), SourceLocation (..)) import IpeDB.Types.InfoProv (InfoProv (..), InfoProvId (..)) import IpeDB.Types.SrcLoc (Point (..), Range (..), SrcLoc (..)) @@ -47,9 +45,9 @@ | otherwise = Just (Text.pack srcFilePath) srcLocColumn :: SrcLoc -> Maybe Int-srcLocColumn SrcLoc{srcRange = Just Range{start = Point{column}}} = Just (GSP.word32ToInt column)+srcLocColumn SrcLoc{srcRange = Just Range{start = Point{column}}} = Just (fromIntegral column) srcLocColumn _ = Nothing srcLocLine :: SrcLoc -> Maybe Int-srcLocLine SrcLoc{srcRange = Just Range{start = Point{line}}} = Just (GSP.word32ToInt line)+srcLocLine SrcLoc{srcRange = Just Range{start = Point{line}}} = Just (fromIntegral line) srcLocLine _ = Nothing