stackprof 0.2.15 → 0.2.16

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19bdf20501dd49e005d6e9837cb614ede25adfe9da5ce42edff45f42c4f8ae7d
4
- data.tar.gz: 02a2418957b11e71bbab119102b3880c8374fbcf52f480ebe0e03bb5a045b8d4
3
+ metadata.gz: 109d5dc07fefb68933ae164c88420aecc662593f9fa96e102a90e7c8d4c605a9
4
+ data.tar.gz: 7cbd4e6919a160f5e7b680bd0994e9e33097df03c7098ffda7317996a8afb1f6
5
5
  SHA512:
6
- metadata.gz: 050df074e6fc876fe212d34a35ec258a6cd7cecbdb9dfa51384bf58fadcd7e3e18d49fb5385c41fb5b79727795e467d27f8c41c98b0f95ff02fb297efa4438df
7
- data.tar.gz: 77dafa295e0de1d75990a636e7c77f7a4632b98d71ff9d7b37db106dee6701306f4dc75cbe161ba6969bf86c181647b29f9ac1da85afb48b92172ae1d506ac54
6
+ metadata.gz: d4c6894359a809ea8e504eca85506179eb598d2b0db6833b1661e08d982efb78ec0fa526e73ba30a7388c443b5b4eb16a466baa7dd3dcd4e139c0bec7b22ab5d
7
+ data.tar.gz: 040bc4d3c1ffb1f724bce5ca7db4ddeae7af7ecdc526f893f878049bf42c715983802def5a2e8905f6b1016401a478457a237ef0a76dc742e7070780940e762b
data/.gitignore CHANGED
@@ -3,3 +3,4 @@
3
3
  /lib/stackprof/stackprof.so
4
4
  *.sw?
5
5
  /pkg
6
+ /Gemfile.lock
@@ -1,3 +1,14 @@
1
+ # 0.2.16
2
+
3
+ * [flamegraph.pl] Update to latest version
4
+ * Add option to ignore GC frames
5
+ * Handle source code not being available
6
+ * Freeze strings in report.rb
7
+ * Use a cursor object instead of array slicing
8
+ * ArgumentError on interval <1 or >1m
9
+ * fix variable name.
10
+ * Fix default mode comment in readme
11
+
1
12
  # 0.2.15
2
13
 
3
- * Mark the metadata object before the GC is invoked to prevent it from being garbage collected.
14
+ * Mark the metadata object before the GC is invoked to prevent it from being garbage collected.
data/README.md CHANGED
@@ -109,8 +109,8 @@ And just open the result by your browser.
109
109
 
110
110
  four sampling modes are supported:
111
111
 
112
- - :wall (using `ITIMER_REAL` and `SIGALRM`)
113
- - :cpu (using `ITIMER_PROF` and `SIGPROF`) [default mode]
112
+ - :wall (using `ITIMER_REAL` and `SIGALRM`) [default mode]
113
+ - :cpu (using `ITIMER_PROF` and `SIGPROF`)
114
114
  - :object (using `RUBY_INTERNAL_EVENT_NEWOBJ`)
115
115
  - :custom (user-defined via `StackProf.sample`)
116
116
 
@@ -141,6 +141,12 @@ StackProf.run(mode: :object, out: 'tmp/stackprof.dump', interval: 1) do
141
141
  end
