From 183c3b79a6667c5671c3ee753c8d7788611e47a1 Mon Sep 17 00:00:00 2001 From: Camilo Lopez Date: Thu, 6 Feb 2014 15:24:05 +0000 Subject: [PATCH 01/12] Allow the use of a Proc to decide if the middleware is enabled --- lib/stackprof/middleware.rb | 9 ++++++++- test/test_middleware.rb | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/stackprof/middleware.rb b/lib/stackprof/middleware.rb index b298499a..23f6aefa 100644 --- a/lib/stackprof/middleware.rb +++ b/lib/stackprof/middleware.rb @@ -29,7 +29,14 @@ def call(env) class << self attr_accessor :enabled, :mode, :interval, :path - alias enabled? enabled + + def enabled? + if enabled.respond_to?(:call) + enabled.call + else + enabled + end + end def save(filename = nil) if results = StackProf.results diff --git a/test/test_middleware.rb b/test/test_middleware.rb index 20752c77..5e9297e8 100644 --- a/test/test_middleware.rb +++ b/test/test_middleware.rb @@ -38,4 +38,12 @@ def test_save_custom StackProf::Middleware.save end + def test_enabled_should_use_a_proc_if_passed + StackProf::Middleware.new(Object.new, enabled: Proc.new{ false }) + refute StackProf::Middleware.enabled? + + StackProf::Middleware.new(Object.new, enabled: Proc.new{ true }) + assert StackProf::Middleware.enabled? + end + end From af0efa0d01446864526d3b3804a2a469119735fb Mon Sep 17 00:00:00 2001 From: Camilo Lopez Date: Thu, 6 Feb 2014 15:48:39 +0000 Subject: [PATCH 02/12] Avoid possibly inconsistent state --- lib/stackprof/middleware.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/stackprof/middleware.rb b/lib/stackprof/middleware.rb index 23f6aefa..1548d4dd 100644 --- a/lib/stackprof/middleware.rb +++ b/lib/stackprof/middleware.rb @@ -15,10 +15,11 @@ def initialize(app, options = {}) end def call(env) - StackProf.start(mode: Middleware.mode, interval: Middleware.interval) if Middleware.enabled? + enabled = Middleware.enabled? + StackProf.start(mode: Middleware.mode, interval: Middleware.interval) if enabled @app.call(env) ensure - if Middleware.enabled? + if enabled StackProf.stop if @num_reqs && (@num_reqs-=1) == 0 @num_reqs = @options[:save_every] From 7200e93717b65b4498e1aa1ba41bd4e0f303c580 Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Thu, 27 Feb 2014 14:16:25 -0500 Subject: [PATCH 03/12] Allow Middleware.enabled to override the mode --- lib/stackprof/middleware.rb | 4 ++-- test/test_middleware.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/stackprof/middleware.rb b/lib/stackprof/middleware.rb index 1548d4dd..8aed5d8c 100644 --- a/lib/stackprof/middleware.rb +++ b/lib/stackprof/middleware.rb @@ -15,8 +15,8 @@ def initialize(app, options = {}) end def call(env) - enabled = Middleware.enabled? - StackProf.start(mode: Middleware.mode, interval: Middleware.interval) if enabled + enabled, mode = Middleware.enabled? + StackProf.start(mode: mode || Middleware.mode, interval: Middleware.interval) if enabled @app.call(env) ensure if enabled diff --git a/test/test_middleware.rb b/test/test_middleware.rb index 5e9297e8..3b044636 100644 --- a/test/test_middleware.rb +++ b/test/test_middleware.rb @@ -46,4 +46,18 @@ def test_enabled_should_use_a_proc_if_passed assert StackProf::Middleware.enabled? 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'] }) + enabled, mode = StackProf::Middleware.enabled? + assert enabled + assert_equal 'foo', mode + + StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval}) + StackProf.expects(:stop) + + middleware.call(nil) + assert proc_called + end + end From 9cb4aba1fb9e68402f72d4212946a850912c911b Mon Sep 17 00:00:00 2001 From: Camilo Lopez Date: Tue, 15 Jul 2014 15:09:09 +0000 Subject: [PATCH 04/12] Bump spec --- stackprof.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackprof.gemspec b/stackprof.gemspec index 9a77f037..02439825 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.6' + s.version = '0.2.7' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From fbfcbc5cadb63e3edb6671fdd2e2472d8b38615a Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 20 Apr 2015 19:47:39 +0000 Subject: [PATCH 05/12] middleware: allow raw dumps Signed-off-by: Simon Eskildsen --- Gemfile.lock | 2 +- lib/stackprof/middleware.rb | 5 +++-- test/test_middleware.rb | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb8078ba..ba5d78f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - stackprof (0.2.6) + stackprof (0.2.7) GEM remote: https://rubygems.org/ diff --git a/lib/stackprof/middleware.rb b/lib/stackprof/middleware.rb index a1756ad9..43c9bb5f 100644 --- a/lib/stackprof/middleware.rb +++ b/lib/stackprof/middleware.rb @@ -11,12 +11,13 @@ def initialize(app, options = {}) Middleware.interval = options[:interval] || 1000 Middleware.enabled = options[:enabled] Middleware.path = options[:path] || 'tmp' + Middleware.raw = options[:raw] || false at_exit{ Middleware.save } if options[:save_at_exit] end def call(env) enabled, mode = Middleware.enabled? - StackProf.start(mode: mode || Middleware.mode, interval: Middleware.interval) if enabled + StackProf.start(mode: mode || Middleware.mode, raw: Middleware.raw, interval: Middleware.interval) if enabled @app.call(env) ensure if enabled @@ -29,7 +30,7 @@ def call(env) end class << self - attr_accessor :enabled, :mode, :interval, :path + attr_accessor :enabled, :mode, :interval, :path, :raw def enabled? if enabled.respond_to?(:call) diff --git a/test/test_middleware.rb b/test/test_middleware.rb index 3b044636..a5752725 100644 --- a/test/test_middleware.rb +++ b/test/test_middleware.rb @@ -53,11 +53,24 @@ def test_enabled_should_override_mode_if_a_proc assert enabled assert_equal 'foo', mode - StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval}) + StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval, raw: false}) StackProf.expects(:stop) middleware.call(nil) assert proc_called end + def test_raw_should_start_with_raw + proc_called = false + + middleware = StackProf::Middleware.new(proc { |env| proc_called = true }, enabled: [true, 'foo'], raw: true) + + StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval, raw: true}) + StackProf.expects(:stop) + + middleware.call(nil) + + assert proc_called + end + end From e415702032c9eeb20bbbc75bbdc7308629788281 Mon Sep 17 00:00:00 2001 From: Shiv Nagarajan Date: Wed, 13 Apr 2016 11:18:34 -0400 Subject: [PATCH 06/12] ensure raw is present in joined output --- lib/stackprof/report.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index b51cf06c..8e4c1a6a 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -15,13 +15,16 @@ def frames(sort_by_total=false) 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]}") + # 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)| + # include raw also + raw_frames = @data[:raw].map {|v| id2hash[v.to_s] || v } if @data[:raw] + 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 @@ -290,7 +293,8 @@ 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 + 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] @@ -325,7 +329,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) From 303e3dc9ef0f8d05fb739bb01ad11730a0ae9153 Mon Sep 17 00:00:00 2001 From: Shiv Nagarajan Date: Mon, 18 Apr 2016 10:23:31 -0400 Subject: [PATCH 07/12] add comments --- lib/stackprof/report.rb | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index 8e4c1a6a..1798eeb9 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -12,14 +12,29 @@ def frames(sort_by_total=false) Hash[ *@data[:frames].sort_by{ |iseq, stats| -stats[sort_by_total ? :total_samples : :samples] }.flatten(1) ] 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| + # 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 - # include raw also + # 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] @@ -293,6 +308,7 @@ 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 + # 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| @@ -322,6 +338,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], From bec11d383b6cdc4151d7518d7c52b83c054d2f6c Mon Sep 17 00:00:00 2001 From: Shiv Nagarajan Date: Sat, 7 May 2016 21:15:25 -0400 Subject: [PATCH 08/12] bump flamegraph script --- vendor/FlameGraph/flamegraph.pl | 684 +++++++++++++++++++++++++++++--- 1 file changed, 633 insertions(+), 51 deletions(-) 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); } From 435df56d75ee49014093b34637de25be77fe0cf5 Mon Sep 17 00:00:00 2001 From: Shiv Nagarajan Date: Tue, 17 May 2016 13:17:01 -0400 Subject: [PATCH 09/12] bump to 0.29 --- Gemfile.lock | 5 +++- lib/stackprof/middleware.rb | 9 ++++--- test/test_middleware.rb | 48 ++++++++++++++++++------------------- 3 files changed, 32 insertions(+), 30 deletions(-) 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 341a87fb..f575b6f6 100644 --- a/lib/stackprof/middleware.rb +++ b/lib/stackprof/middleware.rb @@ -12,13 +12,12 @@ def initialize(app, options = {}) Middleware.raw = options[:raw] || false Middleware.enabled = options[:enabled] Middleware.path = options[:path] || 'tmp' - Middleware.raw = options[:raw] || false 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 @@ -33,9 +32,9 @@ def call(env) class << self attr_accessor :enabled, :mode, :interval, :raw, :path - def enabled? + def enabled?(env) if enabled.respond_to?(:call) - enabled.call + enabled.call(env) else enabled end diff --git a/test/test_middleware.rb b/test/test_middleware.rb index 16ce459a..76e41802 100644 --- a/test/test_middleware.rb +++ b/test/test_middleware.rb @@ -39,36 +39,21 @@ def test_save_custom end def test_enabled_should_use_a_proc_if_passed + env = {} + StackProf::Middleware.new(Object.new, enabled: Proc.new{ false }) - refute StackProf::Middleware.enabled? + refute StackProf::Middleware.enabled?(env) StackProf::Middleware.new(Object.new, enabled: Proc.new{ true }) - assert StackProf::Middleware.enabled? - 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'] }) - enabled, mode = StackProf::Middleware.enabled? - assert enabled - assert_equal 'foo', mode - - StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval, raw: false}) - StackProf.expects(:stop) - - middleware.call(nil) - assert proc_called + assert StackProf::Middleware.enabled?(env) end - def test_raw_should_start_with_raw - proc_called = false - - middleware = StackProf::Middleware.new(proc { |env| proc_called = true }, enabled: [true, 'foo'], raw: true) + def test_enabled_should_use_a_proc_if_passed_and_use_the_request_env + enable_proc = Proc.new {|env| env['PROFILE'] } - StackProf.expects(:start).with({mode: 'foo', interval: StackProf::Middleware.interval, raw: true}) - StackProf.expects(:stop) - - middleware.call(nil) + env = Hash.new { false } + StackProf::Middleware.new(Object.new, enabled: enable_proc) + refute StackProf::Middleware.enabled?(env) env = Hash.new { true } StackProf::Middleware.new(Object.new, enabled: enable_proc) @@ -79,4 +64,19 @@ 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 end From 631ee05ea5196521c7f5754232b581c6ab0fb0e3 Mon Sep 17 00:00:00 2001 From: Masataka Kuwabara Date: Mon, 13 Jul 2015 18:21:49 +0900 Subject: [PATCH 10/12] Fix modeline --- ext/stackprof.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/stackprof.c b/ext/stackprof.c index b5964a90..d7b3a35b 100644 --- a/ext/stackprof.c +++ b/ext/stackprof.c @@ -2,7 +2,7 @@ stackprof.c - Sampling call-stack frame profiler for MRI. - vim: setl noexpandtab shiftwidth=4 tabstop=8 softtabstop=4 + vim: noexpandtab shiftwidth=4 tabstop=8 softtabstop=4 **********************************************************************/ From 10ad738c874fb2916b58d5ddf459676f0cd45e60 Mon Sep 17 00:00:00 2001 From: Scott Francis Date: Tue, 23 May 2017 21:03:23 -0400 Subject: [PATCH 11/12] Fix missed samples stat in object mode --- ext/stackprof.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ext/stackprof.c b/ext/stackprof.c index d7b3a35b..eee32aa6 100644 --- a/ext/stackprof.c +++ b/ext/stackprof.c @@ -40,6 +40,7 @@ static struct { size_t overall_signals; size_t overall_samples; + size_t newobj_signals; size_t during_gc; st_table *frames; @@ -86,6 +87,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self) _stackprof.frames = st_init_numtable(); _stackprof.overall_signals = 0; _stackprof.overall_samples = 0; + _stackprof.newobj_signals = 0; _stackprof.during_gc = 0; } @@ -436,9 +438,10 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext) static void stackprof_newobj_handler(VALUE tpval, void *data) { - _stackprof.overall_signals++; - if (RTEST(_stackprof.interval) && _stackprof.overall_signals % NUM2LONG(_stackprof.interval)) + _stackprof.newobj_signals++; + if (RTEST(_stackprof.interval) && _stackprof.newobj_signals % NUM2LONG(_stackprof.interval)) return; + _stackprof.overall_signals++; stackprof_job_handler(0); } From bb9fbc179985eea3c49079e29656dfc09a6e3518 Mon Sep 17 00:00:00 2001 From: Scott Francis Date: Wed, 24 May 2017 08:30:51 -0400 Subject: [PATCH 12/12] Add test --- test/test_stackprof.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_stackprof.rb b/test/test_stackprof.rb index e09147d9..aa39b3cf 100644 --- a/test/test_stackprof.rb +++ b/test/test_stackprof.rb @@ -58,6 +58,14 @@ def test_object_allocation_interval assert_equal 10, profile[:samples] end + def test_object_allocation_missed_samples + profile = StackProf.run(mode: :object, interval: 100) do + 1000.times { Object.new } + end + assert_equal 10, profile[:samples] + assert_equal 0, profile[:missed_samples] + end + def test_cputime profile = StackProf.run(mode: :cpu, interval: 500) do math