ghc-events-analyze-0.2.0: src/GHC/RTS/Events/Analyze/Reports/Timed/SVG.hs
module GHC.RTS.Events.Analyze.Reports.Timed.SVG (
writeReport
) where
import Data.Maybe (catMaybes)
import Data.Monoid (mempty, mconcat, (<>))
import Diagrams.Backend.SVG (B, renderSVG)
import Diagrams.Prelude (Diagram, Colour, R2, SizeSpec2D, (#), (|||))
import GHC.RTS.Events (Timestamp)
import Graphics.SVGFonts.ReadFont (TextOpts(..))
import Text.Printf (printf)
import qualified Data.Map as Map
import qualified Diagrams.Prelude as D
import qualified Graphics.SVGFonts.ReadFont as F
import GHC.RTS.Events.Analyze.Types
import GHC.RTS.Events.Analyze.Reports.Timed hiding (writeReport)
writeReport :: Options -> Quantized -> Report -> FilePath -> IO ()
writeReport options quantized report path =
uncurry (renderSVG path) $ renderReport options quantized report
type D = Diagram B R2
renderReport :: Options -> Quantized -> Report -> (SizeSpec2D, D)
renderReport Options{optionsNumBuckets}
Quantized{quantBucketSize}
report = (D.sizeSpec2D rendered, rendered)
where
rendered :: D
rendered = D.vcat $ map renderSVGFragment (SVGTimeline : fragments)
fragments :: [SVGFragment]
fragments = map renderFragment report
renderSVGFragment :: SVGFragment -> D
renderSVGFragment (SVGSection title) =
padHeader (2 * blockSize) title
renderSVGFragment (SVGLine header blocks) =
-- Add empty block at the start so that the whole thing doesn't shift up
padHeader blockSize header ||| (blocks <> (block 0 # D.lw 0))
renderSVGFragment SVGTimeline =
padHeader blockSize mempty ||| timeline optionsNumBuckets quantBucketSize
padHeader :: Double -> D -> D
padHeader height h =
D.translateX (0.5 * blockSize) h
<> D.rect headerWidth height # D.alignL # D.lw 0
headerWidth :: Double
headerWidth = blockSize -- extra padding
+ (maximum . catMaybes . map headerWidthOf $ fragments)
headerWidthOf :: SVGFragment -> Maybe Double
headerWidthOf (SVGLine header _) = Just (D.width header)
headerWidthOf _ = Nothing
data SVGFragment =
SVGTimeline
| SVGSection D
| SVGLine D D
renderFragment :: ReportFragment -> SVGFragment
renderFragment (ReportSection title) = SVGSection (renderText title (blockSize + 2))
renderFragment (ReportLine line) = uncurry SVGLine $ renderLine line
renderLine :: ReportLine -> (D, D)
renderLine line@ReportLineData{..} =
( renderText lineHeader (blockSize + 2)
, blocks <> bgBlocks lineBackground
)
where
blocks :: D
blocks = mconcat . map (mkBlock $ lineColor line) $ Map.toList lineValues
mkBlock :: Colour Double -> (Int, Double) -> D
mkBlock c (b, q) = block b # D.fcA (c `D.withOpacity` qOpacity q)
lineColor :: ReportLine -> Colour Double
lineColor = eventColor . head . lineEventIds
eventColor :: EventId -> Colour Double
eventColor EventGC = D.red
eventColor (EventUser _) = D.green
eventColor (EventThread _) = D.blue
bgBlocks :: Maybe (Int, Int) -> D
bgBlocks Nothing = mempty
bgBlocks (Just (fr, to)) = mconcat [
block b # D.fcA (D.black `D.withOpacity` 0.1)
| b <- [fr .. to]
]
renderText :: String -> Double -> D
renderText str size =
D.stroke (F.textSVG' (textOpts str size)) # D.fc D.black # D.lc D.black # D.alignL
textOpts :: String -> Double -> TextOpts
textOpts str size =
TextOpts {
txt = str
, fdo = F.lin
, mode = F.INSIDE_H
, spacing = F.KERN
, underline = False
, textWidth = 0 -- not important
, textHeight = size
}
-- | Translate quantized value to opacity
--
-- For every event and every bucket we record the percentage of that bucket
-- that the event was using. However, if we use this percentage directly as the
-- opacity value for the corresponding block in the diagram then a thread that
-- does _something_, but only a tiny amount, is indistinguishable from a thread
-- that does nothing -- but typically we are interested in knowing that a
-- thread does something, anything at all, rather than nothing, while the
-- difference between using 30% and 40% is probably less important and hard to
-- visually see anyway.
qOpacity :: Double -> Double
qOpacity 0 = 0
qOpacity q = 0.1 + q * 0.9
block :: Int -> D
block i = D.translateX (blockSize * fromIntegral i)
$ D.rect blockSize blockSize
blockSize :: Double
blockSize = 10
timeline :: Int -> Timestamp -> D
timeline numBuckets bucketSize =
mconcat [ timelineBlock b # D.translateX (fromIntegral b * blockSize)
| b <- [0 .. numBuckets - 1]
]
where
timelineBlock b
| b `rem`5 == 0 = D.strokeLine bigLine # D.lw 0.5
<> (renderText (bucketTime b) 9 # D.translateY 8)
| otherwise = D.strokeLine smallLine # D.lw 0.5 # D.translateY 1
bucketTime :: Int -> String
bucketTime b = let timeNs :: Timestamp
timeNs = fromIntegral b * bucketSize
timeS :: Double
timeS = fromIntegral timeNs / 1000000000
in printf "%0.1fs" timeS
bigLine = mkLine [(0, 4), (blockSize, 0)]
smallLine = mkLine [(0, 3), (blockSize, 0)]
mkLine = D.fromSegments . map (D.straight . D.r2)