threadscope-0.2.0: GUI/Timeline/Sparks.hs
module GUI.Timeline.Sparks (
renderSparkCreation,
renderSparkConversion,
renderSparkPool,
) where
import GUI.Timeline.Render.Constants
import Events.SparkTree
import qualified Events.SparkStats as SparkStats
import GUI.Types
import GUI.Timeline.CairoDrawing
import GUI.ViewerColours
import Graphics.Rendering.Cairo
import qualified Graphics.Rendering.Cairo as C
import Graphics.UI.Gtk
import GHC.RTS.Events hiding (Event, GCWork, GCIdle)
import Control.Monad
import Text.Printf
-- Rendering sparks. No approximation nor extrapolation is going on here.
-- The sample data, recalculated for a given slice size in sparkProfile,
-- is straightforwardly rendered.
-- TODO; Function sparkProfile, for a given slice size and viewport,
-- is called separately by each render* function, The unused parts of the result
-- won't get computed, but the drilling down the tree and allocating
-- thunks is repeated for each graph, while it could be done just once,
-- for a given zoom level and scroll directive.
renderSparkCreation :: ViewParameters -> Timestamp -> Timestamp -> SparkTree
-> Double -> Render ()
renderSparkCreation params !start0 !end0 t !maxSparkValue = do
let f1 c = SparkStats.rateDud c
f2 c = f1 c + SparkStats.rateCreated c
f3 c = f2 c + SparkStats.rateOverflowed c
renderSpark params start0 end0 t
f1 fizzledDudsColour f2 createdConvertedColour f3 overflowedColour
maxSparkValue
renderSparkConversion :: ViewParameters -> Timestamp -> Timestamp -> SparkTree
-> Double -> Render ()
renderSparkConversion params !start0 !end0 t !maxSparkValue = do
let f1 c = SparkStats.rateFizzled c
f2 c = f1 c + SparkStats.rateGCd c
f3 c = f2 c + SparkStats.rateConverted c
renderSpark params start0 end0 t
f1 fizzledDudsColour f2 gcColour f3 createdConvertedColour
maxSparkValue
renderSparkPool :: ViewParameters -> Timestamp -> Timestamp -> SparkTree
-> Double -> Render ()
renderSparkPool params@ViewParameters{..} !start0 !end0 t !maxSparkPool = do
let slice = round (fromIntegral spark_detail * scaleValue)
-- round the start time down, and the end time up, to a slice boundary
start = (start0 `div` slice) * slice
end = ((end0 + slice) `div` slice) * slice
prof = sparkProfile slice start end t
f1 c = SparkStats.minPool c
f2 c = SparkStats.meanPool c
f3 c = SparkStats.maxPool c
addSparks outerPercentilesColour maxSparkPool f1 f2 start slice prof
addSparks outerPercentilesColour maxSparkPool f2 f3 start slice prof
outlineSparks maxSparkPool f2 start slice prof
outlineSparks maxSparkPool (const 0) start slice prof
addScale params maxSparkPool start end
renderSpark :: ViewParameters -> Timestamp -> Timestamp -> SparkTree
-> (SparkStats.SparkStats -> Double) -> Color
-> (SparkStats.SparkStats -> Double) -> Color
-> (SparkStats.SparkStats -> Double) -> Color
-> Double -> Render ()
renderSpark params@ViewParameters{..} start0 end0 t
f1 c1 f2 c2 f3 c3 maxSparkValue = do
let slice = round (fromIntegral spark_detail * scaleValue)
-- round the start time down, and the end time up, to a slice boundary
start = (start0 `div` slice) * slice
end = ((end0 + slice) `div` slice) * slice
prof = sparkProfile slice start end t
-- Maximum number of sparks per slice for current data.
maxSliceSpark = fromIntegral slice * maxSparkValue
-- Maximum spark transition rate in spark/ms.
maxSlice = maxSparkValue * 1000000
outlineSparks maxSliceSpark f3 start slice prof
addSparks c1 maxSliceSpark (const 0) f1 start slice prof
addSparks c2 maxSliceSpark f1 f2 start slice prof
addSparks c3 maxSliceSpark f2 f3 start slice prof
addScale params maxSlice start end
spark_detail :: Int
spark_detail = 4 -- in pixels
off :: Double -> (SparkStats.SparkStats -> Double)
-> SparkStats.SparkStats
-> Double
off maxSliceSpark f t = fromIntegral hecSparksHeight * (1 - f t / maxSliceSpark)
dashedLine1 :: Render ()
dashedLine1 = do
save
identityMatrix
setDash [10,10] 0.0
setLineWidth 1
stroke
restore
outlineSparks :: Double
-> (SparkStats.SparkStats -> Double)
-> Timestamp -> Timestamp
-> [SparkStats.SparkStats]
-> Render ()
outlineSparks maxSliceSpark f start slice ts = do
case ts of
[] -> return ()
ts -> do
let dstart = fromIntegral start
dslice = fromIntegral slice
points = [dstart-dslice/2, dstart+dslice/2 ..]
t = zip points (map (off maxSliceSpark f) ts)
newPath
moveTo (dstart-dslice/2) (snd $ head t)
mapM_ (uncurry lineTo) t
setSourceRGBAhex black 1.0
save
identityMatrix
setLineWidth 1
stroke
restore
addSparks :: Color
-> Double
-> (SparkStats.SparkStats -> Double)
-> (SparkStats.SparkStats -> Double)
-> Timestamp -> Timestamp
-> [SparkStats.SparkStats]
-> Render ()
addSparks colour maxSliceSpark f0 f1 start slice ts = do
case ts of
[] -> return ()
ts -> do
-- liftIO $ printf "ts: %s\n" (show (map f1 (ts)))
-- liftIO $ printf "off: %s\n"
-- (show (map (off maxSliceSpark f1) (ts) :: [Double]))
let dstart = fromIntegral start
dslice = fromIntegral slice
points = [dstart-dslice/2, dstart+dslice/2 ..]
t0 = zip points (map (off maxSliceSpark f0) ts)
t1 = zip points (map (off maxSliceSpark f1) ts)
newPath
moveTo (dstart-dslice/2) (snd $ head t1)
mapM_ (uncurry lineTo) t1
mapM_ (uncurry lineTo) (reverse t0)
setSourceRGBAhex colour 1.0
fill
-- TODO: redesign and refactor the scales code after some feedback and testing.
-- There are ten minor ticks to a major tick and a semi-major tick
-- occurs half way through a major tick (overlapping the corresponding
-- minor tick).
-- The timestamp values are in nanoseconds (1e-9), i.e.,
-- a timestamp value of 1000000000 represents 1s.
-- The x-position on the drawing canvas is in milliseconds (ms) (1e-3).
-- scaleValue is used to divide a timestamp value to yield a pixel value.
addScale :: ViewParameters -> Double -> Timestamp -> Timestamp -> Render ()
addScale ViewParameters{..} maxSpark start end = do
let dstart = fromIntegral start
dend = fromIntegral end
dheight = fromIntegral hecSparksHeight
-- TODO: this is slightly incorrect, but probably at most 1 pixel off
maxS = if maxSpark < 100
then maxSpark -- too small, accuracy would suffer
else fromIntegral (2 * (ceiling maxSpark ` div` 2))
-- TODO: divide maxSpark instead, for nicer round numbers display
incr = hecSparksHeight `div` 10
majorTick = 10 * incr
-- dashed lines across the graphs
setSourceRGBAhex black 0.3
save
forM_ [0 .. 1] $ \h -> do
let y = fromIntegral (floor (fromIntegral h * fromIntegral majorTick / 2)) - 0.5
moveTo dstart y
lineTo dend y
dashedLine1
restore
-- draw scales only if the drawn area includes the very start
-- TODO: this draws the scale too often, because the drawn area begins
-- to the left of the 0 mark; will be fixed when scales are moved outside
-- the scrollable area.
when (start == 0) $ do
newPath
moveTo dstart 0
lineTo dstart dheight
setSourceRGBAhex blue 1.0
save
identityMatrix
setLineWidth 1
stroke
restore
selectFontFace "sans serif" FontSlantNormal FontWeightNormal
setFontSize 12
setSourceRGBAhex blue 1.0
save
scale scaleValue 1.0
setLineWidth 0.5
drawTicks maxS start scaleValue 0 incr majorTick hecSparksHeight
restore
-- TODO: make it more robust when parameters change, e.g., if incr is too small
drawTicks :: Double -> Timestamp -> Double -> Int -> Int -> Int -> Int -> Render ()
drawTicks maxS offset scaleValue pos incr majorTick endPos
= if pos <= endPos then do
draw_line (x0, hecSparksHeight - y0) (x1, hecSparksHeight - y1)
when (pos > 0
&& (atMajorTick || atMidTick || tickWidthInPixels > 30)) $ do
move_to (offset + 15,
fromIntegral hecSparksHeight - pos + 4)
m <- getMatrix
identityMatrix
tExtent <- textExtents tickText
(fourPixels, _) <- deviceToUserDistance 4 0
when (textExtentsWidth tExtent + fourPixels < fromIntegral tickWidthInPixels || atMidTick || atMajorTick) $ do
textPath tickText
C.fill
setMatrix m
drawTicks maxS offset scaleValue (pos+incr) incr majorTick endPos
else
return ()
where
tickWidthInPixels :: Int
tickWidthInPixels = truncate ((fromIntegral incr) / scaleValue)
tickText = showTickText (maxS * fromIntegral pos
/ fromIntegral hecSparksHeight)
atMidTick = pos `mod` (majorTick `div` 2) == 0
atMajorTick = pos `mod` majorTick == 0
(x0, y0, x1, y1) = if atMajorTick then
(offset, pos, offset+13, pos)
else
if atMidTick then
(offset, pos, offset+10, pos)
else
(offset, pos, offset+6, pos)
showTickText :: Double -> String
showTickText pos
= deZero (printf "%.2f" pos)
deZero :: String -> String
deZero str
= if length str >= 4 && take 3 revstr == "00." then
reverse (drop 3 revstr)
else
str
where
revstr = reverse str