-module(tpFlame). %% 火焰图分析 -export([ pfs/2 %% 分析单个文件 , pfm/3 %% 分析多个文件 ]). -record(state, { outputPath = "", pid, lastTs, count = 0, acc = [] }). -spec pfs(file:filename_all(), file:filename_all()) -> ok. pfs(InputFile, OutputPath) -> {ok, FinalState} = tpFReader:fold(fun handleEvent/2, #state{outputPath = OutputPath}, InputFile), flush(FinalState). -spec pfm(file:filename(), filelib:dirname(), file:filename()) -> ok. pfm(InputFiles, Cwd, OutputPath) -> PfFiles = filelib:wildcard(InputFiles, Cwd), doPfm(PfFiles, #state{outputPath = OutputPath}). doPfm([], State) -> flush(State); doPfm([InputFile | PfFiles], State) -> {ok, NewState} = tpFReader:fold(fun handleEvent/2, State, InputFile), doPfm(PfFiles, NewState). handleEvent({_TraceTag, _Pid, _Ts, _Arg} = Trace, State) -> Pid = element(2, Trace), PidState = getPidState(Pid), NewPidState = doExpInner(Trace, PidState), setPidState(Pid, NewPidState), State. %% in & out, without call context, don't help us doExpInner({InOut, _Pid, _TS, _MFA}, #state{lastTs = undefined} = PS) when InOut == in; InOut == out -> PS; %% return_from and return_to, without call context, don't help us doExpInner({Return, _Pid, _TS, _MFA}, #state{lastTs = undefined} = PS) when Return == return_from; Return == return_to -> PS; doExpInner({call, Pid, TS, MFA, BIN}, #state{lastTs = LastTS, acc = Acc, count = Count} = PS) -> try %% Calculate time elapsed, TS-LastTs. 计算经过的时间,TS-LastTs。 %% 0. If Acc is empty, then skip step #1. 0. 如果 Acc 为空,则跳过步骤 1。 %% 1. Credit elapsed time to the stack on the top of Acc. 1. 将经过的时间计入 Acc 顶部的堆栈。 %% 2. Push a 0 usec item with this stack onto Acc. 2. 将带有此堆栈的 0 usec 项目推送到 Acc。 Stack = lists:filter(fun(<<"unknown function">>) -> false;(_) -> true end, stackToBin(BIN)), TrimStack = stackTrim(Stack), MFABin = mfaToBin(MFA), NewStack = [MFABin | lists:reverse(TrimStack)], NewAcc = case Acc of [] -> [{NewStack, 0}]; [{LastStack, LastTime} | Tail] -> USec = TS - LastTS, % io:format("Stack1: ~p ~p\n", [Stack1, USec]), [{NewStack, 0}, {LastStack, LastTime + USec} | Tail] end, %% TODO: more state tracking here. PS#state{pid = Pid, lastTs = TS, count = Count + 1, acc = NewAcc} catch Class:Reason:StackTrace -> io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, Class, Reason, StackTrace]), PS end; doExpInner({return_to, _Pid, TS, MFA}, #state{lastTs = LastTS, acc = Acc} = PS) -> try %% Calculate time elapsed, TS-LastTs. %% 1. Credit elapsed time to the stack on the top of Acc. %% 2. Push a 0 usec item with the "best" stack onto Acc. %% "best" = MFA exists in the middle of the stack onto Acc, %% or else MFA exists at the top of a stack elsewhere in Acc. [{LastStack, LastTime} | Tail] = Acc, MFABin = mfaToBin(MFA), BestStack = lists:dropwhile(fun(SomeMFA) when SomeMFA /= MFABin -> true;(_) -> false end, findMatchingStack(MFABin, Acc)), USec = TS - LastTS, NewAcc = [{BestStack, 0}, {LastStack, LastTime + USec} | Tail], % io:format(user, "return-to: ~p\n", [lists:sublist(Acc2, 4)]), PS#state{lastTs = TS, acc = NewAcc} catch Class:Reason:StackTrace -> io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, Class, Reason, StackTrace]), PS end; doExpInner({gc_start, _Pid, TS, _Info}, #state{lastTs = LastTS, acc = Acc} = PS) -> try %% Push a 0 usec item onto Acc. [{LastStack, LastTime} | Tail] = Acc, NewStack = [<<"GARBAGE-COLLECTION">> | LastStack], USec = TS - LastTS, NewAcc = [{NewStack, 0}, {LastStack, LastTime + USec} | Tail], % io:format(user, "GC 1: ~p\n", [lists:sublist(Acc2, 4)]), PS#state{lastTs = TS, acc = NewAcc} catch Class:Reason:StackTrace -> io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, Class, Reason, StackTrace]), PS end; doExpInner({gc_end, _Pid, TS, _Info}, #state{lastTs = LastTS, acc = Acc} = PS) -> try %% Push the GC time onto Acc, then push 0 usec item from last exec %% stack onto Acc. [{GCStack, GCTime}, {LastExecStack, _} | Tail] = Acc, USec = TS - LastTS, NewAcc = [{LastExecStack, 0}, {GCStack, GCTime + USec} | Tail], % io:format(user, "GC 2: ~p\n", [lists:sublist(Acc2, 4)]), PS#state{lastTs = TS, acc = NewAcc} catch Class:Reason:StackTrace -> io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, Class, Reason, StackTrace]), PS end; doExpInner({out, _Pid, TS, MFA}, #state{lastTs = LastTS, acc = Acc} = PS) -> try %% Push a 0 usec item onto Acc. %% The MFA reported here probably doesn't appear in the stacktrace %% given to us by the last 'call', so add it here. [{LastStack, LastTime} | Tail] = Acc, MFA_bin = mfaToBin(MFA), NewStack = [<<"SLEEP">>, MFA_bin | LastStack], USec = TS - LastTS, NewAcc = [{NewStack, 0}, {LastStack, LastTime + USec} | Tail], PS#state{lastTs = TS, acc = NewAcc} catch Class:Reason:StackTrace -> io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, Class, Reason, StackTrace]), PS end; doExpInner({in, _Pid, TS, MFA}, #state{lastTs = LastTS, acc = Acc} = PS) -> try %% Push the Sleep time onto Acc, then push 0 usec item from last %% exec stack onto Acc. %% The MFA reported here probably doesn't appear in the stacktrace %% given to us by the last 'call', so add it here. MFA_bin = mfaToBin(MFA), [{SleepStack, SleepTime}, {LastExecStack, _} | Tail] = Acc, USec = TS - LastTS, NewAcc = [{[MFA_bin | LastExecStack], 0}, {SleepStack, SleepTime + USec} | Tail], PS#state{lastTs = TS, acc = NewAcc} catch Class:Reason:StackTrace -> io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, Class, Reason, StackTrace]), PS end; doExpInner(_Else, PS) -> % io:format("?? ~P\n", [_Else, 10]), PS. findMatchingStack(MFABin, [{H, _Time} | _] = Acc) -> case lists:member(MFABin, H) of true -> H; _ -> forFindMatchingStack(MFABin, Acc) end. forFindMatchingStack(MFABin, [{[MFABin | _StackTail] = Stack, _Time} | _]) -> Stack; forFindMatchingStack(MFABin, [_H | T]) -> forFindMatchingStack(MFABin, T); forFindMatchingStack(_MFABin, []) -> [<<"FIND-MATCHING-FAILED">>]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% intersperse(_, []) -> []; intersperse(_, [X]) -> [X]; intersperse(Sep, [X | Xs]) -> [X, Sep | intersperse(Sep, Xs)]. stackTrim([<<"proc_lib:init_p_do_apply/3">>, <<"gen_fsm:decode_msg/9">>, <<"gen_fsm:handle_msg/7">>, <<"gen_fsm:loop/7">> | T]) -> stackTrim([<<"GEN-FSM">> | T]); stackTrim([<<"GEN-FSM">>, <<"gen_fsm:decode_msg/9">>, <<"gen_fsm:handle_msg/7">>, <<"gen_fsm:loop/7">> | T]) -> stackTrim([<<"GEN-FSM">> | T]); stackTrim(Else) -> Else. stackToBin(Bin) when is_binary(Bin) -> [list_to_binary(X) || X <- stack(Bin)]; stackToBin(X) -> eFmt:formatBin("~w", [X]). mfaToBin({M, F, A}) -> eFmt:formatBin("~w:~w/~w", [M, F, A]); mfaToBin(X) -> eFmt:formatBin("~w", [X]). %% Borrowed from redbug.erl stack(Bin) -> lists:foldl(fun munge/2, [], string:tokens(binary_to_list(Bin), "\n")). munge(I, Out) -> case I of %% lists:reverse(I) of "..." ++ _ -> ["truncated!!!" | Out]; _ -> case string:str(I, "Return addr") of 0 -> case string:str(I, "cp = ") of 0 -> Out; _ -> [mfaf(I) | Out] end; _ -> case string:str(I, "erminate process normal") of 0 -> [mfaf(I) | Out]; _ -> Out end end end. mfaf(I) -> [_, C | _] = string:tokens(I, "()+"), string:strip(C). flush(#state{outputPath = OutputPath}) -> PidStates = get(), {ok, FH} = file:open(OutputPath, [write, raw, binary, delayed_write]), io:format("\n\nWriting to ~s for ~w processes... ", [OutputPath, length(PidStates)]), [ [ begin PidStr = eFmt:formatBin("~w", [Pid]), Size = byte_size(PidStr), TimeStr = integer_to_binary(Time), file:write(FH, [<<"(">>, binary:part(PidStr, 1, Size -2) , <<")">>, $;, intersperse($;, lists:reverse(Stack)), 32, TimeStr, 10]) end || {Stack, Time} <- Acc ] || {Pid, #state{acc = Acc} = _PS} <- PidStates], ok = file:close(FH), io:format("finished!\n"), ok. setPidState(Pid, PidState) -> erlang:put(Pid, PidState). getPidState(Pid) -> case erlang:get(Pid) of undefined -> io:format("~p ", [Pid]), #state{}; SomeState -> SomeState end.