├── README.md ├── demo ├── bar.erl ├── ecg.png ├── foo.erl └── trace.txt ├── draw.py └── tracer.erl /README.md: -------------------------------------------------------------------------------- 1 | # ECG 2 | 3 | ECG is an **E**rlang function **C**all graph **G**enerator, which draws function calls and process spawns automatically. ECG utilizes Erlang's powerful trace mechanism and the visualization tool `graphviz`, make sure you have `graphviz` installed, and since Python binding is used here, you also need `Digraph` package installed. The graph generation flow is as follows: 4 | 5 | ``` 6 | Start tracer -> Start program -> Stop trace -> Analyze trace binary -> Draw graph 7 | ``` 8 | 9 | ## Usage 10 | 11 | For a simple usage, compile `tracer.erl` and add `tracer.beam` to Erlang's code load path together with your program. And specify the modules you want to trace and trace mode: `global` or `local` which means global or local function calls to trace, then start you program, and stop trace at where you want to end analysis, then parse trace binary and finally draw the graph. 12 | 13 | ## Demo 14 | 15 | Suppose we want to draw the graph for modules in `demo` directory, we can do following steps: 16 | 17 | ### Compile 18 | 19 | ``` shell 20 | # Go to demo directory 21 | mkdir ebin 22 | erlc -o ebin *.erl ../tracer.erl 23 | erl -pa ebin 24 | ``` 25 | 26 | ### Trace 27 | 28 | ```Erlang 29 | %% Trace modules foo and bar in local mode 30 | tracer:trace("trace.bin", [foo, bar], local). 31 | foo:start(). 32 | %% Stop trace when you want to 33 | tracer:stop(). 34 | tracer:analyze("trace.bin", "trace.txt"). 35 | ``` 36 | 37 | ### Draw 38 | 39 | ```Python 40 | python ../draw.py trace.txt 41 | ``` 42 | 43 | Finally, we get the graph: 44 | 45 | ![ecg](demo/ecg.png) 46 | 47 | In the graph, we can see that every function call and process spawn is tagged by a sequence number which means the execution order. Each process is sperated by a blue rectangle in which inner function calls are grouped. The red arrows connecting the rectangles mean the process spawn. 48 | 49 | ## License 50 | MIT 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /demo/bar.erl: -------------------------------------------------------------------------------- 1 | -module(bar). 2 | 3 | -export([run/0, 4 | rest/0 5 | ]). 6 | 7 | run() -> 8 | spawn(fun play_music/0), 9 | {10, miles}. 10 | 11 | rest() -> 12 | {have, a, stretch}. 13 | 14 | play_music() -> 15 | {run, like, hell}. -------------------------------------------------------------------------------- /demo/ecg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clanchun/ecg/b29ea01ae70d699463400c5d8412d862bf9f150f/demo/ecg.png -------------------------------------------------------------------------------- /demo/foo.erl: -------------------------------------------------------------------------------- 1 | -module(foo). 2 | 3 | -export([start/0]). 4 | 5 | start() -> 6 | walk(), 7 | bar:run(), 8 | bar:rest(), 9 | {good, day}. 10 | 11 | walk() -> 12 | spawn(fun sing/0), 13 | spawn(fun answer_call/0). 14 | 15 | sing() -> 16 | {the, show, must, go, on}. 17 | 18 | answer_call() -> 19 | spawn(fun sit_down/0), 20 | {"hello Bob", "ok, bye"}. 21 | 22 | sit_down() -> 23 | {on, a, bench}. 24 | -------------------------------------------------------------------------------- /demo/trace.txt: -------------------------------------------------------------------------------- 1 | <0.35.0>;call;foo;start;0;ignore 2 | <0.35.0>;call;foo;walk;0;ignore 3 | <0.35.0>;spawn;<0.39.0>;erlang;apply;2;ignore 4 | <0.35.0>;spawn;<0.40.0>;erlang;apply;2;ignore 5 | <0.35.0>;returned from;foo;walk;0;ignore 6 | <0.35.0>;call;bar;run;0;ignore 7 | <0.35.0>;spawn;<0.41.0>;erlang;apply;2;ignore 8 | <0.35.0>;returned from;bar;run;0;ignore 9 | <0.35.0>;call;bar;rest;0;ignore 10 | <0.35.0>;returned from;bar;rest;0;ignore 11 | <0.35.0>;returned from;foo;start;0;ignore 12 | <0.39.0>;call;foo;'-walk/0-fun-0-';0;spawn 13 | <0.39.0>;call;foo;sing;0;spawn 14 | <0.39.0>;returned from;foo;sing;0;ignore 15 | <0.39.0>;returned from;foo;'-walk/0-fun-0-';0;ignore 16 | <0.40.0>;call;foo;'-walk/0-fun-1-';0;spawn 17 | <0.40.0>;call;foo;answer_call;0;spawn 18 | <0.40.0>;spawn;<0.42.0>;erlang;apply;2;ignore 19 | <0.40.0>;returned from;foo;answer_call;0;ignore 20 | <0.40.0>;returned from;foo;'-walk/0-fun-1-';0;ignore 21 | <0.41.0>;call;bar;'-run/0-fun-0-';0;spawn 22 | <0.41.0>;call;bar;play_music;0;spawn 23 | <0.41.0>;returned from;bar;play_music;0;ignore 24 | <0.41.0>;returned from;bar;'-run/0-fun-0-';0;ignore 25 | <0.42.0>;call;foo;'-answer_call/0-fun-0-';0;spawn 26 | <0.42.0>;call;foo;sit_down;0;spawn 27 | <0.42.0>;returned from;foo;sit_down;0;ignore 28 | <0.42.0>;returned from;foo;'-answer_call/0-fun-0-';0;ignore 29 | <0.27.0>;spawn;<0.43.0>;erlang;apply;2;ignore 30 | -------------------------------------------------------------------------------- /draw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from graphviz import Digraph 4 | import sys 5 | 6 | CALL = 'call' 7 | SPAWN = 'spawn' 8 | RETURN = 'returned from' 9 | 10 | class Node: 11 | def __init__(self, pid=None, tag=None, mod=None, 12 | fun=None, arity=None, msg=None, seq=None): 13 | self.pid = pid 14 | self.tag = tag 15 | self.mod = mod 16 | self.fun = fun 17 | self.arity = arity 18 | self.msg = msg 19 | self.seq = seq 20 | 21 | def toStr(self): 22 | return self.seq + ': ' + self.mod + ':' + self.fun + '/' + self.arity 23 | 24 | def draw(f, dot): 25 | fd = open(f, 'r') 26 | seq = 1 27 | callStack = [] 28 | spawns = {} 29 | subGraphs = [] 30 | line = fd.readline() 31 | 32 | while line: 33 | els = line.strip(' \n').split(';') 34 | if len(els) == 6: 35 | [pid, tag, mod, fun, arity, msg] = els 36 | elif len(els) == 7: 37 | [pid, tag, childPid, mod, fun, arity, msg] = els 38 | 39 | updateGraph(pid, subGraphs) 40 | 41 | if tag == CALL: 42 | sg = findGraph(pid, subGraphs) 43 | callee = Node(pid, tag, mod, fun, arity, msg, str(seq)) 44 | caller = findCaller(callee, callStack) 45 | sg.node(callee.seq, callee.toStr()) 46 | 47 | if caller: 48 | sg.edge(caller.seq, callee.seq) 49 | 50 | if callee.pid in spawns: 51 | dot.edge(spawns[callee.pid][0], 52 | callee.seq, 53 | color = 'red', 54 | lhead ='cluster' + callee.pid, 55 | label = spawns[callee.pid][1]) 56 | del spawns[callee.pid] 57 | 58 | seq += 1 59 | callStack.append(callee) 60 | 61 | elif tag == SPAWN: 62 | callee = Node(pid = pid, tag = tag, seq = str(seq)) 63 | caller = findCaller(callee, callStack) 64 | callStack.append(callee) 65 | 66 | seq += 1 67 | if caller: 68 | spawns[childPid] = [caller.seq, callee.seq] 69 | 70 | elif tag == RETURN: 71 | removeCaller(pid, callStack) 72 | 73 | line = fd.readline() 74 | 75 | for sg in subGraphs: 76 | dot.subgraph(sg) 77 | 78 | dot.render(f + '.ecg', view = True) 79 | 80 | def updateGraph(pid, graphs): 81 | egs = [g for g in graphs if g.name == 'cluster' + pid] 82 | if egs == []: 83 | sg = Digraph('cluster' + pid) 84 | sg.body.append('label = ' + pid) 85 | sg.body.append('color=blue') 86 | sg.node_attr.update(style = 'filled') 87 | graphs.append(sg) 88 | return True 89 | else: 90 | return False 91 | 92 | def findGraph(pid, graphs): 93 | for g in graphs: 94 | if g.name == 'cluster' + pid: 95 | return g 96 | 97 | return None 98 | 99 | def findCaller(callee, stack): 100 | for node in reversed(stack): 101 | if node.pid == callee.pid and node.tag != 'spawn': 102 | return node 103 | 104 | return None 105 | 106 | def removeCaller(pid, stack): 107 | for node in reversed(stack): 108 | if pid == node.pid and node.tag != 'spawn': 109 | stack.remove(node) 110 | return 111 | 112 | return 113 | 114 | def main(): 115 | dot = Digraph("ecg") 116 | dot.body.append('compound=true') 117 | draw(sys.argv[1], dot) 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /tracer.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author clanchun 3 | %%% @copyright (C) 2016, clanchun 4 | %%% @doc 5 | %%% 6 | %%% @end 7 | %%% Created : 18 Oct 2016 by clanchun 8 | %%%------------------------------------------------------------------- 9 | -module(tracer). 10 | 11 | %% API 12 | -export([trace/2, 13 | trace/3, 14 | analyze/2, 15 | stop/0 16 | ]). 17 | 18 | -export([parse/2]). 19 | 20 | %%%=================================================================== 21 | %%% API 22 | %%%=================================================================== 23 | 24 | trace(File, Modules) -> 25 | trace(File, Modules, global). 26 | 27 | trace(File, Modules, Mode) -> 28 | {ok, Tracer} = dbg:tracer(port, dbg:trace_port(file, File)), 29 | dbg:p(all, [c, p]), 30 | case Mode of 31 | global -> 32 | [dbg:tp(Mod, cx) || Mod <- Modules]; 33 | local -> 34 | [dbg:tpl(Mod, cx) || Mod <- Modules] 35 | end, 36 | {ok, Tracer}. 37 | 38 | analyze(InFile, OutFile) -> 39 | dbg:flush_trace_port(), 40 | {ok, Fd} = file:open(OutFile, [write]), 41 | dbg:trace_client(file, InFile, {fun parse/2, Fd}), 42 | ok. 43 | 44 | stop() -> 45 | dbg:stop(). 46 | 47 | %%-------------------------------------------------------------------- 48 | %% @doc 49 | %% @spec 50 | %% @end 51 | %%-------------------------------------------------------------------- 52 | 53 | %%%=================================================================== 54 | %%% Internal functions 55 | %%%=================================================================== 56 | 57 | parse(end_of_trace, Out) -> 58 | Out; 59 | parse(Trace, Out) when element(1, Trace) == trace, tuple_size(Trace) >= 3 -> 60 | parse2(Trace, tuple_size(Trace), Out); 61 | parse(_Trace, Out) -> 62 | Out. 63 | 64 | parse2(Trace, Size, Out) -> 65 | From = element(2, Trace), 66 | case element(3, Trace) of 67 | call -> 68 | case element(4, Trace) of 69 | MFA when Size == 5 -> 70 | Message = element(5, Trace), 71 | io:format(Out, "~p;call;~s;~s~n", 72 | [From, mfa(MFA), message(Message)]); 73 | MFA -> 74 | io:format(Out, "~p;call;~s;~s~n", 75 | [From, mfa(MFA), message(nil)]) 76 | end; 77 | return_from -> 78 | MFA = element(4, Trace), 79 | io:format(Out, "~p;returned from;~s;~s~n", 80 | [From, mfa(MFA), message(nil)]); 81 | spawn when Size == 5 -> 82 | Pid = element(4, Trace), 83 | MFA = element(5, Trace), 84 | io:format(Out, "~p;spawn;~p;~s;~s~n", 85 | [From, Pid, mfa(MFA), message(nil)]); 86 | _ -> 87 | ignore 88 | end, 89 | Out. 90 | 91 | mfa({M,F,Argl}) when is_list(Argl) -> 92 | io_lib:format("~p;~p;~p", [M, F, length(Argl)]); 93 | mfa({M,F,Arity}) -> 94 | io_lib:format("~p;~p;~p", [M,F,Arity]); 95 | mfa(X) -> io_lib:format("~p", [X]). 96 | 97 | message(undefined) -> 98 | io_lib:format("spawn", []); 99 | message({proc_lib, init_p_do_apply, 3}) -> 100 | io_lib:format("proc_lib", []); 101 | message(_) -> 102 | io_lib:format("ignore", []). 103 | --------------------------------------------------------------------------------