graph-trace-dot-0.1.0.0: src/Graph/Trace/Dot.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
module Graph.Trace.Dot
( parseLogEntries
, parseLogEntry
, buildGraph
, graphToDot
, Key(..)
, LogEntry(..)
) where
import Control.Applicative ((<|>))
import Control.Monad
import qualified Data.Attoparsec.ByteString.Char8 as Atto
import qualified Data.Attoparsec.ByteString.Lazy as AttoL
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BSL
import Data.Foldable (foldl')
import qualified Data.List as List
import qualified Data.Map as M
import Data.Maybe (isJust)
import Data.Monoid (Alt(..))
import Data.Ord (Down(..))
import Data.Semigroup (Min(..))
parseLogEntries :: BSL.ByteString -> Either String [LogEntry]
parseLogEntries = AttoL.parseOnly (Atto.many' parseLogEntry <* Atto.endOfInput)
data Key = Key { keyId :: !Word
, keyName :: !BS.ByteString
}
deriving (Eq, Ord, Show)
data LogEntry
= Entry
Key
(Maybe Key) -- called by
(Maybe SrcCodeLoc) -- definition site
(Maybe SrcCodeLoc) -- call site
| Trace Key BS.ByteString (Maybe SrcCodeLoc)
deriving Show
data SrcCodeLoc =
SrcCodeLoc
{ srcMod :: BS.ByteString
, srcLine :: Int
, srcCol :: Int
} deriving (Eq, Ord, Show)
-- | Use this to escape special characters that appear in the HTML portion of
-- the dot code. Other strings such as node names should not be escaped.
htmlEscape :: BS.ByteString -> BS.ByteString
htmlEscape bs = foldl' doReplacement bs replacements
where
doReplacement acc (c, re) =
case BS8.break (== c) acc of
(before, after)
| BS.null after -> acc
| otherwise -> before <> re <> BS8.tail after
replacements =
[ ('&', "&")
, ('<', "<")
, ('>', ">")
, ('\\', "\\\\") -- not really an HTML escape, but still needed
]
parseKey :: Atto.Parser Key
parseKey = do
kName <- Atto.takeTill (== '§') <* Atto.char '§'
kId <- Atto.decimal <* Atto.char '§'
pure $ Key { keyId = kId, keyName = kName }
parseLogEntry :: Atto.Parser LogEntry
parseLogEntry = (parseEntryEvent <|> parseTraceEvent) <* Atto.many' Atto.space
parseEntryEvent :: Atto.Parser LogEntry
parseEntryEvent = do
_ <- Atto.string "entry§"
curKey <- parseKey
mPrevKey <- Just <$> parseKey
<|> Nothing <$ Atto.string "§§"
defSite <- parseSrcCodeLoc
callSite <- parseSrcCodeLoc
_ <- Atto.many' Atto.space
pure $ Entry curKey mPrevKey defSite callSite
parseTraceEvent :: Atto.Parser LogEntry
parseTraceEvent = do
_ <- Atto.string "trace§"
key <- parseKey
message <- Atto.takeTill (== '§') <* Atto.char '§'
callSite <- parseSrcCodeLoc
_ <- Atto.many' Atto.space
let removeNewlines = BS8.unwords . BS8.lines
pure $ Trace key (htmlEscape . removeNewlines $ message) callSite
parseSrcCodeLoc :: Atto.Parser (Maybe SrcCodeLoc)
parseSrcCodeLoc = do
let parseLoc = do
srcMod <- Atto.takeTill (== '§') <* Atto.char '§'
srcLine <- Atto.decimal <* Atto.char '§'
srcCol <- Atto.decimal <* Atto.char '§'
pure SrcCodeLoc{..}
Just <$> parseLoc <|> Nothing <$ Atto.string "§§§"
data NodeEntry
= Message BS.ByteString -- ^ The trace message
(Maybe SrcCodeLoc) -- ^ call site
| Edge Key -- ^ Id of the invocation to link to
(Maybe SrcCodeLoc) -- ^ call site
deriving Show
-- Remembers the order in which the elements were inserted
type Graph =
M.Map Key ( Min Int -- order
, [NodeEntry] -- contents
, Alt Maybe SrcCodeLoc -- definition site
)
buildGraph :: [LogEntry] -> Graph
buildGraph = foldl' build mempty where
build graph entry =
case entry of
Trace tag msg callSite ->
M.insertWith (<>)
tag
(graphSize, [Message msg callSite], Alt Nothing)
graph
Entry curTag (Just prevTag) defSite callSite ->
M.insertWith (<>)
curTag
(graphSize + 1, [], Alt defSite)
$ M.insertWith (<>)
prevTag
(graphSize, [Edge curTag callSite], Alt Nothing)
graph
Entry curTag Nothing defSite _ ->
M.insertWith (<>) curTag (graphSize, [], Alt defSite) graph
where
graphSize = Min $ M.size graph
graphToDot :: Graph -> BSB.Builder
graphToDot graph = header <> graphContent <> "}"
where
orderedEntries = map (\(key, (_, content, srcLoc)) -> (key, (content, getAlt srcLoc)))
. List.sortOn (Down . (\(x,_,_) -> x) . snd)
$ M.toList graph
graphContent =
-- knot-tying is used to get the color of a node from the edge pointing to that node.
-- TODO consider doing separate traversals for edges and nodes so that the
-- result can be built strictly.
let (output, _, colorMap) =
foldl'
(doNode colorMap)
(mempty, cycle edgeColors, mempty)
orderedEntries
in output
header :: BSB.Builder
header = "digraph {\nnode [tooltip=\" \" shape=plaintext colorscheme=set28]\n"
doNode finalColorMap (acc, colors, colorMapAcc) (key, (entries, mSrcLoc)) =
let (cells, edges, colors', colorMapAcc')
= foldl' doEntry ([], [], colors, colorMapAcc) (zip entries [1..])
acc' =
-- don't render nodes that have in inbound edge but no content
if null entries && isJust mEdgeData
then acc
else tableStart
<> tableEl cells
<> tableEnd
<> mconcat edges
<> acc
in (acc', colors', colorMapAcc')
where
keyStr (Key i k) = BSB.byteString k <> BSB.wordDec i
keyStrEsc k = keyStr k { keyName = htmlEscape $ keyName k }
quoted bs = "\"" <> bs <> "\""
-- Building a node
mEdgeData = M.lookup key finalColorMap
nodeColor = snd <$> mEdgeData
nodeToolTip = foldMap (("defined at " <>) . pprSrcCodeLoc) mSrcLoc
backHref = foldMap (\(k, _) -> "#" <> keyStr k) mEdgeData
labelCell =
el "TR" []
[ el "TD" [ "HREF" .= backHref
, "TOOLTIP" .= nodeToolTip
, "BGCOLOR" .=? nodeColor
]
[ foldMap (const $ el "FONT" ["POINT-SIZE" .= "7"] ["←"])
mEdgeData
, " "
, el "B" [] [ BSB.byteString . htmlEscape $ keyName key ]
]
]
tableEl cells =
el "TABLE" [ "BORDER" .= "0"
, "CELLBORDER" .= "1"
, "CELLSPACING" .= "0"
, "CELLPADDING" .= "4"
]
[ labelCell
, mconcat cells
]
tableStart, tableEnd :: BSB.Builder
tableStart = quoted (keyStr key) <> " [label=<\n"
tableEnd = ">];"
-- Building an entry in a node
doEntry (cs, es, colors'@(color:nextColors), colorMap) ev = case ev of
(Message str mCallSite, idx) ->
let msgToolTip =
foldMap (("printed at " <>) . pprSrcCodeLoc) mCallSite
msgEl =
el "TR" []
[ el "TD" [ "HREF" .= ""
, "TOOLTIP" .= msgToolTip
, "ALIGN" .= "LEFT"
, "PORT" .= BSB.wordDec idx
]
[ BSB.byteString str ]
]
in (msgEl : cs, es, colors', colorMap)
(Edge edgeKey mCallSite, idx) ->
let href = foldMap (const $ "#" <> keyStrEsc edgeKey) mEdge
elToolTip =
foldMap (("called at " <>) . pprSrcCodeLoc) mCallSite
edgeEl =
el "TR" []
[ el "TD" [ "TOOLTIP" .= elToolTip
, "ALIGN" .= "LEFT"
, "CELLPADDING" .= "1"
, "BGCOLOR" .= color
, "PORT" .= BSB.wordDec idx
, "HREF" .= href
]
[ el "FONT" [ "POINT-SIZE" .= "8" ]
[ BSB.byteString . htmlEscape $ keyName edgeKey ]
]
]
mEdge = do
(_, targetContent, _) <- M.lookup edgeKey graph
guard . not $ null targetContent
Just $
quoted (keyStr key) <> ":" <> BSB.wordDec idx
<> " -> " <> quoted (keyStr edgeKey)
<> " [tooltip=\" \" colorscheme=set28 color=" <> color <> "];"
in ( edgeEl : cs
, maybe id (:) mEdge es
, nextColors
, M.insert edgeKey (key, color) colorMap
)
doEntry ac _ = ac
type Element = BSB.Builder
type Attr = (BSB.Builder, Maybe BSB.Builder)
(.=) :: BSB.Builder -> BSB.Builder -> Attr
name .= val = name .=? Just val
(.=?) :: BSB.Builder -> Maybe BSB.Builder -> Attr
name .=? val = (name, val)
el :: BSB.Builder -> [Attr] -> [BSB.Builder] -> Element
el name attrs children =
"<" <> name <> foldMap renderAttr attrs <> ">"
<> mconcat children <> "</" <> name <> ">"
where
renderAttr (aName, Just aVal) = " " <> aName <> "=\"" <> aVal <> "\""
renderAttr (_, Nothing) = mempty
edgeColors :: [BSB.Builder]
edgeColors = BSB.intDec <$> [1..8 :: Int]
pprSrcCodeLoc :: SrcCodeLoc -> BSB.Builder
pprSrcCodeLoc loc
= BSB.byteString (srcMod loc) <> ":"
<> BSB.intDec (srcLine loc) <> ":"
<> BSB.intDec (srcCol loc)