flameboyant 0.1.0
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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +65 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/flameboyant.gemspec +29 -0
- data/flameboyant.jpg +0 -0
- data/lib/flameboyant.rb +61 -0
- data/lib/flameboyant/version.rb +3 -0
- data/lib/flamegraph.pl +1111 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bfa186aaa2c0e8800643b18f17fc63c94a49d4e9
|
4
|
+
data.tar.gz: af5b86c5869e4d4198ec6dd752b9d26f0fa4a207
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8b5e058e0ab44ef61882a7ca8d21f5babb5764662d8d4447fd8723b32c27dff60401c769ed14ab47495d57739e58a1566684a6754b58304cc077ab2518e39a80
|
7
|
+
data.tar.gz: 582544d45dbc969bd99acea64bbf2d08d607e891879c559151d9bc75f1280901b1817a4896cd8a7ea473a6f2fbf67f7ce4098ce11b249bf630fae1ebd2488629
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Scott Pierce
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Flameboyant
|
2
|
+
|
3
|
+
Generate an interactive SVG flame chart.
|
4
|
+
|
5
|
+
<img src='flameboyant.jpg' alt='example flame chart'/>
|
6
|
+
|
7
|
+
## Thanks
|
8
|
+
- https://github.com/oozou/ruby-prof-flamegraph
|
9
|
+
- https://github.com/brendangregg/FlameGraph
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
### Example basic ruby
|
14
|
+
```ruby
|
15
|
+
Flameboyant.profile(name: 'foo', width: 1024) do
|
16
|
+
# interesting code here
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
Files are written to current directory.
|
21
|
+
|
22
|
+
### Example Rails Console
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
Flameboyant.profile do
|
26
|
+
app.get '/interesting/api/call'
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## `profile` Options
|
31
|
+
+ name (String, optional) :: The output file will be prefixed with this name.
|
32
|
+
+ width (Integer, optional) :: defaults to 1920.
|
33
|
+
|
34
|
+
When running with Rails SVG files are written to `Rails.root/tmp/flames`
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
|
38
|
+
Add this line to your application's Gemfile:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
gem 'flameboyant'
|
42
|
+
```
|
43
|
+
|
44
|
+
And then execute:
|
45
|
+
|
46
|
+
$ bundle
|
47
|
+
|
48
|
+
Or install it yourself as:
|
49
|
+
|
50
|
+
$ gem install flameboyant
|
51
|
+
|
52
|
+
|
53
|
+
## Development
|
54
|
+
|
55
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
56
|
+
|
57
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
58
|
+
|
59
|
+
## Contributing
|
60
|
+
|
61
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ddrscott/flameboyant.
|
62
|
+
|
63
|
+
## License
|
64
|
+
|
65
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "flameboyant"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/flameboyant.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'flameboyant/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'flameboyant'
|
8
|
+
spec.version = Flameboyant::VERSION
|
9
|
+
spec.authors = ['Scott Pierce']
|
10
|
+
spec.email = ['ddrscott@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'Generate an interactive SVG flame chart'
|
13
|
+
spec.homepage = 'https://github.com/ddrscott/flameboyant'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
26
|
+
|
27
|
+
spec.add_dependency 'ruby-prof'
|
28
|
+
spec.add_dependency 'ruby-prof-flamegraph'
|
29
|
+
end
|
data/flameboyant.jpg
ADDED
Binary file
|
data/lib/flameboyant.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'flameboyant/version'
|
2
|
+
require 'ruby-prof-flamegraph'
|
3
|
+
|
4
|
+
# rubocop:disable all
|
5
|
+
module Flameboyant
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# ensure directory exists
|
9
|
+
GRAPHER = File.join(__dir__, 'flamegraph.pl')
|
10
|
+
|
11
|
+
def profile(name: nil, width: 1920, &block)
|
12
|
+
name = "#{name}_#{timestamp}"
|
13
|
+
|
14
|
+
RubyProf::FlameGraphPrinter
|
15
|
+
|
16
|
+
block_result = nil
|
17
|
+
log 'starting profile'
|
18
|
+
result = RubyProf.profile do
|
19
|
+
block_result = block.call
|
20
|
+
end
|
21
|
+
|
22
|
+
# print a graph profile to text
|
23
|
+
printer = RubyProf::FlameGraphPrinter.new(result)
|
24
|
+
|
25
|
+
FileUtils.mkdir_p(dest_dir)
|
26
|
+
|
27
|
+
dst_data = dest_dir.join("#{name}.txt")
|
28
|
+
dst_svg = dest_dir.join("#{name}.svg")
|
29
|
+
|
30
|
+
log "writing: #{dst_data}"
|
31
|
+
File.open(dst_data, 'w') do |f|
|
32
|
+
printer.print(f, {})
|
33
|
+
end
|
34
|
+
|
35
|
+
log 'generating SVG'
|
36
|
+
if system("#{GRAPHER} --countname=ms --width=#{width} #{dst_data} > #{dst_svg}")
|
37
|
+
log "created: #{dst_svg}"
|
38
|
+
log "removing: #{dst_data}"
|
39
|
+
FileUtils.rm(dst_data)
|
40
|
+
end
|
41
|
+
block_result
|
42
|
+
end
|
43
|
+
|
44
|
+
def dest_dir
|
45
|
+
if defined? Rails
|
46
|
+
Rails.root.join('tmp', 'flames')
|
47
|
+
else
|
48
|
+
Dir.pwd
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def timestamp
|
53
|
+
'%.04f' % Time.now.to_f
|
54
|
+
end
|
55
|
+
|
56
|
+
def log(msg)
|
57
|
+
full_msg = "[flame] #{msg}"
|
58
|
+
Rails.logger.info full_msg if defined? Rails
|
59
|
+
$stderr.puts full_msg
|
60
|
+
end
|
61
|
+
end
|
data/lib/flamegraph.pl
ADDED
@@ -0,0 +1,1111 @@
|
|
1
|
+
#!/usr/bin/perl -w
|
2
|
+
#
|
3
|
+
# flamegraph.pl flame stack grapher.
|
4
|
+
#
|
5
|
+
# This takes stack samples and renders a call graph, allowing hot functions
|
6
|
+
# and codepaths to be quickly identified. Stack samples can be generated using
|
7
|
+
# tools such as DTrace, perf, SystemTap, and Instruments.
|
8
|
+
#
|
9
|
+
# USAGE: ./flamegraph.pl [options] input.txt > graph.svg
|
10
|
+
#
|
11
|
+
# grep funcA input.txt | ./flamegraph.pl [options] > graph.svg
|
12
|
+
#
|
13
|
+
# 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
|
+
#
|
16
|
+
# Options are listed in the usage message (--help).
|
17
|
+
#
|
18
|
+
# The input is stack frames and sample counts formatted as single lines. Each
|
19
|
+
# frame in the stack is semicolon separated, with a space and count at the end
|
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.
|
30
|
+
#
|
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.
|
45
|
+
#
|
46
|
+
# While intended to process stack samples, this can also process stack traces.
|
47
|
+
# For example, tracing stacks for memory allocation, or resource usage. You
|
48
|
+
# can use --title to set the title to reflect the content, and --countname
|
49
|
+
# to change "samples" to "bytes" etc.
|
50
|
+
#
|
51
|
+
# There are a few different palettes, selectable using --color. By default,
|
52
|
+
# the colors are selected at random (except for differentials). Functions
|
53
|
+
# called "-" will be printed gray, which can be used for stack separators (eg,
|
54
|
+
# between user and kernel stacks).
|
55
|
+
#
|
56
|
+
# HISTORY
|
57
|
+
#
|
58
|
+
# This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb
|
59
|
+
# program, which visualized function entry and return trace events. As Neel
|
60
|
+
# wrote: "The output displayed is inspired by Roch's CallStackAnalyzer which
|
61
|
+
# was in turn inspired by the work on vftrace by Jan Boerhout". See:
|
62
|
+
# https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and
|
63
|
+
#
|
64
|
+
# Copyright 2016 Netflix, Inc.
|
65
|
+
# Copyright 2011 Joyent, Inc. All rights reserved.
|
66
|
+
# Copyright 2011 Brendan Gregg. All rights reserved.
|
67
|
+
#
|
68
|
+
# CDDL HEADER START
|
69
|
+
#
|
70
|
+
# The contents of this file are subject to the terms of the
|
71
|
+
# Common Development and Distribution License (the "License").
|
72
|
+
# You may not use this file except in compliance with the License.
|
73
|
+
#
|
74
|
+
# You can obtain a copy of the license at docs/cddl1.txt or
|
75
|
+
# http://opensource.org/licenses/CDDL-1.0.
|
76
|
+
# See the License for the specific language governing permissions
|
77
|
+
# and limitations under the License.
|
78
|
+
#
|
79
|
+
# When distributing Covered Code, include this CDDL HEADER in each
|
80
|
+
# file and include the License file at docs/cddl1.txt.
|
81
|
+
# If applicable, add the following below this CDDL HEADER, with the
|
82
|
+
# fields enclosed by brackets "[]" replaced with your own identifying
|
83
|
+
# information: Portions Copyright [yyyy] [name of copyright owner]
|
84
|
+
#
|
85
|
+
# CDDL HEADER END
|
86
|
+
#
|
87
|
+
# 11-Oct-2014 Adrien Mahieux Added zoom.
|
88
|
+
# 21-Nov-2013 Shawn Sterling Added consistent palette file option
|
89
|
+
# 17-Mar-2013 Tim Bunce Added options and more tunables.
|
90
|
+
# 15-Dec-2011 Dave Pacheco Support for frames with whitespace.
|
91
|
+
# 10-Sep-2011 Brendan Gregg Created this.
|
92
|
+
|
93
|
+
use strict;
|
94
|
+
|
95
|
+
use Getopt::Long;
|
96
|
+
|
97
|
+
use open qw(:std :utf8);
|
98
|
+
|
99
|
+
# tunables
|
100
|
+
my $encoding;
|
101
|
+
my $fonttype = "Verdana";
|
102
|
+
my $imagewidth = 1200; # max width, pixels
|
103
|
+
my $frameheight = 16; # max height is dynamic
|
104
|
+
my $fontsize = 12; # base text size
|
105
|
+
my $fontwidth = 0.59; # avg width relative to fontsize
|
106
|
+
my $minwidth = 0.1; # min function width, pixels
|
107
|
+
my $nametype = "Function:"; # what are the names in the data?
|
108
|
+
my $countname = "samples"; # what are the counts in the data?
|
109
|
+
my $colors = "hot"; # color theme
|
110
|
+
my $bgcolor1 = "#eeeeee"; # background color gradient start
|
111
|
+
my $bgcolor2 = "#eeeeb0"; # background color gradient stop
|
112
|
+
my $nameattrfile; # file holding function attributes
|
113
|
+
my $timemax; # (override the) sum of the counts
|
114
|
+
my $factor = 1; # factor to scale counts by
|
115
|
+
my $hash = 0; # color by function name
|
116
|
+
my $palette = 0; # if we use consistent palettes (default off)
|
117
|
+
my %palette_map; # palette map hash
|
118
|
+
my $pal_file = "palette.map"; # palette map file name
|
119
|
+
my $stackreverse = 0; # reverse stack order, switching merge end
|
120
|
+
my $inverted = 0; # icicle graph
|
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 $help = 0;
|
127
|
+
|
128
|
+
sub usage {
|
129
|
+
die <<USAGE_END;
|
130
|
+
USAGE: $0 [options] infile > outfile.svg\n
|
131
|
+
--title # change title text
|
132
|
+
--width # width of image (default 1200)
|
133
|
+
--height # height of each frame (default 16)
|
134
|
+
--minwidth # omit smaller functions (default 0.1 pixels)
|
135
|
+
--fonttype # font type (default "Verdana")
|
136
|
+
--fontsize # font size (default 12)
|
137
|
+
--countname # count type label (default "samples")
|
138
|
+
--nametype # name type label (default "Function:")
|
139
|
+
--colors # set color palette. choices are: hot (default), mem, io,
|
140
|
+
# wakeup, chain, java, js, perl, red, green, blue, aqua,
|
141
|
+
# yellow, purple, orange
|
142
|
+
--hash # colors are keyed by function name hash
|
143
|
+
--cp # use consistent palette (palette.map)
|
144
|
+
--reverse # generate stack-reversed flame graph
|
145
|
+
--inverted # icicle graph
|
146
|
+
--negate # switch differential hues (blue<->red)
|
147
|
+
--help # this message
|
148
|
+
|
149
|
+
eg,
|
150
|
+
$0 --title="Flame Graph: malloc()" trace.txt > graph.svg
|
151
|
+
USAGE_END
|
152
|
+
}
|
153
|
+
|
154
|
+
GetOptions(
|
155
|
+
'fonttype=s' => \$fonttype,
|
156
|
+
'width=i' => \$imagewidth,
|
157
|
+
'height=i' => \$frameheight,
|
158
|
+
'encoding=s' => \$encoding,
|
159
|
+
'fontsize=f' => \$fontsize,
|
160
|
+
'fontwidth=f' => \$fontwidth,
|
161
|
+
'minwidth=f' => \$minwidth,
|
162
|
+
'title=s' => \$titletext,
|
163
|
+
'nametype=s' => \$nametype,
|
164
|
+
'countname=s' => \$countname,
|
165
|
+
'nameattr=s' => \$nameattrfile,
|
166
|
+
'total=s' => \$timemax,
|
167
|
+
'factor=f' => \$factor,
|
168
|
+
'colors=s' => \$colors,
|
169
|
+
'hash' => \$hash,
|
170
|
+
'cp' => \$palette,
|
171
|
+
'reverse' => \$stackreverse,
|
172
|
+
'inverted' => \$inverted,
|
173
|
+
'negate' => \$negate,
|
174
|
+
'help' => \$help,
|
175
|
+
) or usage();
|
176
|
+
$help && usage();
|
177
|
+
|
178
|
+
# internals
|
179
|
+
my $ypad1 = $fontsize * 4; # pad top, include title
|
180
|
+
my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels
|
181
|
+
my $xpad = 10; # pad lefm and right
|
182
|
+
my $framepad = 1; # vertical padding for frames
|
183
|
+
my $depthmax = 0;
|
184
|
+
my %Events;
|
185
|
+
my %nameattr;
|
186
|
+
|
187
|
+
if ($titletext eq "") {
|
188
|
+
unless ($inverted) {
|
189
|
+
$titletext = $titledefault;
|
190
|
+
} else {
|
191
|
+
$titletext = $titleinverted;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
if ($nameattrfile) {
|
196
|
+
# The name-attribute file format is a function name followed by a tab then
|
197
|
+
# a sequence of tab separated name=value pairs.
|
198
|
+
open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n";
|
199
|
+
while (<$attrfh>) {
|
200
|
+
chomp;
|
201
|
+
my ($funcname, $attrstr) = split /\t/, $_, 2;
|
202
|
+
die "Invalid format in $nameattrfile" unless defined $attrstr;
|
203
|
+
$nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr };
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
# background colors:
|
208
|
+
# - yellow gradient: default (hot, java, js, perl)
|
209
|
+
# - blue gradient: mem, chain
|
210
|
+
# - gray gradient: io, wakeup, flat colors (red, green, blue, ...)
|
211
|
+
if ($colors eq "mem" or $colors eq "chain") {
|
212
|
+
$bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff";
|
213
|
+
}
|
214
|
+
if ($colors =~ /^(io|wakeup|red|green|blue|aqua|yellow|purple|orange)$/) {
|
215
|
+
$bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8";
|
216
|
+
}
|
217
|
+
|
218
|
+
# SVG functions
|
219
|
+
{ package SVG;
|
220
|
+
sub new {
|
221
|
+
my $class = shift;
|
222
|
+
my $self = {};
|
223
|
+
bless ($self, $class);
|
224
|
+
return $self;
|
225
|
+
}
|
226
|
+
|
227
|
+
sub header {
|
228
|
+
my ($self, $w, $h) = @_;
|
229
|
+
my $enc_attr = '';
|
230
|
+
if (defined $encoding) {
|
231
|
+
$enc_attr = qq{ encoding="$encoding"};
|
232
|
+
}
|
233
|
+
$self->{svg} .= <<SVG;
|
234
|
+
<?xml version="1.0"$enc_attr standalone="no"?>
|
235
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
236
|
+
<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">
|
237
|
+
<!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. -->
|
238
|
+
SVG
|
239
|
+
}
|
240
|
+
|
241
|
+
sub include {
|
242
|
+
my ($self, $content) = @_;
|
243
|
+
$self->{svg} .= $content;
|
244
|
+
}
|
245
|
+
|
246
|
+
sub colorAllocate {
|
247
|
+
my ($self, $r, $g, $b) = @_;
|
248
|
+
return "rgb($r,$g,$b)";
|
249
|
+
}
|
250
|
+
|
251
|
+
sub group_start {
|
252
|
+
my ($self, $attr) = @_;
|
253
|
+
|
254
|
+
my @g_attr = map {
|
255
|
+
exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : ()
|
256
|
+
} qw(class style onmouseover onmouseout onclick);
|
257
|
+
push @g_attr, $attr->{g_extra} if $attr->{g_extra};
|
258
|
+
$self->{svg} .= sprintf qq/<g %s>\n/, join(' ', @g_attr);
|
259
|
+
|
260
|
+
$self->{svg} .= sprintf qq/<title>%s<\/title>/, $attr->{title}
|
261
|
+
if $attr->{title}; # should be first element within g container
|
262
|
+
|
263
|
+
if ($attr->{href}) {
|
264
|
+
my @a_attr;
|
265
|
+
push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href};
|
266
|
+
# default target=_top else links will open within SVG <object>
|
267
|
+
push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top";
|
268
|
+
push @a_attr, $attr->{a_extra} if $attr->{a_extra};
|
269
|
+
$self->{svg} .= sprintf qq/<a %s>/, join(' ', @a_attr);
|
270
|
+
}
|
271
|
+
}
|
272
|
+
|
273
|
+
sub group_end {
|
274
|
+
my ($self, $attr) = @_;
|
275
|
+
$self->{svg} .= qq/<\/a>\n/ if $attr->{href};
|
276
|
+
$self->{svg} .= qq/<\/g>\n/;
|
277
|
+
}
|
278
|
+
|
279
|
+
sub filledRectangle {
|
280
|
+
my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_;
|
281
|
+
$x1 = sprintf "%0.1f", $x1;
|
282
|
+
$x2 = sprintf "%0.1f", $x2;
|
283
|
+
my $w = sprintf "%0.1f", $x2 - $x1;
|
284
|
+
my $h = sprintf "%0.1f", $y2 - $y1;
|
285
|
+
$extra = defined $extra ? $extra : "";
|
286
|
+
$self->{svg} .= qq/<rect x="$x1" y="$y1" width="$w" height="$h" fill="$fill" $extra \/>\n/;
|
287
|
+
}
|
288
|
+
|
289
|
+
sub stringTTF {
|
290
|
+
my ($self, $color, $font, $size, $angle, $x, $y, $str, $loc, $extra) = @_;
|
291
|
+
$x = sprintf "%0.2f", $x;
|
292
|
+
$loc = defined $loc ? $loc : "left";
|
293
|
+
$extra = defined $extra ? $extra : "";
|
294
|
+
$self->{svg} .= qq/<text text-anchor="$loc" x="$x" y="$y" font-size="$size" font-family="$font" fill="$color" $extra >$str<\/text>\n/;
|
295
|
+
}
|
296
|
+
|
297
|
+
sub svg {
|
298
|
+
my $self = shift;
|
299
|
+
return "$self->{svg}</svg>\n";
|
300
|
+
}
|
301
|
+
1;
|
302
|
+
}
|
303
|
+
|
304
|
+
sub namehash {
|
305
|
+
# Generate a vector hash for the name string, weighting early over
|
306
|
+
# later characters. We want to pick the same colors for function
|
307
|
+
# names across different flame graphs.
|
308
|
+
my $name = shift;
|
309
|
+
my $vector = 0;
|
310
|
+
my $weight = 1;
|
311
|
+
my $max = 1;
|
312
|
+
my $mod = 10;
|
313
|
+
# if module name present, trunc to 1st char
|
314
|
+
$name =~ s/.(.*?)`//;
|
315
|
+
foreach my $c (split //, $name) {
|
316
|
+
my $i = (ord $c) % $mod;
|
317
|
+
$vector += ($i / ($mod++ - 1)) * $weight;
|
318
|
+
$max += 1 * $weight;
|
319
|
+
$weight *= 0.70;
|
320
|
+
last if $mod > 12;
|
321
|
+
}
|
322
|
+
return (1 - $vector / $max)
|
323
|
+
}
|
324
|
+
|
325
|
+
sub color {
|
326
|
+
my ($type, $hash, $name) = @_;
|
327
|
+
my ($v1, $v2, $v3);
|
328
|
+
|
329
|
+
if ($hash) {
|
330
|
+
$v1 = namehash($name);
|
331
|
+
$v2 = $v3 = namehash(scalar reverse $name);
|
332
|
+
} else {
|
333
|
+
$v1 = rand(1);
|
334
|
+
$v2 = rand(1);
|
335
|
+
$v3 = rand(1);
|
336
|
+
}
|
337
|
+
|
338
|
+
# theme palettes
|
339
|
+
if (defined $type and $type eq "hot") {
|
340
|
+
my $r = 205 + int(50 * $v3);
|
341
|
+
my $g = 0 + int(230 * $v1);
|
342
|
+
my $b = 0 + int(55 * $v2);
|
343
|
+
return "rgb($r,$g,$b)";
|
344
|
+
}
|
345
|
+
if (defined $type and $type eq "mem") {
|
346
|
+
my $r = 0;
|
347
|
+
my $g = 190 + int(50 * $v2);
|
348
|
+
my $b = 0 + int(210 * $v1);
|
349
|
+
return "rgb($r,$g,$b)";
|
350
|
+
}
|
351
|
+
if (defined $type and $type eq "io") {
|
352
|
+
my $r = 80 + int(60 * $v1);
|
353
|
+
my $g = $r;
|
354
|
+
my $b = 190 + int(55 * $v2);
|
355
|
+
return "rgb($r,$g,$b)";
|
356
|
+
}
|
357
|
+
|
358
|
+
# multi palettes
|
359
|
+
if (defined $type and $type eq "java") {
|
360
|
+
# Handle both annotations (_[j], _[i], ...; which are
|
361
|
+
# accurate), as well as input that lacks any annotations, as
|
362
|
+
# best as possible. Without annotations, we get a little hacky
|
363
|
+
# and match on java|org|com, etc.
|
364
|
+
if ($name =~ m:_\[j\]$:) { # jit annotation
|
365
|
+
$type = "green";
|
366
|
+
} elsif ($name =~ m:_\[i\]$:) { # inline annotation
|
367
|
+
$type = "aqua";
|
368
|
+
} elsif ($name =~ m:^L?(java|org|com|io|sun)/:) { # Java
|
369
|
+
$type = "green";
|
370
|
+
} elsif ($name =~ /::/) { # C++
|
371
|
+
$type = "yellow";
|
372
|
+
} elsif ($name =~ m:_\[k\]$:) { # kernel annotation
|
373
|
+
$type = "orange";
|
374
|
+
} else { # system
|
375
|
+
$type = "red";
|
376
|
+
}
|
377
|
+
# fall-through to color palettes
|
378
|
+
}
|
379
|
+
if (defined $type and $type eq "perl") {
|
380
|
+
if ($name =~ /::/) { # C++
|
381
|
+
$type = "yellow";
|
382
|
+
} elsif ($name =~ m:Perl: or $name =~ m:\.pl:) { # Perl
|
383
|
+
$type = "green";
|
384
|
+
} elsif ($name =~ m:_\[k\]$:) { # kernel
|
385
|
+
$type = "orange";
|
386
|
+
} else { # system
|
387
|
+
$type = "red";
|
388
|
+
}
|
389
|
+
# fall-through to color palettes
|
390
|
+
}
|
391
|
+
if (defined $type and $type eq "js") {
|
392
|
+
# Handle both annotations (_[j], _[i], ...; which are
|
393
|
+
# accurate), as well as input that lacks any annotations, as
|
394
|
+
# best as possible. Without annotations, we get a little hacky,
|
395
|
+
# and match on a "/" with a ".js", etc.
|
396
|
+
if ($name =~ m:_\[j\]$:) { # jit annotation
|
397
|
+
if ($name =~ m:/:) {
|
398
|
+
$type = "green"; # source
|
399
|
+
} else {
|
400
|
+
$type = "aqua"; # builtin
|
401
|
+
}
|
402
|
+
} elsif ($name =~ /::/) { # C++
|
403
|
+
$type = "yellow";
|
404
|
+
} elsif ($name =~ m:/.*\.js:) { # JavaScript (match "/" in path)
|
405
|
+
$type = "green";
|
406
|
+
} elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin)
|
407
|
+
$type = "aqua";
|
408
|
+
} elsif ($name =~ m/^ $/) { # Missing symbol
|
409
|
+
$type = "green";
|
410
|
+
} elsif ($name =~ m:_\[k\]:) { # kernel
|
411
|
+
$type = "orange";
|
412
|
+
} else { # system
|
413
|
+
$type = "red";
|
414
|
+
}
|
415
|
+
# fall-through to color palettes
|
416
|
+
}
|
417
|
+
if (defined $type and $type eq "wakeup") {
|
418
|
+
$type = "aqua";
|
419
|
+
# fall-through to color palettes
|
420
|
+
}
|
421
|
+
if (defined $type and $type eq "chain") {
|
422
|
+
if ($name =~ m:_\[w\]:) { # waker
|
423
|
+
$type = "aqua"
|
424
|
+
} else { # off-CPU
|
425
|
+
$type = "blue";
|
426
|
+
}
|
427
|
+
# fall-through to color palettes
|
428
|
+
}
|
429
|
+
|
430
|
+
# color palettes
|
431
|
+
if (defined $type and $type eq "red") {
|
432
|
+
my $r = 200 + int(55 * $v1);
|
433
|
+
my $x = 50 + int(80 * $v1);
|
434
|
+
return "rgb($r,$x,$x)";
|
435
|
+
}
|
436
|
+
if (defined $type and $type eq "green") {
|
437
|
+
my $g = 200 + int(55 * $v1);
|
438
|
+
my $x = 50 + int(60 * $v1);
|
439
|
+
return "rgb($x,$g,$x)";
|
440
|
+
}
|
441
|
+
if (defined $type and $type eq "blue") {
|
442
|
+
my $b = 205 + int(50 * $v1);
|
443
|
+
my $x = 80 + int(60 * $v1);
|
444
|
+
return "rgb($x,$x,$b)";
|
445
|
+
}
|
446
|
+
if (defined $type and $type eq "yellow") {
|
447
|
+
my $x = 175 + int(55 * $v1);
|
448
|
+
my $b = 50 + int(20 * $v1);
|
449
|
+
return "rgb($x,$x,$b)";
|
450
|
+
}
|
451
|
+
if (defined $type and $type eq "purple") {
|
452
|
+
my $x = 190 + int(65 * $v1);
|
453
|
+
my $g = 80 + int(60 * $v1);
|
454
|
+
return "rgb($x,$g,$x)";
|
455
|
+
}
|
456
|
+
if (defined $type and $type eq "aqua") {
|
457
|
+
my $r = 50 + int(60 * $v1);
|
458
|
+
my $g = 165 + int(55 * $v1);
|
459
|
+
my $b = 165 + int(55 * $v1);
|
460
|
+
return "rgb($r,$g,$b)";
|
461
|
+
}
|
462
|
+
if (defined $type and $type eq "orange") {
|
463
|
+
my $r = 190 + int(65 * $v1);
|
464
|
+
my $g = 90 + int(65 * $v1);
|
465
|
+
return "rgb($r,$g,0)";
|
466
|
+
}
|
467
|
+
|
468
|
+
return "rgb(0,0,0)";
|
469
|
+
}
|
470
|
+
|
471
|
+
sub color_scale {
|
472
|
+
my ($value, $max) = @_;
|
473
|
+
my ($r, $g, $b) = (255, 255, 255);
|
474
|
+
$value = -$value if $negate;
|
475
|
+
if ($value > 0) {
|
476
|
+
$g = $b = int(210 * ($max - $value) / $max);
|
477
|
+
} elsif ($value < 0) {
|
478
|
+
$r = $g = int(210 * ($max + $value) / $max);
|
479
|
+
}
|
480
|
+
return "rgb($r,$g,$b)";
|
481
|
+
}
|
482
|
+
|
483
|
+
sub color_map {
|
484
|
+
my ($colors, $func) = @_;
|
485
|
+
if (exists $palette_map{$func}) {
|
486
|
+
return $palette_map{$func};
|
487
|
+
} else {
|
488
|
+
$palette_map{$func} = color($colors, $hash, $func);
|
489
|
+
return $palette_map{$func};
|
490
|
+
}
|
491
|
+
}
|
492
|
+
|
493
|
+
sub write_palette {
|
494
|
+
open(FILE, ">$pal_file");
|
495
|
+
foreach my $key (sort keys %palette_map) {
|
496
|
+
print FILE $key."->".$palette_map{$key}."\n";
|
497
|
+
}
|
498
|
+
close(FILE);
|
499
|
+
}
|
500
|
+
|
501
|
+
sub read_palette {
|
502
|
+
if (-e $pal_file) {
|
503
|
+
open(FILE, $pal_file) or die "can't open file $pal_file: $!";
|
504
|
+
while ( my $line = <FILE>) {
|
505
|
+
chomp($line);
|
506
|
+
(my $key, my $value) = split("->",$line);
|
507
|
+
$palette_map{$key}=$value;
|
508
|
+
}
|
509
|
+
close(FILE)
|
510
|
+
}
|
511
|
+
}
|
512
|
+
|
513
|
+
my %Node; # Hash of merged frame data
|
514
|
+
my %Tmp;
|
515
|
+
|
516
|
+
# flow() merges two stacks, storing the merged frames and value data in %Node.
|
517
|
+
sub flow {
|
518
|
+
my ($last, $this, $v, $d) = @_;
|
519
|
+
|
520
|
+
my $len_a = @$last - 1;
|
521
|
+
my $len_b = @$this - 1;
|
522
|
+
|
523
|
+
my $i = 0;
|
524
|
+
my $len_same;
|
525
|
+
for (; $i <= $len_a; $i++) {
|
526
|
+
last if $i > $len_b;
|
527
|
+
last if $last->[$i] ne $this->[$i];
|
528
|
+
}
|
529
|
+
$len_same = $i;
|
530
|
+
|
531
|
+
for ($i = $len_a; $i >= $len_same; $i--) {
|
532
|
+
my $k = "$last->[$i];$i";
|
533
|
+
# a unique ID is constructed from "func;depth;etime";
|
534
|
+
# func-depth isn't unique, it may be repeated later.
|
535
|
+
$Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime};
|
536
|
+
if (defined $Tmp{$k}->{delta}) {
|
537
|
+
$Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta};
|
538
|
+
}
|
539
|
+
delete $Tmp{$k};
|
540
|
+
}
|
541
|
+
|
542
|
+
for ($i = $len_same; $i <= $len_b; $i++) {
|
543
|
+
my $k = "$this->[$i];$i";
|
544
|
+
$Tmp{$k}->{stime} = $v;
|
545
|
+
if (defined $d) {
|
546
|
+
$Tmp{$k}->{delta} += $i == $len_b ? $d : 0;
|
547
|
+
}
|
548
|
+
}
|
549
|
+
|
550
|
+
return $this;
|
551
|
+
}
|
552
|
+
|
553
|
+
# parse input
|
554
|
+
my @Data;
|
555
|
+
my $last = [];
|
556
|
+
my $time = 0;
|
557
|
+
my $delta = undef;
|
558
|
+
my $ignored = 0;
|
559
|
+
my $line;
|
560
|
+
my $maxdelta = 1;
|
561
|
+
|
562
|
+
# reverse if needed
|
563
|
+
foreach (<>) {
|
564
|
+
chomp;
|
565
|
+
$line = $_;
|
566
|
+
if ($stackreverse) {
|
567
|
+
# there may be an extra samples column for differentials
|
568
|
+
# XXX todo: redo these REs as one. It's repeated below.
|
569
|
+
my($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
|
570
|
+
my $samples2 = undef;
|
571
|
+
if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) {
|
572
|
+
$samples2 = $samples;
|
573
|
+
($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
|
574
|
+
unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2";
|
575
|
+
} else {
|
576
|
+
unshift @Data, join(";", reverse split(";", $stack)) . " $samples";
|
577
|
+
}
|
578
|
+
} else {
|
579
|
+
unshift @Data, $line;
|
580
|
+
}
|
581
|
+
}
|
582
|
+
|
583
|
+
# process and merge frames
|
584
|
+
foreach (sort @Data) {
|
585
|
+
chomp;
|
586
|
+
# process: folded_stack count
|
587
|
+
# eg: func_a;func_b;func_c 31
|
588
|
+
my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
|
589
|
+
unless (defined $samples and defined $stack) {
|
590
|
+
++$ignored;
|
591
|
+
next;
|
592
|
+
}
|
593
|
+
|
594
|
+
# there may be an extra samples column for differentials:
|
595
|
+
my $samples2 = undef;
|
596
|
+
if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) {
|
597
|
+
$samples2 = $samples;
|
598
|
+
($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/);
|
599
|
+
}
|
600
|
+
$delta = undef;
|
601
|
+
if (defined $samples2) {
|
602
|
+
$delta = $samples2 - $samples;
|
603
|
+
$maxdelta = abs($delta) if abs($delta) > $maxdelta;
|
604
|
+
}
|
605
|
+
|
606
|
+
# for chain graphs, annotate waker frames with "_[w]", for later
|
607
|
+
# coloring. This is a hack, but has a precedent ("_[k]" from perf).
|
608
|
+
if ($colors eq "chain") {
|
609
|
+
my @parts = split ";--;", $stack;
|
610
|
+
my @newparts = ();
|
611
|
+
$stack = shift @parts;
|
612
|
+
$stack .= ";--;";
|
613
|
+
foreach my $part (@parts) {
|
614
|
+
$part =~ s/;/_[w];/g;
|
615
|
+
$part .= "_[w]";
|
616
|
+
push @newparts, $part;
|
617
|
+
}
|
618
|
+
$stack .= join ";--;", @parts;
|
619
|
+
}
|
620
|
+
|
621
|
+
# merge frames and populate %Node:
|
622
|
+
$last = flow($last, [ '', split ";", $stack ], $time, $delta);
|
623
|
+
|
624
|
+
if (defined $samples2) {
|
625
|
+
$time += $samples2;
|
626
|
+
} else {
|
627
|
+
$time += $samples;
|
628
|
+
}
|
629
|
+
}
|
630
|
+
flow($last, [], $time, $delta);
|
631
|
+
|
632
|
+
warn "Ignored $ignored lines with invalid format\n" if $ignored;
|
633
|
+
unless ($time) {
|
634
|
+
warn "ERROR: No stack counts found\n";
|
635
|
+
my $im = SVG->new();
|
636
|
+
# emit an error message SVG, for tools automating flamegraph use
|
637
|
+
my $imageheight = $fontsize * 5;
|
638
|
+
$im->header($imagewidth, $imageheight);
|
639
|
+
$im->stringTTF($im->colorAllocate(0, 0, 0), $fonttype, $fontsize + 2,
|
640
|
+
0.0, int($imagewidth / 2), $fontsize * 2,
|
641
|
+
"ERROR: No valid input provided to flamegraph.pl.", "middle");
|
642
|
+
print $im->svg;
|
643
|
+
exit 2;
|
644
|
+
}
|
645
|
+
if ($timemax and $timemax < $time) {
|
646
|
+
warn "Specified --total $timemax is less than actual total $time, so ignored\n"
|
647
|
+
if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc)
|
648
|
+
undef $timemax;
|
649
|
+
}
|
650
|
+
$timemax ||= $time;
|
651
|
+
|
652
|
+
my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax;
|
653
|
+
my $minwidth_time = $minwidth / $widthpertime;
|
654
|
+
|
655
|
+
# prune blocks that are too narrow and determine max depth
|
656
|
+
while (my ($id, $node) = each %Node) {
|
657
|
+
my ($func, $depth, $etime) = split ";", $id;
|
658
|
+
my $stime = $node->{stime};
|
659
|
+
die "missing start for $id" if not defined $stime;
|
660
|
+
|
661
|
+
if (($etime-$stime) < $minwidth_time) {
|
662
|
+
delete $Node{$id};
|
663
|
+
next;
|
664
|
+
}
|
665
|
+
$depthmax = $depth if $depth > $depthmax;
|
666
|
+
}
|
667
|
+
|
668
|
+
# draw canvas, and embed interactive JavaScript program
|
669
|
+
my $imageheight = ($depthmax * $frameheight) + $ypad1 + $ypad2;
|
670
|
+
my $im = SVG->new();
|
671
|
+
$im->header($imagewidth, $imageheight);
|
672
|
+
my $inc = <<INC;
|
673
|
+
<defs >
|
674
|
+
<linearGradient id="background" y1="0" y2="1" x1="0" x2="0" >
|
675
|
+
<stop stop-color="$bgcolor1" offset="5%" />
|
676
|
+
<stop stop-color="$bgcolor2" offset="95%" />
|
677
|
+
</linearGradient>
|
678
|
+
</defs>
|
679
|
+
<style type="text/css">
|
680
|
+
.func_g:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
|
681
|
+
</style>
|
682
|
+
<script type="text/ecmascript">
|
683
|
+
<![CDATA[
|
684
|
+
var details, searchbtn, matchedtxt, svg;
|
685
|
+
function init(evt) {
|
686
|
+
details = document.getElementById("details").firstChild;
|
687
|
+
searchbtn = document.getElementById("search");
|
688
|
+
matchedtxt = document.getElementById("matched");
|
689
|
+
svg = document.getElementsByTagName("svg")[0];
|
690
|
+
searching = 0;
|
691
|
+
}
|
692
|
+
|
693
|
+
// mouse-over for info
|
694
|
+
function s(node) { // show
|
695
|
+
info = g_to_text(node);
|
696
|
+
details.nodeValue = "$nametype " + info;
|
697
|
+
}
|
698
|
+
function c() { // clear
|
699
|
+
details.nodeValue = ' ';
|
700
|
+
}
|
701
|
+
|
702
|
+
// ctrl-F for search
|
703
|
+
window.addEventListener("keydown",function (e) {
|
704
|
+
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
|
705
|
+
e.preventDefault();
|
706
|
+
search_prompt();
|
707
|
+
}
|
708
|
+
})
|
709
|
+
|
710
|
+
// functions
|
711
|
+
function find_child(parent, name, attr) {
|
712
|
+
var children = parent.childNodes;
|
713
|
+
for (var i=0; i<children.length;i++) {
|
714
|
+
if (children[i].tagName == name)
|
715
|
+
return (attr != undefined) ? children[i].attributes[attr].value : children[i];
|
716
|
+
}
|
717
|
+
return;
|
718
|
+
}
|
719
|
+
function orig_save(e, attr, val) {
|
720
|
+
if (e.attributes["_orig_"+attr] != undefined) return;
|
721
|
+
if (e.attributes[attr] == undefined) return;
|
722
|
+
if (val == undefined) val = e.attributes[attr].value;
|
723
|
+
e.setAttribute("_orig_"+attr, val);
|
724
|
+
}
|
725
|
+
function orig_load(e, attr) {
|
726
|
+
if (e.attributes["_orig_"+attr] == undefined) return;
|
727
|
+
e.attributes[attr].value = e.attributes["_orig_"+attr].value;
|
728
|
+
e.removeAttribute("_orig_"+attr);
|
729
|
+
}
|
730
|
+
function g_to_text(e) {
|
731
|
+
var text = find_child(e, "title").firstChild.nodeValue;
|
732
|
+
return (text)
|
733
|
+
}
|
734
|
+
function g_to_func(e) {
|
735
|
+
var func = g_to_text(e);
|
736
|
+
// if there's any manipulation we want to do to the function
|
737
|
+
// name before it's searched, do it here before returning.
|
738
|
+
return (func);
|
739
|
+
}
|
740
|
+
function update_text(e) {
|
741
|
+
var r = find_child(e, "rect");
|
742
|
+
var t = find_child(e, "text");
|
743
|
+
var w = parseFloat(r.attributes["width"].value) -3;
|
744
|
+
var txt = find_child(e, "title").textContent.replace(/\\([^(]*\\)\$/,"");
|
745
|
+
t.attributes["x"].value = parseFloat(r.attributes["x"].value) +3;
|
746
|
+
|
747
|
+
// Smaller than this size won't fit anything
|
748
|
+
if (w < 2*$fontsize*$fontwidth) {
|
749
|
+
t.textContent = "";
|
750
|
+
return;
|
751
|
+
}
|
752
|
+
|
753
|
+
t.textContent = txt;
|
754
|
+
// Fit in full text width
|
755
|
+
if (/^ *\$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
|
756
|
+
return;
|
757
|
+
|
758
|
+
for (var x=txt.length-2; x>0; x--) {
|
759
|
+
if (t.getSubStringLength(0, x+2) <= w) {
|
760
|
+
t.textContent = txt.substring(0,x) + "..";
|
761
|
+
return;
|
762
|
+
}
|
763
|
+
}
|
764
|
+
t.textContent = "";
|
765
|
+
}
|
766
|
+
|
767
|
+
// zoom
|
768
|
+
function zoom_reset(e) {
|
769
|
+
if (e.attributes != undefined) {
|
770
|
+
orig_load(e, "x");
|
771
|
+
orig_load(e, "width");
|
772
|
+
}
|
773
|
+
if (e.childNodes == undefined) return;
|
774
|
+
for(var i=0, c=e.childNodes; i<c.length; i++) {
|
775
|
+
zoom_reset(c[i]);
|
776
|
+
}
|
777
|
+
}
|
778
|
+
function zoom_child(e, x, ratio) {
|
779
|
+
if (e.attributes != undefined) {
|
780
|
+
if (e.attributes["x"] != undefined) {
|
781
|
+
orig_save(e, "x");
|
782
|
+
e.attributes["x"].value = (parseFloat(e.attributes["x"].value) - x - $xpad) * ratio + $xpad;
|
783
|
+
if(e.tagName == "text") e.attributes["x"].value = find_child(e.parentNode, "rect", "x") + 3;
|
784
|
+
}
|
785
|
+
if (e.attributes["width"] != undefined) {
|
786
|
+
orig_save(e, "width");
|
787
|
+
e.attributes["width"].value = parseFloat(e.attributes["width"].value) * ratio;
|
788
|
+
}
|
789
|
+
}
|
790
|
+
|
791
|
+
if (e.childNodes == undefined) return;
|
792
|
+
for(var i=0, c=e.childNodes; i<c.length; i++) {
|
793
|
+
zoom_child(c[i], x-$xpad, ratio);
|
794
|
+
}
|
795
|
+
}
|
796
|
+
function zoom_parent(e) {
|
797
|
+
if (e.attributes) {
|
798
|
+
if (e.attributes["x"] != undefined) {
|
799
|
+
orig_save(e, "x");
|
800
|
+
e.attributes["x"].value = $xpad;
|
801
|
+
}
|
802
|
+
if (e.attributes["width"] != undefined) {
|
803
|
+
orig_save(e, "width");
|
804
|
+
e.attributes["width"].value = parseInt(svg.width.baseVal.value) - ($xpad*2);
|
805
|
+
}
|
806
|
+
}
|
807
|
+
if (e.childNodes == undefined) return;
|
808
|
+
for(var i=0, c=e.childNodes; i<c.length; i++) {
|
809
|
+
zoom_parent(c[i]);
|
810
|
+
}
|
811
|
+
}
|
812
|
+
function zoom(node) {
|
813
|
+
var attr = find_child(node, "rect").attributes;
|
814
|
+
var width = parseFloat(attr["width"].value);
|
815
|
+
var xmin = parseFloat(attr["x"].value);
|
816
|
+
var xmax = parseFloat(xmin + width);
|
817
|
+
var ymin = parseFloat(attr["y"].value);
|
818
|
+
var ratio = (svg.width.baseVal.value - 2*$xpad) / width;
|
819
|
+
|
820
|
+
// XXX: Workaround for JavaScript float issues (fix me)
|
821
|
+
var fudge = 0.0001;
|
822
|
+
|
823
|
+
var unzoombtn = document.getElementById("unzoom");
|
824
|
+
unzoombtn.style["opacity"] = "1.0";
|
825
|
+
|
826
|
+
var el = document.getElementsByTagName("g");
|
827
|
+
for(var i=0;i<el.length;i++){
|
828
|
+
var e = el[i];
|
829
|
+
var a = find_child(e, "rect").attributes;
|
830
|
+
var ex = parseFloat(a["x"].value);
|
831
|
+
var ew = parseFloat(a["width"].value);
|
832
|
+
// Is it an ancestor
|
833
|
+
if ($inverted == 0) {
|
834
|
+
var upstack = parseFloat(a["y"].value) > ymin;
|
835
|
+
} else {
|
836
|
+
var upstack = parseFloat(a["y"].value) < ymin;
|
837
|
+
}
|
838
|
+
if (upstack) {
|
839
|
+
// Direct ancestor
|
840
|
+
if (ex <= xmin && (ex+ew+fudge) >= xmax) {
|
841
|
+
e.style["opacity"] = "0.5";
|
842
|
+
zoom_parent(e);
|
843
|
+
e.onclick = function(e){unzoom(); zoom(this);};
|
844
|
+
update_text(e);
|
845
|
+
}
|
846
|
+
// not in current path
|
847
|
+
else
|
848
|
+
e.style["display"] = "none";
|
849
|
+
}
|
850
|
+
// Children maybe
|
851
|
+
else {
|
852
|
+
// no common path
|
853
|
+
if (ex < xmin || ex + fudge >= xmax) {
|
854
|
+
e.style["display"] = "none";
|
855
|
+
}
|
856
|
+
else {
|
857
|
+
zoom_child(e, xmin, ratio);
|
858
|
+
e.onclick = function(e){zoom(this);};
|
859
|
+
update_text(e);
|
860
|
+
}
|
861
|
+
}
|
862
|
+
}
|
863
|
+
}
|
864
|
+
function unzoom() {
|
865
|
+
var unzoombtn = document.getElementById("unzoom");
|
866
|
+
unzoombtn.style["opacity"] = "0.0";
|
867
|
+
|
868
|
+
var el = document.getElementsByTagName("g");
|
869
|
+
for(i=0;i<el.length;i++) {
|
870
|
+
el[i].style["display"] = "block";
|
871
|
+
el[i].style["opacity"] = "1";
|
872
|
+
zoom_reset(el[i]);
|
873
|
+
update_text(el[i]);
|
874
|
+
}
|
875
|
+
}
|
876
|
+
|
877
|
+
// search
|
878
|
+
function reset_search() {
|
879
|
+
var el = document.getElementsByTagName("rect");
|
880
|
+
for (var i=0; i < el.length; i++) {
|
881
|
+
orig_load(el[i], "fill")
|
882
|
+
}
|
883
|
+
}
|
884
|
+
function search_prompt() {
|
885
|
+
if (!searching) {
|
886
|
+
var term = prompt("Enter a search term (regexp " +
|
887
|
+
"allowed, eg: ^ext4_)", "");
|
888
|
+
if (term != null) {
|
889
|
+
search(term)
|
890
|
+
}
|
891
|
+
} else {
|
892
|
+
reset_search();
|
893
|
+
searching = 0;
|
894
|
+
searchbtn.style["opacity"] = "0.1";
|
895
|
+
searchbtn.firstChild.nodeValue = "Search"
|
896
|
+
matchedtxt.style["opacity"] = "0.0";
|
897
|
+
matchedtxt.firstChild.nodeValue = ""
|
898
|
+
}
|
899
|
+
}
|
900
|
+
function search(term) {
|
901
|
+
var re = new RegExp(term);
|
902
|
+
var el = document.getElementsByTagName("g");
|
903
|
+
var matches = new Object();
|
904
|
+
var maxwidth = 0;
|
905
|
+
for (var i = 0; i < el.length; i++) {
|
906
|
+
var e = el[i];
|
907
|
+
if (e.attributes["class"].value != "func_g")
|
908
|
+
continue;
|
909
|
+
var func = g_to_func(e);
|
910
|
+
var rect = find_child(e, "rect");
|
911
|
+
if (rect == null) {
|
912
|
+
// the rect might be wrapped in an anchor
|
913
|
+
// if nameattr href is being used
|
914
|
+
if (rect = find_child(e, "a")) {
|
915
|
+
rect = find_child(r, "rect");
|
916
|
+
}
|
917
|
+
}
|
918
|
+
if (func == null || rect == null)
|
919
|
+
continue;
|
920
|
+
|
921
|
+
// Save max width. Only works as we have a root frame
|
922
|
+
var w = parseFloat(rect.attributes["width"].value);
|
923
|
+
if (w > maxwidth)
|
924
|
+
maxwidth = w;
|
925
|
+
|
926
|
+
if (func.match(re)) {
|
927
|
+
// highlight
|
928
|
+
var x = parseFloat(rect.attributes["x"].value);
|
929
|
+
orig_save(rect, "fill");
|
930
|
+
rect.attributes["fill"].value =
|
931
|
+
"$searchcolor";
|
932
|
+
|
933
|
+
// remember matches
|
934
|
+
if (matches[x] == undefined) {
|
935
|
+
matches[x] = w;
|
936
|
+
} else {
|
937
|
+
if (w > matches[x]) {
|
938
|
+
// overwrite with parent
|
939
|
+
matches[x] = w;
|
940
|
+
}
|
941
|
+
}
|
942
|
+
searching = 1;
|
943
|
+
}
|
944
|
+
}
|
945
|
+
if (!searching)
|
946
|
+
return;
|
947
|
+
|
948
|
+
searchbtn.style["opacity"] = "1.0";
|
949
|
+
searchbtn.firstChild.nodeValue = "Reset Search"
|
950
|
+
|
951
|
+
// calculate percent matched, excluding vertical overlap
|
952
|
+
var count = 0;
|
953
|
+
var lastx = -1;
|
954
|
+
var lastw = 0;
|
955
|
+
var keys = Array();
|
956
|
+
for (k in matches) {
|
957
|
+
if (matches.hasOwnProperty(k))
|
958
|
+
keys.push(k);
|
959
|
+
}
|
960
|
+
// sort the matched frames by their x location
|
961
|
+
// ascending, then width descending
|
962
|
+
keys.sort(function(a, b){
|
963
|
+
return a - b;
|
964
|
+
if (a < b || a > b)
|
965
|
+
return a - b;
|
966
|
+
return matches[b] - matches[a];
|
967
|
+
});
|
968
|
+
// Step through frames saving only the biggest bottom-up frames
|
969
|
+
// thanks to the sort order. This relies on the tree property
|
970
|
+
// where children are always smaller than their parents.
|
971
|
+
for (var k in keys) {
|
972
|
+
var x = parseFloat(keys[k]);
|
973
|
+
var w = matches[keys[k]];
|
974
|
+
if (x >= lastx + lastw) {
|
975
|
+
count += w;
|
976
|
+
lastx = x;
|
977
|
+
lastw = w;
|
978
|
+
}
|
979
|
+
}
|
980
|
+
// display matched percent
|
981
|
+
matchedtxt.style["opacity"] = "1.0";
|
982
|
+
pct = 100 * count / maxwidth;
|
983
|
+
if (pct == 100)
|
984
|
+
pct = "100"
|
985
|
+
else
|
986
|
+
pct = pct.toFixed(1)
|
987
|
+
matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
|
988
|
+
}
|
989
|
+
function searchover(e) {
|
990
|
+
searchbtn.style["opacity"] = "1.0";
|
991
|
+
}
|
992
|
+
function searchout(e) {
|
993
|
+
if (searching) {
|
994
|
+
searchbtn.style["opacity"] = "1.0";
|
995
|
+
} else {
|
996
|
+
searchbtn.style["opacity"] = "0.1";
|
997
|
+
}
|
998
|
+
}
|
999
|
+
]]>
|
1000
|
+
</script>
|
1001
|
+
INC
|
1002
|
+
$im->include($inc);
|
1003
|
+
$im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)');
|
1004
|
+
my ($white, $black, $vvdgrey, $vdgrey, $dgrey) = (
|
1005
|
+
$im->colorAllocate(255, 255, 255),
|
1006
|
+
$im->colorAllocate(0, 0, 0),
|
1007
|
+
$im->colorAllocate(40, 40, 40),
|
1008
|
+
$im->colorAllocate(160, 160, 160),
|
1009
|
+
$im->colorAllocate(200, 200, 200),
|
1010
|
+
);
|
1011
|
+
$im->stringTTF($black, $fonttype, $fontsize + 5, 0.0, int($imagewidth / 2), $fontsize * 2, $titletext, "middle");
|
1012
|
+
$im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $imageheight - ($ypad2 / 2), " ", "", 'id="details"');
|
1013
|
+
$im->stringTTF($black, $fonttype, $fontsize, 0.0, $xpad, $fontsize * 2,
|
1014
|
+
"Reset Zoom", "", 'id="unzoom" onclick="unzoom()" style="opacity:0.0;cursor:pointer"');
|
1015
|
+
$im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100,
|
1016
|
+
$fontsize * 2, "Search", "", 'id="search" onmouseover="searchover()" onmouseout="searchout()" onclick="search_prompt()" style="opacity:0.1;cursor:pointer"');
|
1017
|
+
$im->stringTTF($black, $fonttype, $fontsize, 0.0, $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " ", "", 'id="matched"');
|
1018
|
+
|
1019
|
+
if ($palette) {
|
1020
|
+
read_palette();
|
1021
|
+
}
|
1022
|
+
|
1023
|
+
# draw frames
|
1024
|
+
while (my ($id, $node) = each %Node) {
|
1025
|
+
my ($func, $depth, $etime) = split ";", $id;
|
1026
|
+
my $stime = $node->{stime};
|
1027
|
+
my $delta = $node->{delta};
|
1028
|
+
|
1029
|
+
$etime = $timemax if $func eq "" and $depth == 0;
|
1030
|
+
|
1031
|
+
my $x1 = $xpad + $stime * $widthpertime;
|
1032
|
+
my $x2 = $xpad + $etime * $widthpertime;
|
1033
|
+
my ($y1, $y2);
|
1034
|
+
unless ($inverted) {
|
1035
|
+
$y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad;
|
1036
|
+
$y2 = $imageheight - $ypad2 - $depth * $frameheight;
|
1037
|
+
} else {
|
1038
|
+
$y1 = $ypad1 + $depth * $frameheight;
|
1039
|
+
$y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad;
|
1040
|
+
}
|
1041
|
+
|
1042
|
+
my $samples = sprintf "%.0f", ($etime - $stime) * $factor;
|
1043
|
+
(my $samples_txt = $samples) # add commas per perlfaq5
|
1044
|
+
=~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
|
1045
|
+
|
1046
|
+
my $info;
|
1047
|
+
if ($func eq "" and $depth == 0) {
|
1048
|
+
$info = "all ($samples_txt $countname, 100%)";
|
1049
|
+
} else {
|
1050
|
+
my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor));
|
1051
|
+
my $escaped_func = $func;
|
1052
|
+
# clean up SVG breaking characters:
|
1053
|
+
$escaped_func =~ s/&/&/g;
|
1054
|
+
$escaped_func =~ s/</</g;
|
1055
|
+
$escaped_func =~ s/>/>/g;
|
1056
|
+
$escaped_func =~ s/"/"/g;
|
1057
|
+
$escaped_func =~ s/_\[[kwij]\]$//; # strip any annotation
|
1058
|
+
unless (defined $delta) {
|
1059
|
+
$info = "$escaped_func ($samples_txt $countname, $pct%)";
|
1060
|
+
} else {
|
1061
|
+
my $d = $negate ? -$delta : $delta;
|
1062
|
+
my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor));
|
1063
|
+
$deltapct = $d > 0 ? "+$deltapct" : $deltapct;
|
1064
|
+
$info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)";
|
1065
|
+
}
|
1066
|
+
}
|
1067
|
+
|
1068
|
+
my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone
|
1069
|
+
$nameattr->{class} ||= "func_g";
|
1070
|
+
$nameattr->{onmouseover} ||= "s(this)";
|
1071
|
+
$nameattr->{onmouseout} ||= "c()";
|
1072
|
+
$nameattr->{onclick} ||= "zoom(this)";
|
1073
|
+
$nameattr->{title} ||= $info;
|
1074
|
+
$im->group_start($nameattr);
|
1075
|
+
|
1076
|
+
my $color;
|
1077
|
+
if ($func eq "--") {
|
1078
|
+
$color = $vdgrey;
|
1079
|
+
} elsif ($func eq "-") {
|
1080
|
+
$color = $dgrey;
|
1081
|
+
} elsif (defined $delta) {
|
1082
|
+
$color = color_scale($delta, $maxdelta);
|
1083
|
+
} elsif ($palette) {
|
1084
|
+
$color = color_map($colors, $func);
|
1085
|
+
} else {
|
1086
|
+
$color = color($colors, $hash, $func);
|
1087
|
+
}
|
1088
|
+
$im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"');
|
1089
|
+
|
1090
|
+
my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth));
|
1091
|
+
my $text = "";
|
1092
|
+
if ($chars >= 3) { # room for one char plus two dots
|
1093
|
+
$func =~ s/_\[[kwij]\]$//; # strip any annotation
|
1094
|
+
$text = substr $func, 0, $chars;
|
1095
|
+
substr($text, -2, 2) = ".." if $chars < length $func;
|
1096
|
+
$text =~ s/&/&/g;
|
1097
|
+
$text =~ s/</</g;
|
1098
|
+
$text =~ s/>/>/g;
|
1099
|
+
}
|
1100
|
+
$im->stringTTF($black, $fonttype, $fontsize, 0.0, $x1 + 3, 3 + ($y1 + $y2) / 2, $text, "");
|
1101
|
+
|
1102
|
+
$im->group_end($nameattr);
|
1103
|
+
}
|
1104
|
+
|
1105
|
+
print $im->svg;
|
1106
|
+
|
1107
|
+
if ($palette) {
|
1108
|
+
write_palette();
|
1109
|
+
}
|
1110
|
+
|
1111
|
+
# vim: ts=8 sts=8 sw=8 noexpandtab
|