diff --git a/Gemfile.lock b/Gemfile.lock index 27c5ac12..286a2881 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - stackprof (0.2.7) + stackprof (0.2.9) GEM remote: https://rubygems.org/ @@ -22,3 +22,6 @@ DEPENDENCIES mocha (~> 0.14) rake-compiler (~> 0.9) stackprof! + +BUNDLED WITH + 1.11.2 diff --git a/lib/stackprof/middleware.rb b/lib/stackprof/middleware.rb index 21d37567..7ad43f31 100644 --- a/lib/stackprof/middleware.rb +++ b/lib/stackprof/middleware.rb @@ -11,26 +11,27 @@ def initialize(app, options = {}) Middleware.interval = options[:interval] || 1000 Middleware.raw = options[:raw] || false Middleware.enabled = options[:enabled] + Middleware.saviour = options[:saviour] Middleware.path = options[:path] || 'tmp' at_exit{ Middleware.save } if options[:save_at_exit] end def call(env) - enabled = Middleware.enabled?(env) - StackProf.start(mode: Middleware.mode, interval: Middleware.interval, raw: Middleware.raw) if enabled + enabled, mode = Middleware.enabled?(env) + StackProf.start(mode: mode || Middleware.mode, interval: Middleware.interval, raw: Middleware.raw) if enabled @app.call(env) ensure if enabled StackProf.stop if @num_reqs && (@num_reqs-=1) == 0 @num_reqs = @options[:save_every] - Middleware.save + Middleware.save(env) end end end class << self - attr_accessor :enabled, :mode, :interval, :raw, :path + attr_accessor :enabled, :saviour, :mode, :interval, :raw, :path def enabled?(env) if enabled.respond_to?(:call) @@ -40,14 +41,18 @@ def enabled?(env) end end - def save(filename = nil) + def save(env = nil, filename = nil) if results = StackProf.results - FileUtils.mkdir_p(Middleware.path) - filename ||= "stackprof-#{results[:mode]}-#{Process.pid}-#{Time.now.to_i}.dump" - File.open(File.join(Middleware.path, filename), 'wb') do |f| - f.write Marshal.dump(results) + if saviour.respond_to?(:call) + saviour.call(env, results) + else + FileUtils.mkdir_p(Middleware.path) + filename ||= "stackprof-#{results[:mode]}-#{Process.pid}-#{Time.now.to_i}.dump" + File.open(File.join(Middleware.path, filename), 'wb') do |f| + f.write Marshal.dump(results) + end + filename end - filename end end diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index c61f1236..b6330ef4 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -12,16 +12,34 @@ def frames(sort_by_total=false) @data[:frames].sort_by{ |iseq, stats| -stats[sort_by_total ? :total_samples : :samples] }.inject({}){|h, (k, v)| h[k] = v; h} end + # normalized frames is used when we want to combine multiple + # output files, (via +() below). In order to simplify combination + # of files that are from "different" source revisions, the frames + # are normalised by converting them to an MD5 using the "name, file, line" + # once frames are converted, every edge that references the original + # needs to adjusted also. def normalized_frames id2hash = {} @data[:frames].each do |frame, info| - id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}") + # id2 hash contains the original frames converted into a MD5 + # based on the name, file, line of the original frame + # flamegraph likes the frames to be numbers, so md5.to_i(16) + id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}").to_i(16) end - @data[:frames].inject(Hash.new) do |hash, (frame, info)| + # Convert all existing raw frames to use the new mapping via id2hash + # the array of raw frames contains a sequence of frame slices. Each + # frame slice is preceded by a count of the number of subsequent frames + # in the slice. Since the counts need not be normalized and we can + # identify frames by looking for a mapping in id2hash, add the + # normalized frame value when it is available, and just copy the + # existing value otherwise + raw_frames = @data[:raw].map {|v| id2hash[v.to_s] || v } if @data[:raw] + # return both the normalized frames and the normalized raw frames + return @data[:frames].inject(Hash.new) do |hash, (frame, info)| info = hash[id2hash[frame.to_s]] = info.dup info[:edges] = info[:edges].inject(Hash.new){ |edges, (edge, weight)| edges[id2hash[edge.to_s]] = weight; edges } if info[:edges] hash - end + end, raw_frames || [] end def version @@ -311,7 +329,9 @@ def +(other) raise ArgumentError, "cannot combine #{modeline} with #{other.modeline}" unless modeline == other.modeline raise ArgumentError, "cannot combine v#{version} with v#{other.version}" unless version == other.version - f1, f2 = normalized_frames, other.normalized_frames + # collect the normalized and the normalized raw frames + f1, raw_frames1 = normalized_frames + f2, raw_frames2 = other.normalized_frames frames = (f1.keys + f2.keys).uniq.inject(Hash.new) do |hash, id| if f1[id].nil? hash[id] = f2[id] @@ -339,6 +359,8 @@ def +(other) end d1, d2 = data, other.data + # ensure that the new data contains the normalized frames + # and the normalized raw frames also data = { version: version, mode: d1[:mode], @@ -346,7 +368,8 @@ def +(other) samples: d1[:samples] + d2[:samples], gc_samples: d1[:gc_samples] + d2[:gc_samples], missed_samples: d1[:missed_samples] + d2[:missed_samples], - frames: frames + frames: frames, + raw: raw_frames1 + raw_frames2 } self.class.new(data) diff --git a/test/test_middleware.rb b/test/test_middleware.rb index 24ad91eb..370083eb 100644 --- a/test/test_middleware.rb +++ b/test/test_middleware.rb @@ -38,6 +38,35 @@ def test_save_custom StackProf::Middleware.save end + def test_save_should_use_a_proc_if_passed + StackProf.stubs(:results).returns({ mode: 'foo' }) + FileUtils.expects(:mkdir_p).with('/foo').never + File.expects(:open).with(regexp_matches(/^\/foo\/stackprof-foo/), 'wb').never + + proc_called = false + StackProf::Middleware.new(Object.new, saviour: Proc.new{ proc_called = true }) + StackProf::Middleware.save + assert proc_called + end + + def test_save_proc_should_receive_env_in_proc_if_passed + StackProf.stubs(:results).returns({ mode: 'foo' }) + + env_set = nil + StackProf::Middleware.new(Object.new, saviour: Proc.new{ |env, results| env_set = env['FOO'] }) + StackProf::Middleware.save({ 'FOO' => 'bar' }) + assert_equal env_set, 'bar' + end + + def test_save_proc_should_receive_results_in_proc_if_passed + StackProf.stubs(:results).returns({ mode: 'foo' }) + + results_received = nil + StackProf::Middleware.new(Object.new, saviour: Proc.new{ |env, results| results_received = results[:mode] }) + StackProf::Middleware.save({}) + assert_equal results_received, 'foo' + end + def test_enabled_should_use_a_proc_if_passed env = {} @@ -64,4 +93,36 @@ def test_raw StackProf::Middleware.new(Object.new, raw: true) assert StackProf::Middleware.raw end + + def test_enabled_should_override_mode_if_a_proc + proc_called = false + middleware = StackProf::Middleware.new(proc {|env| proc_called = true}, enabled: Proc.new{ [true, 'foo'] }) + env = Hash.new { true } + enabled, mode = StackProf::Middleware.enabled?(env) + assert enabled + assert_equal 'foo', mode + + StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval, raw: false}) + StackProf.expects(:stop) + + middleware.call(env) + assert proc_called + end + + def test_saviour_should_be_called_when_enabled_with_env + proc_called = false + env_set = nil + results_received = nil + enable_proc = Proc.new{ [true, 'foo'] } + saviour_proc = Proc.new{ |env, results| env_set = env['FOO'] ; results_received = results[:mode] } + middleware = StackProf::Middleware.new(proc {|env| proc_called = true}, enabled: enable_proc, saviour: saviour_proc, save_every: 1) + StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval, raw: false}) + StackProf.expects(:stop) + StackProf.stubs(:results).returns({ mode: 'foo' }) + + middleware.call({ 'FOO' => 'bar' }) + assert proc_called + assert_equal env_set, 'bar' + assert_equal results_received, 'foo' + end end diff --git a/vendor/FlameGraph/flamegraph.pl b/vendor/FlameGraph/flamegraph.pl index 58cdd6c7..b099510a 100755 --- a/vendor/FlameGraph/flamegraph.pl +++ b/vendor/FlameGraph/flamegraph.pl @@ -10,6 +10,9 @@ # # grep funcA input.txt | ./flamegraph.pl [options] > graph.svg # +# Then open the resulting .svg in a web browser, for interactivity: mouse-over +# frames for info, click to zoom, and ctrl-F to search. +# # Options are listed in the usage message (--help). # # The input is stack frames and sample counts formatted as single lines. Each @@ -17,6 +20,11 @@ # of the line. These can be generated using DTrace with stackcollapse.pl, # and other tools using the stackcollapse variants. # +# An optional extra column of counts can be provided to generate a differential +# flame graph of the counts, colored red for more, and blue for less. This +# can be useful when using flame graphs for non-regression testing. +# See the header comment in the difffolded.pl program for instructions. +# # The output graph shows relative presence of functions in stack samples. The # ordering on the x-axis has no meaning; since the data is samples, time order # of events is not known. The order used sorts function names alphabetically. @@ -26,7 +34,8 @@ # can use --title to set the title to reflect the content, and --countname # to change "samples" to "bytes" etc. # -# There are a few different palettes, selectable using --color. Functions +# There are a few different palettes, selectable using --color. By default, +# the colors are selected at random (except for differentials). Functions # called "-" will be printed gray, which can be used for stack separators (eg, # between user and kernel stacks). # @@ -38,6 +47,7 @@ # was in turn inspired by the work on vftrace by Jan Boerhout". See: # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and # +# Copyright 2016 Netflix, Inc. # Copyright 2011 Joyent, Inc. All rights reserved. # Copyright 2011 Brendan Gregg. All rights reserved. # @@ -60,6 +70,7 @@ # # CDDL HEADER END # +# 11-Oct-2014 Adrien Mahieux Added zoom. # 21-Nov-2013 Shawn Sterling Added consistent palette file option # 17-Mar-2013 Tim Bunce Added options and more tunables. # 15-Dec-2011 Dave Pacheco Support for frames with whitespace. @@ -77,7 +88,6 @@ my $fontsize = 12; # base text size my $fontwidth = 0.59; # avg width relative to fontsize my $minwidth = 0.1; # min function width, pixels -my $titletext = "Flame Graph"; # centered heading my $nametype = "Function:"; # what are the names in the data? my $countname = "samples"; # what are the counts in the data? my $colors = "hot"; # color theme @@ -90,6 +100,40 @@ my $palette = 0; # if we use consistent palettes (default off) my %palette_map; # palette map hash my $pal_file = "palette.map"; # palette map file name +my $stackreverse = 0; # reverse stack order, switching merge end +my $inverted = 0; # icicle graph +my $negate = 0; # switch differential hues +my $titletext = ""; # centered heading +my $titledefault = "Flame Graph"; # overwritten by --title +my $titleinverted = "Icicle Graph"; # " " +my $searchcolor = "rgb(230,0,230)"; # color for search highlighting +my $help = 0; + +sub usage { + die < outfile.svg\n + --title # change title text + --width # width of image (default 1200) + --height # height of each frame (default 16) + --minwidth # omit smaller functions (default 0.1 pixels) + --fonttype # font type (default "Verdana") + --fontsize # font size (default 12) + --countname # count type label (default "samples") + --nametype # name type label (default "Function:") + --colors # set color palette. choices are: hot (default), mem, io, + # wakeup, chain, java, js, perl, red, green, blue, aqua, + # yellow, purple, orange + --hash # colors are keyed by function name hash + --cp # use consistent palette (palette.map) + --reverse # generate stack-reversed flame graph + --inverted # icicle graph + --negate # switch differential hues (blue<->red) + --help # this message + + eg, + $0 --title="Flame Graph: malloc()" trace.txt > graph.svg +USAGE_END +} GetOptions( 'fonttype=s' => \$fonttype, @@ -108,32 +152,30 @@ 'colors=s' => \$colors, 'hash' => \$hash, 'cp' => \$palette, -) or die < outfile.svg\n - --title # change title text - --width # width of image (default 1200) - --height # height of each frame (default 16) - --minwidth # omit smaller functions (default 0.1 pixels) - --fonttype # font type (default "Verdana") - --fontsize # font size (default 12) - --countname # count type label (default "samples") - --nametype # name type label (default "Function:") - --colors # "hot", "mem", "io" palette (default "hot") - --hash # colors are keyed by function name hash - --cp # use consistent palette (palette.map) - - eg, - $0 --title="Flame Graph: malloc()" trace.txt > graph.svg -USAGE_END + 'reverse' => \$stackreverse, + 'inverted' => \$inverted, + 'negate' => \$negate, + 'help' => \$help, +) or usage(); +$help && usage(); # internals my $ypad1 = $fontsize * 4; # pad top, include title my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels my $xpad = 10; # pad lefm and right +my $framepad = 1; # vertical padding for frames my $depthmax = 0; my %Events; my %nameattr; +if ($titletext eq "") { + unless ($inverted) { + $titletext = $titledefault; + } else { + $titletext = $titleinverted; + } +} + if ($nameattrfile) { # The name-attribute file format is a function name followed by a tab then # a sequence of tab separated name=value pairs. @@ -146,8 +188,16 @@ } } -if ($colors eq "mem") { $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; } -if ($colors eq "io") { $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; } +# background colors: +# - yellow gradient: default (hot, java, js, perl) +# - blue gradient: mem, chain +# - gray gradient: io, wakeup, flat colors (red, green, blue, ...) +if ($colors eq "mem" or $colors eq "chain") { + $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; +} +if ($colors =~ /^(io|wakeup|red|green|blue|aqua|yellow|purple|orange)$/) { + $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; +} # SVG functions { package SVG; @@ -168,6 +218,7 @@ + SVG } @@ -186,7 +237,7 @@ my @g_attr = map { exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () - } qw(class style onmouseover onmouseout); + } qw(class style onmouseover onmouseout onclick); push @g_attr, $attr->{g_extra} if $attr->{g_extra}; $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); @@ -221,6 +272,7 @@ sub stringTTF { my ($self, $color, $font, $size, $angle, $x, $y, $str, $loc, $extra) = @_; + $x = sprintf "%0.2f", $x; $loc = defined $loc ? $loc : "left"; $extra = defined $extra ? $extra : ""; $self->{svg} .= qq/$str<\/text>\n/; @@ -257,6 +309,7 @@ sub namehash { sub color { my ($type, $hash, $name) = @_; my ($v1, $v2, $v3); + if ($hash) { $v1 = namehash($name); $v2 = $v3 = namehash(scalar reverse $name); @@ -265,6 +318,8 @@ sub color { $v2 = rand(1); $v3 = rand(1); } + + # theme palettes if (defined $type and $type eq "hot") { my $r = 205 + int(50 * $v3); my $g = 0 + int(230 * $v1); @@ -283,15 +338,120 @@ sub color { my $b = 190 + int(55 * $v2); return "rgb($r,$g,$b)"; } + + # multi palettes + if (defined $type and $type eq "java") { + if ($name =~ m:/:) { # Java (match "/" in path) + $type = "green" + } elsif ($name =~ /::/) { # C++ + $type = "yellow"; + } elsif ($name =~ m:_\[k\]:) { # kernel + $type = "orange" + } else { # system + $type = "red"; + } + # fall-through to color palettes + } + if (defined $type and $type eq "perl") { + if ($name =~ /::/) { # C++ + $type = "yellow"; + } elsif ($name =~ m:Perl: or $name =~ m:\.pl:) { # Perl + $type = "green"; + } elsif ($name =~ m:_\[k\]:) { # kernel + $type = "orange" + } else { # system + $type = "red"; + } + # fall-through to color palettes + } + if (defined $type and $type eq "js") { + if ($name =~ /::/) { # C++ + $type = "yellow"; + } elsif ($name =~ m:/:) { # JavaScript (match "/" in path) + $type = "green" + } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin) + $type = "aqua" + } elsif ($name =~ m/^ $/) { # Missing symbol + $type = "green" + } elsif ($name =~ m:_\[k\]:) { # kernel + $type = "orange" + } else { # system + $type = "red"; + } + # fall-through to color palettes + } + if (defined $type and $type eq "wakeup") { + $type = "aqua"; + # fall-through to color palettes + } + if (defined $type and $type eq "chain") { + if ($name =~ m:_\[w\]:) { # waker + $type = "aqua" + } else { # off-CPU + $type = "blue"; + } + # fall-through to color palettes + } + + # color palettes + if (defined $type and $type eq "red") { + my $r = 200 + int(55 * $v1); + my $x = 50 + int(80 * $v1); + return "rgb($r,$x,$x)"; + } + if (defined $type and $type eq "green") { + my $g = 200 + int(55 * $v1); + my $x = 50 + int(60 * $v1); + return "rgb($x,$g,$x)"; + } + if (defined $type and $type eq "blue") { + my $b = 205 + int(50 * $v1); + my $x = 80 + int(60 * $v1); + return "rgb($x,$x,$b)"; + } + if (defined $type and $type eq "yellow") { + my $x = 175 + int(55 * $v1); + my $b = 50 + int(20 * $v1); + return "rgb($x,$x,$b)"; + } + if (defined $type and $type eq "purple") { + my $x = 190 + int(65 * $v1); + my $g = 80 + int(60 * $v1); + return "rgb($x,$g,$x)"; + } + if (defined $type and $type eq "aqua") { + my $r = 50 + int(60 * $v1); + my $g = 165 + int(55 * $v1); + my $b = 165 + int(55 * $v1); + return "rgb($r,$g,$b)"; + } + if (defined $type and $type eq "orange") { + my $r = 190 + int(65 * $v1); + my $g = 90 + int(65 * $v1); + return "rgb($r,$g,0)"; + } + return "rgb(0,0,0)"; } +sub color_scale { + my ($value, $max) = @_; + my ($r, $g, $b) = (255, 255, 255); + $value = -$value if $negate; + if ($value > 0) { + $g = $b = int(210 * ($max - $value) / $max); + } elsif ($value < 0) { + $r = $g = int(210 * ($max + $value) / $max); + } + return "rgb($r,$g,$b)"; +} + sub color_map { my ($colors, $func) = @_; if (exists $palette_map{$func}) { return $palette_map{$func}; } else { - $palette_map{$func} = color($colors); + $palette_map{$func} = color($colors, $hash, $func); return $palette_map{$func}; } } @@ -316,11 +476,12 @@ sub read_palette { } } -my %Node; +my %Node; # Hash of merged frame data my %Tmp; +# flow() merges two stacks, storing the merged frames and value data in %Node. sub flow { - my ($last, $this, $v) = @_; + my ($last, $this, $v, $d) = @_; my $len_a = @$last - 1; my $len_b = @$this - 1; @@ -338,37 +499,118 @@ sub flow { # a unique ID is constructed from "func;depth;etime"; # func-depth isn't unique, it may be repeated later. $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime}; + if (defined $Tmp{$k}->{delta}) { + $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta}; + } delete $Tmp{$k}; } for ($i = $len_same; $i <= $len_b; $i++) { my $k = "$this->[$i];$i"; $Tmp{$k}->{stime} = $v; + if (defined $d) { + $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; + } } return $this; } -# Parse input -my @Data = <>; +# parse input +my @Data; my $last = []; my $time = 0; +my $delta = undef; my $ignored = 0; +my $line; +my $maxdelta = 1; + +# reverse if needed +foreach (<>) { + chomp; + $line = $_; + if ($stackreverse) { + # there may be an extra samples column for differentials + # XXX todo: redo these REs as one. It's repeated below. + my($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + my $samples2 = undef; + if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { + $samples2 = $samples; + ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2"; + } else { + unshift @Data, join(";", reverse split(";", $stack)) . " $samples"; + } + } else { + unshift @Data, $line; + } +} + +# process and merge frames foreach (sort @Data) { chomp; - my ($stack, $samples) = (/^(.*)\s+(\d+(?:\.\d*)?)$/); - unless (defined $samples) { + # process: folded_stack count + # eg: func_a;func_b;func_c 31 + my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + unless (defined $samples and defined $stack) { ++$ignored; next; } + + # there may be an extra samples column for differentials: + my $samples2 = undef; + if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { + $samples2 = $samples; + ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); + } + $delta = undef; + if (defined $samples2) { + $delta = $samples2 - $samples; + $maxdelta = abs($delta) if abs($delta) > $maxdelta; + } + + # clean up SVG breaking characters: $stack =~ tr/<>/()/; - $last = flow($last, [ '', split ";", $stack ], $time); - $time += $samples; + + # for chain graphs, annotate waker frames with "_[w]", for later + # coloring. This is a hack, but has a precedent ("_[k]" from perf). + if ($colors eq "chain") { + my @parts = split ";-;", $stack; + my @newparts = (); + $stack = shift @parts; + $stack .= ";-;"; + foreach my $part (@parts) { + $part =~ s/;/_[w];/g; + $part .= "_[w]"; + push @newparts, $part; + } + $stack .= join ";-;", @parts; + } + + # merge frames and populate %Node: + $last = flow($last, [ '', split ";", $stack ], $time, $delta); + + if (defined $samples2) { + $time += $samples2; + } else { + $time += $samples; + } } -flow($last, [], $time); -warn "Ignored $ignored lines with invalid format\n" if $ignored; -die "ERROR: No stack counts found\n" unless $time; +flow($last, [], $time, $delta); +warn "Ignored $ignored lines with invalid format\n" if $ignored; +unless ($time) { + warn "ERROR: No stack counts found\n"; + my $im = SVG->new(); + # emit an error message SVG, for tools automating flamegraph use + my $imageheight = $fontsize * 5; + $im->header($imagewidth, $imageheight); + $im->stringTTF($im->colorAllocate(0, 0, 0), $fonttype, $fontsize + 2, + 0.0, int($imagewidth / 2), $fontsize * 2, + "ERROR: No valid input provided to flamegraph.pl.", "middle"); + print $im->svg; + exit 2; +} if ($timemax and $timemax < $time) { warn "Specified --total $timemax is less than actual total $time, so ignored\n" if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) @@ -392,7 +634,7 @@ sub flow { $depthmax = $depth if $depth > $depthmax; } -# Draw canvas +# draw canvas, and embed interactive JavaScript program my $imageheight = ($depthmax * $frameheight) + $ypad1 + $ypad2; my $im = SVG->new(); $im->header($imagewidth, $imageheight); @@ -404,14 +646,325 @@ sub flow { INC @@ -425,22 +978,34 @@ sub flow { ); $im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize * 2, $titletext, "middle"); $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), " ", "", 'id="details"'); +$im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $fontsize * 2, + "Reset Zoom", "", 'id="unzoom" onclick="unzoom()" style="opacity:0.0;cursor:pointer"'); +$im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100, + $fontsize * 2, "Search", "", 'id="search" onmouseover="searchover()" onmouseout="searchout()" onclick="search_prompt()" style="opacity:0.1;cursor:pointer"'); +$im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " ", "", 'id="matched"'); if ($palette) { read_palette(); } -# Draw frames +# draw frames while (my ($id, $node) = each %Node) { my ($func, $depth, $etime) = split ";", $id; my $stime = $node->{stime}; + my $delta = $node->{delta}; $etime = $timemax if $func eq "" and $depth == 0; my $x1 = $xpad + $stime * $widthpertime; my $x2 = $xpad + $etime * $widthpertime; - my $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + 1; - my $y2 = $imageheight - $ypad2 - $depth * $frameheight; + my ($y1, $y2); + unless ($inverted) { + $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad; + $y2 = $imageheight - $ypad2 - $depth * $frameheight; + } else { + $y1 = $ypad1 + $depth * $frameheight; + $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad; + } my $samples = sprintf "%.0f", ($etime - $stime) * $factor; (my $samples_txt = $samples) # add commas per perlfaq5 @@ -455,32 +1020,49 @@ sub flow { $escaped_func =~ s/&/&/g; $escaped_func =~ s//>/g; - $info = "$escaped_func ($samples_txt $countname, $pct%)"; + $escaped_func =~ s/"/"/g; + $escaped_func =~ s/_\[[kw]\]$//; # strip any annotation + unless (defined $delta) { + $info = "$escaped_func ($samples_txt $countname, $pct%)"; + } else { + my $d = $negate ? -$delta : $delta; + my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor)); + $deltapct = $d > 0 ? "+$deltapct" : $deltapct; + $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)"; + } } my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone $nameattr->{class} ||= "func_g"; - $nameattr->{onmouseover} ||= "s('".$info."')"; + $nameattr->{onmouseover} ||= "s(this)"; $nameattr->{onmouseout} ||= "c()"; + $nameattr->{onclick} ||= "zoom(this)"; $nameattr->{title} ||= $info; $im->group_start($nameattr); - if ($palette) { - $im->filledRectangle($x1, $y1, $x2, $y2, color_map($colors, $func), 'rx="2" ry="2"'); + my $color; + if ($func eq "-") { + $color = $vdgrey; + } elsif (defined $delta) { + $color = color_scale($delta, $maxdelta); + } elsif ($palette) { + $color = color_map($colors, $func); } else { - my $color = $func eq "-" ? $vdgrey : color($colors, $hash, $func); - $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); + $color = color($colors, $hash, $func); } + $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); + my $text = ""; if ($chars >= 3) { # room for one char plus two dots - my $text = substr $func, 0, $chars; + $func =~ s/_\[[kw]\]$//; # strip any annotation + $text = substr $func, 0, $chars; substr($text, -2, 2) = ".." if $chars < length $func; $text =~ s/&/&/g; $text =~ s//>/g; - $im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, ""); } + $im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, ""); $im->group_end($nameattr); }