142
142
  ```
143
143
 
144
+ by default, samples taken during garbage collection will show as garbage collection frames
145
+ including both mark and sweep phases. for longer traces, these can leave gaps in a flamegraph
146
+ that are hard to follow and can be disabled by setting the `ignore_gc` option to true.
147
+ garbage collection time will still be present in the profile but not explicitly marked with
148
+ its own frame.
149
+
144
150
  samples are taken using a combination of three new C-APIs in ruby 2.1:
145
151
 
146
152
  - signal handlers enqueue a sampling job using `rb_postponed_job_register_one`.
@@ -328,6 +334,7 @@ Option | Meaning
328
334
  `mode` | mode of sampling: `:cpu`, `:wall`, `:object`, or `:custom` [c.f.](#sampling)
329
335
  `out` | the target file, which will be overwritten
330
336
  `interval` | mode-relative sample rate [c.f.](#sampling)
337
+ `ignore_gc` | Ignore garbage collection frames
331
338
  `aggregate` | defaults: `true` - if `false` disables [aggregation](#aggregation)
332
339
  `raw` | defaults `false` - if `true` collects the extra data required by the `--flamegraph` and `--stackcollapse` report types
333
340
  `metadata` | defaults to `{}`. Must be a `Hash`. metadata associated with this profile
@@ -16,6 +16,7 @@
16
16
  #include <pthread.h>
17
17
 
18
18
  #define BUF_SIZE 2048
19
+ #define MICROSECONDS_IN_SECOND 1000000
19
20
 
20
21
  #define FAKE_FRAME_GC INT2FIX(0)
21
22
  #define FAKE_FRAME_MARK INT2FIX(1)
@@ -46,6 +47,7 @@ static struct {
46
47
  VALUE interval;
47
48
  VALUE out;
48
49
  VALUE metadata;
50
+ int ignore_gc;
49
51
 
50
52
  VALUE *raw_samples;
51
53
  size_t raw_samples_len;
@@ -73,8 +75,8 @@ static struct {
73
75
 
74
76
  static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
75
77
  static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines;
76
- static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_metadata, sym_frames, sym_out, sym_aggregate, sym_raw_timestamp_deltas;
77
- static VALUE sym_state, sym_marking, sym_sweeping;
78
+ static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_metadata, sym_frames, sym_ignore_gc, sym_out;
79
+ static VALUE sym_aggregate, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping;
78
80
  static VALUE sym_gc_samples, objtracer;
79
81
  static VALUE gc_hook;
80
82
  static VALUE rb_mStackProf;
@@ -88,6 +90,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
88
90
  struct sigaction sa;
89
91
  struct itimerval timer;
90
92
  VALUE opts = Qnil, mode = Qnil, interval = Qnil, metadata = rb_hash_new(), out = Qfalse;
93
+ int ignore_gc = 0;
91
94
  int raw = 0, aggregate = 1;
92
95
 
93
96
  if (_stackprof.running)
@@ -99,6 +102,9 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
99
102
  mode = rb_hash_aref(opts, sym_mode);
100
103
  interval = rb_hash_aref(opts, sym_interval);
101
104
  out = rb_hash_aref(opts, sym_out);
105
+ if (RTEST(rb_hash_aref(opts, sym_ignore_gc))) {
106
+ ignore_gc = 1;
107
+ }
102
108
 
103
109
  VALUE metadata_val = rb_hash_aref(opts, sym_metadata);
104
110
  if (RTEST(metadata_val)) {
@@ -115,6 +121,10 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
115
121
  }
116
122
  if (!RTEST(mode)) mode = sym_wall;
117
123
 
124
+ if (!NIL_P(interval) && (NUM2INT(interval) < 1 || NUM2INT(interval) >= MICROSECONDS_IN_SECOND)) {
125
+ rb_raise(rb_eArgError, "interval is a number of microseconds between 1 and 1 million");
126
+ }
127
+
118
128
  if (!_stackprof.frames) {
119
129
  _stackprof.frames = st_init_numtable();
120
130
  _stackprof.overall_signals = 0;
@@ -151,6 +161,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
151
161
  _stackprof.aggregate = aggregate;
152
162
  _stackprof.mode = mode;
153
163
  _stackprof.interval = interval;
164
+ _stackprof.ignore_gc = ignore_gc;
154
165
  _stackprof.metadata = metadata;
155
166
  _stackprof.out = out;
156
167
 
@@ -610,7 +621,7 @@ static void
610
621
  stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
611
622
  {
612
623
  _stackprof.overall_signals++;
613
- if (rb_during_gc()) {
624
+ if (!_stackprof.ignore_gc && rb_during_gc()) {
614
625
  VALUE mode = rb_gc_latest_gc_info(sym_state);
615
626
  if (mode == sym_marking) {
616
627
  _stackprof.unrecorded_gc_marking_samples++;
@@ -722,6 +733,7 @@ Init_stackprof(void)
722
733
  S(raw_timestamp_deltas);
723
734
  S(out);
724
735
  S(metadata);
736
+ S(ignore_gc);
725
737
  S(frames);
726
738
  S(aggregate);
727
739
  S(state);
@@ -1,7 +1,7 @@
1
1
  require "stackprof/stackprof"
2
2
 
3
3
  module StackProf
4
- VERSION = '0.2.15'
4
+ VERSION = '0.2.16'
5
5
  end
6
6
 
7
7
  StackProf.autoload :Report, "stackprof/report.rb"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pp'
2
4
  require 'digest/md5'
3
5
 
@@ -38,7 +40,7 @@ module StackProf
38
40
  end
39
41
 
40
42
  def max_samples
41
- @data[:max_samples] ||= frames.max_by{ |addr, frame| frame[:samples] }.last[:samples]
43
+ @data[:max_samples] ||= @data[:frames].values.max_by{ |frame| frame[:samples] }[:samples]
42
44
  end
43
45
 
44
46
  def files
@@ -93,17 +95,50 @@ module StackProf
93
95
  print_flamegraph(f, skip_common, true)
94
96
  end
95
97
 
98
+ StackCursor = Struct.new(:raw, :idx, :length) do
99
+ def weight
100
+ @weight ||= raw[1 + idx + length]
101
+ end
102
+
103
+ def [](i)
104
+ if i >= length
105
+ nil
106
+ else
107
+ raw[1 + idx + i]
108
+ end
109
+ end
110
+
111
+ def <=>(other)
112
+ i = 0
113
+ while i < length && i < other.length
114
+ if self[i] != other[i]
115
+ return self[i] <=> other[i]
116
+ end
117
+ i += 1
118
+ end
119
+
120
+ return length <=> other.length
121
+ end
122
+ end
123
+
96
124
  def print_flamegraph(f, skip_common, alphabetical=false)
97
125
  raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
98
126
 
99
127
  stacks = []
100
128
  max_x = 0
101
129
  max_y = 0
102
- while len = raw.shift
130
+
131
+ idx = 0
132
+ loop do
133
+ len = raw[idx]
134
+ break unless len
103
135
  max_y = len if len > max_y
104
- stack = raw.slice!(0, len+1)
136
+
137
+ stack = StackCursor.new(raw, idx, len)
105
138
  stacks << stack
106
- max_x += stack.last
139
+ max_x += stack.weight
140
+
141
+ idx += len + 2
107
142
  end
108
143
 
109
144
  stacks.sort! if alphabetical
@@ -115,7 +150,7 @@ module StackProf
115
150
  x = 0
116
151
 
117
152
  stacks.each do |stack|
118
- weight = stack.last
153
+ weight = stack.weight
119
154
  cell = stack[y] unless y == stack.length-1
120
155
 
121
156
  if cell.nil?
@@ -157,7 +192,7 @@ module StackProf
157
192
  end
158
193
 
159
194
  def flamegraph_row(f, x, y, weight, addr)
160
- frame = frames[addr]
195
+ frame = @data[:frames][addr]
161
196
  f.print ',' if @rows_started
162
197
  @rows_started = true
163
198
  f.puts %{{"x":#{x},"y":#{y},"width":#{weight},"frame_id":#{addr},"frame":#{frame[:name].dump},"file":#{frame[:file].dump}}}
@@ -178,7 +213,7 @@ module StackProf
178
213
  weight += stack.last
179
214
  end
180
215
  else
181
- frame = frames[val]
216
+ frame = @data[:frames][val]
182
217
  child_name = "#{ frame[:name] } : #{ frame[:file] }"
183
218
  child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
184
219
  weight += child_data["value"]
@@ -654,7 +689,8 @@ module StackProf
654
689
  end
655
690
  end
656
691
  end
692
+ rescue SystemCallError
693
+ f.puts " SOURCE UNAVAILABLE"
657
694
  end
658
-
659
695
  end
660
696
  end
@@ -1,11 +1,18 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'stackprof'
3
- s.version = '0.2.15'
3
+ s.version = '0.2.16'
4
4
  s.homepage = 'http://github.com/tmm1/stackprof'
5
5
 
6
6
  s.authors = 'Aman Gupta'
7
7
  s.email = 'aman@tmm1.net'
8
8
 
9
+ s.metadata = {
10
+ 'bug_tracker_uri' => 'https://github.com/tmm1/stackprof/issues',
11
+ 'changelog_uri' => "https://github.com/tmm1/stackprof/blob/v#{s.version}/CHANGELOG.md",
12
+ 'documentation_uri' => "https://www.rubydoc.info/gems/stackprof/#{s.version}",
13
+ 'source_code_uri' => "https://github.com/tmm1/stackprof/tree/v#{s.version}"
14
+ }
15
+
9
16
  s.files = `git ls-files`.split("\n")
10
17
  s.extensions = 'ext/stackprof/extconf.rb'
11
18
 
@@ -232,6 +232,15 @@ class StackProfTest < MiniTest::Test
232
232
  refute_empty profile[:frames]
233
233
  end
234
234
 
235
+ def test_min_max_interval
236
+ [-1, 0, 1_000_000, 1_000_001].each do |invalid_interval|
237
+ err = assert_raises(ArgumentError, "invalid interval #{invalid_interval}") do
238
+ StackProf.run(interval: invalid_interval, debug: true) {}
239
+ end
240
+ assert_match(/microseconds/, err.message)
241
+ end
242
+ end
243
+
235
244
  def math
236
245
  250_000.times do
237
246
  2 ** 10
@@ -10,23 +10,46 @@
10
10
  #
11
11
  # grep funcA input.txt | ./flamegraph.pl [options] > graph.svg
12
12
  #
13
+ # Then open the resulting .svg in a web browser, for interactivity: mouse-over
14
+ # frames for info, click to zoom, and ctrl-F to search.
15
+ #
13
16
  # Options are listed in the usage message (--help).
14
17
  #
15
18
  # The input is stack frames and sample counts formatted as single lines. Each
16
19
  # 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.
20
+ # of the line. These can be generated for Linux perf script output using
21
+ # stackcollapse-perf.pl, for DTrace using stackcollapse.pl, and for other tools
22
+ # using the other stackcollapse programs. Example input:
23
+ #
24
+ # swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 1
25
+ #
26
+ # An optional extra column of counts can be provided to generate a differential
27
+ # flame graph of the counts, colored red for more, and blue for less. This
28
+ # can be useful when using flame graphs for non-regression testing.
29
+ # See the header comment in the difffolded.pl program for instructions.
19
30
  #
20
- # The output graph shows relative presence of functions in stack samples. The
21
- # ordering on the x-axis has no meaning; since the data is samples, time order
22
- # of events is not known. The order used sorts function names alphabetically.
31
+ # The input functions can optionally have annotations at the end of each
32
+ # function name, following a precedent by some tools (Linux perf's _[k]):
33
+ # _[k] for kernel
34
+ # _[i] for inlined
35
+ # _[j] for jit
36
+ # _[w] for waker
37
+ # Some of the stackcollapse programs support adding these annotations, eg,
38
+ # stackcollapse-perf.pl --kernel --jit. They are used merely for colors by
39
+ # some palettes, eg, flamegraph.pl --color=java.
40
+ #
41
+ # The output flame graph shows relative presence of functions in stack samples.
42
+ # The ordering on the x-axis has no meaning; since the data is samples, time
43
+ # order of events is not known. The order used sorts function names
44
+ # alphabetically.
23
45
  #
24
46
  # While intended to process stack samples, this can also process stack traces.
25
47
  # For example, tracing stacks for memory allocation, or resource usage. You
26
48
  # can use --title to set the title to reflect the content, and --countname
27
49
  # to change "samples" to "bytes" etc.
28
50
  #
29
- # There are a few different palettes, selectable using --color. Functions
51
+ # There are a few different palettes, selectable using --color. By default,
52
+ # the colors are selected at random (except for differentials). Functions
30
53
  # called "-" will be printed gray, which can be used for stack separators (eg,
31
54
  # between user and kernel stacks).
32
55
  #
@@ -38,6 +61,7 @@
38
61
  # was in turn inspired by the work on vftrace by Jan Boerhout". See:
39
62
  # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and
40
63
  #
64
+ # Copyright 2016 Netflix, Inc.
41
65
  # Copyright 2011 Joyent, Inc. All rights reserved.
42
66
  # Copyright 2011 Brendan Gregg. All rights reserved.
43
67
  #
@@ -60,6 +84,7 @@
60
84
  #
61
85
  # CDDL HEADER END
62
86
  #
87
+ # 11-Oct-2014 Adrien Mahieux Added zoom.
63
88
  # 21-Nov-2013 Shawn Sterling Added consistent palette file option
64
89
  # 17-Mar-2013 Tim Bunce Added options and more tunables.
65
90
  # 15-Dec-2011 Dave Pacheco Support for frames with whitespace.
@@ -69,6 +94,8 @@ use strict;
69
94
 
70
95
  use Getopt::Long;
71
96
 
97
+ use open qw(:std :utf8);
98
+
72
99
  # tunables
73
100
  my $encoding;
74
101
  my $fonttype = "Verdana";
@@ -77,12 +104,10 @@ my $frameheight = 16; # max height is dynamic
77
104
  my $fontsize = 12; # base text size
78
105
  my $fontwidth = 0.59; # avg width relative to fontsize
79
106
  my $minwidth = 0.1; # min function width, pixels
80
- my $titletext = "Flame Graph"; # centered heading
81
107
  my $nametype = "Function:"; # what are the names in the data?
82
108
  my $countname = "samples"; # what are the counts in the data?
83
109
  my $colors = "hot"; # color theme
84
- my $bgcolor1 = "#eeeeee"; # background color gradient start
85
- my $bgcolor2 = "#eeeeb0"; # background color gradient stop
110
+ my $bgcolors = ""; # background color theme
86
111
  my $nameattrfile; # file holding function attributes
87
112
  my $timemax; # (override the) sum of the counts
88
113
  my $factor = 1; # factor to scale counts by
@@ -90,6 +115,48 @@ my $hash = 0; # color by function name
90
115
  my $palette = 0; # if we use consistent palettes (default off)
91
116
  my %palette_map; # palette map hash
92
117
  my $pal_file = "palette.map"; # palette map file name
118
+ my $stackreverse = 0; # reverse stack order, switching merge end
119
+ my $inverted = 0; # icicle graph
120
+ my $flamechart = 0; # produce a flame chart (sort by time, do not merge stacks)
121
+ my $negate = 0; # switch differential hues
122
+ my $titletext = ""; # centered heading
123
+ my $titledefault = "Flame Graph"; # overwritten by --title
124
+ my $titleinverted = "Icicle Graph"; # " "
125
+ my $searchcolor = "rgb(230,0,230)"; # color for search highlighting
126
+ my $notestext = ""; # embedded notes in SVG
127
+ my $subtitletext = ""; # second level title (optional)
128
+ my $help = 0;
129
+
130
+ sub usage {
131
+ die <<USAGE_END;
132
+ USAGE: $0 [options] infile > outfile.svg\n
133
+ --title TEXT # change title text
134
+ --subtitle TEXT # second level title (optional)
135
+ --width NUM # width of image (default 1200)
136
+ --height NUM # height of each frame (default 16)
137
+ --minwidth NUM # omit smaller functions (default 0.1 pixels)
138
+ --fonttype FONT # font type (default "Verdana")
139
+ --fontsize NUM # font size (default 12)
140
+ --countname TEXT # count type label (default "samples")
141
+ --nametype TEXT # name type label (default "Function:")
142
+ --colors PALETTE # set color palette. choices are: hot (default), mem,
143
+ # io, wakeup, chain, java, js, perl, red, green, blue,
144
+ # aqua, yellow, purple, orange
145
+ --bgcolors COLOR # set background colors. gradient choices are yellow
146
+ # (default), blue, green, grey; flat colors use "#rrggbb"
147
+ --hash # colors are keyed by function name hash
148
+ --cp # use consistent palette (palette.map)
149
+ --reverse # generate stack-reversed flame graph
150
+ --inverted # icicle graph
151
+ --flamechart # produce a flame chart (sort by time, do not merge stacks)
152
+ --negate # switch differential hues (blue<->red)
153
+ --notes TEXT # add notes comment in SVG (for debugging)
154
+ --help # this message
155
+
156
+ eg,
157
+ $0 --title="Flame Graph: malloc()" trace.txt > graph.svg
158
+ USAGE_END
159
+ }
93
160
 
94
161
  GetOptions(
95
162
  'fonttype=s' => \$fonttype,
@@ -100,40 +167,47 @@ GetOptions(
100
167
  'fontwidth=f' => \$fontwidth,
101
168
  'minwidth=f' => \$minwidth,
102
169
  'title=s' => \$titletext,
170
+ 'subtitle=s' => \$subtitletext,
103
171
  'nametype=s' => \$nametype,
104
172
  'countname=s' => \$countname,
105
173
  'nameattr=s' => \$nameattrfile,
106
174
  'total=s' => \$timemax,
107
175
  'factor=f' => \$factor,
108
176
  'colors=s' => \$colors,
177
+ 'bgcolors=s' => \$bgcolors,
109
178
  'hash' => \$hash,
110
179
  'cp' => \$palette,
111
- ) or die <<USAGE_END;
112
- USAGE: $0 [options] infile > outfile.svg\n
113
- --title # change title text
114
- --width # width of image (default 1200)
115
- --height # height of each frame (default 16)
116
- --minwidth # omit smaller functions (default 0.1 pixels)
117
- --fonttype # font type (default "Verdana")
118
- --fontsize # font size (default 12)
119
- --countname # count type label (default "samples")
120
- --nametype # name type label (default "Function:")
121
- --colors # "hot", "mem", "io" palette (default "hot")
122
- --hash # colors are keyed by function name hash
123
- --cp # use consistent palette (palette.map)
124
-
125
- eg,
126
- $0 --title="Flame Graph: malloc()" trace.txt > graph.svg
127
- USAGE_END
180
+ 'reverse' => \$stackreverse,
181
+ 'inverted' => \$inverted,
182
+ 'flamechart' => \$flamechart,
183
+ 'negate' => \$negate,
184
+ 'notes=s' => \$notestext,
185
+ 'help' => \$help,
186
+ ) or usage();
187
+ $help && usage();
128
188
 
129
189
  # internals
130
- my $ypad1 = $fontsize * 4; # pad top, include title
190
+ my $ypad1 = $fontsize * 3; # pad top, include title
131
191
  my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels
192
+ my $ypad3 = $fontsize * 2; # pad top, include subtitle (optional)
132
193
  my $xpad = 10; # pad lefm and right
194
+ my $framepad = 1; # vertical padding for frames
133
195
  my $depthmax = 0;
134
196
  my %Events;
135
197
  my %nameattr;
136
198
 
199
+ if ($flamechart && $titletext eq "") {
200
+ $titletext = "Flame Chart";
201
+ }
202
+
203
+ if ($titletext eq "") {
204
+ unless ($inverted) {
205
+ $titletext = $titledefault;
206
+ } else {
207
+ $titletext = $titleinverted;
208
+ }
209
+ }
210
+
137
211
  if ($nameattrfile) {
138
212
  # The name-attribute file format is a function name followed by a tab then
139
213
  # a sequence of tab separated name=value pairs.
@@ -146,8 +220,42 @@ if ($nameattrfile) {
146
220
  }
147
221
  }
148
222
 
149
- if ($colors eq "mem") { $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff"; }
150
- if ($colors eq "io") { $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; }
223
+ if ($notestext =~ /[<>]/) {
224
+ die "Notes string can't contain < or >"
225
+ }
226
+
227
+ # background colors:
228
+ # - yellow gradient: default (hot, java, js, perl)
229
+ # - green gradient: mem
230
+ # - blue gradient: io, wakeup, chain
231
+ # - gray gradient: flat colors (red, green, blue, ...)
232
+ if ($bgcolors eq "") {
233
+ # choose a default
234
+ if ($colors eq "mem") {
235
+ $bgcolors = "green";
236
+ } elsif ($colors =~ /^(io|wakeup|chain)$/) {
237
+ $bgcolors = "blue";
238
+ } elsif ($colors =~ /^(red|green|blue|aqua|yellow|purple|orange)$/) {
239
+ $bgcolors = "grey";
240
+ } else {
241
+ $bgcolors = "yellow";
242
+ }
243
+ }
244
+ my ($bgcolor1, $bgcolor2);
245
+ if ($bgcolors eq "yellow") {
246
+ $bgcolor1 = "#eeeeee"; # background color gradient start
247
+ $bgcolor2 = "#eeeeb0"; # background color gradient stop
248
+ } elsif ($bgcolors eq "blue") {
249
+ $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff";
250
+ } elsif ($bgcolors eq "green") {
251
+ $bgcolor1 = "#eef2ee"; $bgcolor2 = "#e0ffe0";
252
+ } elsif ($bgcolors eq "grey") {
253
+ $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8";
254
+ } elsif ($bgcolors =~ /^#......$/) {
255
+ $bgcolor1 = $bgcolor2 = $bgcolors;
256
+ } else {
257
+ die "Unrecognized bgcolor option \"$bgcolors\""
258
+ }
151
259
 
152
260
  # SVG functions
153
261
  { package SVG;
@@ -168,6 +276,8 @@ if ($colors eq "io") { $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8"; }
168
276
  <?xml version="1.0"$enc_attr standalone="no"?>
169
277
  <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
170
278
  <svg version="1.1" width="$w" height="$h" onload="init(evt)" viewBox="0 0 $w $h" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
279
+ <!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. -->
280
+ <!-- NOTES: $notestext -->
171
281
  SVG
172
282
  }
173
283
 
@@ -186,27 +296,26 @@ SVG
186
296
 
187
297
  my @g_attr = map {
188
298
  exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : ()
189
- } qw(class style onmouseover onmouseout);
299
+ } qw(id class);
190
300
  push @g_attr, $attr->{g_extra} if $attr->{g_extra};
191
- $self->{svg} .= sprintf qq/<g %s>\n/, join(' ', @g_attr);
192
-
193
- $self->{svg} .= sprintf qq/<title>%s<\/title>/, $attr->{title}
194
- if $attr->{title}; # should be first element within g container
195
-
196
301
  if ($attr->{href}) {
197
302
  my @a_attr;
198
303
  push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href};
199
304
  # default target=_top else links will open within SVG <object>
200
305
  push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top";
201
306
  push @a_attr, $attr->{a_extra} if $attr->{a_extra};
202
- $self->{svg} .= sprintf qq/<a %s>/, join(' ', @a_attr);
307
+ $self->{svg} .= sprintf qq/<a %s>\n/, join(' ', (@a_attr, @g_attr));
308
+ } else {
309
+ $self->{svg} .= sprintf qq/<g %s>\n/, join(' ', @g_attr);
203
310
  }
311
+
312
+ $self->{svg} .= sprintf qq/<title>%s<\/title>/, $attr->{title}
313
+ if $attr->{title}; # should be first element within g container
204
314
  }
205
315
 
206
316
  sub group_end {
207
317
  my ($self, $attr) = @_;
208
- $self->{svg} .= qq/<\/a>\n/ if $attr->{href};
209
- $self->{svg} .= qq/<\/g>\n/;
318
+ $self->{svg} .= $attr->{href} ? qq/<\/a>\n/ : qq/<\/g>\n/;
210
319
  }
211
320
 
212
321
  sub filledRectangle {
@@ -220,10 +329,11 @@ SVG
220
329
  }
221
330
 
222
331
  sub stringTTF {
223
- my ($self, $color, $font, $size, $angle, $x, $y, $str, $loc, $extra) = @_;
224
- $loc = defined $loc ? $loc : "left";
225
- $extra = defined $extra ? $extra : "";
226
- $self->{svg} .= qq/<text text-anchor="$loc" x="$x" y="$y" font-size="$size" font-family="$font" fill="$color" $extra >$str<\/text>\n/;
332
+ my ($self, $id, $x, $y, $str, $extra) = @_;
333
+ $x = sprintf "%0.2f", $x;
334
+ $id = defined $id ? qq/id="$id"/ : "";
335
+ $extra ||= "";
336
+ $self->{svg} .= qq/<text $id x="$x" y="$y" $extra>$str<\/text>\n/;
227
337
  }
228
338
 
229
339
  sub svg {
@@ -257,6 +367,7 @@ sub namehash {
257
367
  sub color {
258
368
  my ($type, $hash, $name) = @_;
259
369
  my ($v1, $v2, $v3);
370
+
260
371
  if ($hash) {
261
372
  $v1 = namehash($name);
262
373
  $v2 = $v3 = namehash(scalar reverse $name);
@@ -265,6 +376,8 @@ sub color {
265
376
  $v2 = rand(1);
266
377
  $v3 = rand(1);
267
378
  }
379
+
380
+ # theme palettes
268
381
  if (defined $type and $type eq "hot") {
269
382
  my $r = 205 + int(50 * $v3);
270
383
  my $g = 0 + int(230 * $v1);
@@ -283,15 +396,138 @@ sub color {
283
396
  my $b = 190 + int(55 * $v2);
284
397
  return "rgb($r,$g,$b)";
285
398
  }
399
+
400
+ # multi palettes
401
+ if (defined $type and $type eq "java") {
402
+ # Handle both annotations (_[j], _[i], ...; which are
403
+ # accurate), as well as input that lacks any annotations, as
404
+ # best as possible. Without annotations, we get a little hacky
405
+ # and match on java|org|com, etc.
406
+ if ($name =~ m:_\[j\]$:) { # jit annotation
407
+ $type = "green";
408
+ } elsif ($name =~ m:_\[i\]$:) { # inline annotation
409
+ $type = "aqua";
410
+ } elsif ($name =~ m:^L?(java|javax|jdk|net|org|com|io|sun)/:) { # Java
411
+ $type = "green";
412
+ } elsif ($name =~ m:_\[k\]$:) { # kernel annotation
413
+ $type = "orange";
414
+ } elsif ($name =~ /::/) { # C++
415
+ $type = "yellow";
416
+ } else { # system
417
+ $type = "red";
418
+ }
419
+ # fall-through to color palettes
420
+ }
421
+ if (defined $type and $type eq "perl") {
422
+ if ($name =~ /::/) { # C++
423
+ $type = "yellow";
424
+ } elsif ($name =~ m:Perl: or $name =~ m:\.pl:) { # Perl
425
+ $type = "green";
426
+ } elsif ($name =~ m:_\[k\]$:) { # kernel
427
+ $type = "orange";
428
+ } else { # system
429
+ $type = "red";
430
+ }
431
+ # fall-through to color palettes
432
+ }
433
+ if (defined $type and $type eq "js") {
434
+ # Handle both annotations (_[j], _[i], ...; which are
435
+ # accurate), as well as input that lacks any annotations, as
436
+ # best as possible. Without annotations, we get a little hacky,
437
+ # and match on a "/" with a ".js", etc.
438
+ if ($name =~ m:_\[j\]$:) { # jit annotation
439
+ if ($name =~ m:/:) {
440
+ $type = "green"; # source
441
+ } else {
442
+ $type = "aqua"; # builtin
443
+ }
444
+ } elsif ($name =~ /::/) { # C++
445
+ $type = "yellow";
446
+ } elsif ($name =~ m:/.*\.js:) { # JavaScript (match "/" in path)
447
+ $type = "green";
448
+ } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin)
449
+ $type = "aqua";
450
+ } elsif ($name =~ m/^ $/) { # Missing symbol
451
+ $type = "green";
452
+ } elsif ($name =~ m:_\[k\]:) { # kernel
453
+ $type = "orange";
454
+ } else { # system
455
+ $type = "red";
456
+ }
457
+ # fall-through to color palettes
458
+ }
459
+ if (defined $type and $type eq "wakeup") {
460
+ $type = "aqua";
461
+ # fall-through to color palettes
462
+ }
463
+ if (defined $type and $type eq "chain") {
464
+ if ($name =~ m:_\[w\]:) { # waker
465
+ $type = "aqua"
466
+ } else { # off-CPU
467
+ $type = "blue";
468
+ }
469
+ # fall-through to color palettes
470
+ }
471
+
472
+ # color palettes
473
+ if (defined $type and $type eq "red") {
474
+ my $r = 200 + int(55 * $v1);
475
+ my $x = 50 + int(80 * $v1);
476
+ return "rgb($r,$x,$x)";
477
+ }
478
+ if (defined $type and $type eq "green") {
479
+ my $g = 200 + int(55 * $v1);
480
+ my $x = 50 + int(60 * $v1);
481
+ return "rgb($x,$g,$x)";
482
+ }
483
+ if (defined $type and $type eq "blue") {
484
+ my $b = 205 + int(50 * $v1);
485
+ my $x = 80 + int(60 * $v1);
486
+ return "rgb($x,$x,$b)";
487
+ }
488
+ if (defined $type and $type eq "yellow") {
489
+ my $x = 175 + int(55 * $v1);
490
+ my $b = 50 + int(20 * $v1);
491
+ return "rgb($x,$x,$b)";
492
+ }
493
+ if (defined $type and $type eq "purple") {
494
+ my $x = 190 + int(65 * $v1);
495
+ my $g = 80 + int(60 * $v1);
496
+ return "rgb($x,$g,$x)";
497
+ }
498
+ if (defined $type and $type eq "aqua") {
499
+ my $r = 50 + int(60 * $v1);
500
+ my $g = 165 + int(55 * $v1);
501
+ my $b = 165 + int(55 * $v1);
502
+ return "rgb($r,$g,$b)";
503
+ }
504
+ if (defined $type and $type eq "orange") {
505
+ my $r = 190 + int(65 * $v1);
506
+ my $g = 90 + int(65 * $v1);
507
+ return "rgb($r,$g,0)";
508
+ }
509
+
286
510
  return "rgb(0,0,0)";
287
511
  }
288
512
 
513
+ sub color_scale {
514
+ my ($value, $max) = @_;
515
+ my ($r, $g, $b) = (255, 255, 255);
516
+ $value = -$value if $negate;
517
+ if ($value > 0) {
518
+ $g = $b = int(210 * ($max - $value) / $max);
519
+ } elsif ($value < 0) {
520
+ $r = $g = int(210 * ($max + $value) / $max);
521
+ }
522
+ return "rgb($r,$g,$b)";
523
+ }
524
+
289
525
  sub color_map {
290
526
  my ($colors, $func) = @_;
291
527
  if (exists $palette_map{$func}) {
292
528
  return $palette_map{$func};
293
529
  } else {
294
- $palette_map{$func} = color($colors);
530
+ $palette_map{$func} = color($colors, $hash, $func);
295
531
  return $palette_map{$func};
296
532
  }
297
533
  }
@@ -316,11 +552,12 @@ sub read_palette {
316
552
  }
317
553
  }
318
554
 
319
- my %Node;
555
+ my %Node; # Hash of merged frame data
320
556
  my %Tmp;
321
557
 
558
+ # flow() merges two stacks, storing the merged frames and value data in %Node.
322
559
  sub flow {
323
- my ($last, $this, $v) = @_;
560
+ my ($last, $this, $v, $d) = @_;
324
561
 
325
562
  my $len_a = @$last - 1;
326
563
  my $len_b = @$this - 1;
@@ -338,37 +575,122 @@ sub flow {
338
575
  # a unique ID is constructed from "func;depth;etime";
339
576
  # func-depth isn't unique, it may be repeated later.
340
577
  $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime};
578
+ if (defined $Tmp{$k}->{delta}) {
579
+ $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta};
580
+ }
341
581
  delete $Tmp{$k};
342
582
  }
343
583
 
344
584
  for ($i = $len_same; $i <= $len_b; $i++) {
345
585
  my $k = "$this->[$i];$i";
346
586
  $Tmp{$k}->{stime} = $v;
587
+ if (defined $d) {
588
+ $Tmp{$k}->{delta} += $i == $len_b ? $d : 0;
589
+ }
347
590
  }
348
591
 
349
592
  return $this;
350
593
  }
351
594
 
352
- # Parse input
353
- my @Data = <>;
595
+ # parse input
596
+ my @Data;
597
+ my @SortedData;
354
598
  my $last = [];
355
599
  my $time = 0;
600
+ my $delta = undef;
356
601
  my $ignored = 0;
357
- foreach (sort @Data) {
602
+ my $line;
603
+ my $maxdelta = 1;
604
+
605
+ # reverse if needed
606
+ foreach (<>) {
358
607
  chomp;
359
- my ($stack, $samples) = (/^(.*)\s+(\d+(?:\.\d*)?)$/);
360
- unless (defined $samples) {
608
+ $line = $_;
609
+ if ($stackreverse) {
610
+ # there may be an extra samples column for differentials
611
+ # XXX todo: redo these REs as one. It's repeated below.
612
+ my($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
613
+ my $samples2 = undef;
614
+ if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) {
615
+ $samples2 = $samples;
616
+ ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
617
+ unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2";
618
+ } else {
619
+ unshift @Data, join(";", reverse split(";", $stack)) . " $samples";
620
+ }
621
+ } else {
622
+ unshift @Data, $line;
623
+ }
624
+ }
625
+
626
+ if ($flamechart) {
627
+ # In flame chart mode, just reverse the data so time moves from left to right.
628
+ @SortedData = reverse @Data;
629
+ } else {
630
+ @SortedData = sort @Data;
631
+ }
632
+
633
+ # process and merge frames
634
+ foreach (@SortedData) {
635
+ chomp;
636
+ # process: folded_stack count
637
+ # eg: func_a;func_b;func_c 31
638
+ my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
639
+ unless (defined $samples and defined $stack) {
361
640
  ++$ignored;
362
641
  next;
363
642
  }
364
- $stack =~ tr/<>/()/;
365
- $last = flow($last, [ '', split ";", $stack ], $time);
366
- $time += $samples;
643
+
644
+ # there may be an extra samples column for differentials:
645
+ my $samples2 = undef;
646
+ if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) {
647
+ $samples2 = $samples;
648
+ ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
649
+ }
650
+ $delta = undef;
651
+ if (defined $samples2) {
652
+ $delta = $samples2 - $samples;
653
+ $maxdelta = abs($delta) if abs($delta) > $maxdelta;
654
+ }
655
+
656
+ # for chain graphs, annotate waker frames with "_[w]", for later
657
+ # coloring. This is a hack, but has a precedent ("_[k]" from perf).
658
+ if ($colors eq "chain") {
659
+ my @parts = split ";--;", $stack;
660
+ my @newparts = ();
661
+ $stack = shift @parts;
662
+ $stack .= ";--;";
663
+ foreach my $part (@parts) {
664
+ $part =~ s/;/_[w];/g;
665
+ $part .= "_[w]";
666
+ push @newparts, $part;
667
+ }
668
+ $stack .= join ";--;", @parts;
669
+ }
670
+
671
+ # merge frames and populate %Node:
672
+ $last = flow($last, [ '', split ";", $stack ], $time, $delta);
673
+
674
+ if (defined $samples2) {
675
+ $time += $samples2;
676
+ } else {
677
+ $time += $samples;
678
+ }
367
679
  }
368
- flow($last, [], $time);
369
- warn "Ignored $ignored lines with invalid format\n" if $ignored;
370
- die "ERROR: No stack counts found\n" unless $time;
680
+ flow($last, [], $time, $delta);
371
681
 
682
+ warn "Ignored $ignored lines with invalid format\n" if $ignored;
683
+ unless ($time) {
684
+ warn "ERROR: No stack counts found\n";
685
+ my $im = SVG->new();
686
+ # emit an error message SVG, for tools automating flamegraph use
687
+ my $imageheight = $fontsize * 5;
688
+ $im->header($imagewidth, $imageheight);
689
+ $im->stringTTF(undef, int($imagewidth / 2), $fontsize * 2,
690
+ "ERROR: No valid input provided to flamegraph.pl.");
691
+ print $im->svg;
692
+ exit 2;
693
+ }
372
694
  if ($timemax and $timemax < $time) {
373
695
  warn "Specified --total $timemax is less than actual total $time, so ignored\n"
374
696
  if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc)
@@ -392,55 +714,382 @@ while (my ($id, $node) = each %Node) {
392
714
  $depthmax = $depth if $depth > $depthmax;
393
715
  }
394
716
 
395
- # Draw canvas
396
- my $imageheight = ($depthmax * $frameheight) + $ypad1 + $ypad2;
717
+ # draw canvas, and embed interactive JavaScript program
718
+ my $imageheight = (($depthmax + 1) * $frameheight) + $ypad1 + $ypad2;
719
+ $imageheight += $ypad3 if $subtitletext ne "";
720
+ my $titlesize = $fontsize + 5;
397
721
  my $im = SVG->new();
722
+ my ($black, $vdgrey, $dgrey) = (
723
+ $im->colorAllocate(0, 0, 0),
724
+ $im->colorAllocate(160, 160, 160),
725
+ $im->colorAllocate(200, 200, 200),
726
+ );
398
727
  $im->header($imagewidth, $imageheight);
399
728
  my $inc = <<INC;
400
- <defs >
729
+ <defs>
401
730
  <linearGradient id="background" y1="0" y2="1" x1="0" x2="0" >
402
731
  <stop stop-color="$bgcolor1" offset="5%" />
403
732
  <stop stop-color="$bgcolor2" offset="95%" />
404
733
  </linearGradient>
405
734
  </defs>
406
735
  <style type="text/css">
407
- .func_g:hover { stroke:black; stroke-width:0.5; }
736
+ text { font-family:$fonttype; font-size:${fontsize}px; fill:$black; }
737
+ #search { opacity:0.1; cursor:pointer; }
738
+ #search:hover, #search.show { opacity:1; }
739
+ #subtitle { text-anchor:middle; font-color:$vdgrey; }
740
+ #title { text-anchor:middle; font-size:${titlesize}px}
741
+ #unzoom { cursor:pointer; }
742
+ #frames > *:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
743
+ .hide { display:none; }
744
+ .parent { opacity:0.5; }
408
745
  </style>
409
746
  <script type="text/ecmascript">
410
747
  <![CDATA[
411
- var details;
412
- function init(evt) { details = document.getElementById("details").firstChild; }
413
- function s(info) { details.nodeValue = "$nametype " + info; }
414
- function c() { details.nodeValue = ' '; }
748
+ "use strict";
749
+ var details, searchbtn, unzoombtn, matchedtxt, svg, searching;
750
+ function init(evt) {
751
+ details = document.getElementById("details").firstChild;
752
+ searchbtn = document.getElementById("search");
753
+ unzoombtn = document.getElementById("unzoom");
754
+ matchedtxt = document.getElementById("matched");
755
+ svg = document.getElementsByTagName("svg")[0];
756
+ searching = 0;
757
+ }
758
+
759
+ window.addEventListener("click", function(e) {
760
+ var target = find_group(e.target);
761
+ if (target) {
762
+ if (target.nodeName == "a") {
763
+ if (e.ctrlKey === false) return;
764
+ e.preventDefault();
765
+ }
766
+ if (target.classList.contains("parent")) unzoom();
767
+ zoom(target);
768
+ }
769
+ else if (e.target.id == "unzoom") unzoom();
770
+ else if (e.target.id == "search") search_prompt();
771
+ }, false)
772
+
773
+ // mouse-over for info
774
+ // show
775
+ window.addEventListener("mouseover", function(e) {
776
+ var target = find_group(e.target);
777
+ if (target) details.nodeValue = "$nametype " + g_to_text(target);
778
+ }, false)
779
+
780
+ // clear
781
+ window.addEventListener("mouseout", function(e) {
782
+ var target = find_group(e.target);
783
+ if (target) details.nodeValue = ' ';
784
+ }, false)
785
+
786
+ // ctrl-F for search
787
+ window.addEventListener("keydown",function (e) {
788
+ if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
789
+ e.preventDefault();
790
+ search_prompt();
791
+ }
792
+ }, false)
793
+
794
+ // functions
795
+ function find_child(node, selector) {
796
+ var children = node.querySelectorAll(selector);
797
+ if (children.length) return children[0];
798
+ return;
799
+ }
800
+ function find_group(node) {
801
+ var parent = node.parentElement;
802
+ if (!parent) return;
803
+ if (parent.id == "frames") return node;
804
+ return find_group(parent);
805
+ }
806
+ function orig_save(e, attr, val) {
807
+ if (e.attributes["_orig_" + attr] != undefined) return;
808
+ if (e.attributes[attr] == undefined) return;
809
+ if (val == undefined) val = e.attributes[attr].value;
810
+ e.setAttribute("_orig_" + attr, val);
811
+ }
812
+ function orig_load(e, attr) {
813
+ if (e.attributes["_orig_"+attr] == undefined) return;
814
+ e.attributes[attr].value = e.attributes["_orig_" + attr].value;
815
+ e.removeAttribute("_orig_"+attr);
816
+ }
817
+ function g_to_text(e) {
818
+ var text = find_child(e, "title").firstChild.nodeValue;
819
+ return (text)
820
+ }
821
+ function g_to_func(e) {
822
+ var func = g_to_text(e);
823
+ // if there's any manipulation we want to do to the function
824
+ // name before it's searched, do it here before returning.
825
+ return (func);
826
+ }
827
+ function update_text(e) {
828
+ var r = find_child(e, "rect");
829
+ var t = find_child(e, "text");
830
+ var w = parseFloat(r.attributes.width.value) -3;
831
+ var txt = find_child(e, "title").textContent.replace(/\\([^(]*\\)\$/,"");
832
+ t.attributes.x.value = parseFloat(r.attributes.x.value) + 3;
833
+
834
+ // Smaller than this size won't fit anything
835
+ if (w < 2 * $fontsize * $fontwidth) {
836
+ t.textContent = "";
837
+ return;
838
+ }
839
+
840
+ t.textContent = txt;
841
+ // Fit in full text width
842
+ if (/^ *\$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
843
+ return;
844
+
845
+ for (var x = txt.length - 2; x > 0; x--) {
846
+ if (t.getSubStringLength(0, x + 2) <= w) {
847
+ t.textContent = txt.substring(0, x) + "..";
848
+ return;
849
+ }
850
+ }
851
+ t.textContent = "";
852
+ }
853
+
854
+ // zoom
855
+ function zoom_reset(e) {
856
+ if (e.attributes != undefined) {
857
+ orig_load(e, "x");
858
+ orig_load(e, "width");
859
+ }
860
+ if (e.childNodes == undefined) return;
861
+ for (var i = 0, c = e.childNodes; i < c.length; i++) {
862
+ zoom_reset(c[i]);
863
+ }
864
+ }
865
+ function zoom_child(e, x, ratio) {
866
+ if (e.attributes != undefined) {
867
+ if (e.attributes.x != undefined) {
868
+ orig_save(e, "x");
869
+ e.attributes.x.value = (parseFloat(e.attributes.x.value) - x - $xpad) * ratio + $xpad;
870
+ if (e.tagName == "text")
871
+ e.attributes.x.value = find_child(e.parentNode, "rect[x]").attributes.x.value + 3;
872
+ }
873
+ if (e.attributes.width != undefined) {
874
+ orig_save(e, "width");
875
+ e.attributes.width.value = parseFloat(e.attributes.width.value) * ratio;
876
+ }
877
+ }
878
+
879
+ if (e.childNodes == undefined) return;
880
+ for (var i = 0, c = e.childNodes; i < c.length; i++) {
881
+ zoom_child(c[i], x - $xpad, ratio);
882
+ }
883
+ }
884
+ function zoom_parent(e) {
885
+ if (e.attributes) {
886
+ if (e.attributes.x != undefined) {
887
+ orig_save(e, "x");
888
+ e.attributes.x.value = $xpad;
889
+ }
890
+ if (e.attributes.width != undefined) {
891
+ orig_save(e, "width");
892
+ e.attributes.width.value = parseInt(svg.width.baseVal.value) - ($xpad * 2);
893
+ }
894
+ }
895
+ if (e.childNodes == undefined) return;
896
+ for (var i = 0, c = e.childNodes; i < c.length; i++) {
897
+ zoom_parent(c[i]);
898
+ }
899
+ }
900
+ function zoom(node) {
901
+ var attr = find_child(node, "rect").attributes;
902
+ var width = parseFloat(attr.width.value);
903
+ var xmin = parseFloat(attr.x.value);
904
+ var xmax = parseFloat(xmin + width);
905
+ var ymin = parseFloat(attr.y.value);
906
+ var ratio = (svg.width.baseVal.value - 2 * $xpad) / width;
907
+
908
+ // XXX: Workaround for JavaScript float issues (fix me)
909
+ var fudge = 0.0001;
910
+
911
+ unzoombtn.classList.remove("hide");
912
+
913
+ var el = document.getElementById("frames").children;
914
+ for (var i = 0; i < el.length; i++) {
915
+ var e = el[i];
916
+ var a = find_child(e, "rect").attributes;
917
+ var ex = parseFloat(a.x.value);
918
+ var ew = parseFloat(a.width.value);
919
+ var upstack;
920
+ // Is it an ancestor
921
+ if ($inverted == 0) {
922
+ upstack = parseFloat(a.y.value) > ymin;
923
+ } else {
924
+ upstack = parseFloat(a.y.value) < ymin;
925
+ }
926
+ if (upstack) {
927
+ // Direct ancestor
928
+ if (ex <= xmin && (ex+ew+fudge) >= xmax) {
929
+ e.classList.add("parent");
930
+ zoom_parent(e);
931
+ update_text(e);
932
+ }
933
+ // not in current path
934
+ else
935
+ e.classList.add("hide");
936
+ }
937
+ // Children maybe
938
+ else {
939
+ // no common path
940
+ if (ex < xmin || ex + fudge >= xmax) {
941
+ e.classList.add("hide");
942
+ }
943
+ else {
944
+ zoom_child(e, xmin, ratio);
945
+ update_text(e);
946
+ }
947
+ }
948
+ }
949
+ }
950
+ function unzoom() {
951
+ unzoombtn.classList.add("hide");
952
+ var el = document.getElementById("frames").children;
953
+ for(var i = 0; i < el.length; i++) {
954
+ el[i].classList.remove("parent");
955
+ el[i].classList.remove("hide");
956
+ zoom_reset(el[i]);
957
+ update_text(el[i]);
958
+ }
959
+ }
960
+
961
+ // search
962
+ function reset_search() {
963
+ var el = document.querySelectorAll("#frames rect");
964
+ for (var i = 0; i < el.length; i++) {
965
+ orig_load(el[i], "fill")
966
+ }
967
+ }
968
+ function search_prompt() {
969
+ if (!searching) {
970
+ var term = prompt("Enter a search term (regexp " +
971
+ "allowed, eg: ^ext4_)", "");
972
+ if (term != null) {
973
+ search(term)
974
+ }
975
+ } else {
976
+ reset_search();
977
+ searching = 0;
978
+ searchbtn.classList.remove("show");
979
+ searchbtn.firstChild.nodeValue = "Search"
980
+ matchedtxt.classList.add("hide");
981
+ matchedtxt.firstChild.nodeValue = ""
982
+ }
983
+ }
984
+ function search(term) {
985
+ var re = new RegExp(term);
986
+ var el = document.getElementById("frames").children;
987
+ var matches = new Object();
988
+ var maxwidth = 0;
989
+ for (var i = 0; i < el.length; i++) {
990
+ var e = el[i];
991
+ var func = g_to_func(e);
992
+ var rect = find_child(e, "rect");
993
+ if (func == null || rect == null)
994
+ continue;
995
+
996
+ // Save max width. Only works as we have a root frame
997
+ var w = parseFloat(rect.attributes.width.value);
998
+ if (w > maxwidth)
999
+ maxwidth = w;
1000
+
1001
+ if (func.match(re)) {
1002
+ // highlight
1003
+ var x = parseFloat(rect.attributes.x.value);
1004
+ orig_save(rect, "fill");
1005
+ rect.attributes.fill.value = "$searchcolor";
1006
+
1007
+ // remember matches
1008
+ if (matches[x] == undefined) {
1009
+ matches[x] = w;
1010
+ } else {
1011
+ if (w > matches[x]) {
1012
+ // overwrite with parent
1013
+ matches[x] = w;
1014
+ }
1015
+ }
1016
+ searching = 1;
1017
+ }
1018
+ }
1019
+ if (!searching)
1020
+ return;
1021
+
1022
+ searchbtn.classList.add("show");
1023
+ searchbtn.firstChild.nodeValue = "Reset Search";
1024
+
1025
+ // calculate percent matched, excluding vertical overlap
1026
+ var count = 0;
1027
+ var lastx = -1;
1028
+ var lastw = 0;
1029
+ var keys = Array();
1030
+ for (k in matches) {
1031
+ if (matches.hasOwnProperty(k))
1032
+ keys.push(k);
1033
+ }
1034
+ // sort the matched frames by their x location
1035
+ // ascending, then width descending
1036
+ keys.sort(function(a, b){
1037
+ return a - b;
1038
+ });
1039
+ // Step through frames saving only the biggest bottom-up frames
1040
+ // thanks to the sort order. This relies on the tree property
1041
+ // where children are always smaller than their parents.
1042
+ var fudge = 0.0001; // JavaScript floating point
1043
+ for (var k in keys) {
1044
+ var x = parseFloat(keys[k]);
1045
+ var w = matches[keys[k]];
1046
+ if (x >= lastx + lastw - fudge) {
1047
+ count += w;
1048
+ lastx = x;
1049
+ lastw = w;
1050
+ }
1051
+ }
1052
+ // display matched percent
1053
+ matchedtxt.classList.remove("hide");
1054
+ var pct = 100 * count / maxwidth;
1055
+ if (pct != 100) pct = pct.toFixed(1)
1056
+ matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
1057
+ }
415
1058
  ]]>
416
1059
  </script>
417
1060
  INC
418
1061
  $im->include($inc);
419
1062
  $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)');
420
- my ($white, $black, $vvdgrey, $vdgrey) = (
421
- $im->colorAllocate(255, 255, 255),
422
- $im->colorAllocate(0, 0, 0),
423
- $im->colorAllocate(40, 40, 40),
424
- $im->colorAllocate(160, 160, 160),
425
- );
426
- $im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize * 2, $titletext, "middle");
427
- $im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), " ", "", 'id="details"');
1063
+ $im->stringTTF("title", int($imagewidth / 2), $fontsize * 2, $titletext);
1064
+ $im->stringTTF("subtitle", int($imagewidth / 2), $fontsize * 4, $subtitletext) if $subtitletext ne "";
1065
+ $im->stringTTF("details", $xpad, $imageheight - ($ypad2 / 2), " ");
1066
+ $im->stringTTF("unzoom", $xpad, $fontsize * 2, "Reset Zoom", 'class="hide"');
1067
+ $im->stringTTF("search", $imagewidth - $xpad - 100, $fontsize * 2, "Search");
1068
+ $im->stringTTF("matched", $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " ");
428
1069
 
429
1070
  if ($palette) {
430
1071
  read_palette();
431
1072
  }
432
- # Draw frames
433
1073
 
1074
+ # draw frames
1075
+ $im->group_start({id => "frames"});
434
1076
  while (my ($id, $node) = each %Node) {
435
1077
  my ($func, $depth, $etime) = split ";", $id;
436
1078
  my $stime = $node->{stime};
1079
+ my $delta = $node->{delta};
437
1080
 
438
1081
  $etime = $timemax if $func eq "" and $depth == 0;
439
1082
 
440
1083
  my $x1 = $xpad + $stime * $widthpertime;
441
1084
  my $x2 = $xpad + $etime * $widthpertime;
442
- my $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + 1;
443
- my $y2 = $imageheight - $ypad2 - $depth * $frameheight;
1085
+ my ($y1, $y2);
1086
+ unless ($inverted) {
1087
+ $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad;
1088
+ $y2 = $imageheight - $ypad2 - $depth * $frameheight;
1089
+ } else {
1090
+ $y1 = $ypad1 + $depth * $frameheight;
1091
+ $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad;
1092
+ }
444
1093
 
445
1094
  my $samples = sprintf "%.0f", ($etime - $stime) * $factor;
446
1095
  (my $samples_txt = $samples) # add commas per perlfaq5
@@ -452,38 +1101,55 @@ while (my ($id, $node) = each %Node) {
452
1101
  } else {
453
1102
  my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor));
454
1103
  my $escaped_func = $func;
1104
+ # clean up SVG breaking characters:
455
1105
  $escaped_func =~ s/&/&amp;/g;
456
1106
  $escaped_func =~ s/</&lt;/g;
457
1107
  $escaped_func =~ s/>/&gt;/g;
458
- $info = "$escaped_func ($samples_txt $countname, $pct%)";
1108
+ $escaped_func =~ s/"/&quot;/g;
1109
+ $escaped_func =~ s/_\[[kwij]\]$//; # strip any annotation
1110
+ unless (defined $delta) {
1111
+ $info = "$escaped_func ($samples_txt $countname, $pct%)";
1112
+ } else {
1113
+ my $d = $negate ? -$delta : $delta;
1114
+ my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor));
1115
+ $deltapct = $d > 0 ? "+$deltapct" : $deltapct;
1116
+ $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)";
1117
+ }
459
1118
  }
460
1119
 
461
1120
  my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone
462
- $nameattr->{class} ||= "func_g";
463
- $nameattr->{onmouseover} ||= "s('".$info."')";
464
- $nameattr->{onmouseout} ||= "c()";
465
1121
  $nameattr->{title} ||= $info;
466
1122
  $im->group_start($nameattr);
467
1123
 
468
- if ($palette) {
469
- $im->filledRectangle($x1, $y1, $x2, $y2, color_map($colors, $func), 'rx="2" ry="2"');
1124
+ my $color;
1125
+ if ($func eq "--") {
1126
+ $color = $vdgrey;
1127
+ } elsif ($func eq "-") {
1128
+ $color = $dgrey;
1129
+ } elsif (defined $delta) {
1130
+ $color = color_scale($delta, $maxdelta);
1131
+ } elsif ($palette) {
1132
+ $color = color_map($colors, $func);
470
1133
  } else {
471
- my $color = $func eq "-" ? $vdgrey : color($colors, $hash, $func);
472
- $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"');
1134
+ $color = color($colors, $hash, $func);
473
1135
  }
1136
+ $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"');
474
1137
 
475
1138
  my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth));
1139
+ my $text = "";
476
1140
  if ($chars >= 3) { # room for one char plus two dots
477
- my $text = substr $func, 0, $chars;
1141
+ $func =~ s/_\[[kwij]\]$//; # strip any annotation
1142
+ $text = substr $func, 0, $chars;
478
1143
  substr($text, -2, 2) = ".." if $chars < length $func;
479
1144
  $text =~ s/&/&amp;/g;
480
1145
  $text =~ s/</&lt;/g;
481
1146
  $text =~ s/>/&gt;/g;
482
- $im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, "");
483
1147
  }
1148
+ $im->stringTTF(undef, $x1 + 3, 3 + ($y1 + $y2) / 2, $text);
484
1149
 
485
1150
  $im->group_end($nameattr);
486
1151
  }
1152
+ $im->group_end();
487
1153
 
488
1154
  print $im->svg;
489
1155