├── .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 | --------------------------------------------------------------------------------