├── .gitignore ├── stack_to_flame.sh ├── src ├── eflame.app.src └── eflame.erl ├── stacks_to_flames.sh ├── LICENSE ├── README.md └── flamegraph.pl /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | ebin/* 3 | *.svg 4 | *.out 5 | erl_crash.dump 6 | -------------------------------------------------------------------------------- /stack_to_flame.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | me="$(dirname $0)" 4 | 5 | uniq -c | awk '{print $2, " ", $1}' | $me/flamegraph.pl 6 | -------------------------------------------------------------------------------- /src/eflame.app.src: -------------------------------------------------------------------------------- 1 | {application, eflame, 2 | [ 3 | {description, ""}, 4 | {vsn, git}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {env, []} 11 | ]}. 12 | -------------------------------------------------------------------------------- /stacks_to_flames.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # This scripts renders separate flame graphs for each process in the file at the first argument 4 | # Canvas sizes are tuned relatively to the longest running process (max is $maxwidth) 5 | 6 | # usage: 7 | # deps/eflame/stacks_to_flames.sh stacks.out 8 | 9 | me="$(dirname $0)" 10 | f=${1:-stacks.out} 11 | maxwidth=${maxwidth:-1430} 12 | 13 | for width pid in $(awk -F';' '{print $1}' $f | uniq -c | tr -d '<>' | sort -rn -k1); do 14 | max=${max:-$width.0} 15 | echo -n "pid: $pid\tsamples: $width\t" 16 | grep $pid $f | $me/flamegraph.pl --title="$title ($pid)" --width=$(($maxwidth / $max * $width)) > flame_$pid.svg 17 | echo flame_$pid.svg 18 | done 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2014 Vladimir Kirillov 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## eflame 2 | 3 | [Flame Graphs](http://dtrace.org/blogs/brendan/2011/12/16/flame-graphs/) for Erlang. Uses `erlang:trace/3` API. 4 | 5 | ![screenshot](http://i.imgur.com/XIDAcd3.png) 6 | 7 | Further development of `eflame` using proper stack sampling-based profiling and experimental Erlang 8 | tracing changes are available under [Scott Lystig Fritchie's](https://github.com/slfritchie/eflame) fork. 9 | It also contains [amazing peeks inside Riak](https://github.com/slfritchie/eflame/blob/master/README-Riak-Example.md). 10 | Make sure to check out his talk, [Actively Measuring And Profiling Erlang Code](http://www.snookles.com/erlang/ef2015/slf-presentation.html). 11 | 12 | Usage example: https://github.com/proger/active/commit/81e7e40c9dc5a4666742636ea4c5dfafc41508a5 13 | 14 | ```erlang 15 | > eflame:apply(normal_with_children, "stacks.out", my_module, awesome_calculation, []). 16 | > eflame:apply(my_module, awesome_calculation, []). % same as above 17 | > eflame:apply(fun my_module:awesome_calculation/0, []). % same as above 18 | > eflame:apply(fun awesome_calculation/0, []). % same as above, when called in my_module.erl 19 | > eflame:apply(normal, "stacks.out", my_module, awesome_calculation, []). % won't trace children 20 | ``` 21 | 22 | ```sh 23 | $ stack_to_flame.sh < stacks.out > flame.svg 24 | $ open flame.svg 25 | ``` 26 | 27 | ### Notes 28 | 29 | * as stacks are collected through tracing, blocking calls are noticed and are drawn in blue 30 | 31 | * unlike the reference implementation, `flamegraph.pl` does not sort the input to preserve the order of calls 32 | (since this is possible due to current method of collecting stacks) 33 | 34 | ```sh 35 | $ grep 0.90.0 stacks.out | deps/eflame/flamegraph.pl > flame.svg 36 | 37 | # this invocation draws a separate flame graph for each traced process 38 | $ for pid in $(cat stacks.out | awk -F';' '{print $1}' | uniq | tr -d '<>'); do 39 | grep $pid stacks.out | deps/eflame/flamegraph.pl --title="$pid" > flame_$pid.svg; 40 | done 41 | 42 | # you may also use stacks_to_flames.sh (uses zsh) 43 | $ deps/eflame/stacks_to_flames.sh stacks.out 44 | ``` 45 | 46 | ### More examples 47 | 48 | Of course you can also apply a bazillion of transformations to get a more understandable stack, for example: 49 | 50 | ```sh 51 | $ grep 0.90.0 stacks.out | sort | uniq -c | sort -n -k1 | sort -k2 | awk '{print $2, "", $1}' > stacks.90 52 | $ perl -pi -e 's#eflame:apply/5;rebar_core:process_commands/2;##' stacks.90 53 | $ perl -pi -e 's#rebar_core:execute/.;##g' stacks.90 54 | $ perl -pi -e 's#rebar_core:process_dir.?/.;##g' stacks.90 55 | $ perl -pi -e 's#rebar_core:process_each/.;##g' stacks.90 56 | $ perl -pi -e 's#rebar_core:run_modules\w*/.;##g' stacks.90 57 | $ perl -pi -e 's#lists:\w+/.;##g' stacks.90 58 | $ perl -pi -e 's#/\d+;#;#g' stacks.90 59 | $ perl -pi -e 's#io_lib_pretty:[^;]+;##g' stacks.90 60 | $ cat stacks.90 | sort -k1 | deps/eflame/flamegraph.pl --width=1430 > flame.svg 61 | ``` 62 | 63 | The following picture is a cleaned flame graph for a run of `rebar compile` (using [active](https://github.com/proger/active)) 64 | on a project with 15 dependencies where all files are already compiled: 65 | 66 | ![rebar compile cleaned flame graph](http://i.imgur.com/hLXx7LO.png) 67 | -------------------------------------------------------------------------------- /src/eflame.erl: -------------------------------------------------------------------------------- 1 | -module(eflame). 2 | -export([apply/2, 3 | apply/3, 4 | apply/4, 5 | apply/5]). 6 | 7 | -define(RESOLUTION, 1000). %% us 8 | -record(dump, {stack=[], us=0, acc=[]}). % per-process state 9 | 10 | -define(DEFAULT_MODE, normal_with_children). 11 | -define(DEFAULT_OUTPUT_FILE, "stacks.out"). 12 | 13 | apply(F, A) -> 14 | apply1(?DEFAULT_MODE, ?DEFAULT_OUTPUT_FILE, {F, A}). 15 | 16 | apply(M, F, A) -> 17 | apply1(?DEFAULT_MODE, ?DEFAULT_OUTPUT_FILE, {{M, F}, A}). 18 | 19 | apply(Mode, OutputFile, Fun, Args) -> 20 | apply1(Mode, OutputFile, {Fun, Args}). 21 | 22 | apply(Mode, OutputFile, M, F, A) -> 23 | apply1(Mode, OutputFile, {{M, F}, A}). 24 | 25 | apply1(Mode, OutputFile, {Fun, Args}) -> 26 | Tracer = spawn_tracer(), 27 | 28 | start_trace(Tracer, self(), Mode), 29 | Return = (catch apply_fun(Fun, Args)), 30 | {ok, Bytes} = stop_trace(Tracer, self()), 31 | 32 | ok = file:write_file(OutputFile, Bytes), 33 | Return. 34 | 35 | apply_fun({M, F}, A) -> 36 | erlang:apply(M, F, A); 37 | apply_fun(F, A) -> 38 | erlang:apply(F, A). 39 | 40 | start_trace(Tracer, Target, Mode) -> 41 | MatchSpec = [{'_', [], [{message, {{cp, {caller}}}}]}], 42 | erlang:trace_pattern(on_load, MatchSpec, [local]), 43 | erlang:trace_pattern({'_', '_', '_'}, MatchSpec, [local]), 44 | erlang:trace(Target, true, [{tracer, Tracer} | trace_flags(Mode)]), 45 | ok. 46 | 47 | stop_trace(Tracer, Target) -> 48 | erlang:trace(Target, false, [all]), 49 | Tracer ! {dump_bytes, self()}, 50 | 51 | Ret = receive {bytes, B} -> {ok, B} 52 | after 5000 -> {error, timeout} 53 | end, 54 | 55 | exit(Tracer, normal), 56 | Ret. 57 | 58 | spawn_tracer() -> spawn(fun() -> trace_listener(dict:new()) end). 59 | 60 | trace_flags(normal) -> 61 | [call, arity, return_to, timestamp, running]; 62 | trace_flags(normal_with_children) -> 63 | [call, arity, return_to, timestamp, running, set_on_spawn]; 64 | trace_flags(like_fprof) -> % fprof does this as 'normal', will not work! 65 | [call, return_to, running, procs, garbage_collection, arity, timestamp, set_on_spawn]. 66 | 67 | trace_listener(State) -> 68 | receive 69 | {dump, Pid} -> 70 | Pid ! {stacks, dict:to_list(State)}; 71 | {dump_bytes, Pid} -> 72 | Bytes = iolist_to_binary([dump_to_iolist(TPid, Dump) || {TPid, [Dump]} <- dict:to_list(State)]), 73 | Pid ! {bytes, Bytes}; 74 | Term -> 75 | trace_ts = element(1, Term), 76 | PidS = element(2, Term), 77 | 78 | PidState = case dict:find(PidS, State) of 79 | {ok, [Ps]} -> Ps; 80 | error -> #dump{} 81 | end, 82 | 83 | NewPidState = trace_proc_stream(Term, PidState), 84 | 85 | D1 = dict:erase(PidS, State), 86 | D2 = dict:append(PidS, NewPidState, D1), 87 | trace_listener(D2) 88 | end. 89 | 90 | us({Mega, Secs, Micro}) -> 91 | Mega*1000*1000*1000*1000 + Secs*1000*1000 + Micro. 92 | 93 | new_state(#dump{us=Us, acc=Acc} = State, Stack, Ts) -> 94 | %io:format("new state: ~p ~p ~p~n", [Us, length(Stack), Ts]), 95 | UsTs = us(Ts), 96 | case Us of 97 | 0 -> State#dump{us=UsTs, stack=Stack}; 98 | _ when Us > 0 -> 99 | Diff = us(Ts) - Us, 100 | NOverlaps = Diff div ?RESOLUTION, 101 | Overlapped = NOverlaps * ?RESOLUTION, 102 | %Rem = Diff - Overlapped, 103 | case NOverlaps of 104 | X when X >= 1 -> 105 | StackRev = lists:reverse(Stack), 106 | Stacks = [StackRev || _ <- lists:seq(1, NOverlaps)], 107 | State#dump{us=Us+Overlapped, acc=lists:append(Stacks, Acc), stack=Stack}; 108 | _ -> 109 | State#dump{stack=Stack} 110 | end 111 | end. 112 | 113 | trace_proc_stream({trace_ts, _Ps, call, MFA, {cp, {_,_,_} = CallerMFA}, Ts}, #dump{stack=[]} = State) -> 114 | new_state(State, [MFA, CallerMFA], Ts); 115 | 116 | trace_proc_stream({trace_ts, _Ps, call, MFA, {cp, undefined}, Ts}, #dump{stack=[]} = State) -> 117 | new_state(State, [MFA], Ts); 118 | 119 | trace_proc_stream({trace_ts, _Ps, call, MFA, {cp, undefined}, Ts}, #dump{stack=[MFA|_] = Stack} = State) -> 120 | new_state(State, Stack, Ts); 121 | 122 | trace_proc_stream({trace_ts, _Ps, call, MFA, {cp, undefined}, Ts}, #dump{stack=Stack} = State) -> 123 | new_state(State, [MFA | Stack], Ts); 124 | 125 | trace_proc_stream({trace_ts, _Ps, call, MFA, {cp, MFA}, Ts}, #dump{stack=[MFA|Stack]} = State) -> 126 | new_state(State, [MFA|Stack], Ts); % collapse tail recursion 127 | 128 | trace_proc_stream({trace_ts, _Ps, call, MFA, {cp, CpMFA}, Ts}, #dump{stack=[CpMFA|Stack]} = State) -> 129 | new_state(State, [MFA, CpMFA|Stack], Ts); 130 | 131 | trace_proc_stream({trace_ts, _Ps, call, _MFA, {cp, _}, _Ts} = TraceTs, #dump{stack=[_|StackRest]} = State) -> 132 | trace_proc_stream(TraceTs, State#dump{stack=StackRest}); 133 | 134 | trace_proc_stream({trace_ts, _Ps, return_to, MFA, Ts}, #dump{stack=[_Current, MFA|Stack]} = State) -> 135 | new_state(State, [MFA|Stack], Ts); % do not try to traverse stack down because we've already collapsed it 136 | 137 | trace_proc_stream({trace_ts, _Ps, return_to, undefined, _Ts}, State) -> 138 | State; 139 | 140 | trace_proc_stream({trace_ts, _Ps, return_to, _, _Ts}, State) -> 141 | State; 142 | 143 | trace_proc_stream({trace_ts, _Ps, in, _MFA, Ts}, #dump{stack=[sleep|Stack]} = State) -> 144 | new_state(new_state(State, [sleep|Stack], Ts), Stack, Ts); 145 | 146 | trace_proc_stream({trace_ts, _Ps, in, _MFA, Ts}, #dump{stack=Stack} = State) -> 147 | new_state(State, Stack, Ts); 148 | 149 | trace_proc_stream({trace_ts, _Ps, out, _MFA, Ts}, #dump{stack=Stack} = State) -> 150 | new_state(State, [sleep|Stack], Ts); 151 | 152 | trace_proc_stream(TraceTs, State) -> 153 | io:format("trace_proc_stream: unknown trace: ~p~n", [TraceTs]), 154 | State. 155 | 156 | stack_collapse(Stack) -> 157 | intercalate(";", [entry_to_iolist(S) || S <- Stack]). 158 | 159 | entry_to_iolist({M, F, A}) -> 160 | [atom_to_binary(M, utf8), <<":">>, atom_to_binary(F, utf8), <<"/">>, integer_to_list(A)]; 161 | entry_to_iolist(A) when is_atom(A) -> 162 | [atom_to_binary(A, utf8)]. 163 | 164 | dump_to_iolist(Pid, #dump{acc=Acc}) -> 165 | [[pid_to_list(Pid), <<";">>, stack_collapse(S), <<"\n">>] || S <- lists:reverse(Acc)]. 166 | 167 | intercalate(Sep, Xs) -> lists:concat(intersperse(Sep, Xs)). 168 | 169 | intersperse(_, []) -> []; 170 | intersperse(_, [X]) -> [X]; 171 | intersperse(Sep, [X | Xs]) -> [X, Sep | intersperse(Sep, Xs)]. 172 | 173 | -------------------------------------------------------------------------------- /flamegraph.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # flamegraph.pl flame stack grapher. 4 | # 5 | # This takes stack samples and renders a call graph, allowing hot functions 6 | # and codepaths to be quickly identified. Stack samples can be generated using 7 | # tools such as DTrace, perf, SystemTap, and Instruments. 8 | # 9 | # USAGE: ./flamegraph.pl [options] input.txt > graph.svg 10 | # 11 | # grep funcA input.txt | ./flamegraph.pl [options] > graph.svg 12 | # 13 | # Options are listed in the usage message (--help). 14 | # 15 | # The input is stack frames and sample counts formatted as single lines. Each 16 | # frame in the stack is semicolon separated, with a space and count at the end 17 | # of the line. These can be generated using DTrace with stackcollapse.pl, 18 | # and other tools using the stackcollapse variants. 19 | # 20 | # An optional extra column of counts can be provided to generate a differential 21 | # flame graph of the counts, colored red for more, and blue for less. This 22 | # can be useful when using flame graphs for non-regression testing. 23 | # See the header comment in the difffolded.pl program for instructions. 24 | # 25 | # The output graph shows relative presence of functions in stack samples. The 26 | # ordering on the x-axis has no meaning; since the data is samples, time order 27 | # of events is not known. The order used sorts function names alphabetically. 28 | # 29 | # While intended to process stack samples, this can also process stack traces. 30 | # For example, tracing stacks for memory allocation, or resource usage. You 31 | # can use --title to set the title to reflect the content, and --countname 32 | # to change "samples" to "bytes" etc. 33 | # 34 | # There are a few different palettes, selectable using --color. By default, 35 | # the colors are selected at random (except for differentials). Functions 36 | # called "-" will be printed gray, which can be used for stack separators (eg, 37 | # between user and kernel stacks). 38 | # 39 | # HISTORY 40 | # 41 | # This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb 42 | # program, which visualized function entry and return trace events. As Neel 43 | # wrote: "The output displayed is inspired by Roch's CallStackAnalyzer which 44 | # was in turn inspired by the work on vftrace by Jan Boerhout". See: 45 | # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and 46 | # 47 | # Copyright 2011 Joyent, Inc. All rights reserved. 48 | # Copyright 2011 Brendan Gregg. All rights reserved. 49 | # 50 | # CDDL HEADER START 51 | # 52 | # The contents of this file are subject to the terms of the 53 | # Common Development and Distribution License (the "License"). 54 | # You may not use this file except in compliance with the License. 55 | # 56 | # You can obtain a copy of the license at docs/cddl1.txt or 57 | # http://opensource.org/licenses/CDDL-1.0. 58 | # See the License for the specific language governing permissions 59 | # and limitations under the License. 60 | # 61 | # When distributing Covered Code, include this CDDL HEADER in each 62 | # file and include the License file at docs/cddl1.txt. 63 | # If applicable, add the following below this CDDL HEADER, with the 64 | # fields enclosed by brackets "[]" replaced with your own identifying 65 | # information: Portions Copyright [yyyy] [name of copyright owner] 66 | # 67 | # CDDL HEADER END 68 | # 69 | # 21-Nov-2013 Shawn Sterling Added consistent palette file option 70 | # 17-Mar-2013 Tim Bunce Added options and more tunables. 71 | # 15-Dec-2011 Dave Pacheco Support for frames with whitespace. 72 | # 10-Sep-2011 Brendan Gregg Created this. 73 | 74 | use strict; 75 | 76 | use Getopt::Long; 77 | use POSIX; 78 | 79 | # tunables 80 | my $encoding; 81 | my $fonttype = "Verdana"; 82 | my $imagewidth = 1200; # max width, pixels 83 | my $frameheight = 16; # max height is dynamic 84 | my $fontsize = 12; # base text size 85 | my $fontwidth = 0.59; # avg width relative to fontsize 86 | my $minwidth = 0.1; # min function width, pixels 87 | my $nametype = "Function:"; # what are the names in the data? 88 | my $countname = "samples"; # what are the counts in the data? 89 | my $colors = "hot"; # color theme 90 | my $bgcolor1 = "#eeeeee"; # background color gradient start 91 | my $bgcolor2 = "#eeeeb0"; # background color gradient stop 92 | my $nameattrfile; # file holding function attributes 93 | my $timemax; # (override the) sum of the counts 94 | my $factor = 1; # factor to scale counts by 95 | my $hash = 0; # color by function name 96 | my $palette = 0; # if we use consistent palettes (default off) 97 | my %palette_map; # palette map hash 98 | my $pal_file = "palette.map"; # palette map file name 99 | my $stackreverse = 0; # reverse stack order, switching merge end 100 | my $inverted = 0; # icicle graph 101 | my $negate = 0; # switch differential hues 102 | my $titletext = ""; # centered heading 103 | my $titledefault = "Flame Graph"; # overwritten by --title 104 | my $titleinverted = "Icicle Graph"; # " " 105 | 106 | GetOptions( 107 | 'fonttype=s' => \$fonttype, 108 | 'width=f' => \$imagewidth, 109 | 'height=i' => \$frameheight, 110 | 'encoding=s' => \$encoding, 111 | 'fontsize=f' => \$fontsize, 112 | 'fontwidth=f' => \$fontwidth, 113 | 'minwidth=f' => \$minwidth, 114 | 'title=s' => \$titletext, 115 | 'nametype=s' => \$nametype, 116 | 'countname=s' => \$countname, 117 | 'nameattr=s' => \$nameattrfile, 118 | 'total=s' => \$timemax, 119 | 'factor=f' => \$factor, 120 | 'colors=s' => \$colors, 121 | 'hash' => \$hash, 122 | 'cp' => \$palette, 123 | 'reverse' => \$stackreverse, 124 | 'inverted' => \$inverted, 125 | 'negate' => \$negate, 126 | ) or die < outfile.svg\n 128 | --title # change title text 129 | --width # width of image (default 1200) 130 | --height # height of each frame (default 16) 131 | --minwidth # omit smaller functions (default 0.1 pixels) 132 | --fonttype # font type (default "Verdana") 133 | --fontsize # font size (default 12) 134 | --countname # count type label (default "samples") 135 | --nametype # name type label (default "Function:") 136 | --colors # set color palette. choices are: hot (default), mem, io, 137 | # java, js, red, green, blue, yellow, purple, orange 138 | --hash # colors are keyed by function name hash 139 | --cp # use consistent palette (palette.map) 140 | --reverse # generate stack-reversed flame graph 141 | --inverted # icicle graph 142 | --negate # switch differential hues (blue<->red) 143 | 144 | eg, 145 | $0 --title="Flame Graph: malloc()" trace.txt > graph.svg 146 | USAGE_END 147 | 148 | $imagewidth = ceil($imagewidth); 149 | 150 | # internals 151 | my $ypad1 = $fontsize * 4; # pad top, include title 152 | my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels 153 | my $xpad = 10; # pad lefm and right 154 | my $framepad = 1; # vertical padding for frames 155 | my $depthmax = 0; 156 | my %Events; 157 | my %nameattr; 158 | 159 | if ($titletext eq "") { 160 | unless ($inverted) { 161 | $titletext = $titledefault; 162 | } else { 163 | $titletext = $titleinverted; 164 | } 165 | } 166 | 167 | if ($nameattrfile) { 168 | # The name-attribute file format is a function name followed by a tab then 169 | # a sequence of tab separated name=value pairs. 170 | open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n"; 171 | while (<$attrfh>) { 172 | chomp; 173 | my ($funcname, $attrstr) = split /\t/, $_, 2; 174 | die "Invalid format in $nameattrfile" unless defined $attrstr; 175 | $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr }; 176 | } 177 | } 178 | 179 | if ($colors eq "mem") { $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; } 180 | if ($colors eq "io") { $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; } 181 | 182 | # SVG functions 183 | { package SVG; 184 | sub new { 185 | my $class = shift; 186 | my $self = {}; 187 | bless ($self, $class); 188 | return $self; 189 | } 190 | 191 | sub header { 192 | my ($self, $w, $h) = @_; 193 | my $enc_attr = ''; 194 | if (defined $encoding) { 195 | $enc_attr = qq{ encoding="$encoding"}; 196 | } 197 | $self->{svg} .= < 199 | 200 | 201 | SVG 202 | } 203 | 204 | sub include { 205 | my ($self, $content) = @_; 206 | $self->{svg} .= $content; 207 | } 208 | 209 | sub colorAllocate { 210 | my ($self, $r, $g, $b) = @_; 211 | return "rgb($r,$g,$b)"; 212 | } 213 | 214 | sub group_start { 215 | my ($self, $attr) = @_; 216 | 217 | my @g_attr = map { 218 | exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () 219 | } qw(class style onmouseover onmouseout onclick); 220 | push @g_attr, $attr->{g_extra} if $attr->{g_extra}; 221 | $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); 222 | 223 | $self->{svg} .= sprintf qq/%s<\/title>/, $attr->{title} 224 | if $attr->{title}; # should be first element within g container 225 | 226 | if ($attr->{href}) { 227 | my @a_attr; 228 | push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href}; 229 | # default target=_top else links will open within SVG 230 | push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top"; 231 | push @a_attr, $attr->{a_extra} if $attr->{a_extra}; 232 | $self->{svg} .= sprintf qq//, join(' ', @a_attr); 233 | } 234 | } 235 | 236 | sub group_end { 237 | my ($self, $attr) = @_; 238 | $self->{svg} .= qq/<\/a>\n/ if $attr->{href}; 239 | $self->{svg} .= qq/<\/g>\n/; 240 | } 241 | 242 | sub filledRectangle { 243 | my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_; 244 | $x1 = sprintf "%0.1f", $x1; 245 | $x2 = sprintf "%0.1f", $x2; 246 | my $w = sprintf "%0.1f", $x2 - $x1; 247 | my $h = sprintf "%0.1f", $y2 - $y1; 248 | $extra = defined $extra ? $extra : ""; 249 | $self->{svg} .= qq/\n/; 250 | } 251 | 252 | sub stringTTF { 253 | my ($self, $color, $font, $size, $angle, $x, $y, $str, $loc, $extra) = @_; 254 | $x = sprintf "%0.2f", $x; 255 | $loc = defined $loc ? $loc : "left"; 256 | $extra = defined $extra ? $extra : ""; 257 | $self->{svg} .= qq/$str<\/text>\n/; 258 | } 259 | 260 | sub svg { 261 | my $self = shift; 262 | return "$self->{svg}\n"; 263 | } 264 | 1; 265 | } 266 | 267 | sub namehash { 268 | # Generate a vector hash for the name string, weighting early over 269 | # later characters. We want to pick the same colors for function 270 | # names across different flame graphs. 271 | my $name = shift; 272 | my $vector = 0; 273 | my $weight = 1; 274 | my $max = 1; 275 | my $mod = 10; 276 | # if module name present, trunc to 1st char 277 | $name =~ s/.(.*?)`//; 278 | foreach my $c (split //, $name) { 279 | my $i = (ord $c) % $mod; 280 | $vector += ($i / ($mod++ - 1)) * $weight; 281 | $max += 1 * $weight; 282 | $weight *= 0.70; 283 | last if $mod > 12; 284 | } 285 | return (1 - $vector / $max) 286 | } 287 | 288 | sub color { 289 | my ($type, $hash, $name) = @_; 290 | my ($v1, $v2, $v3); 291 | 292 | if ($hash) { 293 | $v1 = namehash($name); 294 | $v2 = $v3 = namehash(scalar reverse $name); 295 | } else { 296 | $v1 = rand(1); 297 | $v2 = rand(1); 298 | $v3 = rand(1); 299 | } 300 | 301 | # theme palettes 302 | if (defined $type and $type eq "hot") { 303 | my $r = 205 + int(50 * $v3); 304 | my $g = 0 + int(230 * $v1); 305 | my $b = 0 + int(55 * $v2); 306 | return "rgb($r,$g,$b)"; 307 | } 308 | if (defined $type and $type eq "mem") { 309 | my $r = 0; 310 | my $g = 190 + int(50 * $v2); 311 | my $b = 0 + int(210 * $v1); 312 | return "rgb($r,$g,$b)"; 313 | } 314 | if (defined $type and $type eq "io") { 315 | my $r = 80 + int(60 * $v1); 316 | my $g = $r; 317 | my $b = 190 + int(55 * $v2); 318 | return "rgb($r,$g,$b)"; 319 | } 320 | 321 | # multi palettes 322 | if (defined $type and $type eq "java") { 323 | if ($name =~ /::/) { # C++ 324 | $type = "yellow"; 325 | } elsif ($name =~ m:/:) { # Java (match "/" in path) 326 | $type = "green" 327 | } else { # system 328 | $type = "red"; 329 | } 330 | # fall-through to color palettes 331 | } 332 | if (defined $type and $type eq "js") { 333 | if ($name =~ /::/) { # C++ 334 | $type = "yellow"; 335 | } elsif ($name =~ m:/:) { # JavaScript (match "/" in path) 336 | $type = "green" 337 | } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin) 338 | $type = "aqua" 339 | } elsif ($name =~ m/^ $/) { # Missing symbol 340 | $type = "green" 341 | } else { # system 342 | $type = "red"; 343 | } 344 | # fall-through to color palettes 345 | } 346 | 347 | # color palettes 348 | if (defined $type and $type eq "red") { 349 | my $r = 200 + int(55 * $v1); 350 | my $x = 50 + int(80 * $v1); 351 | return "rgb($r,$x,$x)"; 352 | } 353 | if (defined $type and $type eq "green") { 354 | my $g = 200 + int(55 * $v1); 355 | my $x = 50 + int(60 * $v1); 356 | return "rgb($x,$g,$x)"; 357 | } 358 | if (defined $type and $type eq "blue") { 359 | my $b = 205 + int(50 * $v1); 360 | my $x = 80 + int(60 * $v1); 361 | return "rgb($x,$x,$b)"; 362 | } 363 | if (defined $type and $type eq "yellow") { 364 | my $x = 175 + int(55 * $v1); 365 | my $b = 50 + int(20 * $v1); 366 | return "rgb($x,$x,$b)"; 367 | } 368 | if (defined $type and $type eq "purple") { 369 | my $x = 190 + int(65 * $v1); 370 | my $g = 80 + int(60 * $v1); 371 | return "rgb($x,$g,$x)"; 372 | } 373 | if (defined $type and $type eq "aqua") { 374 | my $r = 50 + int(60 * $v1); 375 | my $g = 165 + int(55 * $v1); 376 | my $b = 165 + int(55 * $v1); 377 | return "rgb($r,$g,$b)"; 378 | } 379 | if (defined $type and $type eq "orange") { 380 | my $r = 190 + int(65 * $v1); 381 | my $g = 90 + int(65 * $v1); 382 | return "rgb($r,$g,0)"; 383 | } 384 | 385 | return "rgb(0,0,0)"; 386 | } 387 | 388 | sub color_scale { 389 | my ($value, $max) = @_; 390 | my ($r, $g, $b) = (255, 255, 255); 391 | $value = -$value if $negate; 392 | if ($value > 0) { 393 | $g = $b = int(210 * ($max - $value) / $max); 394 | } elsif ($value < 0) { 395 | $r = $g = int(210 * ($max + $value) / $max); 396 | } 397 | return "rgb($r,$g,$b)"; 398 | } 399 | 400 | sub color_map { 401 | my ($colors, $func) = @_; 402 | if (exists $palette_map{$func}) { 403 | return $palette_map{$func}; 404 | } else { 405 | $palette_map{$func} = color($colors); 406 | return $palette_map{$func}; 407 | } 408 | } 409 | 410 | sub write_palette { 411 | open(FILE, ">$pal_file"); 412 | foreach my $key (sort keys %palette_map) { 413 | print FILE $key."->".$palette_map{$key}."\n"; 414 | } 415 | close(FILE); 416 | } 417 | 418 | sub read_palette { 419 | if (-e $pal_file) { 420 | open(FILE, $pal_file) or die "can't open file $pal_file: $!"; 421 | while ( my $line = ) { 422 | chomp($line); 423 | (my $key, my $value) = split("->",$line); 424 | $palette_map{$key}=$value; 425 | } 426 | close(FILE) 427 | } 428 | } 429 | 430 | my %Node; # Hash of merged frame data 431 | my %Tmp; 432 | 433 | # flow() merges two stacks, storing the merged frames and value data in %Node. 434 | sub flow { 435 | my ($last, $this, $v, $d) = @_; 436 | 437 | my $len_a = @$last - 1; 438 | my $len_b = @$this - 1; 439 | 440 | my $i = 0; 441 | my $len_same; 442 | for (; $i <= $len_a; $i++) { 443 | last if $i > $len_b; 444 | last if $last->[$i] ne $this->[$i]; 445 | } 446 | $len_same = $i; 447 | 448 | for ($i = $len_a; $i >= $len_same; $i--) { 449 | my $k = "$last->[$i];$i"; 450 | # a unique ID is constructed from "func;depth;etime"; 451 | # func-depth isn't unique, it may be repeated later. 452 | $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime}; 453 | if (defined $Tmp{$k}->{delta}) { 454 | $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta}; 455 | } 456 | delete $Tmp{$k}; 457 | } 458 | 459 | for ($i = $len_same; $i <= $len_b; $i++) { 460 | my $k = "$this->[$i];$i"; 461 | $Tmp{$k}->{stime} = $v; 462 | if (defined $d) { 463 | $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; 464 | } 465 | } 466 | 467 | return $this; 468 | } 469 | 470 | # parse input 471 | my @Data; 472 | my $last = []; 473 | my $time = 0; 474 | my $delta = undef; 475 | my $ignored = 0; 476 | my $line; 477 | my $maxdelta = 1; 478 | 479 | # reverse if needed 480 | foreach (<>) { 481 | chomp; 482 | $line = $_; 483 | if ($stackreverse) { 484 | # there may be an extra samples column for differentials 485 | # XXX todo: redo these REs as one. It's repeated below. 486 | my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 487 | my $samples2 = undef; 488 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 489 | $samples2 = $samples; 490 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 491 | unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2"; 492 | } else { 493 | unshift @Data, join(";", reverse split(";", $stack)) . " $samples"; 494 | } 495 | } else { 496 | unshift @Data, $line; 497 | } 498 | } 499 | 500 | # process and merge frames 501 | foreach (reverse @Data) { 502 | chomp; 503 | # process: folded_stack count 504 | # eg: func_a;func_b;func_c 31 505 | my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 506 | unless (defined $samples and defined $stack) { 507 | ++$ignored; 508 | next; 509 | } 510 | 511 | # there may be an extra samples column for differentials: 512 | my $samples2 = undef; 513 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 514 | $samples2 = $samples; 515 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 516 | } 517 | $delta = undef; 518 | if (defined $samples2) { 519 | $delta = $samples2 - $samples; 520 | $maxdelta = abs($delta) if abs($delta) > $maxdelta; 521 | } 522 | 523 | $stack =~ tr/<>/()/; 524 | 525 | # merge frames and populate %Node: 526 | $last = flow($last, [ '', split ";", $stack ], $time, $delta); 527 | 528 | if (defined $samples2) { 529 | $time += $samples2; 530 | } else { 531 | $time += $samples; 532 | } 533 | } 534 | flow($last, [], $time, $delta); 535 | 536 | warn "Ignored $ignored lines with invalid format\n" if $ignored; 537 | unless ($time) { 538 | warn "ERROR: No stack counts found\n"; 539 | my $im = SVG->new(); 540 | # emit an error message SVG, for tools automating flamegraph use 541 | my $imageheight = $fontsize * 5; 542 | $im->header($imagewidth, $imageheight); 543 | $im->stringTTF($im->colorAllocate(0, 0, 0), $fonttype, $fontsize + 2, 544 | 0.0, int($imagewidth / 2), $fontsize * 2, 545 | "ERROR: No valid input provided to flamegraph.pl.", "middle"); 546 | print $im->svg; 547 | exit 2; 548 | } 549 | if ($timemax and $timemax < $time) { 550 | warn "Specified --total $timemax is less than actual total $time, so ignored\n" 551 | if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) 552 | undef $timemax; 553 | } 554 | $timemax ||= $time; 555 | 556 | my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax; 557 | my $minwidth_time = $minwidth / $widthpertime; 558 | 559 | # prune blocks that are too narrow and determine max depth 560 | while (my ($id, $node) = each %Node) { 561 | my ($func, $depth, $etime) = split ";", $id; 562 | my $stime = $node->{stime}; 563 | die "missing start for $id" if not defined $stime; 564 | 565 | if (($etime-$stime) < $minwidth_time) { 566 | delete $Node{$id}; 567 | next; 568 | } 569 | $depthmax = $depth if $depth > $depthmax; 570 | } 571 | 572 | # draw canvas, and embed interactive JavaScript program 573 | my $imageheight = ($depthmax * $frameheight) + $ypad1 + $ypad2; 574 | my $im = SVG->new(); 575 | $im->header($imagewidth, $imageheight); 576 | my $inc = < 578 | 579 | 580 | 581 | 582 | 583 | 586 | 750 | INC 751 | $im->include($inc); 752 | $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)'); 753 | my ($white, $black, $vvdgrey, $vdgrey) = ( 754 | $im->colorAllocate(255, 255, 255), 755 | $im->colorAllocate(0, 0, 0), 756 | $im->colorAllocate(40, 40, 40), 757 | $im->colorAllocate(160, 160, 160), 758 | ); 759 | $im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize * 2, $titletext, "middle"); 760 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), " ", "", 'id="details"'); 761 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $fontsize * 2, 762 | "Reset Zoom", "", 'id="unzoom" onclick="unzoom()" style="opacity:0.0;cursor:pointer"'); 763 | 764 | if ($palette) { 765 | read_palette(); 766 | } 767 | 768 | # draw frames 769 | while (my ($id, $node) = each %Node) { 770 | my ($func, $depth, $etime) = split ";", $id; 771 | my $stime = $node->{stime}; 772 | my $delta = $node->{delta}; 773 | 774 | $etime = $timemax if $func eq "" and $depth == 0; 775 | 776 | my $x1 = $xpad + $stime * $widthpertime; 777 | my $x2 = $xpad + $etime * $widthpertime; 778 | my ($y1, $y2); 779 | unless ($inverted) { 780 | $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad; 781 | $y2 = $imageheight - $ypad2 - $depth * $frameheight; 782 | } else { 783 | $y1 = $ypad1 + $depth * $frameheight; 784 | $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad; 785 | } 786 | 787 | my $samples = sprintf "%.0f", ($etime - $stime) * $factor; 788 | (my $samples_txt = $samples) # add commas per perlfaq5 789 | =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; 790 | 791 | my $info; 792 | if ($func eq "" and $depth == 0) { 793 | $info = "all ($samples_txt $countname, 100%)"; 794 | } else { 795 | my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor)); 796 | my $escaped_func = $func; 797 | $escaped_func =~ s/&/&/g; 798 | $escaped_func =~ s//>/g; 800 | $escaped_func =~ s/"//g; 801 | unless (defined $delta) { 802 | $info = "$escaped_func ($samples_txt $countname, $pct%)"; 803 | } else { 804 | my $deltapct = sprintf "%.2f", ((100 * $delta) / ($timemax * $factor)); 805 | $deltapct = $delta > 0 ? "+$deltapct" : $deltapct; 806 | $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)"; 807 | } 808 | } 809 | 810 | my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone 811 | $nameattr->{class} ||= "func_g"; 812 | $nameattr->{onmouseover} ||= "s('".$info."')"; 813 | $nameattr->{onmouseout} ||= "c()"; 814 | $nameattr->{onclick} ||= "zoom(this)"; 815 | $nameattr->{title} ||= $info; 816 | $im->group_start($nameattr); 817 | 818 | my $color; 819 | if ($func eq "-") { 820 | $color = $vdgrey; 821 | } elsif ($func =~ /sleep/ || $func eq "SLEEP") { 822 | $color = color("blue", $hash, $func); 823 | } elsif (defined $delta) { 824 | $color = color_scale($delta, $maxdelta); 825 | } elsif ($palette) { 826 | $color = color_map($colors, $func); 827 | } else { 828 | $color = color($colors, $hash, $func); 829 | } 830 | $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); 831 | 832 | my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); 833 | my $text = ""; 834 | if ($chars >= 3) { # room for one char plus two dots 835 | $text = substr $func, 0, $chars; 836 | substr($text, -2, 2) = ".." if $chars < length $func; 837 | $text =~ s/&/&/g; 838 | $text =~ s//>/g; 840 | } 841 | $im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, ""); 842 | 843 | $im->group_end($nameattr); 844 | } 845 | 846 | print $im->svg; 847 | 848 | if ($palette) { 849 | write_palette(); 850 | } 851 | 852 | # vim: ts=8 sts=8 sw=8 noexpandtab 853 | --------------------------------------------------------------------------------