├── .gitignore
├── doc
├── sshot1.png
├── sshot2.png
├── sshot3.png
├── sshot4.png
└── sshot6.png
├── rebar.config
├── src
├── eep.app.src
├── eep_app.erl
└── eep.erl
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | rebar
2 | src/load.erl
3 | old/
4 | ebin/
5 |
--------------------------------------------------------------------------------
/doc/sshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virtan/eep/HEAD/doc/sshot1.png
--------------------------------------------------------------------------------
/doc/sshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virtan/eep/HEAD/doc/sshot2.png
--------------------------------------------------------------------------------
/doc/sshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virtan/eep/HEAD/doc/sshot3.png
--------------------------------------------------------------------------------
/doc/sshot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virtan/eep/HEAD/doc/sshot4.png
--------------------------------------------------------------------------------
/doc/sshot6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virtan/eep/HEAD/doc/sshot6.png
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %% -*- erlang -*-
2 | {erl_opts, [debug_info]}.
3 | {deps, [
4 | ]}.
5 | {cover_enabled, true}.
6 |
--------------------------------------------------------------------------------
/src/eep.app.src:
--------------------------------------------------------------------------------
1 | {application, eep,
2 | [
3 | {description, "Erlang Easy Profiling (dbg:trace* to kcachegrind)"},
4 | {vsn, "1.0"},
5 | {registered, []},
6 | {modules, []},
7 | {applications, [
8 | kernel,
9 | stdlib
10 | ]},
11 | {mod, { eep_app, []}},
12 | {env, []}
13 | ]}.
14 |
--------------------------------------------------------------------------------
/src/eep_app.erl:
--------------------------------------------------------------------------------
1 | -module(eep_app).
2 |
3 | -behaviour(application).
4 |
5 | %% Application callbacks
6 | -export([start/2, stop/1]).
7 |
8 | %% ===================================================================
9 | %% Application callbacks
10 | %% ===================================================================
11 |
12 | start(_StartType, _StartArgs) ->
13 | {ok, self()}.
14 |
15 | stop(_State) ->
16 | ok.
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Erlang Easy Profiling (eep)
2 | ===========================
3 |
4 | Erlang Easy Profiling (eep) application provides a way to analyze application performance and call hierarchy.
5 |
6 | Main features:
7 | * no need to modify sources (doesn't need sources at all)
8 | * no need to stop your running system
9 | * start and stop collecting runtime data at arbitrary time
10 | * profile arbitrary module or whole system
11 | * minimal impact on profiled system performance (unlike [fprof] [6])
12 | * very informative visualization of time costs and call stacks ([kcachegrind] [7])
13 | * ability to export call graphs in dot or image format
14 | * optional process separation
15 | * based on [dbg] [8] module and built-in low overhead [trace ports] [9]
16 | * optionally route runtime data over network to minimize disk load
17 |
18 | Limitations:
19 | * doesn't work with natively compiled code
20 | * doesn't support parent-child links (will appear in future versions)
21 |
22 | [6]: http://www.erlang.org/doc/man/fprof.html
23 | [7]: http://kcachegrind.sourceforge.net/
24 | [8]: http://www.erlang.org/doc/man/dbg.html
25 | [9]: http://www.erlang.org/doc/man/dbg.html#trace_port-2
26 |
27 | How to
28 | ------
29 |
30 | On target system:
31 |
32 | 1. Make sure the target system can use eep module (link eep to your rebar project or place compiled eep.beam at any code path)
33 | 2. Collect runtime data to local file
34 |
35 | 1> eep:start_file_tracing("file_name"), timer:sleep(10000), eep:stop_tracing().
36 |
37 | 3. Copy $PWD/file_name.trace from the target system
38 |
39 | Outside the target system:
40 |
41 | 1. Make sure collected runtime data is in current directory ($PWD/file_name.trace)
42 | 2. Convert to callgrind format
43 |
44 | 1> eep:convert_tracing("file_name").
45 |
46 | 3. Start kcachegrind
47 |
48 | $ kcachegrind callgrind.out.file_name
49 |
50 |
51 | Also
52 | ----
53 |
54 | 1. Collect specific module calls only
55 |
56 | 1> eep:start_file_tracing("file_name", [], [my_module_1, my_module_2]).
57 |
58 | 2. Include time spent waiting for event (not running)
59 |
60 | 1> eep:convert_tracing("file_name", [waits]).
61 |
62 | 3. Dump collected runtime data
63 |
64 | 1> eep:dump_tracing("file_name").
65 |
66 | 4. Remove separation by erlang process
67 |
68 | $ grep -v "^ob=" callgrind.out.file_name > callgrind.out.merged_file_name
69 |
70 | 5. Route runtime data to other host, then process trace on that host
71 |
72 | (eep@otherhost) 1> eep:start_net_client("targethost", 1088, "file_name", wait).
73 | (eep@targethost) 1> eep:start_net_tracing(1088).
74 |
75 |
76 | Useful
77 | ------
78 |
79 | * Turn off kcachegrind "cycle detection", eep detects cycles by itself
80 | * Absolute numbers in kcachegrind are microseconds
81 | * ELF Objects in kcachegrind are erlang pids
82 | * By default kcachegrind limits caller depth and node cost (can be changed in call graph context menu in Graph submenu)
83 | * Tail recursion loop within group of functions has incorrect calls and time cost values
84 |
85 | Screenshots
86 | -----------
87 |
88 | * [Overall view] [1]
89 | * [Call hierarchy] [2]
90 | * [Functions navigator] [3]
91 | * [Callees ordered by cost] [4]
92 | * [Relative costs view] [5]
93 |
94 | [1]: https://raw.github.com/virtan/eep/master/doc/sshot1.png
95 | [2]: https://raw.github.com/virtan/eep/master/doc/sshot2.png
96 | [3]: https://raw.github.com/virtan/eep/master/doc/sshot3.png
97 | [4]: https://raw.github.com/virtan/eep/master/doc/sshot4.png
98 | [5]: https://raw.github.com/virtan/eep/master/doc/sshot6.png
99 |
100 | Author
101 | ------
102 |
103 | Igor Milyakov
104 | [virtan@virtan.com] [10]
105 |
106 | [10]: mailto:virtan@virtan.com?subject=Eep
107 |
108 | License
109 | -------
110 |
111 | The MIT License (MIT)
112 |
113 | Copyright (c) 2013 Igor Milyakov virtan@virtan.com
114 |
115 | Permission is hereby granted, free of charge, to any person obtaining a copy
116 | of this software and associated documentation files (the "Software"), to deal
117 | in the Software without restriction, including without limitation the rights
118 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
119 | copies of the Software, and to permit persons to whom the Software is
120 | furnished to do so, subject to the following conditions:
121 |
122 | The above copyright notice and this permission notice shall be included in
123 | all copies or substantial portions of the Software.
124 |
125 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
126 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
127 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
128 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
129 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
130 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
131 | THE SOFTWARE.
132 |
--------------------------------------------------------------------------------
/src/eep.erl:
--------------------------------------------------------------------------------
1 | -module(eep).
2 | -export([
3 | start_file_tracing/1,
4 | start_file_tracing/2,
5 | start_file_tracing/3,
6 | start_net_tracing/1,
7 | start_net_tracing/2,
8 | start_net_tracing/3,
9 | start_net_client/3,
10 | start_net_client/4,
11 | add_pid_to_tracing/1,
12 | add_pid_to_tracing/2,
13 | stop_tracing/0,
14 | convert_tracing/1,
15 | convert_tracing/2,
16 | dump_tracing/1,
17 | callgrind_convertor/2,
18 | convertor_child/1,
19 | save_kcachegrind_format/1,
20 | test_unwind/0
21 | ]).
22 |
23 | start_file_tracing(FileName) ->
24 | start_file_tracing(FileName, [], ['_']).
25 |
26 | start_file_tracing(FileName, Options) ->
27 | start_file_tracing(FileName, Options, ['_']).
28 |
29 | % Options:
30 | % spawn — include link between parent and child processes (experimental and doesn't look well)
31 | % Note:
32 | % specifying modules doesn't work as expected:
33 | % return_to and scheduler in/out contain other modules too
34 | % TODO: analyze later, may be apply filtering during conversion
35 | start_file_tracing(FileName, Options, Modules) ->
36 | TraceFun = dbg:trace_port(file, tracefile(FileName)),
37 | start_tracing(TraceFun, Options, Modules).
38 |
39 | start_tracing(TraceFun, Options, Modules) ->
40 | {ok, _Tracer} = dbg:tracer(port, TraceFun),
41 | [dbg:tpl(Module, []) || Module <- Modules],
42 | case proplists:get_value(coverage, Options, all) of
43 | none -> dont_do_p;
44 | Coverage ->
45 | dbg:p(Coverage, [call, timestamp, return_to, arity, running] ++
46 | proplists:substitute_aliases([{spawn, procs}],
47 | proplists:delete(coverage, Options)))
48 | end.
49 |
50 | add_pid_to_tracing(Pid) -> add_pid_to_tracing(Pid, []).
51 |
52 | add_pid_to_tracing(Pid, Options) ->
53 | dbg:p(Pid, [call, timestamp, return_to, arity, running] ++
54 | proplists:substitute_aliases([{spawn, procs}],
55 | proplists:delete(coverage, Options))).
56 |
57 | start_net_tracing(Port) ->
58 | start_net_tracing(Port, [], ['_']).
59 |
60 | start_net_tracing(Port, Options) ->
61 | start_net_tracing(Port, Options, ['_']).
62 |
63 | start_net_tracing(Port, Options, Modules) ->
64 | TraceFun = dbg:trace_port(ip, Port),
65 | start_tracing(TraceFun, Options, Modules).
66 |
67 | start_net_client(IP, Port, FileName) ->
68 | start_net_client(IP, Port, FileName, dont_wait).
69 |
70 | start_net_client(Host, Port, FileName, Wait) ->
71 | ConnectingF = fun (timer) -> 1000;
72 | (NextF1) ->
73 | case gen_tcp:connect(Host, Port, [binary, {packet, 0}, {active, false}]) of
74 | {ok, Sock1} ->
75 | io:format("Connected to ~s:~p, gathering data...~n", [Host, Port]),
76 | Sock1;
77 | {error, Reason} ->
78 | io:format("Can't connect to ~s:~p: ~p~n", [Host, Port, Reason]),
79 | timer:sleep(NextF1(timer)),
80 | NextF1(NextF1)
81 | end
82 | end,
83 | case ConnectingF(case Wait of dont_wait -> fun (timer) -> 0; (_) -> unsuccessful end; wait -> ConnectingF end) of
84 | unsuccessful -> do_nothing;
85 | Sock ->
86 | {ok, IOD} = file:open(tracefile(FileName), [write, binary, delayed_write]),
87 | ReadingF = fun(NextF) ->
88 | case gen_tcp:recv(Sock, 0) of
89 | {ok, Data} ->
90 | case file:write(IOD, Data) of
91 | ok -> NextF(NextF);
92 | {error, Reason} -> Reason
93 | end;
94 | {error, Reason} -> Reason
95 | end
96 | end,
97 | ErrorReason = ReadingF(ReadingF),
98 | file:close(IOD),
99 | gen_tcp:close(Sock),
100 | io:format("Net client stopped: ~p~n", [ErrorReason])
101 | end,
102 | done.
103 |
104 | stop_tracing() ->
105 | dbg:stop_clear().
106 |
107 | convert_tracing(FileName) ->
108 | convert_tracing(FileName, []).
109 |
110 | % Options:
111 | % waits — include time spent waiting for event (by default don't include)
112 | convert_tracing(FileName, Options) ->
113 | do_something_with_tracing({FileName, Options}, fun callgrind_convertor/2).
114 |
115 | dump_tracing(FileName) ->
116 | do_something_with_tracing({FileName, []}, fun dbg_format_dumper/2).
117 |
118 | do_something_with_tracing({FileName, Options}, F) ->
119 | case file:read_file_info(tracefile(FileName)) of
120 | {ok, _} ->
121 | dbg:trace_client(file, tracefile(FileName), {F, F(default_state, {FileName, Options})}),
122 | working;
123 | {error, Reason} ->
124 | io:format("Error: can't open ~p: ~p~n", [tracefile(FileName), Reason])
125 | end.
126 |
127 | tracefile(FileName) ->
128 | FileName ++ ".trace".
129 |
130 | kcgfile(FileName) ->
131 | "callgrind.out." ++ FileName.
132 |
133 | dumpfile(FileName) ->
134 | FileName ++ ".dump".
135 |
136 | -record(cvn_state, {processes, saver, options}).
137 | -record(cvn_child_state, {pid, delta = 0, delta_ts = undefined, min_time = undefined, max_time = undefined, saver,
138 | stack = queue:new(), options}).
139 | -record(cvn_item, {mfa, self = 0, ts, calls = 1, returns = 0, subcalls = orddict:new()}).
140 |
141 | dbg_format_dumper(Msg, {IOD, Buffer, ReportedTime, S}) when element(1, Msg) == trace_ts ->
142 | Bytes = iolist_to_binary(io_lib:format("~100000p~n", [Msg])),
143 | NewBuffer = case size(Buffer) of
144 | N when N < 20*1024*1024 -> <>;
145 | _ -> file:write(IOD, <>), <<>>
146 | end,
147 | NewStamp = case td(ReportedTime, os:timestamp()) of
148 | N1 when N1 > 1000000 -> io:format("~b processed~n", [S]), os:timestamp();
149 | _ -> ReportedTime
150 | end,
151 | {IOD, NewBuffer, NewStamp, S + 1};
152 | dbg_format_dumper({drop, _}, State) ->
153 | % ignore dropped
154 | State;
155 | dbg_format_dumper(default_state, {FileName, _}) ->
156 | {ok, IOD} = file:open(dumpfile(FileName), [write, binary, delayed_write]),
157 | {IOD, <<>>, os:timestamp(), 0};
158 | dbg_format_dumper(end_of_trace, {IOD, Buffer, _, S}) ->
159 | file:write(IOD, Buffer),
160 | file:close(IOD),
161 | io:format("~b processed~ndone~n", [S]).
162 |
163 | callgrind_convertor(Msg, #cvn_state{processes = Processes, saver = Saver, options = Options} = State)
164 | when element(1, Msg) == trace_ts ->
165 | Pid = element(2, Msg),
166 | case ets:lookup(Processes, Pid) of
167 | [] ->
168 | Child = spawn(?MODULE, convertor_child, [#cvn_child_state{pid = Pid, saver = Saver, options = Options}]),
169 | Child ! Msg,
170 | ets:insert(Processes, {Pid, Child}),
171 | State;
172 | [{Pid, Child}] ->
173 | Child ! Msg,
174 | State
175 | end;
176 | callgrind_convertor({drop, _}, State) ->
177 | % ignore dropped
178 | State;
179 | callgrind_convertor(default_state, {FileName, Options}) ->
180 | Processes = ets:new(unnamed, [public, {write_concurrency, true}, {read_concurrency, true}]),
181 | Saver = case FileName of
182 | nofile -> self();
183 | _ ->
184 | Pid = spawn_link(?MODULE, save_kcachegrind_format, [FileName]),
185 | ets:give_away(Processes, Pid, none),
186 | Pid
187 | end,
188 | DefaultState = #cvn_state{saver = Saver, options = Options, processes = Processes},
189 | Saver ! {process_table, Processes},
190 | DefaultState;
191 | callgrind_convertor(end_of_trace, #cvn_state{processes = Processes}) ->
192 | ets:foldl(fun({_, Child}, _) -> Child ! finalize end, nothing, Processes),
193 | end_of_cycle;
194 | callgrind_convertor(UnknownMessage, #cvn_state{}) ->
195 | io:format("Unknown message: ~p~n", [UnknownMessage]).
196 |
197 | convertor_child(#cvn_child_state{pid = Pid, delta = Delta, delta_ts = DeltaTS, min_time = MinTime, max_time = MaxTime,
198 | saver = Saver, stack = Stack, options = Options} = State) ->
199 | receive
200 | {trace_ts, Pid, call, MFA, TS} ->
201 | NewStack = case queue:out_r(Stack) of
202 | {empty, Dropped} ->
203 | queue:in(#cvn_item{mfa = MFA, ts = ts(TS) - Delta}, Dropped);
204 | {{value, Last}, Dropped} ->
205 | case Last of
206 | #cvn_item{mfa = MFA, calls = Calls} ->
207 | queue:in(Last#cvn_item{calls = Calls + 1}, Dropped);
208 | #cvn_item{self = Self, ts = PTS} ->
209 | queue:in(#cvn_item{mfa = MFA, ts = ts(TS) - Delta},
210 | queue:in(Last#cvn_item{self = Self + td(PTS, ts(TS) - Delta), ts = ts(TS) - Delta},
211 | Dropped))
212 | end
213 | end,
214 | convertor_child(State#cvn_child_state{
215 | stack = NewStack,
216 | min_time = min_ts(MinTime, TS),
217 | max_time = max_ts(MaxTime, TS)});
218 | {trace_ts, Pid, return_to, MFA, TS} ->
219 | NewStack = convertor_unwind(MFA, ts(TS) - Delta, nosub, queue:out_r(Stack), {Pid, Saver}),
220 | convertor_child(State#cvn_child_state{
221 | stack = NewStack,
222 | min_time = min_ts(MinTime, TS),
223 | max_time = max_ts(MaxTime, TS)});
224 | {trace_ts, Pid, out, _, TS} ->
225 | convertor_child(State#cvn_child_state{
226 | delta_ts = ts(TS),
227 | min_time = min_ts(MinTime, TS),
228 | max_time = max_ts(MaxTime, TS)});
229 | {trace_ts, Pid, in, _, TS} ->
230 | NewDelta = Delta + case {proplists:get_value(waits, Options), DeltaTS} of
231 | {_, undefined} -> 0;
232 | {true, _} -> 0;
233 | _ -> td(DeltaTS, TS)
234 | end,
235 | convertor_child(State#cvn_child_state{
236 | delta = NewDelta,
237 | min_time = min_ts(MinTime, TS),
238 | max_time = max_ts(MaxTime, TS)});
239 | {trace_ts, Pid, spawn, _Pid2, {M, F, Args}, TS} ->
240 | Arity = length(Args),
241 | NewStack = case queue:out_r(Stack) of
242 | {empty, Dropped} ->
243 | queue:in(#cvn_item{mfa = {nonexistent, nonexistent, 999}, ts = ts(TS) - Delta,
244 | subcalls = subcall_update({{M, F, Arity}, 1}, orddict:new(), 0)}, Dropped);
245 | {{value, #cvn_item{subcalls = SubCalls} = Last}, Dropped} ->
246 | queue:in(Last#cvn_item{subcalls = subcall_update({{M, F, Arity}, 1}, SubCalls, 0)}, Dropped)
247 | end,
248 | convertor_child(State#cvn_child_state{
249 | stack = NewStack,
250 | min_time = min_ts(MinTime, TS),
251 | max_time = max_ts(MaxTime, TS)});
252 | {trace_ts, Pid, Activity, _, TS} when Activity =:= exit orelse Activity =:= register orelse Activity =:= unregister
253 | orelse Activity =:= link orelse Activity =:= unlink
254 | orelse Activity =:= getting_linked orelse Activity =:= getting_unlinked ->
255 | convertor_child(State#cvn_child_state{
256 | min_time = min_ts(MinTime, TS),
257 | max_time = max_ts(MaxTime, TS)});
258 | finalize ->
259 | convertor_unwind({nonexistent, nonexistent, 999}, MaxTime - Delta, nosub, queue:out_r(Stack), {Pid, Saver}),
260 | Saver ! {finalize, Pid, MinTime, MaxTime - Delta},
261 | get_out
262 | end.
263 |
264 | convertor_unwind(MFA, _TS, nosub, {{value, #cvn_item{mfa = MFA, calls = Calls, returns = Returns} = Last},
265 | Dropped}, _) when Calls > Returns + 1 ->
266 | queue:in(Last#cvn_item{returns = Returns + 1}, Dropped);
267 | convertor_unwind(MFA, TS, nosub, {{value, #cvn_item{mfa = MFA, ts = TS} = Last}, Dropped}, _) ->
268 | % unexpected frame
269 | queue:in(Last#cvn_item{ts = ts(TS)}, Dropped);
270 | convertor_unwind(MFA, TS, nosub, {{value, #cvn_item{mfa = CMFA, self = Self, calls = CCalls, ts = CTS} = Last},
271 | Dropped}, {Pid, Saver}) ->
272 | TD = Self + td(CTS, TS),
273 | Saver ! {bytes, pid_item_to_bytes({Pid, Last#cvn_item{self = TD div CCalls}}), TS},
274 | convertor_unwind(MFA, TS, {CMFA, CCalls}, queue:out_r(Dropped), {Pid, Saver});
275 | convertor_unwind(MFA, TS, Sub, {{value, #cvn_item{mfa = MFA, ts = CTS, subcalls = SubCalls} = Last}, Dropped}, _) ->
276 | queue:in(Last#cvn_item{ts = ts(TS), subcalls = subcall_update(Sub, SubCalls, td(CTS, TS))}, Dropped);
277 | convertor_unwind(MFA, TS, Sub, {{value, #cvn_item{mfa = CMFA, calls = CCalls, ts = CTS,
278 | subcalls = CSubCalls} = Last}, Dropped}, {Pid, Saver}) ->
279 | Saver ! {bytes, pid_item_to_bytes({Pid, Last#cvn_item{subcalls = subcall_update(Sub, CSubCalls, td(CTS, TS))}}), TS},
280 | convertor_unwind(MFA, TS, {CMFA, CCalls}, queue:out_r(Dropped), {Pid, Saver});
281 | convertor_unwind(MFA, TS, nosub, {empty, EmptyQueue}, _) ->
282 | % recreating top level
283 | queue:in(#cvn_item{mfa = MFA, ts = ts(TS)}, EmptyQueue);
284 | convertor_unwind(MFA, TS, Sub, {empty, EmptyQueue}, _) ->
285 | % recreating top level
286 | queue:in(#cvn_item{mfa = MFA, ts = ts(TS), subcalls = subcall_update(Sub, orddict:new(), 1)}, EmptyQueue);
287 | convertor_unwind(A1, A2, A3, A4, A5) ->
288 | io:format("Shouldn't happen ~p ~p ~p ~p ~p~n", [A1, A2, A3, A4, A5]).
289 |
290 | subcall_update({SMFA, SCalls}, SubCalls, SpentInSub) ->
291 | orddict:update(SMFA, fun({SCalls2, STD2}) -> {SCalls2 + SCalls, STD2 + SpentInSub} end, {SCalls, SpentInSub}, SubCalls).
292 |
293 | save_kcachegrind_format(FileName) ->
294 | erlang:process_flag(priority, high),
295 | RealFileName = kcgfile(FileName),
296 | TempFileName = case os:type() of
297 | {win32, _} -> RealFileName ++ ".tmp";
298 | _ -> RealFileName
299 | end,
300 | case file:open(TempFileName, [read, write, binary, delayed_write, read_ahead]) of
301 | {ok, IOD} ->
302 | file:delete(TempFileName),
303 | {ok, Timer} = timer:send_interval(1000, status),
304 | {GTD} = save_receive_cycle(IOD, 1, ts(os:timestamp()), 0, ts(os:timestamp()), <<>>, 0, undefined),
305 | timer:cancel(Timer),
306 | {ok, IOD2} = file:open(RealFileName, [write, binary, delayed_write]),
307 | save_header(IOD2, GTD),
308 | file:position(IOD, {bof, 0}),
309 | save_copy(IOD, IOD2),
310 | file:close(IOD),
311 | file:close(IOD2),
312 | io:format("done~n", []);
313 | {error, Reason} ->
314 | io:format("Error: can't create file ~p: ~p~n", [RealFileName, Reason]),
315 | error(problem)
316 | end.
317 |
318 | save_receive_cycle(IOD, P, MinTime, MaxTime, StartTime, Buffer, Stuck, ProcessTable) ->
319 | receive
320 | status when Stuck >= 2 ->
321 | working_stat(P, MinTime, max(MinTime, MaxTime), StartTime),
322 | io:format("No end_of_trace and no data, finishing forcibly (~b processes)~n", [ets:info(ProcessTable, size)]),
323 | ets:foldl(fun({_, Child}, _) -> Child ! finalize end, nothing, ProcessTable),
324 | save_receive_cycle(IOD, P, MinTime, MaxTime, StartTime, Buffer, Stuck + 1, ProcessTable);
325 | status ->
326 | working_stat(P, MinTime, max(MinTime, MaxTime), StartTime),
327 | save_receive_cycle(IOD, P, MinTime, MaxTime, StartTime, Buffer, Stuck + 1, ProcessTable);
328 | {bytes, Bytes, TS} ->
329 | NewBuffer = case size(Buffer) of
330 | N when N < 20*1024*1024 -> <>;
331 | _ -> file:write(IOD, <>), <<>>
332 | end,
333 | save_receive_cycle(IOD, P + 1, min_ts(MinTime, TS), max_ts(MaxTime, TS), StartTime, NewBuffer, 0, ProcessTable);
334 | {process_table, NewProcessTable} ->
335 | save_receive_cycle(IOD, P, MinTime, MaxTime, StartTime, Buffer, Stuck, NewProcessTable);
336 | {finalize, Pid, MinTime1, MaxTime1} ->
337 | ets:delete(ProcessTable, Pid),
338 | Minimum = min(MinTime, MinTime1),
339 | Maximum = max(MaxTime, MaxTime1),
340 | case ets:info(ProcessTable, size) of
341 | 0 ->
342 | file:write(IOD, Buffer),
343 | working_stat(P, Minimum, Maximum, StartTime),
344 | ets:delete(ProcessTable),
345 | {td(Minimum, Maximum)};
346 | _ ->
347 | save_receive_cycle(IOD, P, Minimum, Maximum, StartTime, Buffer, Stuck, ProcessTable)
348 | end;
349 | _ ->
350 | save_receive_cycle(IOD, P, MinTime, MaxTime, StartTime, Buffer, Stuck, ProcessTable)
351 | end.
352 |
353 | pid_item_to_bytes({Pid, #cvn_item{mfa = undefined} = Item}) ->
354 | pid_item_to_bytes({Pid, Item#cvn_item{mfa = {undefined, undefined, 0}}});
355 | pid_item_to_bytes({Pid, #cvn_item{mfa = {M, F, A}, self = Self, subcalls = SubCalls}}) ->
356 | Block1 = io_lib:format("ob=~s~n"
357 | "fl=~w~n"
358 | "fn=~w:~w/~b~n"
359 | "1 ~b~n",
360 | [pid_to_list(Pid), M, M, F, A, Self]),
361 | Block3 = orddict:fold(fun(CMFA, {CCalls, Cumulative}, Acc) ->
362 | {CM, CF, CA} = case CMFA of
363 | undefined -> {undefined, undefined, 0};
364 | Defined -> Defined
365 | end,
366 | Block2 = io_lib:format("cfl=~w~n"
367 | "cfn=~w:~w/~b~n"
368 | "calls=~b 1~n"
369 | "1 ~b~n",
370 | [CM, CM, CF, CA, CCalls, Cumulative]),
371 | [Block2 | Acc]
372 | end, [], SubCalls),
373 | iolist_to_binary([Block1, lists:reverse(Block3), $\n]).
374 |
375 | working_stat(Msgs, MinTime, MaxTime, StartTime) ->
376 | io:format("~b msgs (~b msgs/sec), ~f secs (~bx slowdown)~n",
377 | [Msgs, round(Msgs / (td(StartTime, os:timestamp()) / 1000000)),
378 | td(MinTime, MaxTime) / 1000000, round(td(StartTime, os:timestamp()) / max(td(MinTime, MaxTime), 1))]).
379 |
380 | save_header(IOD, GTD) ->
381 | Block4 = io_lib:format("events: Time~n"
382 | "creator: Erlang Easy Profiling https://github.com/virtan/eep~n"
383 | "summary: ~b~n~n",
384 | [GTD]),
385 | file:write(IOD, iolist_to_binary(Block4)).
386 |
387 | save_copy(From, To) ->
388 | case file:read(From, 64*1024) of
389 | {ok, Data} ->
390 | case file:write(To, Data) of
391 | ok -> save_copy(From, To);
392 | {error, Reason} ->
393 | io:format("Error: can't save results: ~p~n", [Reason]),
394 | error(problem)
395 | end;
396 | eof -> done;
397 | {error, Reason} ->
398 | io:format("Error: can't save results: ~p~n", [Reason]),
399 | error(problem)
400 | end.
401 |
402 | ts({Mega, Secs, Micro}) -> (Mega * 1000000000000) + (Secs * 1000000) + Micro;
403 | ts(Number) -> Number.
404 |
405 | td(From, To) -> ts(To) - ts(From).
406 |
407 | min_ts(undefined, undefined) -> undefined;
408 | min_ts(undefined, Two) -> ts(Two);
409 | min_ts(One, undefined) -> ts(One);
410 | min_ts(One, Two) ->
411 | case {ts(One), ts(Two)} of
412 | {X, Y} when X < Y -> X;
413 | {_, Y} -> Y
414 | end.
415 |
416 | max_ts(undefined, undefined) -> undefined;
417 | max_ts(undefined, Two) -> ts(Two);
418 | max_ts(One, undefined) -> ts(One);
419 | max_ts(One, Two) ->
420 | case {ts(One), ts(Two)} of
421 | {X, Y} when X > Y -> X;
422 | {_, Y} -> Y
423 | end.
424 |
425 | receive_all(Prev) ->
426 | receive
427 | {process_table, _} -> receive_all(Prev);
428 | M -> receive_all([M | Prev])
429 | after 500 -> lists:reverse(Prev)
430 | end.
431 |
432 | test_unwind() ->
433 | test_unwind_1(),
434 | test_unwind_2(),
435 | test_unwind_3(),
436 | ok.
437 |
438 | test_unwind_1() ->
439 | Pid = list_to_pid("<0.1.0>"),
440 | TestSet = [
441 | {trace_ts, Pid, call, {a,b,1}, 1},
442 | {trace_ts, Pid, call, {a,b,1}, 3},
443 | {trace_ts, Pid, call, {a,b,1}, 7},
444 | {trace_ts, Pid, return_to, {x,b,1}, 10}
445 | ],
446 | lists:foldl(fun(El, St) -> callgrind_convertor(El, St) end, callgrind_convertor(default_state, {nofile, []}), TestSet),
447 | [{bytes,<<"ob=<0.1.0>\nfl=a\nfn=a:b/1\n1 3\n\n">>,10}] = receive_all([]).
448 |
449 | test_unwind_2() ->
450 | Pid = list_to_pid("<0.1.0>"),
451 | TestSet = [
452 | {trace_ts, Pid, call, {a,b,1}, 1},
453 | {trace_ts, Pid, call, {a,b,2}, 3},
454 | {trace_ts, Pid, call, {a,b,3}, 7},
455 | {trace_ts, Pid, return_to, {x,b,1}, 10}
456 | ],
457 | lists:foldl(fun(El, St) -> callgrind_convertor(El, St) end, callgrind_convertor(default_state, {nofile, []}), TestSet),
458 | [{bytes,<<"ob=<0.1.0>\nfl=a\nfn=a:b/3\n1 3\n\n">>,10},
459 | {bytes,<<"ob=<0.1.0>\nfl=a\nfn=a:b/2\n1 4\ncfl=a\ncfn=a:b/3\ncalls=1 1\n1 3\n\n">>, 10},
460 | {bytes,<<"ob=<0.1.0>\nfl=a\nfn=a:b/1\n1 2\ncfl=a\ncfn=a:b/2\ncalls=1 1\n1 7\n\n">>, 10}] = receive_all([]).
461 |
462 | test_unwind_3() ->
463 | Pid = list_to_pid("<0.1.0>"),
464 | TestSet = [
465 | {trace_ts,Pid,call,{prim_file,write,2},{1384,248547,512336}},
466 | {trace_ts,Pid,call,{prim_file,drv_command_nt,3},{1384,248547,512342}},
467 | {trace_ts,Pid,call,{erlang,port_command,2},{1384,248547,512344}},
468 | {trace_ts,Pid,return_to,{prim_file,drv_command_nt,3},{1384,248547,512347}},
469 | {trace_ts,Pid,call,{prim_file,drv_get_response,2},{1384,248547,512354}},
470 | {trace_ts,Pid,call,{prim_file,drv_get_response,1},{1384,248547,512356}},
471 | {trace_ts,Pid,call,{erlang,bump_reductions,1},{1384,248547,512357}},
472 | {trace_ts,Pid,return_to,{prim_file,drv_get_response,1},{1384,248547,512366}},
473 | {trace_ts,Pid,call,{prim_file,translate_response,2},{1384,248547,512368}},
474 | {trace_ts,Pid,call,{prim_file,get_uint64,1},{1384,248547,512375}},
475 | {trace_ts,Pid,call,{prim_file,get_uint32,1},{1384,248547,512380}},
476 | {trace_ts,Pid,return_to,{prim_file,get_uint64,1},{1384,248547,512382}},
477 | {trace_ts,Pid,call,{prim_file,get_uint32,1},{1384,248547,512383}},
478 | {trace_ts,Pid,return_to,{prim_file,get_uint64,1},{1384,248547,512385}},
479 | {trace_ts,Pid,return_to,{prim_file,translate_response,2},{1384,248547,512386}},
480 | {trace_ts,Pid,return_to,{prim_file,drv_get_response,1},{1384,248547,512392}},
481 | {trace_ts,Pid,return_to,{prim_file,drv_command_nt,3},{1384,248547,512397}},
482 | {trace_ts,Pid,return_to,{prim_file,write,2},{1384,248547,512404}}
483 | ],
484 | lists:foldl(fun(El, St) -> callgrind_convertor(El, St) end, callgrind_convertor(default_state, {nofile, []}), TestSet),
485 | [{bytes,<<"ob=<0.1.0>\nfl=erlang\nfn=erlang:port_command/2\n1 3\n\n">>,1384248547512347},
486 | {bytes,<<"ob=<0.1.0>\nfl=erlang\nfn=erlang:bump_reductions/1\n1 9\n\n">>,1384248547512366},
487 | {bytes,<<"ob=<0.1.0>\nfl=prim_file\nfn=prim_file:get_uint32/1\n1 2\n\n">>,1384248547512382},
488 | {bytes,<<"ob=<0.1.0>\nfl=prim_file\nfn=prim_file:get_uint32/1\n1 2\n\n">>,1384248547512385},
489 | {bytes,<<"ob=<0.1.0>\nfl=prim_file\nfn=prim_file:get_uint64/1\n1 7\ncfl=prim_file\ncfn=prim_file:get_uint32/1\ncalls=2 1\n1 4\n\n">>,1384248547512386},
490 | {bytes,<<"ob=<0.1.0>\nfl=prim_file\nfn=prim_file:translate_response/2\n1 13\ncfl=prim_file\ncfn=prim_file:get_uint64/1\ncalls=1 1\n1 11\n\n">>,1384248547512392},
491 | {bytes,<<"ob=<0.1.0>\nfl=prim_file\nfn=prim_file:drv_get_response/1\n1 8\ncfl=erlang\ncfn=erlang:bump_reductions/1\ncalls=1 1\n1 9\ncfl=prim_file\ncfn=prim_file:translate_response/2\ncalls=1 1\n1 24\n\n">>,1384248547512397},
492 | {bytes,<<"ob=<0.1.0>\nfl=prim_file\nfn=prim_file:drv_get_response/2\n1 2\ncfl=prim_file\ncfn=prim_file:drv_get_response/1\ncalls=1 1\n1 41\n\n">>,1384248547512397},
493 | {bytes,<<"ob=<0.1.0>\nfl=prim_file\nfn=prim_file:drv_command_nt/3\n1 16\ncfl=erlang\ncfn=erlang:port_command/2\ncalls=1 1\n1 3\ncfl=prim_file\ncfn=prim_file:drv_get_response/2\ncalls=1 1\n1 43\n\n">>,1384248547512404}] = receive_all([]).
494 |
--------------------------------------------------------------------------------