stackprof 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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