working on readme

This commit is contained in:
Aaron Allen 2021-12-20 21:43:40 -06:00
parent a75e50bc07
commit a5129c42d9
5 changed files with 426 additions and 131 deletions

186
README.md
View File

@ -1,7 +1,183 @@
# Graph Trace # Graph Trace
A ghc plugin that a creates a log file which can be compiled to a graph for A GHC plugin that causes your Haskell program to create a log file which can be
display with `graphviz` or some other means. Contrary to traditional debug compiled to a graph for display with `Graphviz` or some other means. Contrary
tracing where all output is interleaved into a flat sequence of lines, the to traditional debug tracing where all output is interleaved into a flat
graph structure produced by this plugin takes into account the actual flow of sequence of lines, the graph structure produced by this plugin takes into
the program with individual function calls represented as edges in the graph. account the actual call graph of the program with individual function calls
represented as edges in the graph.
__Contents:__
- [Demonstration](#demonstration)
- [Quickstart](#quickstart)
- [User's guide](#users-guide)
- [`Graph.Trace` plugin](#graphtrace-plugin)
- [`graph-trace-viz` utility](#graph-trace-viz-utility)
- [Caveats](#caveats)
## Demonstration
Consider this simplistic program that greets the user by name:
```haskell
import Graph.Trace (TraceDeep, traceM)
import Data.Char (toUpper, toLower)
main :: TraceDeep => IO ()
main = do
firstName <- prompt "Enter your first name"
lastName <- prompt "Enter your last name"
greet firstName lastName
prompt :: String -> IO String
prompt str = do
putStrLn str
input <- getLine
traceM $ "input: " <> input
pure $ capitalize input
capitalize :: String -> String
capitalize [] = []
capitalize (x:xs) = toUpper x : map toLower xs
greet :: String -> String -> IO ()
greet firstName lastName =
putStrLn $ "Hello, " <> firstName <> " " <> lastName <> "!"
```
Using the `Graph.Trace` plugin along with the `graph-trace-viz` utility, we can
run this program to generate the following trace of the call graph:
![demo image](/images/demo.svg)
## Quickstart
1. Add the `graph-trace` package as a dependency to your project.
2. Enable the plugin by adding the following GHC options:
`-fplugin=Graph.Trace -fplugin-opt Graph.Trace:debug-all -fno-full-laziness -fno-cse`.
This can be placed in the `ghc-options` field of the cabal or
package.yaml file (depending on whether you use cabal or stack to build).
For example:
in `package.yaml`:
```
executables:
my-exe:
...
ghc-options:
-fplugin=Graph.Trace
-fplugin-opt Graph.Trace:debug-all
-fno-full-laziness
-fno-cse
```
or in `foo.cabal`:
```
executable my-exe
...
ghc-options:
-fplugin=Graph.Trace
-fplugin-opt Graph.Trace:debug-all
-fno-full-laziness
-fno-cse
```
3. Build your project (`cabal build all` or `stack build`).
4. Running your program should now generate a file called `<executable-name>.trace`.
5. Install [Graphviz](https://graphviz.org) and the `graph-trace-viz` program.
Invoke `graph-trace-viz` within the same directory as the trace file.
6. There should now be a file such as `<executable-name>.html` which can be
viewed in your browser.
## User's Guide
### `Graph.Trace` plugin
To use this plugin simply add `graph-trace` as a package dependency and pass
the `-fplugin=Graph.Trace` option to GHC.
The main functionality of the `Graph.Trace` plugin is to automatically
instrument your code so that it will emit trace logging to a `*.trace` file
when run. There are two types of traces:
- __Function call__
This trace is emitted when the term returned by a function is evaluated to
WHNF. Each emission of an entry trace generates a unique ID for that particular
function invocation. The trace also includes the ID of the code from which the
function was called (if applicable) so that a graph edge can be constructed
between the two. Entry traces are only emitted for functions that have
signatures. If the function does not have a signature then it will simply
inherit the ID of the calling context and will therefore not generate a new
node in the call graph.
- __Debug trace__
Debug traces are textual messages that the user can emit from the body of a
function. When rendered, they appear as plain text in the body of a call graph
node. The API for debug traces matches that of the familiar `Debug.Trace`
module and is available through the `Graph.Trace` module. A debug trace will
only be emitted when the thing it is applied to is evaluated to WHNF.
The plugin gives you some control over how and when traces are emitted. There
are a variety of constraints you can put on function signatures to control
tracing, all of which are exported by `Graph.Trace`:
- `Trace`
On its own, this constraint says that traces should be emitted for a given
function using the name of that function as the identifier. Notably, function
calls made from within the body of the function do not inherit this behavior
and so will not emit traces unless otherwise instructed to do so.
- `TraceDeep`
This is similar to `Trace` except that function calls made within the function
body will also emit traces even if they don't have a trace constraint (unless
they are being muted). For example, putting `TraceDeep` on the `main` function
will result in the full execution the program being traced.
- `TraceMute`
If a function has this constraint then any invocation of it as well as the
function calls it makes will not emit any traces. This constraint overrides all
the others and is inherited by function calls made from within its body.
- `TraceKey`
This constraint is the same as `Trace` but it takes a type level string
argument which will be used as the function identifier instead of the
function's actual name. Notably, if the identifier of a called function is
overridden to be the same as that of the function calling it then its output
will be placed in the graph node of the calling function rather than
generating its own node. There is also a `TraceDeepKey` constraint that does
the same thing but for `TraceDeep`.
If you want every function in your program to emit traces, you can use the
`debug-all` plugin option which effectively adds the `Trace` constraint to all
function definitons. To use this option, pass the `-fplugin-opt Graph.Trace:debug-all`
option to GHC in addition to `-fplugin=Graph.Trace`.
If a trace file already exists for your executable being then new entries will
be appended to that file. To start a new trace you'll need to rename or delete
the old file.
### `graph-trace-viz` utility
Once you've generated a `*.trace` file by compiling your program with the
`Graph.Trace` plugin and running it, the `graph-trace-viz` utility can render
the graph as an html document. It is dependent on
[Graphviz](https://graphviz.org), so you must install that on your system
(there should be an executable called `dot` on your PATH). Simply invoke
`graph-trace-viz` in the same directory as the `*.trace` file and it will write
an `*.html` document. By default it will read all trace files in the current
directory but you can specify the files by giving them as command line
arguments instead.
## Caveats
There are several known caveats you should be aware of:
- __Undesirable optimisations__
If you compile with the common sub-expression or full laziness optimisations,
which are on by default for `O1` and `O2` settings, graph nodes that should
be distinct can sometimes get merged into a single node. To prevent this, it
is recommended that you turn these optimisations off using the `-fno-cse` and
`-fno-full-laziness` flags when compiling with the `graph-trace` plugin.
- __Type class methods__
For type class instance methods to be traced correctly, the class method
definitions must be in a package compiled with the plugin and you'll also
need to put a type signature on the instance method declarations, which
requires the `InstanceSigs` GHC extension.
- __Impredicative types__
If you have a function binding that takes a rank-n quantified type as a
parameter, this can cause compilation with the plugin to fail. With GHC 9.2
and above, giving a type signature to the binding will resolve the issue.
- The plugin does not support GHC versions less than 8.10

View File

@ -1,9 +1,10 @@
{-# OPTIONS_GHC -fplugin=Graph.Trace #-}
module Class where module Class where
import Graph.Trace import Graph.Trace
class Show a => Classy a where class Show a => Classy a where
classy :: Trace => a -> String classy :: a -> String
deff :: Trace => a -> String deff :: Trace => a -> String
deff = show deff = show

View File

@ -3,6 +3,8 @@
-- {-# OPTIONS_GHC -fno-full-laziness -fno-cse #-} -- {-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--{-# OPTIONS_GHC -ddump-rn-ast #-} --{-# OPTIONS_GHC -ddump-rn-ast #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
@ -17,123 +19,152 @@ import Data.Functor.Identity (Identity(..))
import Graph.Trace import Graph.Trace
--import Debug.Trace --import Debug.Trace
import Class import Class
import Data.Char
import qualified System.Random as Rand import qualified System.Random as Rand
import System.IO.Unsafe import System.IO.Unsafe
main :: TraceDeep => IO () main :: TraceDeep => IO ()
main = trace bah print unassuming >> buzzard main = do
where firstName <- prompt "Enter your first name"
unassuming :: Either Bool Int lastName <- prompt "Enter your last name"
--thisIsABoolean :: Bool greet firstName lastName
unassuming@(Left thisIsABoolean@True) =
trace bah $! (Left True :: Either Bool Int)
buzzard = do prompt :: String -> IO String
putStrLn $ "please, help" <&> "boo" prompt str = do
traceM bah putStrLn str
input <- getLine
traceM $ "input: " <> input
pure $ capitalize input
bah :: String capitalize :: String -> String
bah = unsafePerformIO $ do capitalize [] = []
getLine capitalize (x:xs) = toUpper x : map toLower xs
(<&>) :: String -> String -> String greet :: String -> String -> IO ()
a <&> b = a greet first last =
putStrLn $ "Hello, " <> first <> " " <> last <> "!"
-- where {-# NOINLINE main #-}
-- inFlight = putStrLn "need help now" -- main :: TraceDeep => IO ()
-- shitty = () -- main = trace bah print unassuming >> buzzard
--replicateM_ 2 $ forkIO test -- where
-- andAnother -- unassuming :: Either Bool Int
-- test' -- --thisIsABoolean :: Bool
-- unassuming@(Left thisIsABoolean@True) =
-- trace bah $! (Left True :: Either Bool Int)
--
-- buzzard = do
-- putStrLn $ "hello" <&> "boo"
-- traceM bah
--
-- bah :: String
-- bah = unsafePerformIO $ do
-- getLine
-- (<&>) :: String -> String -> String
-- a <&> b = a
-- test' :: IO () -- main :: Trace => IO ()
-- test' = do -- main = test'
-- andAnother
-- trace "test\ntest" pure () test' :: Trace => IO ()
-- traceM "yo" test' = do
-- putStrLn $ deff (I 3) andAnother
-- x <- readLn trace "test\ntest" pure ()
-- case x of traceM "yo"
-- 3 -> putStrLn $ classy (I x) putStrLn $ deff (I 3)
-- _ -> pure () x <- readLn
-- putStrLn $ classier (I 5) case x of
-- inWhere 3 -> putStrLn $ classy (I x)
-- let inLet :: IO () _ -> pure ()
-- inLet = do putStrLn $ classier (I 5)
-- letWhere inWhere
-- another let inLet :: Trace => IO ()
-- where letWhere = trace ("hello" \/& "two") pure () inLet = do
-- inLet letWhere
-- !_ <- another another
-- let letBound = letBoundThing where letWhere = trace ("hello" \/& "two") pure ()
-- trace letBound pure () inLet
-- trace "leaving" pure () !_ <- another
-- where let letBound = letBoundThing
-- inWhere :: Debug => IO () trace letBound pure ()
-- inWhere = do trace "leaving" pure ()
-- innerWhere where
-- where inWhere :: Trace => IO ()
-- innerWhere :: Debug => IO () inWhere = do
-- innerWhere = trace "innerWhere" pure () innerWhere
-- where
-- another :: Debug => IO () innerWhere :: Trace => IO ()
-- another innerWhere = trace "innerWhere" pure ()
-- | trace "another" True = do
-- pure () another :: Trace => IO ()
-- | otherwise = pure () another
-- | trace "another" True = do
-- andAnother :: Debug => IO () pure ()
-- andAnother = trace "hello!" pure () | otherwise = pure ()
--
-- letBoundThing :: Debug => String andAnother :: (Trace, Monad m) => m ()
-- letBoundThing = "bound by let" andAnother = trace "hello!" pure ()
--
-- (\/&) :: String -> String -> String letBoundThing :: Trace => String
-- a \/& b = the a <> ('\\' : b) letBoundThing = "bound by let"
--
-- the :: a -> a (\/&) :: String -> String -> String
-- the = id a \/& b = the a <> ('\\' : b)
--
-- newtype I = I Int deriving Show the :: a -> a
-- the = id
-- instance Classy I where
-- classy :: Debug => I -> String newtype I = I Int deriving Show
-- classy = boo
-- where instance Classy I where
-- boo :: Debug => I -> String classy :: Trace => I -> String
-- boo i = trace (show i) "..." classy = boo
-- where
-- instance Classier I where boo :: Trace => I -> String
-- classier = show boo i = trace (show i) "..."
instance Classier I where
classier = show
-- --
-- -- test :: (?x :: String) => IO () -- -- test :: (?x :: String) => IO ()
-- -- test = print ?x -- -- test = print ?x
-- --
-- data FieldUpdate a data FieldUpdate a
-- = FieldValue a = FieldValue a
-- | FieldOmitted | FieldOmitted
-- | FieldNull | FieldNull
--
-- mkUpdater :: f FieldUpdate mkUpdater :: f FieldUpdate
-- -> f Maybe -> f Maybe
-- -> (forall a. f a -> a x) -> (forall a. f a -> a x)
-- -> Maybe x -> Maybe x
-- mkUpdater update original getField = mkUpdater update original getField =
-- case getField update of case getField update of
-- FieldValue a -> Just a FieldValue a -> Just a
-- FieldOmitted -> getField original FieldOmitted -> getField original
-- FieldNull -> Nothing FieldNull -> Nothing
--
-- data T f = data T f =
-- MkT MkT
-- { t1 :: f Bool { t1 :: f Bool
-- , t2 :: f String , t2 :: f String
-- } }
--
type TY = forall x. (forall a. T a -> a x) -> Maybe x
-- zz :: Int
-- zz =
-- let x :: [forall x. x -> x]
-- x = [id, id]
-- in id head x 4
zzz :: Int
zzz = id head [1,2,3]
-- zzzz :: T FieldUpdate -> T Maybe -> T Maybe -- zzzz :: T FieldUpdate -> T Maybe -> T Maybe
-- zzzz update orig = -- zzzz update orig =
-- let updater :: DebugMute => (forall a. T a -> a x) -> Maybe x -- let updater :: TY --(forall a. T a -> a x) -> Maybe x
-- updater | let ?x = 1 -- updater | let ?x = 1
-- = mkUpdater update orig -- = mkUpdater update orig
-- in MkT -- in MkT
@ -141,34 +172,15 @@ a <&> b = a
-- , t2 = updater t2 -- , t2 = updater t2
-- } -- }
-- fzzz :: (?_debug_ip :: Maybe DebugIPTy) => T FieldUpdate -> T Maybe -> T Maybe -- fzzz :: Trace => T FieldUpdate -> T Maybe -> T Maybe
-- fzzz update orig = entry $ -- fzzz update orig = entry $
-- let --updater :: (?_debug_ip :: Maybe DebugIPTy) -- let updater :: -- (?_debug_ip :: Maybe DebugContext)
-- -- => (forall a. T a -> a x) -> Maybe x -- (forall a. T a -> a x) -> Maybe x
-- updater -- | let ?_debug_ip = newIP' -- updater -- | let ?_debug_ip = newIP'
-- = --entry $ -- = --entry $
-- mkUpdater update orig -- mkUpdater update orig
-- where -- addOne = (+1)
-- newIP' =
-- let mPrevTag = fmap snd ?_debug_ip
-- in unsafePerformIO $ do
-- newId <- Rand.randomIO :: IO Word
-- let newTag = DT
-- { invocationId = newId
-- , debugKey = Right "test"
-- }
-- pure $ Just (mPrevTag, newTag)
-- in MkT -- in MkT
-- { t1 = updater t1 -- { t1 = updater t1
-- , t2 = updater t2 -- , t2 = updater t2
-- } -- }
-- where
-- newIP =
-- let mPrevTag = fmap snd ?_debug_ip
-- in unsafePerformIO $ do
-- newId <- Rand.randomIO :: IO Word
-- let newTag = DT
-- { invocationId = newId
-- , debugKey = Right "test"
-- }
-- pure $ Just (mPrevTag, newTag)

View File

@ -34,4 +34,4 @@ executable test-exe
, random , random
-- hs-source-dirs: -- hs-source-dirs:
default-language: Haskell2010 default-language: Haskell2010
-- ghc-options: -fplugin=Graph.Trace -- ghc-options: -fplugin=Graph.Trace -fplugin-opt Graph.Trace:debug-all -fno-cse -fno-full-laziness

106
images/demo.svg Normal file
View File

@ -0,0 +1,106 @@
<svg width="198pt" height="187pt"
viewBox="0.00 0.00 197.50 187.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 183)">
<polygon fill="white" stroke="transparent" points="-4,4 -4,-183 193.5,-183 193.5,4 -4,4"/>
<!-- main138835950170271710 -->
<g id="node1" class="node">
<title>main138835950170271710</title>
<g id="a_node1"><a xlink:title=" ">
<g id="a_node1_0"><a xlink:title="defined at app/Main.hs:28:1">
<polygon fill="none" stroke="black" points="75,-150 75,-175 116,-175 116,-150 75,-150"/>
<text text-anchor="start" x="80" y="-159.8" font-family="Times,serif" font-weight="bold" font-size="14.00">main</text>
</a>
</g>
<g id="a_node1_1"><a xlink:href="#prompt924477568213965436" xlink:title="called at app/Main.hs:29:16">
<polygon fill="#a6d854" stroke="transparent" points="75,-137 75,-150 116,-150 116,-137 75,-137"/>
<polygon fill="none" stroke="black" points="75,-137 75,-150 116,-150 116,-137 75,-137"/>
<text text-anchor="start" x="77" y="-141.6" font-family="Times,serif" font-size="8.00">prompt</text>
</a>
</g>
<g id="a_node1_2"><a xlink:href="#prompt8331185687311588863" xlink:title="called at app/Main.hs:30:15">
<polygon fill="#e78ac3" stroke="transparent" points="75,-124 75,-137 116,-137 116,-124 75,-124"/>
<polygon fill="none" stroke="black" points="75,-124 75,-137 116,-137 116,-124 75,-124"/>
<text text-anchor="start" x="77" y="-128.6" font-family="Times,serif" font-size="8.00">prompt</text>
</a>
</g>
<g id="a_node1_3"><a xlink:title="called at app/Main.hs:31:3">
<polygon fill="#8da0cb" stroke="transparent" points="75,-111 75,-124 116,-124 116,-111 75,-111"/>
<polygon fill="none" stroke="black" points="75,-111 75,-124 116,-124 116,-111 75,-111"/>
<text text-anchor="start" x="77" y="-115.6" font-family="Times,serif" font-size="8.00">greet</text>
</a>
</g>
</a>
</g>
</g>
<!-- prompt924477568213965436 -->
<g id="node2" class="node">
<title>prompt924477568213965436</title>
<g id="a_node2"><a xlink:title=" ">
<g id="a_node2_4"><a xlink:href="#main138835950170271710" xlink:title="defined at app/Main.hs:34:1">
<polygon fill="#a6d854" stroke="transparent" points="8,-41.5 8,-66.5 80,-66.5 80,-41.5 8,-41.5"/>
<polygon fill="none" stroke="black" points="8,-41.5 8,-66.5 80,-66.5 80,-41.5 8,-41.5"/>
<text text-anchor="start" x="16" y="-51.3" font-family="Times,serif" font-size="7.00"></text>
<text text-anchor="start" x="23" y="-51.3" font-family="Times,serif" font-size="14.00"> </text>
<text text-anchor="start" x="27" y="-51.3" font-family="Times,serif" font-weight="bold" font-size="14.00">prompt</text>
</a>
</g>
<g id="a_node2_5"><a xlink:title="printed at app/Main.hs:37:3">
<polygon fill="none" stroke="black" points="8,-16.5 8,-41.5 80,-41.5 80,-16.5 8,-16.5"/>
<text text-anchor="start" x="13" y="-25.3" font-family="Times,serif" font-size="14.00">input: john</text>
</a>
</g>
<g id="a_node2_6"><a xlink:title="called at app/Main.hs:38:10">
<polygon fill="#fc8d62" stroke="transparent" points="8,-3.5 8,-16.5 80,-16.5 80,-3.5 8,-3.5"/>
<polygon fill="none" stroke="black" points="8,-3.5 8,-16.5 80,-16.5 80,-3.5 8,-3.5"/>
<text text-anchor="start" x="10" y="-8.1" font-family="Times,serif" font-size="8.00">capitalize</text>
</a>
</g>
</a>
</g>
</g>
<!-- main138835950170271710&#45;&gt;prompt924477568213965436 -->
<g id="edge1" class="edge">
<title>main138835950170271710:3&#45;&gt;prompt924477568213965436</title>
<g id="a_edge1"><a xlink:title=" ">
<path fill="none" stroke="#a6d854" d="M74,-144C46.44,-144 40.24,-110.82 40.18,-81.32"/>
<polygon fill="#a6d854" stroke="#a6d854" points="43.68,-81.17 40.39,-71.11 36.69,-81.03 43.68,-81.17"/>
</a>
</g>
</g>
<!-- prompt8331185687311588863 -->
<g id="node3" class="node">
<title>prompt8331185687311588863</title>
<g id="a_node3"><a xlink:title=" ">
<g id="a_node3_7"><a xlink:href="#main138835950170271710" xlink:title="defined at app/Main.hs:34:1">
<polygon fill="#e78ac3" stroke="transparent" points="115,-41.5 115,-66.5 182,-66.5 182,-41.5 115,-41.5"/>
<polygon fill="none" stroke="black" points="115,-41.5 115,-66.5 182,-66.5 182,-41.5 115,-41.5"/>
<text text-anchor="start" x="120.5" y="-51.3" font-family="Times,serif" font-size="7.00"></text>
<text text-anchor="start" x="127.5" y="-51.3" font-family="Times,serif" font-size="14.00"> </text>
<text text-anchor="start" x="131.5" y="-51.3" font-family="Times,serif" font-weight="bold" font-size="14.00">prompt</text>
</a>
</g>
<g id="a_node3_8"><a xlink:title="printed at app/Main.hs:37:3">
<polygon fill="none" stroke="black" points="115,-16.5 115,-41.5 182,-41.5 182,-16.5 115,-16.5"/>
<text text-anchor="start" x="120" y="-25.3" font-family="Times,serif" font-size="14.00">input: doe</text>
</a>
</g>
<g id="a_node3_9"><a xlink:title="called at app/Main.hs:38:10">
<polygon fill="#66c2a5" stroke="transparent" points="115,-3.5 115,-16.5 182,-16.5 182,-3.5 115,-3.5"/>
<polygon fill="none" stroke="black" points="115,-3.5 115,-16.5 182,-16.5 182,-3.5 115,-3.5"/>
<text text-anchor="start" x="117" y="-8.1" font-family="Times,serif" font-size="8.00">capitalize</text>
</a>
</g>
</a>
</g>
</g>
<!-- main138835950170271710&#45;&gt;prompt8331185687311588863 -->
<g id="edge2" class="edge">
<title>main138835950170271710:2&#45;&gt;prompt8331185687311588863</title>
<g id="a_edge2"><a xlink:title=" ">
<path fill="none" stroke="#e78ac3" d="M117,-130C139.52,-130 147.07,-105.26 149.1,-81.23"/>
<polygon fill="#e78ac3" stroke="#e78ac3" points="152.61,-81.23 149.66,-71.05 145.62,-80.84 152.61,-81.23"/>
</a>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB