├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── stackprof ├── stackprof-flamegraph.pl └── stackprof-gprof2dot.py ├── ext └── stackprof │ ├── extconf.rb │ └── stackprof.c ├── lib ├── stackprof.rb └── stackprof │ ├── autorun.rb │ ├── flamegraph │ ├── flamegraph.js │ └── viewer.html │ ├── middleware.rb │ ├── report.rb │ └── truffleruby.rb ├── sample.rb ├── stackprof.gemspec ├── test ├── fixtures │ ├── profile.dump │ └── profile.json ├── test_middleware.rb ├── test_report.rb ├── test_stackprof.rb └── test_truffleruby.rb └── vendor ├── FlameGraph ├── README └── flamegraph.pl └── gprof2dot ├── gprof2dot.py └── hotshotmain.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rubies: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: [ ruby-head, '3.4', '3.3','3.2', '3.1', '3.0', truffleruby ] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | - name: Install dependencies 20 | run: bundle install 21 | - name: Run test 22 | run: rake 23 | - name: Install gem 24 | run: rake install 25 | platforms: 26 | strategy: 27 | matrix: 28 | os: [macos] 29 | ruby: ['3.0'] 30 | runs-on: ${{ matrix.os }}-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v2 34 | - name: Set up Ruby 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: ${{ matrix.ruby }} 38 | - name: Install dependencies 39 | run: bundle install 40 | - name: Run test 41 | run: rake 42 | - name: Install gem 43 | run: rake install 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /lib/stackprof/stackprof.bundle 3 | /lib/stackprof/stackprof.so 4 | *.sw? 5 | /pkg 6 | /Gemfile.lock 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.25 2 | 3 | * Fix GC marking 4 | 5 | # 0.2.16 6 | 7 | * [flamegraph.pl] Update to latest version 8 | * Add option to ignore GC frames 9 | * Handle source code not being available 10 | * Freeze strings in report.rb 11 | * Use a cursor object instead of array slicing 12 | * ArgumentError on interval <1 or >1m 13 | * fix variable name. 14 | * Fix default mode comment in readme 15 | 16 | # 0.2.15 17 | 18 | * Mark the metadata object before the GC is invoked to prevent it from being garbage collected. 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Aman Gupta 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stackprof 2 | 3 | A sampling call-stack profiler for Ruby. 4 | 5 | Inspired heavily by [gperftools](https://code.google.com/p/gperftools/), and written as a replacement for [perftools.rb](https://github.com/tmm1/perftools.rb). 6 | 7 | ## Requirements 8 | 9 | * Ruby 2.2+ 10 | * Linux-based OS 11 | 12 | ## Getting Started 13 | 14 | ### Install 15 | 16 | In your Gemfile add: 17 | 18 | ```ruby 19 | gem 'stackprof' 20 | ``` 21 | 22 | Then run `$ bundle install`. Alternatively you can run `$ gem install stackprof`. 23 | 24 | 25 | ### Run 26 | 27 | in ruby: 28 | 29 | ``` ruby 30 | StackProf.run(mode: :cpu, out: 'tmp/stackprof-cpu-myapp.dump') do 31 | #... 32 | end 33 | ``` 34 | 35 | via rack: 36 | 37 | ``` ruby 38 | use StackProf::Middleware, enabled: true, 39 | mode: :cpu, 40 | interval: 1000, 41 | save_every: 5 42 | ``` 43 | 44 | reporting: 45 | 46 | ``` 47 | $ stackprof tmp/stackprof-cpu-*.dump --text --limit 1 48 | ================================== 49 | Mode: cpu(1000) 50 | Samples: 60395 (1.09% miss rate) 51 | GC: 2851 (4.72%) 52 | ================================== 53 | TOTAL (pct) SAMPLES (pct) FRAME 54 | 1660 (2.7%) 1595 (2.6%) String#blank? 55 | 56 | $ stackprof tmp/stackprof-cpu-*.dump --method 'String#blank?' 57 | String#blank? (gems/activesupport-2.3.14.github30/lib/active_support/core_ext/object/blank.rb:80) 58 | samples: 1595 self (2.6%) / 1660 total (2.7%) 59 | callers: 60 | 373 ( 41.0%) ApplicationHelper#current_user 61 | 192 ( 21.1%) ApplicationHelper#current_repository 62 | callers: 63 | 803 ( 48.4%) Object#present? 64 | code: 65 | | 80 | def blank? 66 | 1225 (2.0%) / 1225 (2.0%) | 81 | self !~ /[^[:space:]]/ 67 | | 82 | end 68 | 69 | $ stackprof tmp/stackprof-cpu-*.dump --method 'Object#present?' 70 | Object#present? (gems/activesupport-2.3.14.github30/lib/active_support/core_ext/object/blank.rb:20) 71 | samples: 59 self (0.1%) / 910 total (1.5%) 72 | callees (851 total): 73 | 803 ( 94.4%) String#blank? 74 | 32 ( 3.8%) Object#blank? 75 | 16 ( 1.9%) NilClass#blank? 76 | code: 77 | | 20 | def present? 78 | 910 (1.5%) / 59 (0.1%) | 21 | !blank? 79 | | 22 | end 80 | ``` 81 | 82 | For an experimental version of WebUI reporting of stackprof, see [stackprof-webnav](https://github.com/alisnic/stackprof-webnav) 83 | 84 | To generate flamegraphs with Stackprof, additional data must be collected using the `raw: true` flag. Once you've collected results with this flag enabled, generate a flamegraph with: 85 | 86 | ``` 87 | $ stackprof --flamegraph tmp/stackprof-cpu-myapp.dump > tmp/flamegraph 88 | ``` 89 | 90 | After the flamegraph has been generated, you can generate a viewer command with: 91 | 92 | ``` 93 | $ stackprof --flamegraph-viewer=tmp/flamegraph 94 | ``` 95 | 96 | The `--flamegraph-viewer` command will output the exact shell command you need to run in order to open the `tmp/flamegraph` you generated with the built-in stackprof flamegraph viewer: 97 | 98 | ![Flamegraph Viewer](http://i.imgur.com/EwndrgD.png) 99 | 100 | Alternatively, you can generate a flamegraph that uses [d3-flame-graph](https://github.com/spiermar/d3-flame-graph): 101 | 102 | ``` 103 | $ stackprof --d3-flamegraph tmp/stackprof-cpu-myapp.dump > flamegraph.html 104 | ``` 105 | 106 | And just open the result by your browser. 107 | 108 | ## Sampling 109 | 110 | Four sampling modes are supported: 111 | 112 | - `:wall` (using `ITIMER_REAL` and `SIGALRM`) [default mode] 113 | - `:cpu` (using `ITIMER_PROF` and `SIGPROF`) 114 | - `:object` (using `RUBY_INTERNAL_EVENT_NEWOBJ`) 115 | - `:custom` (user-defined via `StackProf.sample`) 116 | 117 | Samplers have a tuneable interval which can be used to reduce overhead or increase granularity: 118 | 119 | - Wall time: sample every _interval_ microseconds of wallclock time (default: 1000) 120 | 121 | ```ruby 122 | StackProf.run(mode: :wall, out: 'tmp/stackprof.dump', interval: 1000) do 123 | #... 124 | end 125 | ``` 126 | 127 | - CPU time: sample every _interval_ microseconds of CPU activity (default: 1000 = 1 millisecond) 128 | 129 | ```ruby 130 | StackProf.run(mode: :cpu, out: 'tmp/stackprof.dump', interval: 1000) do 131 | #... 132 | end 133 | ``` 134 | 135 | - Object allocation: sample every _interval_ allocations (default: 1) 136 | 137 | 138 | ```ruby 139 | StackProf.run(mode: :object, out: 'tmp/stackprof.dump', interval: 1) do 140 | #... 141 | end 142 | ``` 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. They 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 | 150 | Samples are taken using a combination of three new C-APIs in ruby 2.1: 151 | 152 | - Signal handlers enqueue a sampling job using `rb_postponed_job_register_one`. 153 | this ensures callstack samples can be taken safely, in case the VM is garbage collecting 154 | or in some other inconsistent state during the interruption. 155 | 156 | - Stack frames are collected via `rb_profile_frames`, which provides low-overhead C-API access 157 | to the VM's call stack. No object allocations occur in this path, allowing stackprof to collect 158 | callstacks in allocation mode. 159 | 160 | - In allocation mode, samples are taken via `rb_tracepoint_new(RUBY_INTERNAL_EVENT_NEWOBJ)`, 161 | which provides a notification every time the VM allocates a new object. 162 | 163 | ## Aggregation 164 | 165 | Each sample consists of N stack frames, where a frame looks something like `MyClass#method` or `block in MySingleton.method`. 166 | For each of these frames in the sample, the profiler collects a few pieces of metadata: 167 | 168 | - `samples`: Number of samples where this was the topmost frame 169 | - `total_samples`: Samples where this frame was in the stack 170 | - `lines`: Samples per line number in this frame 171 | - `edges`: Samples per callee frame (methods invoked by this frame) 172 | 173 | The aggregation algorithm is roughly equivalent to the following pseudo code: 174 | 175 | ``` ruby 176 | trap('PROF') do 177 | top, *rest = caller 178 | 179 | top.samples += 1 180 | top.lines[top.lineno] += 1 181 | top.total_samples += 1 182 | 183 | prev = top 184 | rest.each do |frame| 185 | frame.edges[prev] += 1 186 | frame.total_samples += 1 187 | prev = frame 188 | end 189 | end 190 | ``` 191 | 192 | This technique builds up an incremental call graph from the samples. On any given frame, 193 | the sum of the outbound edge weights is equal to total samples collected on that frame 194 | (`frame.total_samples == frame.edges.values.sum`). 195 | 196 | ## Reporting 197 | 198 | Multiple reporting modes are supported: 199 | - Text 200 | - Dotgraph 201 | - Source annotation 202 | 203 | ### `StackProf::Report.new(data).print_text` 204 | 205 | ``` 206 | TOTAL (pct) SAMPLES (pct) FRAME 207 | 91 (48.4%) 91 (48.4%) A#pow 208 | 58 (30.9%) 58 (30.9%) A.newobj 209 | 34 (18.1%) 34 (18.1%) block in A#math 210 | 188 (100.0%) 3 (1.6%) block (2 levels) in
211 | 185 (98.4%) 1 (0.5%) A#initialize 212 | 35 (18.6%) 1 (0.5%) A#math 213 | 188 (100.0%) 0 (0.0%)
214 | 188 (100.0%) 0 (0.0%) block in
215 | 188 (100.0%) 0 (0.0%)
216 | ``` 217 | 218 | ### `StackProf::Report.new(data).print_graphviz` 219 | 220 | ``` 221 | digraph profile { 222 | 70346498324780 [size=23.5531914893617] [fontsize=23.5531914893617] [shape=box] [label="A#pow\n91 (48.4%)\r"]; 223 | 70346498324680 [size=18.638297872340424] [fontsize=18.638297872340424] [shape=box] [label="A.newobj\n58 (30.9%)\r"]; 224 | 70346498324480 [size=15.063829787234042] [fontsize=15.063829787234042] [shape=box] [label="block in A#math\n34 (18.1%)\r"]; 225 | 70346498324220 [size=10.446808510638299] [fontsize=10.446808510638299] [shape=box] [label="block (2 levels) in
\n3 (1.6%)\rof 188 (100.0%)\r"]; 226 | 70346498324220 -> 70346498324900 [label="185"]; 227 | 70346498324900 [size=10.148936170212766] [fontsize=10.148936170212766] [shape=box] [label="A#initialize\n1 (0.5%)\rof 185 (98.4%)\r"]; 228 | 70346498324900 -> 70346498324780 [label="91"]; 229 | 70346498324900 -> 70346498324680 [label="58"]; 230 | 70346498324900 -> 70346498324580 [label="35"]; 231 | 70346498324580 [size=10.148936170212766] [fontsize=10.148936170212766] [shape=box] [label="A#math\n1 (0.5%)\rof 35 (18.6%)\r"]; 232 | 70346498324580 -> 70346498324480 [label="34"]; 233 | 70346497983360 [size=10.0] [fontsize=10.0] [shape=box] [label="
\n0 (0.0%)\rof 188 (100.0%)\r"]; 234 | 70346497983360 -> 70346498325080 [label="188"]; 235 | 70346498324300 [size=10.0] [fontsize=10.0] [shape=box] [label="block in
\n0 (0.0%)\rof 188 (100.0%)\r"]; 236 | 70346498324300 -> 70346498324220 [label="188"]; 237 | 70346498325080 [size=10.0] [fontsize=10.0] [shape=box] [label="
\n0 (0.0%)\rof 188 (100.0%)\r"]; 238 | 70346498325080 -> 70346498324300 [label="188"]; 239 | } 240 | ``` 241 | 242 | ### `StackProf::Report.new(data).print_method(/pow|newobj|math/)` 243 | 244 | ``` 245 | A#pow (/Users/tmm1/code/stackprof/sample.rb:11) 246 | | 11 | def pow 247 | 91 (48.4% / 100.0%) | 12 | 2 ** 100 248 | | 13 | end 249 | A.newobj (/Users/tmm1/code/stackprof/sample.rb:15) 250 | | 15 | def self.newobj 251 | 33 (17.6% / 56.9%) | 16 | Object.new 252 | 25 (13.3% / 43.1%) | 17 | Object.new 253 | | 18 | end 254 | A#math (/Users/tmm1/code/stackprof/sample.rb:20) 255 | | 20 | def math 256 | 1 (0.5% / 100.0%) | 21 | 2.times do 257 | | 22 | 2 + 3 * 4 ^ 5 / 6 258 | block in A#math (/Users/tmm1/code/stackprof/sample.rb:21) 259 | | 21 | 2.times do 260 | 34 (18.1% / 100.0%) | 22 | 2 + 3 * 4 ^ 5 / 6 261 | | 23 | end 262 | ``` 263 | 264 | ## Usage 265 | 266 | The profiler is compiled as a C-extension and exposes a simple api: `StackProf.run(mode: [:cpu|:wall|:object])`. 267 | The `run` method takes a block of code and returns a profile as a simple hash. 268 | 269 | ``` ruby 270 | # sample after every 1ms of cpu activity 271 | profile = StackProf.run(mode: :cpu, interval: 1000) do 272 | MyCode.execute 273 | end 274 | ``` 275 | 276 | This profile data structure is part of the public API, and is intended to be saved 277 | (as json/marshal for example) for later processing. The reports above can be generated 278 | by passing this structure into `StackProf::Report.new`. 279 | 280 | The format itself is very simple. It contains a header and a list of frames. Each frame has a unique ID and 281 | identifying information such as its name, file, and line. The frame also contains sampling data, including per-line 282 | samples, and a list of relationships to other frames represented as weighted edges. 283 | 284 | ``` ruby 285 | {:version=>1.0, 286 | :mode=>:cpu, 287 | :inteval=>1000, 288 | :samples=>188, 289 | :missed_samples=>0, 290 | :frames=> 291 | {70346498324780=> 292 | {:name=>"A#pow", 293 | :file=>"/Users/tmm1/code/stackprof/sample.rb", 294 | :line=>11, 295 | :total_samples=>91, 296 | :samples=>91, 297 | :lines=>{12=>91}}, 298 | 70346498324900=> 299 | {:name=>"A#initialize", 300 | :file=>"/Users/tmm1/code/stackprof/sample.rb", 301 | :line=>5, 302 | :total_samples=>185, 303 | :samples=>1, 304 | :edges=>{70346498324780=>91, 70346498324680=>58, 70346498324580=>35}, 305 | :lines=>{8=>1}}, 306 | ``` 307 | 308 | Above, `A#pow` was involved in 91 samples, and in all cases it was at the top of the stack on line 12. 309 | 310 | `A#initialize` was in 185 samples, but it was at the top of the stack in only 1 sample. The rest of the samples are 311 | divided up between its callee edges. All 91 calls to `A#pow` came from `A#initialize`, as seen by the edge numbered 312 | `70346498324780`. 313 | 314 | ## Advanced usage 315 | 316 | The profiler can be started and stopped manually. Results are accumulated until retrieval, across 317 | multiple `start`/`stop` invocations. 318 | 319 | ``` ruby 320 | StackProf.running? # => false 321 | StackProf.start(mode: :cpu) 322 | StackProf.running? # => true 323 | StackProf.stop 324 | StackProf.results('/tmp/some.file') 325 | ``` 326 | 327 | ## All options 328 | 329 | `StackProf.run` accepts an options hash. Currently, the following options are recognized: 330 | 331 | Option | Meaning 332 | ------- | --------- 333 | `mode` | Mode of sampling: `:cpu`, `:wall`, `:object`, or `:custom` [c.f.](#sampling) 334 | `out` | The target file, which will be overwritten 335 | `interval` | Mode-relative sample rate [c.f.](#sampling) 336 | `ignore_gc` | Ignore garbage collection frames 337 | `aggregate` | Defaults: `true` - if `false` disables [aggregation](#aggregation) 338 | `raw` | Defaults `false` - if `true` collects the extra data required by the `--flamegraph` and `--stackcollapse` report types 339 | `metadata` | Defaults to `{}`. Must be a `Hash`. metadata associated with this profile 340 | `save_every`| (Rack middleware only) write the target file after this many requests 341 | 342 | ## Todo 343 | 344 | * file/iseq blacklist 345 | * restore signal handlers on stop 346 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | if RUBY_ENGINE == "truffleruby" 11 | task :compile do 12 | # noop 13 | end 14 | 15 | task :clean do 16 | # noop 17 | end 18 | else 19 | require "rake/extensiontask" 20 | 21 | Rake::ExtensionTask.new("stackprof") do |ext| 22 | ext.ext_dir = "ext/stackprof" 23 | ext.lib_dir = "lib/stackprof" 24 | end 25 | end 26 | 27 | task default: %i(compile test) 28 | -------------------------------------------------------------------------------- /bin/stackprof: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'optparse' 3 | require 'stackprof' 4 | 5 | banner = <<-END 6 | Usage: stackprof run [--mode=MODE|--out=FILE|--interval=INTERVAL|--format=FORMAT] -- COMMAND 7 | Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz] 8 | END 9 | 10 | if ARGV.first == "run" 11 | ARGV.shift 12 | env = {} 13 | parser = OptionParser.new(banner) do |o| 14 | o.on('--mode [MODE]', String, 'Mode of sampling: cpu, wall, object, default to wall') do |mode| 15 | env["STACKPROF_MODE"] = mode 16 | end 17 | 18 | o.on('--out [FILENAME]', String, 'The target file, which will be overwritten. Defaults to a random temporary file') do |out| 19 | env['STACKPROF_OUT'] = out 20 | end 21 | 22 | o.on('--interval [MILLISECONDS]', Integer, 'Mode-relative sample rate') do |interval| 23 | env['STACKPROF_INTERVAL'] = interval.to_s 24 | end 25 | 26 | o.on('--raw', 'collects the extra data required by the --flamegraph and --stackcollapse report types') do |raw| 27 | env['STACKPROF_RAW'] = raw.to_s 28 | end 29 | 30 | o.on('--ignore-gc', 'Ignore garbage collection frames') do |gc| 31 | env['STACKPROF_IGNORE_GC'] = gc.to_s 32 | end 33 | end 34 | parser.parse! 35 | parser.abort(parser.help) if ARGV.empty? 36 | stackprof_path = File.expand_path('../lib', __dir__) 37 | env['RUBYOPT'] = "-I #{stackprof_path} -r stackprof/autorun #{ENV['RUBYOPT']}" 38 | Kernel.exec(env, *ARGV) 39 | else 40 | options = {} 41 | 42 | parser = OptionParser.new(banner) do |o| 43 | o.on('--text', 'Text summary per method (default)'){ options[:format] = :text } 44 | o.on('--json', 'JSON output (use with web viewers)'){ options[:format] = :json } 45 | o.on('--files', 'List of files'){ |f| options[:format] = :files } 46 | o.on('--limit [num]', Integer, 'Limit --text, --files, or --graphviz output to N entries'){ |n| options[:limit] = n } 47 | o.on('--sort-total', "Sort --text or --files output on total samples\n\n"){ options[:sort] = true } 48 | o.on('--method [grep]', 'Zoom into specified method'){ |f| options[:format] = :method; options[:filter] = f } 49 | o.on('--file [grep]', "Show annotated code for specified file"){ |f| options[:format] = :file; options[:filter] = f } 50 | o.on('--walk', "Walk the stacktrace interactively\n\n"){ |f| options[:walk] = true } 51 | o.on('--callgrind', 'Callgrind output (use with kcachegrind, stackprof-gprof2dot.py)'){ options[:format] = :callgrind } 52 | o.on('--graphviz', "Graphviz output (use with dot)"){ options[:format] = :graphviz } 53 | o.on('--node-fraction [frac]', OptionParser::DecimalNumeric, 'Drop nodes representing less than [frac] fraction of samples'){ |n| options[:node_fraction] = n } 54 | o.on('--stackcollapse', 'stackcollapse.pl compatible output (use with stackprof-flamegraph.pl)'){ options[:format] = :stackcollapse } 55 | o.on('--timeline-flamegraph', "timeline-flamegraph output (js)"){ options[:format] = :timeline_flamegraph } 56 | o.on('--alphabetical-flamegraph', "alphabetical-flamegraph output (js)"){ options[:format] = :alphabetical_flamegraph } 57 | o.on('--flamegraph', "alias to --timeline-flamegraph"){ options[:format] = :timeline_flamegraph } 58 | o.on('--flamegraph-viewer [f.js]', String, "open html viewer for flamegraph output"){ |file| 59 | puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}") 60 | exit 61 | } 62 | o.on('--d3-flamegraph', "flamegraph output (html using d3-flame-graph)\n\n"){ options[:format] = :d3_flamegraph } 63 | o.on('--select-files []', String, 'Show results of matching files'){ |path| (options[:select_files] ||= []) << File.expand_path(path) } 64 | o.on('--reject-files []', String, 'Exclude results of matching files'){ |path| (options[:reject_files] ||= []) << File.expand_path(path) } 65 | o.on('--select-names []', Regexp, 'Show results of matching method names'){ |regexp| (options[:select_names] ||= []) << regexp } 66 | o.on('--reject-names []', Regexp, 'Exclude results of matching method names'){ |regexp| (options[:reject_names] ||= []) << regexp } 67 | o.on('--dump', 'Print marshaled profile dump (combine multiple profiles)'){ options[:format] = :dump } 68 | o.on('--debug', 'Pretty print raw profile data'){ options[:format] = :debug } 69 | end 70 | 71 | parser.parse! 72 | parser.abort(parser.help) if ARGV.empty? 73 | 74 | reports = [] 75 | while ARGV.size > 0 76 | begin 77 | file = ARGV.pop 78 | reports << StackProf::Report.from_file(file) 79 | rescue TypeError => e 80 | STDERR.puts "** error parsing #{file}: #{e.inspect}" 81 | end 82 | end 83 | report = reports.inject(:+) 84 | 85 | default_options = { 86 | :format => :text, 87 | :sort => false, 88 | :limit => 30 89 | } 90 | 91 | if options[:format] == :graphviz 92 | default_options[:limit] = 120 93 | default_options[:node_fraction] = 0.005 94 | end 95 | 96 | options = default_options.merge(options) 97 | options.delete(:limit) if options[:limit] == 0 98 | 99 | case options[:format] 100 | when :text 101 | report.print_text(options[:sort], options[:limit], options[:select_files], options[:reject_files], options[:select_names], options[:reject_names]) 102 | when :json 103 | report.print_json 104 | when :debug 105 | report.print_debug 106 | when :dump 107 | report.print_dump 108 | when :callgrind 109 | report.print_callgrind 110 | when :graphviz 111 | report.print_graphviz(options) 112 | when :stackcollapse 113 | report.print_stackcollapse 114 | when :timeline_flamegraph 115 | report.print_timeline_flamegraph 116 | when :alphabetical_flamegraph 117 | report.print_alphabetical_flamegraph 118 | when :d3_flamegraph 119 | report.print_d3_flamegraph 120 | when :method 121 | options[:walk] ? report.walk_method(options[:filter]) : report.print_method(options[:filter]) 122 | when :file 123 | report.print_file(options[:filter]) 124 | when :files 125 | report.print_files(options[:sort], options[:limit]) 126 | else 127 | raise ArgumentError, "unknown format: #{options[:format]}" 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /bin/stackprof-flamegraph.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec(File.expand_path("../../vendor/FlameGraph/flamegraph.pl", __FILE__), *ARGV) 3 | -------------------------------------------------------------------------------- /bin/stackprof-gprof2dot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec(File.expand_path("../../vendor/gprof2dot/gprof2dot.py", __FILE__), *ARGV) 3 | -------------------------------------------------------------------------------- /ext/stackprof/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | if RUBY_ENGINE == 'truffleruby' 4 | File.write('Makefile', dummy_makefile($srcdir).join("")) 5 | return 6 | end 7 | 8 | if have_func('rb_postponed_job_register_one') && 9 | have_func('rb_profile_frames') && 10 | have_func('rb_tracepoint_new') && 11 | have_const('RUBY_INTERNAL_EVENT_NEWOBJ') 12 | create_makefile('stackprof/stackprof') 13 | else 14 | fail 'missing API: are you using ruby 2.1+?' 15 | end 16 | -------------------------------------------------------------------------------- /ext/stackprof/stackprof.c: -------------------------------------------------------------------------------- 1 | /********************************************************************** 2 | 3 | stackprof.c - Sampling call-stack frame profiler for MRI. 4 | 5 | vim: noexpandtab shiftwidth=4 tabstop=8 softtabstop=4 6 | 7 | **********************************************************************/ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define BUF_SIZE 2048 22 | #define MICROSECONDS_IN_SECOND 1000000 23 | #define NANOSECONDS_IN_SECOND 1000000000 24 | 25 | #define FAKE_FRAME_GC INT2FIX(0) 26 | #define FAKE_FRAME_MARK INT2FIX(1) 27 | #define FAKE_FRAME_SWEEP INT2FIX(2) 28 | 29 | static const char *fake_frame_cstrs[] = { 30 | "(garbage collection)", 31 | "(marking)", 32 | "(sweeping)", 33 | }; 34 | 35 | static int stackprof_use_postponed_job = 1; 36 | static int ruby_vm_running = 0; 37 | 38 | #define TOTAL_FAKE_FRAMES (sizeof(fake_frame_cstrs) / sizeof(char *)) 39 | 40 | #ifdef _POSIX_MONOTONIC_CLOCK 41 | #define timestamp_t timespec 42 | typedef struct timestamp_t timestamp_t; 43 | 44 | static void capture_timestamp(timestamp_t *ts) { 45 | clock_gettime(CLOCK_MONOTONIC, ts); 46 | } 47 | 48 | static int64_t delta_usec(timestamp_t *start, timestamp_t *end) { 49 | int64_t result = MICROSECONDS_IN_SECOND * (end->tv_sec - start->tv_sec); 50 | if (end->tv_nsec < start->tv_nsec) { 51 | result -= MICROSECONDS_IN_SECOND; 52 | result += (NANOSECONDS_IN_SECOND + end->tv_nsec - start->tv_nsec) / 1000; 53 | } else { 54 | result += (end->tv_nsec - start->tv_nsec) / 1000; 55 | } 56 | return result; 57 | } 58 | 59 | static uint64_t timestamp_usec(timestamp_t *ts) { 60 | return (MICROSECONDS_IN_SECOND * ts->tv_sec) + (ts->tv_nsec / 1000); 61 | } 62 | #else 63 | #define timestamp_t timeval 64 | typedef struct timestamp_t timestamp_t; 65 | 66 | static void capture_timestamp(timestamp_t *ts) { 67 | gettimeofday(ts, NULL); 68 | } 69 | 70 | static int64_t delta_usec(timestamp_t *start, timestamp_t *end) { 71 | struct timeval diff; 72 | timersub(end, start, &diff); 73 | return (MICROSECONDS_IN_SECOND * diff.tv_sec) + diff.tv_usec; 74 | } 75 | 76 | static uint64_t timestamp_usec(timestamp_t *ts) { 77 | return (MICROSECONDS_IN_SECOND * ts.tv_sec) + diff.tv_usec 78 | } 79 | #endif 80 | 81 | typedef struct { 82 | size_t total_samples; 83 | size_t caller_samples; 84 | size_t seen_at_sample_number; 85 | st_table *edges; 86 | st_table *lines; 87 | } frame_data_t; 88 | 89 | typedef struct { 90 | uint64_t timestamp_usec; 91 | int64_t delta_usec; 92 | } sample_time_t; 93 | 94 | /* We need to ensure that various memory operations are visible across 95 | * threads. Ruby doesn't offer a portable way to do this sort of detection 96 | * across all the Ruby versions we support, so we use something that casts a 97 | * wide net (Clang, along with ICC, defines __GNUC__). */ 98 | #if defined(__GNUC__) && defined(__ATOMIC_SEQ_CST) 99 | #define STACKPROF_HAVE_ATOMICS 1 100 | #else 101 | #define STACKPROF_HAVE_ATOMICS 0 102 | #endif 103 | 104 | static struct { 105 | /* Access this field with the `STACKPROF_RUNNING` macro, below, since we 106 | * can't properly express that this field has an atomic type. */ 107 | int running; 108 | int raw; 109 | int aggregate; 110 | 111 | VALUE mode; 112 | VALUE interval; 113 | VALUE out; 114 | VALUE metadata; 115 | int ignore_gc; 116 | 117 | uint64_t *raw_samples; 118 | size_t raw_samples_len; 119 | size_t raw_samples_capa; 120 | size_t raw_sample_index; 121 | 122 | struct timestamp_t last_sample_at; 123 | sample_time_t *raw_sample_times; 124 | size_t raw_sample_times_len; 125 | size_t raw_sample_times_capa; 126 | 127 | size_t overall_signals; 128 | size_t overall_samples; 129 | size_t during_gc; 130 | size_t unrecorded_gc_samples; 131 | size_t unrecorded_gc_marking_samples; 132 | size_t unrecorded_gc_sweeping_samples; 133 | st_table *frames; 134 | 135 | timestamp_t gc_start_timestamp; 136 | 137 | VALUE fake_frame_names[TOTAL_FAKE_FRAMES]; 138 | VALUE empty_string; 139 | 140 | int buffer_count; 141 | sample_time_t buffer_time; 142 | VALUE frames_buffer[BUF_SIZE]; 143 | int lines_buffer[BUF_SIZE]; 144 | 145 | pthread_t target_thread; 146 | } _stackprof; 147 | 148 | #if STACKPROF_HAVE_ATOMICS 149 | #define STACKPROF_RUNNING() __atomic_load_n(&_stackprof.running, __ATOMIC_ACQUIRE) 150 | #else 151 | #define STACKPROF_RUNNING() _stackprof.running 152 | #endif 153 | 154 | static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line; 155 | static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines; 156 | static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_raw_lines, sym_metadata, sym_frames, sym_ignore_gc, sym_out; 157 | static VALUE sym_aggregate, sym_raw_sample_timestamps, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping; 158 | static VALUE sym_gc_samples, objtracer; 159 | static VALUE gc_hook; 160 | static VALUE rb_mStackProf; 161 | 162 | static void stackprof_newobj_handler(VALUE, void*); 163 | static void stackprof_signal_handler(int sig, siginfo_t* sinfo, void* ucontext); 164 | 165 | static VALUE 166 | stackprof_start(int argc, VALUE *argv, VALUE self) 167 | { 168 | struct sigaction sa; 169 | struct itimerval timer; 170 | VALUE opts = Qnil, mode = Qnil, interval = Qnil, metadata = rb_hash_new(), out = Qfalse; 171 | int ignore_gc = 0; 172 | int raw = 0, aggregate = 1; 173 | VALUE metadata_val; 174 | 175 | if (STACKPROF_RUNNING()) 176 | return Qfalse; 177 | 178 | rb_scan_args(argc, argv, "0:", &opts); 179 | 180 | if (RTEST(opts)) { 181 | mode = rb_hash_aref(opts, sym_mode); 182 | interval = rb_hash_aref(opts, sym_interval); 183 | out = rb_hash_aref(opts, sym_out); 184 | if (RTEST(rb_hash_aref(opts, sym_ignore_gc))) { 185 | ignore_gc = 1; 186 | } 187 | 188 | metadata_val = rb_hash_aref(opts, sym_metadata); 189 | if (RTEST(metadata_val)) { 190 | if (!RB_TYPE_P(metadata_val, T_HASH)) 191 | rb_raise(rb_eArgError, "metadata should be a hash"); 192 | 193 | metadata = metadata_val; 194 | } 195 | 196 | if (RTEST(rb_hash_aref(opts, sym_raw))) 197 | raw = 1; 198 | if (rb_hash_lookup2(opts, sym_aggregate, Qundef) == Qfalse) 199 | aggregate = 0; 200 | } 201 | if (!RTEST(mode)) mode = sym_wall; 202 | 203 | if (!NIL_P(interval) && (NUM2INT(interval) < 1 || NUM2INT(interval) >= MICROSECONDS_IN_SECOND)) { 204 | rb_raise(rb_eArgError, "interval is a number of microseconds between 1 and 1 million"); 205 | } 206 | 207 | if (!_stackprof.frames) { 208 | _stackprof.frames = st_init_numtable(); 209 | _stackprof.overall_signals = 0; 210 | _stackprof.overall_samples = 0; 211 | _stackprof.during_gc = 0; 212 | } 213 | 214 | if (mode == sym_object) { 215 | if (!RTEST(interval)) interval = INT2FIX(1); 216 | 217 | objtracer = rb_tracepoint_new(Qnil, RUBY_INTERNAL_EVENT_NEWOBJ, stackprof_newobj_handler, 0); 218 | rb_tracepoint_enable(objtracer); 219 | } else if (mode == sym_wall || mode == sym_cpu) { 220 | if (!RTEST(interval)) interval = INT2FIX(1000); 221 | 222 | sa.sa_sigaction = stackprof_signal_handler; 223 | sa.sa_flags = SA_RESTART | SA_SIGINFO; 224 | sigemptyset(&sa.sa_mask); 225 | sigaction(mode == sym_wall ? SIGALRM : SIGPROF, &sa, NULL); 226 | 227 | timer.it_interval.tv_sec = 0; 228 | timer.it_interval.tv_usec = NUM2LONG(interval); 229 | timer.it_value = timer.it_interval; 230 | setitimer(mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0); 231 | } else if (mode == sym_custom) { 232 | /* sampled manually */ 233 | interval = Qnil; 234 | } else { 235 | rb_raise(rb_eArgError, "unknown profiler mode"); 236 | } 237 | 238 | _stackprof.raw = raw; 239 | _stackprof.aggregate = aggregate; 240 | _stackprof.mode = mode; 241 | _stackprof.interval = interval; 242 | _stackprof.ignore_gc = ignore_gc; 243 | _stackprof.metadata = metadata; 244 | _stackprof.out = out; 245 | _stackprof.target_thread = pthread_self(); 246 | /* We need to ensure previous initialization stores are visible across 247 | * threads. */ 248 | #if STACKPROF_HAVE_ATOMICS 249 | __atomic_store_n(&_stackprof.running, 1, __ATOMIC_SEQ_CST); 250 | #else 251 | _stackprof.running = 1; 252 | #endif 253 | 254 | if (raw) { 255 | capture_timestamp(&_stackprof.last_sample_at); 256 | } 257 | 258 | return Qtrue; 259 | } 260 | 261 | static VALUE 262 | stackprof_stop(VALUE self) 263 | { 264 | struct sigaction sa; 265 | struct itimerval timer; 266 | 267 | #if STACKPROF_HAVE_ATOMICS 268 | int was_running = __atomic_exchange_n(&_stackprof.running, 0, __ATOMIC_SEQ_CST); 269 | if (!was_running) 270 | return Qfalse; 271 | #else 272 | if (!_stackprof.running) 273 | return Qfalse; 274 | _stackprof.running = 0; 275 | #endif 276 | 277 | if (_stackprof.mode == sym_object) { 278 | rb_tracepoint_disable(objtracer); 279 | } else if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) { 280 | memset(&timer, 0, sizeof(timer)); 281 | setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0); 282 | 283 | sa.sa_handler = SIG_IGN; 284 | sa.sa_flags = SA_RESTART; 285 | sigemptyset(&sa.sa_mask); 286 | sigaction(_stackprof.mode == sym_wall ? SIGALRM : SIGPROF, &sa, NULL); 287 | } else if (_stackprof.mode == sym_custom) { 288 | /* sampled manually */ 289 | } else { 290 | rb_raise(rb_eArgError, "unknown profiler mode"); 291 | } 292 | 293 | return Qtrue; 294 | } 295 | 296 | #if SIZEOF_VOIDP == SIZEOF_LONG 297 | # define PTR2NUM(x) (LONG2NUM((long)(x))) 298 | #else 299 | # define PTR2NUM(x) (LL2NUM((LONG_LONG)(x))) 300 | #endif 301 | 302 | static int 303 | frame_edges_i(st_data_t key, st_data_t val, st_data_t arg) 304 | { 305 | VALUE edges = (VALUE)arg; 306 | 307 | intptr_t weight = (intptr_t)val; 308 | rb_hash_aset(edges, PTR2NUM(key), INT2FIX(weight)); 309 | return ST_CONTINUE; 310 | } 311 | 312 | static int 313 | frame_lines_i(st_data_t key, st_data_t val, st_data_t arg) 314 | { 315 | VALUE lines = (VALUE)arg; 316 | 317 | size_t weight = (size_t)val; 318 | size_t total = weight & (~(size_t)0 << (8*SIZEOF_SIZE_T/2)); 319 | weight -= total; 320 | total = total >> (8*SIZEOF_SIZE_T/2); 321 | rb_hash_aset(lines, INT2FIX(key), rb_ary_new3(2, ULONG2NUM(total), ULONG2NUM(weight))); 322 | return ST_CONTINUE; 323 | } 324 | 325 | static int 326 | frame_i(st_data_t key, st_data_t val, st_data_t arg) 327 | { 328 | VALUE frame = (VALUE)key; 329 | frame_data_t *frame_data = (frame_data_t *)val; 330 | VALUE results = (VALUE)arg; 331 | VALUE details = rb_hash_new(); 332 | VALUE name, file, edges, lines; 333 | VALUE line; 334 | 335 | rb_hash_aset(results, PTR2NUM(frame), details); 336 | 337 | if (FIXNUM_P(frame)) { 338 | name = _stackprof.fake_frame_names[FIX2INT(frame)]; 339 | file = _stackprof.empty_string; 340 | line = INT2FIX(0); 341 | } else { 342 | name = rb_profile_frame_full_label(frame); 343 | 344 | file = rb_profile_frame_absolute_path(frame); 345 | if (NIL_P(file)) 346 | file = rb_profile_frame_path(frame); 347 | line = rb_profile_frame_first_lineno(frame); 348 | } 349 | 350 | rb_hash_aset(details, sym_name, name); 351 | rb_hash_aset(details, sym_file, file); 352 | if (line != INT2FIX(0)) { 353 | rb_hash_aset(details, sym_line, line); 354 | } 355 | 356 | rb_hash_aset(details, sym_total_samples, SIZET2NUM(frame_data->total_samples)); 357 | rb_hash_aset(details, sym_samples, SIZET2NUM(frame_data->caller_samples)); 358 | 359 | if (frame_data->edges) { 360 | edges = rb_hash_new(); 361 | rb_hash_aset(details, sym_edges, edges); 362 | st_foreach(frame_data->edges, frame_edges_i, (st_data_t)edges); 363 | st_free_table(frame_data->edges); 364 | frame_data->edges = NULL; 365 | } 366 | 367 | if (frame_data->lines) { 368 | lines = rb_hash_new(); 369 | rb_hash_aset(details, sym_lines, lines); 370 | st_foreach(frame_data->lines, frame_lines_i, (st_data_t)lines); 371 | st_free_table(frame_data->lines); 372 | frame_data->lines = NULL; 373 | } 374 | 375 | xfree(frame_data); 376 | return ST_DELETE; 377 | } 378 | 379 | static VALUE 380 | stackprof_results(int argc, VALUE *argv, VALUE self) 381 | { 382 | VALUE results, frames; 383 | 384 | if (!_stackprof.frames || STACKPROF_RUNNING()) 385 | return Qnil; 386 | 387 | results = rb_hash_new(); 388 | rb_hash_aset(results, sym_version, DBL2NUM(1.2)); 389 | rb_hash_aset(results, sym_mode, _stackprof.mode); 390 | rb_hash_aset(results, sym_interval, _stackprof.interval); 391 | rb_hash_aset(results, sym_samples, SIZET2NUM(_stackprof.overall_samples)); 392 | rb_hash_aset(results, sym_gc_samples, SIZET2NUM(_stackprof.during_gc)); 393 | rb_hash_aset(results, sym_missed_samples, SIZET2NUM(_stackprof.overall_signals - _stackprof.overall_samples)); 394 | rb_hash_aset(results, sym_metadata, _stackprof.metadata); 395 | 396 | _stackprof.metadata = Qnil; 397 | 398 | frames = rb_hash_new(); 399 | rb_hash_aset(results, sym_frames, frames); 400 | st_foreach(_stackprof.frames, frame_i, (st_data_t)frames); 401 | 402 | st_free_table(_stackprof.frames); 403 | _stackprof.frames = NULL; 404 | 405 | if (_stackprof.raw && _stackprof.raw_samples_len) { 406 | size_t len, n, o; 407 | VALUE raw_sample_timestamps, raw_timestamp_deltas; 408 | VALUE raw_samples = rb_ary_new_capa(_stackprof.raw_samples_len); 409 | VALUE raw_lines = rb_ary_new_capa(_stackprof.raw_samples_len); 410 | 411 | for (n = 0; n < _stackprof.raw_samples_len; n++) { 412 | len = (size_t)_stackprof.raw_samples[n]; 413 | rb_ary_push(raw_samples, SIZET2NUM(len)); 414 | rb_ary_push(raw_lines, SIZET2NUM(len)); 415 | 416 | for (o = 0, n++; o < len; n++, o++) { 417 | // Line is in the upper 16 bits 418 | rb_ary_push(raw_lines, INT2NUM(_stackprof.raw_samples[n] >> 48)); 419 | 420 | VALUE frame = _stackprof.raw_samples[n] & ~((uint64_t)0xFFFF << 48); 421 | rb_ary_push(raw_samples, PTR2NUM(frame)); 422 | } 423 | 424 | rb_ary_push(raw_samples, SIZET2NUM((size_t)_stackprof.raw_samples[n])); 425 | rb_ary_push(raw_lines, SIZET2NUM((size_t)_stackprof.raw_samples[n])); 426 | } 427 | 428 | free(_stackprof.raw_samples); 429 | _stackprof.raw_samples = NULL; 430 | _stackprof.raw_samples_len = 0; 431 | _stackprof.raw_samples_capa = 0; 432 | _stackprof.raw_sample_index = 0; 433 | 434 | rb_hash_aset(results, sym_raw, raw_samples); 435 | rb_hash_aset(results, sym_raw_lines, raw_lines); 436 | 437 | raw_sample_timestamps = rb_ary_new_capa(_stackprof.raw_sample_times_len); 438 | raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_sample_times_len); 439 | 440 | for (n = 0; n < _stackprof.raw_sample_times_len; n++) { 441 | rb_ary_push(raw_sample_timestamps, ULL2NUM(_stackprof.raw_sample_times[n].timestamp_usec)); 442 | rb_ary_push(raw_timestamp_deltas, LL2NUM(_stackprof.raw_sample_times[n].delta_usec)); 443 | } 444 | 445 | free(_stackprof.raw_sample_times); 446 | _stackprof.raw_sample_times = NULL; 447 | _stackprof.raw_sample_times_len = 0; 448 | _stackprof.raw_sample_times_capa = 0; 449 | 450 | rb_hash_aset(results, sym_raw_sample_timestamps, raw_sample_timestamps); 451 | rb_hash_aset(results, sym_raw_timestamp_deltas, raw_timestamp_deltas); 452 | 453 | _stackprof.raw = 0; 454 | } 455 | 456 | if (argc == 1) 457 | _stackprof.out = argv[0]; 458 | 459 | if (RTEST(_stackprof.out)) { 460 | VALUE file; 461 | if (rb_respond_to(_stackprof.out, rb_intern("to_io"))) { 462 | file = rb_io_check_io(_stackprof.out); 463 | } else { 464 | file = rb_file_open_str(_stackprof.out, "w"); 465 | } 466 | 467 | rb_marshal_dump(results, file); 468 | rb_io_flush(file); 469 | _stackprof.out = Qnil; 470 | return file; 471 | } else { 472 | return results; 473 | } 474 | } 475 | 476 | static VALUE 477 | stackprof_run(int argc, VALUE *argv, VALUE self) 478 | { 479 | rb_need_block(); 480 | stackprof_start(argc, argv, self); 481 | rb_ensure(rb_yield, Qundef, stackprof_stop, self); 482 | return stackprof_results(0, 0, self); 483 | } 484 | 485 | static VALUE 486 | stackprof_running_p(VALUE self) 487 | { 488 | return STACKPROF_RUNNING() ? Qtrue : Qfalse; 489 | } 490 | 491 | static inline frame_data_t * 492 | sample_for(VALUE frame) 493 | { 494 | st_data_t key = (st_data_t)frame, val = 0; 495 | frame_data_t *frame_data; 496 | 497 | if (st_lookup(_stackprof.frames, key, &val)) { 498 | frame_data = (frame_data_t *)val; 499 | } else { 500 | frame_data = ALLOC_N(frame_data_t, 1); 501 | MEMZERO(frame_data, frame_data_t, 1); 502 | val = (st_data_t)frame_data; 503 | st_insert(_stackprof.frames, key, val); 504 | } 505 | 506 | return frame_data; 507 | } 508 | 509 | static int 510 | numtable_increment_callback(st_data_t *key, st_data_t *value, st_data_t arg, int existing) 511 | { 512 | size_t *weight = (size_t *)value; 513 | size_t increment = (size_t)arg; 514 | 515 | if (existing) 516 | (*weight) += increment; 517 | else 518 | *weight = increment; 519 | 520 | return ST_CONTINUE; 521 | } 522 | 523 | void 524 | st_numtable_increment(st_table *table, st_data_t key, size_t increment) 525 | { 526 | st_update(table, key, numtable_increment_callback, (st_data_t)increment); 527 | } 528 | 529 | void 530 | stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t timestamp_delta) 531 | { 532 | int i, n; 533 | VALUE prev_frame = Qnil; 534 | 535 | _stackprof.overall_samples++; 536 | 537 | if (_stackprof.raw && num > 0) { 538 | int found = 0; 539 | 540 | /* If there's no sample buffer allocated, then allocate one. The buffer 541 | * format is the number of frames (num), then the list of frames (from 542 | * `_stackprof.raw_samples`), followed by the number of times this 543 | * particular stack has been seen in a row. Each "new" stack is added 544 | * to the end of the buffer, but if the previous stack is the same as 545 | * the current stack, the counter will be incremented. */ 546 | if (!_stackprof.raw_samples) { 547 | _stackprof.raw_samples_capa = num * 100; 548 | _stackprof.raw_samples = malloc(sizeof(VALUE) * _stackprof.raw_samples_capa); 549 | } 550 | 551 | /* If we can't fit all the samples in the buffer, double the buffer size. */ 552 | while (_stackprof.raw_samples_capa <= _stackprof.raw_samples_len + (num + 2)) { 553 | _stackprof.raw_samples_capa *= 2; 554 | _stackprof.raw_samples = realloc(_stackprof.raw_samples, sizeof(VALUE) * _stackprof.raw_samples_capa); 555 | } 556 | 557 | /* If we've seen this stack before in the last sample, then increment the "seen" count. */ 558 | if (_stackprof.raw_samples_len > 0 && _stackprof.raw_samples[_stackprof.raw_sample_index] == (VALUE)num) { 559 | /* The number of samples could have been the same, but the stack 560 | * might be different, so we need to check the stack here. Stacks 561 | * in the raw buffer are stored in the opposite direction of stacks 562 | * in the frames buffer that came from Ruby. */ 563 | for (i = num-1, n = 0; i >= 0; i--, n++) { 564 | VALUE frame = _stackprof.frames_buffer[i]; 565 | int line = _stackprof.lines_buffer[i]; 566 | 567 | // Encode the line in to the upper 16 bits. 568 | uint64_t key = ((uint64_t)line << 48) | (uint64_t)frame; 569 | 570 | if (_stackprof.raw_samples[_stackprof.raw_sample_index + 1 + n] != key) 571 | break; 572 | } 573 | if (i == -1) { 574 | _stackprof.raw_samples[_stackprof.raw_samples_len-1] += 1; 575 | found = 1; 576 | } 577 | } 578 | 579 | /* If we haven't seen the stack, then add it to the buffer along with 580 | * the length of the stack and a 1 for the "seen" count */ 581 | if (!found) { 582 | /* Bump the `raw_sample_index` up so that the next iteration can 583 | * find the previously recorded stack size. */ 584 | _stackprof.raw_sample_index = _stackprof.raw_samples_len; 585 | _stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)num; 586 | for (i = num-1; i >= 0; i--) { 587 | VALUE frame = _stackprof.frames_buffer[i]; 588 | int line = _stackprof.lines_buffer[i]; 589 | 590 | // Encode the line in to the upper 16 bits. 591 | uint64_t key = ((uint64_t)line << 48) | (uint64_t)frame; 592 | 593 | _stackprof.raw_samples[_stackprof.raw_samples_len++] = key; 594 | } 595 | _stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)1; 596 | } 597 | 598 | /* If there's no timestamp delta buffer, allocate one */ 599 | if (!_stackprof.raw_sample_times) { 600 | _stackprof.raw_sample_times_capa = 100; 601 | _stackprof.raw_sample_times = malloc(sizeof(sample_time_t) * _stackprof.raw_sample_times_capa); 602 | _stackprof.raw_sample_times_len = 0; 603 | } 604 | 605 | /* Double the buffer size if it's too small */ 606 | while (_stackprof.raw_sample_times_capa <= _stackprof.raw_sample_times_len + 1) { 607 | _stackprof.raw_sample_times_capa *= 2; 608 | _stackprof.raw_sample_times = realloc(_stackprof.raw_sample_times, sizeof(sample_time_t) * _stackprof.raw_sample_times_capa); 609 | } 610 | 611 | /* Store the time delta (which is the amount of microseconds between samples). */ 612 | _stackprof.raw_sample_times[_stackprof.raw_sample_times_len++] = (sample_time_t) { 613 | .timestamp_usec = sample_timestamp, 614 | .delta_usec = timestamp_delta, 615 | }; 616 | } 617 | 618 | for (i = 0; i < num; i++) { 619 | int line = _stackprof.lines_buffer[i]; 620 | VALUE frame = _stackprof.frames_buffer[i]; 621 | frame_data_t *frame_data = sample_for(frame); 622 | 623 | if (frame_data->seen_at_sample_number != _stackprof.overall_samples) { 624 | frame_data->total_samples++; 625 | } 626 | frame_data->seen_at_sample_number = _stackprof.overall_samples; 627 | 628 | if (i == 0) { 629 | frame_data->caller_samples++; 630 | } else if (_stackprof.aggregate) { 631 | if (!frame_data->edges) 632 | frame_data->edges = st_init_numtable(); 633 | st_numtable_increment(frame_data->edges, (st_data_t)prev_frame, 1); 634 | } 635 | 636 | if (_stackprof.aggregate && line > 0) { 637 | size_t half = (size_t)1<<(8*SIZEOF_SIZE_T/2); 638 | size_t increment = i == 0 ? half + 1 : half; 639 | if (!frame_data->lines) 640 | frame_data->lines = st_init_numtable(); 641 | st_numtable_increment(frame_data->lines, (st_data_t)line, increment); 642 | } 643 | 644 | prev_frame = frame; 645 | } 646 | 647 | if (_stackprof.raw) { 648 | capture_timestamp(&_stackprof.last_sample_at); 649 | } 650 | } 651 | 652 | // buffer the current profile frames 653 | // This must be async-signal-safe 654 | // Returns immediately if another set of frames are already in the buffer 655 | void 656 | stackprof_buffer_sample(void) 657 | { 658 | uint64_t start_timestamp = 0; 659 | int64_t timestamp_delta = 0; 660 | int num; 661 | 662 | if (_stackprof.buffer_count > 0) { 663 | // Another sample is already pending 664 | return; 665 | } 666 | 667 | if (_stackprof.raw) { 668 | struct timestamp_t t; 669 | capture_timestamp(&t); 670 | start_timestamp = timestamp_usec(&t); 671 | timestamp_delta = delta_usec(&_stackprof.last_sample_at, &t); 672 | } 673 | 674 | num = rb_profile_frames(0, sizeof(_stackprof.frames_buffer) / sizeof(VALUE), _stackprof.frames_buffer, _stackprof.lines_buffer); 675 | 676 | _stackprof.buffer_count = num; 677 | _stackprof.buffer_time.timestamp_usec = start_timestamp; 678 | _stackprof.buffer_time.delta_usec = timestamp_delta; 679 | } 680 | 681 | // Postponed job 682 | void 683 | stackprof_record_gc_samples(void) 684 | { 685 | int64_t delta_to_first_unrecorded_gc_sample = 0; 686 | uint64_t start_timestamp = 0; 687 | size_t i; 688 | if (_stackprof.raw) { 689 | struct timestamp_t t = _stackprof.gc_start_timestamp; 690 | start_timestamp = timestamp_usec(&t); 691 | 692 | // We don't know when the GC samples were actually marked, so let's 693 | // assume that they were marked at a perfectly regular interval. 694 | delta_to_first_unrecorded_gc_sample = delta_usec(&_stackprof.last_sample_at, &t) - (_stackprof.unrecorded_gc_samples - 1) * NUM2LONG(_stackprof.interval); 695 | if (delta_to_first_unrecorded_gc_sample < 0) { 696 | delta_to_first_unrecorded_gc_sample = 0; 697 | } 698 | } 699 | 700 | for (i = 0; i < _stackprof.unrecorded_gc_samples; i++) { 701 | int64_t timestamp_delta = i == 0 ? delta_to_first_unrecorded_gc_sample : NUM2LONG(_stackprof.interval); 702 | 703 | if (_stackprof.unrecorded_gc_marking_samples) { 704 | _stackprof.frames_buffer[0] = FAKE_FRAME_MARK; 705 | _stackprof.lines_buffer[0] = 0; 706 | _stackprof.frames_buffer[1] = FAKE_FRAME_GC; 707 | _stackprof.lines_buffer[1] = 0; 708 | _stackprof.unrecorded_gc_marking_samples--; 709 | 710 | stackprof_record_sample_for_stack(2, start_timestamp, timestamp_delta); 711 | } else if (_stackprof.unrecorded_gc_sweeping_samples) { 712 | _stackprof.frames_buffer[0] = FAKE_FRAME_SWEEP; 713 | _stackprof.lines_buffer[0] = 0; 714 | _stackprof.frames_buffer[1] = FAKE_FRAME_GC; 715 | _stackprof.lines_buffer[1] = 0; 716 | 717 | _stackprof.unrecorded_gc_sweeping_samples--; 718 | 719 | stackprof_record_sample_for_stack(2, start_timestamp, timestamp_delta); 720 | } else { 721 | _stackprof.frames_buffer[0] = FAKE_FRAME_GC; 722 | _stackprof.lines_buffer[0] = 0; 723 | stackprof_record_sample_for_stack(1, start_timestamp, timestamp_delta); 724 | } 725 | } 726 | _stackprof.during_gc += _stackprof.unrecorded_gc_samples; 727 | _stackprof.unrecorded_gc_samples = 0; 728 | _stackprof.unrecorded_gc_marking_samples = 0; 729 | _stackprof.unrecorded_gc_sweeping_samples = 0; 730 | } 731 | 732 | // record the sample previously buffered by stackprof_buffer_sample 733 | static void 734 | stackprof_record_buffer(void) 735 | { 736 | stackprof_record_sample_for_stack(_stackprof.buffer_count, _stackprof.buffer_time.timestamp_usec, _stackprof.buffer_time.delta_usec); 737 | 738 | // reset the buffer 739 | _stackprof.buffer_count = 0; 740 | } 741 | 742 | static void 743 | stackprof_sample_and_record(void) 744 | { 745 | stackprof_buffer_sample(); 746 | stackprof_record_buffer(); 747 | } 748 | 749 | static void 750 | stackprof_job_record_gc(void *data) 751 | { 752 | if (!STACKPROF_RUNNING()) return; 753 | 754 | stackprof_record_gc_samples(); 755 | } 756 | 757 | static void 758 | stackprof_job_sample_and_record(void *data) 759 | { 760 | if (!STACKPROF_RUNNING()) return; 761 | 762 | stackprof_sample_and_record(); 763 | } 764 | 765 | static void 766 | stackprof_job_record_buffer(void *data) 767 | { 768 | if (!STACKPROF_RUNNING()) return; 769 | 770 | stackprof_record_buffer(); 771 | } 772 | 773 | static void 774 | stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext) 775 | { 776 | static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 777 | 778 | _stackprof.overall_signals++; 779 | 780 | if (!STACKPROF_RUNNING()) return; 781 | 782 | // There's a possibility that the signal handler is invoked *after* the Ruby 783 | // VM has been shut down (e.g. after ruby_cleanup(0)). In this case, things 784 | // that rely on global VM state (e.g. rb_during_gc) will segfault. 785 | if (!ruby_vm_running) return; 786 | 787 | if (_stackprof.mode == sym_wall) { 788 | // In "wall" mode, the SIGALRM signal will arrive at an arbitrary thread. 789 | // In order to provide more useful results, especially under threaded web 790 | // servers, we want to forward this signal to the original thread 791 | // StackProf was started from. 792 | // According to POSIX.1-2008 TC1 pthread_kill and pthread_self should be 793 | // async-signal-safe. 794 | if (pthread_self() != _stackprof.target_thread) { 795 | pthread_kill(_stackprof.target_thread, sig); 796 | return; 797 | } 798 | } else { 799 | if (!ruby_native_thread_p()) return; 800 | } 801 | 802 | if (pthread_mutex_trylock(&lock)) return; 803 | 804 | if (!_stackprof.ignore_gc && rb_during_gc()) { 805 | VALUE mode = rb_gc_latest_gc_info(sym_state); 806 | if (mode == sym_marking) { 807 | _stackprof.unrecorded_gc_marking_samples++; 808 | } else if (mode == sym_sweeping) { 809 | _stackprof.unrecorded_gc_sweeping_samples++; 810 | } 811 | if(!_stackprof.unrecorded_gc_samples) { 812 | // record start 813 | capture_timestamp(&_stackprof.gc_start_timestamp); 814 | } 815 | _stackprof.unrecorded_gc_samples++; 816 | rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0); 817 | } else { 818 | if (stackprof_use_postponed_job) { 819 | rb_postponed_job_register_one(0, stackprof_job_sample_and_record, (void*)0); 820 | } else { 821 | // Buffer a sample immediately, if an existing sample exists this will 822 | // return immediately 823 | stackprof_buffer_sample(); 824 | // Enqueue a job to record the sample 825 | rb_postponed_job_register_one(0, stackprof_job_record_buffer, (void*)0); 826 | } 827 | } 828 | pthread_mutex_unlock(&lock); 829 | } 830 | 831 | static void 832 | stackprof_newobj_handler(VALUE tpval, void *data) 833 | { 834 | _stackprof.overall_signals++; 835 | if (RTEST(_stackprof.interval) && _stackprof.overall_signals % NUM2LONG(_stackprof.interval)) 836 | return; 837 | stackprof_sample_and_record(); 838 | } 839 | 840 | static VALUE 841 | stackprof_sample(VALUE self) 842 | { 843 | if (!STACKPROF_RUNNING()) 844 | return Qfalse; 845 | 846 | _stackprof.overall_signals++; 847 | stackprof_sample_and_record(); 848 | return Qtrue; 849 | } 850 | 851 | static int 852 | frame_mark_i(st_data_t key, st_data_t val, st_data_t arg) 853 | { 854 | VALUE frame = (VALUE)key; 855 | rb_gc_mark(frame); 856 | return ST_CONTINUE; 857 | } 858 | 859 | static void 860 | stackprof_gc_mark(void *data) 861 | { 862 | if (RTEST(_stackprof.metadata)) 863 | rb_gc_mark(_stackprof.metadata); 864 | 865 | if (RTEST(_stackprof.out)) 866 | rb_gc_mark(_stackprof.out); 867 | 868 | if (_stackprof.frames) 869 | st_foreach(_stackprof.frames, frame_mark_i, 0); 870 | 871 | int i; 872 | for (i = 0; i < _stackprof.buffer_count; i++) { 873 | rb_gc_mark(_stackprof.frames_buffer[i]); 874 | } 875 | } 876 | 877 | static size_t 878 | stackprof_memsize(const void *data) 879 | { 880 | return sizeof(_stackprof); 881 | } 882 | 883 | static void 884 | stackprof_atfork_prepare(void) 885 | { 886 | struct itimerval timer; 887 | if (STACKPROF_RUNNING()) { 888 | if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) { 889 | memset(&timer, 0, sizeof(timer)); 890 | setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0); 891 | } 892 | } 893 | } 894 | 895 | static void 896 | stackprof_atfork_parent(void) 897 | { 898 | struct itimerval timer; 899 | if (STACKPROF_RUNNING()) { 900 | if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) { 901 | timer.it_interval.tv_sec = 0; 902 | timer.it_interval.tv_usec = NUM2LONG(_stackprof.interval); 903 | timer.it_value = timer.it_interval; 904 | setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0); 905 | } 906 | } 907 | } 908 | 909 | static void 910 | stackprof_atfork_child(void) 911 | { 912 | stackprof_stop(rb_mStackProf); 913 | } 914 | 915 | static VALUE 916 | stackprof_use_postponed_job_l(VALUE self) 917 | { 918 | stackprof_use_postponed_job = 1; 919 | return Qnil; 920 | } 921 | 922 | static void 923 | stackprof_at_exit(ruby_vm_t* vm) 924 | { 925 | ruby_vm_running = 0; 926 | } 927 | 928 | static const rb_data_type_t stackprof_type = { 929 | "StackProf", 930 | { 931 | stackprof_gc_mark, 932 | NULL, 933 | stackprof_memsize, 934 | } 935 | }; 936 | 937 | void 938 | Init_stackprof(void) 939 | { 940 | size_t i; 941 | /* 942 | * As of Ruby 3.0, it should be safe to read stack frames at any time, unless YJIT is enabled 943 | * See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21 944 | */ 945 | stackprof_use_postponed_job = RUBY_API_VERSION_MAJOR < 3; 946 | 947 | ruby_vm_running = 1; 948 | ruby_vm_at_exit(stackprof_at_exit); 949 | 950 | #define S(name) sym_##name = ID2SYM(rb_intern(#name)); 951 | S(object); 952 | S(custom); 953 | S(wall); 954 | S(cpu); 955 | S(name); 956 | S(file); 957 | S(line); 958 | S(total_samples); 959 | S(gc_samples); 960 | S(missed_samples); 961 | S(samples); 962 | S(edges); 963 | S(lines); 964 | S(version); 965 | S(mode); 966 | S(interval); 967 | S(raw); 968 | S(raw_lines); 969 | S(raw_sample_timestamps); 970 | S(raw_timestamp_deltas); 971 | S(out); 972 | S(metadata); 973 | S(ignore_gc); 974 | S(frames); 975 | S(aggregate); 976 | S(state); 977 | S(marking); 978 | S(sweeping); 979 | #undef S 980 | 981 | /* Need to run this to warm the symbol table before we call this during GC */ 982 | rb_gc_latest_gc_info(sym_state); 983 | 984 | rb_global_variable(&gc_hook); 985 | gc_hook = TypedData_Wrap_Struct(rb_cObject, &stackprof_type, &_stackprof); 986 | 987 | _stackprof.raw_samples = NULL; 988 | _stackprof.raw_samples_len = 0; 989 | _stackprof.raw_samples_capa = 0; 990 | _stackprof.raw_sample_index = 0; 991 | 992 | _stackprof.raw_sample_times = NULL; 993 | _stackprof.raw_sample_times_len = 0; 994 | _stackprof.raw_sample_times_capa = 0; 995 | 996 | _stackprof.empty_string = rb_str_new_cstr(""); 997 | rb_global_variable(&_stackprof.empty_string); 998 | 999 | for (i = 0; i < TOTAL_FAKE_FRAMES; i++) { 1000 | _stackprof.fake_frame_names[i] = rb_str_new_cstr(fake_frame_cstrs[i]); 1001 | rb_global_variable(&_stackprof.fake_frame_names[i]); 1002 | } 1003 | 1004 | rb_mStackProf = rb_define_module("StackProf"); 1005 | rb_define_singleton_method(rb_mStackProf, "running?", stackprof_running_p, 0); 1006 | rb_define_singleton_method(rb_mStackProf, "run", stackprof_run, -1); 1007 | rb_define_singleton_method(rb_mStackProf, "start", stackprof_start, -1); 1008 | rb_define_singleton_method(rb_mStackProf, "stop", stackprof_stop, 0); 1009 | rb_define_singleton_method(rb_mStackProf, "results", stackprof_results, -1); 1010 | rb_define_singleton_method(rb_mStackProf, "sample", stackprof_sample, 0); 1011 | rb_define_singleton_method(rb_mStackProf, "use_postponed_job!", stackprof_use_postponed_job_l, 0); 1012 | 1013 | pthread_atfork(stackprof_atfork_prepare, stackprof_atfork_parent, stackprof_atfork_child); 1014 | } 1015 | -------------------------------------------------------------------------------- /lib/stackprof.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'truffleruby' 2 | require "stackprof/truffleruby" 3 | else 4 | require "stackprof/stackprof" 5 | end 6 | 7 | if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? 8 | if RUBY_VERSION < "3.3" 9 | # On 3.3 we don't need postponed jobs: 10 | # https://github.com/ruby/ruby/commit/a1dc1a3de9683daf5a543d6f618e17aabfcb8708 11 | StackProf.use_postponed_job! 12 | end 13 | elsif RUBY_VERSION == "3.2.0" 14 | # 3.2.0 crash is the signal is received at the wrong time. 15 | # Fixed in https://github.com/ruby/ruby/pull/7116 16 | # The fix is backported in 3.2.1: https://bugs.ruby-lang.org/issues/19336 17 | StackProf.use_postponed_job! 18 | end 19 | 20 | module StackProf 21 | VERSION = '0.2.27' 22 | end 23 | 24 | StackProf.autoload :Report, "stackprof/report.rb" 25 | StackProf.autoload :Middleware, "stackprof/middleware.rb" 26 | -------------------------------------------------------------------------------- /lib/stackprof/autorun.rb: -------------------------------------------------------------------------------- 1 | require "stackprof" 2 | 3 | options = {} 4 | options[:mode] = ENV["STACKPROF_MODE"].to_sym if ENV.key?("STACKPROF_MODE") 5 | options[:interval] = Integer(ENV["STACKPROF_INTERVAL"]) if ENV.key?("STACKPROF_INTERVAL") 6 | options[:raw] = true if ENV["STACKPROF_RAW"] 7 | options[:ignore_gc] = true if ENV["STACKPROF_IGNORE_GC"] 8 | 9 | at_exit do 10 | StackProf.stop 11 | output_path = ENV.fetch("STACKPROF_OUT") do 12 | require "tempfile" 13 | Tempfile.create(["stackprof", ".dump"]).path 14 | end 15 | StackProf.results(output_path) 16 | $stderr.puts("StackProf results dumped at: #{output_path}") 17 | end 18 | 19 | StackProf.start(**options) 20 | -------------------------------------------------------------------------------- /lib/stackprof/flamegraph/flamegraph.js: -------------------------------------------------------------------------------- 1 | if (typeof Element.prototype.matches !== 'function') { 2 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.webkitMatchesSelector || function matches(selector) { 3 | var element = this 4 | var elements = (element.document || element.ownerDocument).querySelectorAll(selector) 5 | var index = 0 6 | 7 | while (elements[index] && elements[index] !== element) { 8 | ++index 9 | } 10 | 11 | return Boolean(elements[index]) 12 | } 13 | } 14 | 15 | if (typeof Element.prototype.closest !== 'function') { 16 | Element.prototype.closest = function closest(selector) { 17 | var element = this 18 | 19 | while (element && element.nodeType === 1) { 20 | if (element.matches(selector)) { 21 | return element 22 | } 23 | 24 | element = element.parentNode 25 | } 26 | 27 | return null 28 | } 29 | } 30 | 31 | if (typeof Object.assign !== 'function') { 32 | (function() { 33 | Object.assign = function(target) { 34 | 'use strict' 35 | // We must check against these specific cases. 36 | if (target === undefined || target === null) { 37 | throw new TypeError('Cannot convert undefined or null to object') 38 | } 39 | 40 | var output = Object(target) 41 | for (var index = 1; index < arguments.length; index++) { 42 | var source = arguments[index] 43 | if (source !== undefined && source !== null) { 44 | for (var nextKey in source) { 45 | if (source.hasOwnProperty(nextKey)) { 46 | output[nextKey] = source[nextKey] 47 | } 48 | } 49 | } 50 | } 51 | return output 52 | } 53 | })() 54 | } 55 | 56 | function EventSource() { 57 | var self = this 58 | 59 | self.eventListeners = {} 60 | } 61 | 62 | EventSource.prototype.on = function(name, callback) { 63 | var self = this 64 | 65 | var listeners = self.eventListeners[name] 66 | if (!listeners) 67 | listeners = self.eventListeners[name] = [] 68 | listeners.push(callback) 69 | } 70 | 71 | EventSource.prototype.dispatch = function(name, data) { 72 | var self = this 73 | 74 | var listeners = self.eventListeners[name] || [] 75 | listeners.forEach(function(c) { 76 | requestAnimationFrame(function() { c(data) }) 77 | }) 78 | } 79 | 80 | function CanvasView(canvas) { 81 | var self = this 82 | 83 | self.canvas = canvas 84 | } 85 | 86 | CanvasView.prototype.setDimensions = function(width, height) { 87 | var self = this 88 | 89 | if (self.resizeRequestID) 90 | cancelAnimationFrame(self.resizeRequestID) 91 | 92 | self.resizeRequestID = requestAnimationFrame(self.setDimensionsNow.bind(self, width, height)) 93 | } 94 | 95 | CanvasView.prototype.setDimensionsNow = function(width, height) { 96 | var self = this 97 | 98 | if (width === self.width && height === self.height) 99 | return 100 | 101 | self.width = width 102 | self.height = height 103 | 104 | self.canvas.style.width = width 105 | self.canvas.style.height = height 106 | 107 | var ratio = window.devicePixelRatio || 1 108 | self.canvas.width = width * ratio 109 | self.canvas.height = height * ratio 110 | 111 | var ctx = self.canvas.getContext('2d') 112 | ctx.setTransform(1, 0, 0, 1, 0, 0) 113 | ctx.scale(ratio, ratio) 114 | 115 | self.repaintNow() 116 | } 117 | 118 | CanvasView.prototype.paint = function() { 119 | } 120 | 121 | CanvasView.prototype.scheduleRepaint = function() { 122 | var self = this 123 | 124 | if (self.repaintRequestID) 125 | return 126 | 127 | self.repaintRequestID = requestAnimationFrame(function() { 128 | self.repaintRequestID = null 129 | self.repaintNow() 130 | }) 131 | } 132 | 133 | CanvasView.prototype.repaintNow = function() { 134 | var self = this 135 | 136 | self.canvas.getContext('2d').clearRect(0, 0, self.width, self.height) 137 | self.paint() 138 | 139 | if (self.repaintRequestID) { 140 | cancelAnimationFrame(self.repaintRequestID) 141 | self.repaintRequestID = null 142 | } 143 | } 144 | 145 | function Flamechart(canvas, data, dataRange, info) { 146 | var self = this 147 | 148 | CanvasView.call(self, canvas) 149 | EventSource.call(self) 150 | 151 | self.canvas = canvas 152 | self.data = data 153 | self.dataRange = dataRange 154 | self.info = info 155 | 156 | self.viewport = { 157 | x: dataRange.minX, 158 | y: dataRange.minY, 159 | width: dataRange.maxX - dataRange.minX, 160 | height: dataRange.maxY - dataRange.minY, 161 | } 162 | } 163 | 164 | Flamechart.prototype = Object.create(CanvasView.prototype) 165 | Flamechart.prototype.constructor = Flamechart 166 | Object.assign(Flamechart.prototype, EventSource.prototype) 167 | 168 | Flamechart.prototype.xScale = function(x) { 169 | var self = this 170 | return self.widthScale(x - self.viewport.x) 171 | } 172 | 173 | Flamechart.prototype.yScale = function(y) { 174 | var self = this 175 | return self.heightScale(y - self.viewport.y) 176 | } 177 | 178 | Flamechart.prototype.widthScale = function(width) { 179 | var self = this 180 | return width * self.width / self.viewport.width 181 | } 182 | 183 | Flamechart.prototype.heightScale = function(height) { 184 | var self = this 185 | return height * self.height / self.viewport.height 186 | } 187 | 188 | Flamechart.prototype.frameRect = function(f) { 189 | return { 190 | x: f.x, 191 | y: f.y, 192 | width: f.width, 193 | height: 1, 194 | } 195 | } 196 | 197 | Flamechart.prototype.dataToCanvas = function(r) { 198 | var self = this 199 | 200 | return { 201 | x: self.xScale(r.x), 202 | y: self.yScale(r.y), 203 | width: self.widthScale(r.width), 204 | height: self.heightScale(r.height), 205 | } 206 | } 207 | 208 | Flamechart.prototype.setViewport = function(viewport) { 209 | var self = this 210 | 211 | if (self.viewport.x === viewport.x && 212 | self.viewport.y === viewport.y && 213 | self.viewport.width === viewport.width && 214 | self.viewport.height === viewport.height) 215 | return 216 | 217 | self.viewport = viewport 218 | 219 | self.scheduleRepaint() 220 | 221 | self.dispatch('viewportchanged', { current: viewport }) 222 | } 223 | 224 | Flamechart.prototype.paint = function(opacity, frames, gemName) { 225 | var self = this 226 | 227 | var ctx = self.canvas.getContext('2d') 228 | 229 | ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)' 230 | 231 | if (self.showLabels) { 232 | ctx.textBaseline = 'middle' 233 | ctx.font = '11px ' + getComputedStyle(this.canvas).fontFamily 234 | // W tends to be one of the widest characters (and if the font is truly 235 | // fixed-width then any character will do). 236 | var characterWidth = ctx.measureText('WWWW').width / 4 237 | } 238 | 239 | if (typeof opacity === 'undefined') 240 | opacity = 1 241 | 242 | frames = frames || self.data 243 | 244 | var blocksByColor = {} 245 | 246 | frames.forEach(function(f) { 247 | if (gemName && f.gemName !== gemName) 248 | return 249 | 250 | var r = self.dataToCanvas(self.frameRect(f)) 251 | 252 | if (r.x >= self.width || 253 | r.y >= self.height || 254 | (r.x + r.width) <= 0 || 255 | (r.y + r.height) <= 0) { 256 | return 257 | } 258 | 259 | var i = self.info[f.frame_id] 260 | var color = colorString(i.color, opacity) 261 | var colorBlocks = blocksByColor[color] 262 | if (!colorBlocks) 263 | colorBlocks = blocksByColor[color] = [] 264 | colorBlocks.push({ rect: r, text: f.frame }) 265 | }) 266 | 267 | var textBlocks = [] 268 | 269 | Object.keys(blocksByColor).forEach(function(color) { 270 | ctx.fillStyle = color 271 | 272 | blocksByColor[color].forEach(function(block) { 273 | if (opacity < 1) 274 | ctx.clearRect(block.rect.x, block.rect.y, block.rect.width, block.rect.height) 275 | 276 | ctx.fillRect(block.rect.x, block.rect.y, block.rect.width, block.rect.height) 277 | 278 | if (block.rect.width > 4 && block.rect.height > 4) 279 | ctx.strokeRect(block.rect.x, block.rect.y, block.rect.width, block.rect.height) 280 | 281 | if (!self.showLabels || block.rect.width / characterWidth < 4) 282 | return 283 | 284 | textBlocks.push(block) 285 | }) 286 | }) 287 | 288 | ctx.fillStyle = '#000' 289 | textBlocks.forEach(function(block) { 290 | var text = block.text 291 | var textRect = Object.assign({}, block.rect) 292 | textRect.x += 1 293 | textRect.width -= 2 294 | if (textRect.width < text.length * characterWidth * 0.75) 295 | text = centerTruncate(block.text, Math.floor(textRect.width / characterWidth)) 296 | ctx.fillText(text, textRect.x, textRect.y + textRect.height / 2, textRect.width) 297 | }) 298 | } 299 | 300 | Flamechart.prototype.frameAtPoint = function(x, y) { 301 | var self = this 302 | 303 | return self.data.find(function(d) { 304 | var r = self.dataToCanvas(self.frameRect(d)) 305 | 306 | return r.x <= x 307 | && r.x + r.width >= x 308 | && r.y <= y 309 | && r.y + r.height >= y 310 | }) 311 | } 312 | 313 | function MainFlamechart(canvas, data, dataRange, info) { 314 | var self = this 315 | 316 | Flamechart.call(self, canvas, data, dataRange, info) 317 | 318 | self.showLabels = true 319 | 320 | self.canvas.addEventListener('mousedown', self.onMouseDown.bind(self)) 321 | self.canvas.addEventListener('mousemove', self.onMouseMove.bind(self)) 322 | self.canvas.addEventListener('mouseout', self.onMouseOut.bind(self)) 323 | self.canvas.addEventListener('wheel', self.onWheel.bind(self)) 324 | } 325 | 326 | MainFlamechart.prototype = Object.create(Flamechart.prototype) 327 | 328 | MainFlamechart.prototype.setDimensionsNow = function(width, height) { 329 | var self = this 330 | 331 | var viewport = Object.assign({}, self.viewport) 332 | viewport.height = height / 16 333 | self.setViewport(viewport) 334 | 335 | CanvasView.prototype.setDimensionsNow.call(self, width, height) 336 | } 337 | 338 | MainFlamechart.prototype.onMouseDown = function(e) { 339 | var self = this 340 | 341 | if (e.button !== 0) 342 | return 343 | 344 | captureMouse({ 345 | mouseup: self.onMouseUp.bind(self), 346 | mousemove: self.onMouseMove.bind(self), 347 | }) 348 | 349 | var clientRect = self.canvas.getBoundingClientRect() 350 | var currentX = e.clientX - clientRect.left 351 | var currentY = e.clientY - clientRect.top 352 | 353 | self.dragging = true 354 | self.dragInfo = { 355 | mouse: { x: currentX, y: currentY }, 356 | viewport: { x: self.viewport.x, y: self.viewport.y }, 357 | } 358 | 359 | e.preventDefault() 360 | } 361 | 362 | MainFlamechart.prototype.onMouseUp = function(e) { 363 | var self = this 364 | 365 | if (!self.dragging) 366 | return 367 | 368 | releaseCapture() 369 | 370 | self.dragging = false 371 | e.preventDefault() 372 | } 373 | 374 | MainFlamechart.prototype.onMouseMove = function(e) { 375 | var self = this 376 | 377 | var clientRect = self.canvas.getBoundingClientRect() 378 | var currentX = e.clientX - clientRect.left 379 | var currentY = e.clientY - clientRect.top 380 | 381 | if (self.dragging) { 382 | var viewport = Object.assign({}, self.viewport) 383 | viewport.x = self.dragInfo.viewport.x - (currentX - self.dragInfo.mouse.x) * viewport.width / self.width 384 | viewport.y = self.dragInfo.viewport.y - (currentY - self.dragInfo.mouse.y) * viewport.height / self.height 385 | viewport.x = Math.min(self.dataRange.maxX - viewport.width, Math.max(self.dataRange.minX, viewport.x)) 386 | viewport.y = Math.min(self.dataRange.maxY - viewport.height, Math.max(self.dataRange.minY, viewport.y)) 387 | self.setViewport(viewport) 388 | return 389 | } 390 | 391 | var frame = self.frameAtPoint(currentX, currentY) 392 | self.setHoveredFrame(frame) 393 | } 394 | 395 | MainFlamechart.prototype.onMouseOut = function() { 396 | var self = this 397 | 398 | if (self.dragging) 399 | return 400 | 401 | self.setHoveredFrame(null) 402 | } 403 | 404 | MainFlamechart.prototype.onWheel = function(e) { 405 | var self = this 406 | 407 | var deltaX = e.deltaX 408 | var deltaY = e.deltaY 409 | 410 | if (e.deltaMode == WheelEvent.prototype.DOM_DELTA_LINE) { 411 | deltaX *= 11 412 | deltaY *= 11 413 | } 414 | 415 | if (e.shiftKey) { 416 | if ('webkitDirectionInvertedFromDevice' in e) { 417 | if (e.webkitDirectionInvertedFromDevice) 418 | deltaY *= -1 419 | } else if (/Mac OS X/.test(navigator.userAgent)) { 420 | // Assume that most Mac users have "Scroll direction: Natural" enabled. 421 | deltaY *= -1 422 | } 423 | 424 | var mouseWheelZoomSpeed = 1 / 120 425 | self.handleZoomGesture(Math.pow(1.2, -(deltaY || deltaX) * mouseWheelZoomSpeed), e.offsetX) 426 | e.preventDefault() 427 | return 428 | } 429 | 430 | var viewport = Object.assign({}, self.viewport) 431 | viewport.x += deltaX * viewport.width / (self.dataRange.maxX - self.dataRange.minX) 432 | viewport.x = Math.min(self.dataRange.maxX - viewport.width, Math.max(self.dataRange.minX, viewport.x)) 433 | viewport.y += (deltaY / 8) * viewport.height / (self.dataRange.maxY - self.dataRange.minY) 434 | viewport.y = Math.min(self.dataRange.maxY - viewport.height, Math.max(self.dataRange.minY, viewport.y)) 435 | self.setViewport(viewport) 436 | e.preventDefault() 437 | } 438 | 439 | MainFlamechart.prototype.handleZoomGesture = function(zoom, originX) { 440 | var self = this 441 | 442 | var viewport = Object.assign({}, self.viewport) 443 | var ratioX = originX / self.width 444 | 445 | var newWidth = Math.min(viewport.width / zoom, self.dataRange.maxX - self.dataRange.minX) 446 | viewport.x = Math.max(self.dataRange.minX, viewport.x + (viewport.width - newWidth) * ratioX) 447 | viewport.width = Math.min(newWidth, self.dataRange.maxX - viewport.x) 448 | 449 | self.setViewport(viewport) 450 | } 451 | 452 | MainFlamechart.prototype.setHoveredFrame = function(frame) { 453 | var self = this 454 | 455 | if (frame === self.hoveredFrame) 456 | return 457 | 458 | var previous = self.hoveredFrame 459 | self.hoveredFrame = frame 460 | 461 | self.dispatch('hoveredframechanged', { previous: previous, current: self.hoveredFrame }) 462 | } 463 | 464 | function OverviewFlamechart(container, viewportOverlay, data, dataRange, info) { 465 | var self = this 466 | 467 | Flamechart.call(self, container.querySelector('.overview'), data, dataRange, info) 468 | 469 | self.container = container 470 | 471 | self.showLabels = false 472 | 473 | self.viewportOverlay = viewportOverlay 474 | 475 | self.canvas.addEventListener('mousedown', self.onMouseDown.bind(self)) 476 | self.viewportOverlay.addEventListener('mousedown', self.onOverlayMouseDown.bind(self)) 477 | } 478 | 479 | OverviewFlamechart.prototype = Object.create(Flamechart.prototype) 480 | 481 | OverviewFlamechart.prototype.setViewportOverlayRect = function(r) { 482 | var self = this 483 | 484 | self.viewportOverlayRect = r 485 | 486 | r = self.dataToCanvas(r) 487 | r.width = Math.max(2, r.width) 488 | r.height = Math.max(2, r.height) 489 | 490 | if ('transform' in self.viewportOverlay.style) { 491 | self.viewportOverlay.style.transform = 'translate(' + r.x + 'px, ' + r.y + 'px) scale(' + r.width + ', ' + r.height + ')' 492 | } else { 493 | self.viewportOverlay.style.left = r.x 494 | self.viewportOverlay.style.top = r.y 495 | self.viewportOverlay.style.width = r.width 496 | self.viewportOverlay.style.height = r.height 497 | } 498 | } 499 | 500 | OverviewFlamechart.prototype.onMouseDown = function(e) { 501 | var self = this 502 | 503 | captureMouse({ 504 | mouseup: self.onMouseUp.bind(self), 505 | mousemove: self.onMouseMove.bind(self), 506 | }) 507 | 508 | self.dragging = true 509 | self.dragStartX = e.clientX - self.canvas.getBoundingClientRect().left 510 | 511 | self.handleDragGesture(e) 512 | 513 | e.preventDefault() 514 | } 515 | 516 | OverviewFlamechart.prototype.onMouseUp = function(e) { 517 | var self = this 518 | 519 | if (!self.dragging) 520 | return 521 | 522 | releaseCapture() 523 | 524 | self.dragging = false 525 | 526 | self.handleDragGesture(e) 527 | 528 | e.preventDefault() 529 | } 530 | 531 | OverviewFlamechart.prototype.onMouseMove = function(e) { 532 | var self = this 533 | 534 | if (!self.dragging) 535 | return 536 | 537 | self.handleDragGesture(e) 538 | 539 | e.preventDefault() 540 | } 541 | 542 | OverviewFlamechart.prototype.handleDragGesture = function(e) { 543 | var self = this 544 | 545 | var clientRect = self.canvas.getBoundingClientRect() 546 | var currentX = e.clientX - clientRect.left 547 | var currentY = e.clientY - clientRect.top 548 | 549 | if (self.dragCurrentX === currentX) 550 | return 551 | 552 | self.dragCurrentX = currentX 553 | 554 | var minX = Math.min(self.dragStartX, self.dragCurrentX) 555 | var maxX = Math.max(self.dragStartX, self.dragCurrentX) 556 | 557 | var rect = Object.assign({}, self.viewportOverlayRect) 558 | rect.x = minX / self.width * self.viewport.width + self.viewport.x 559 | rect.width = Math.max(self.viewport.width / 1000, (maxX - minX) / self.width * self.viewport.width) 560 | 561 | rect.y = Math.max(self.viewport.y, Math.min(self.viewport.height - self.viewport.y, currentY / self.height * self.viewport.height + self.viewport.y - rect.height / 2)) 562 | 563 | self.setViewportOverlayRect(rect) 564 | self.dispatch('overlaychanged', { current: self.viewportOverlayRect }) 565 | } 566 | 567 | OverviewFlamechart.prototype.onOverlayMouseDown = function(e) { 568 | var self = this 569 | 570 | captureMouse({ 571 | mouseup: self.onOverlayMouseUp.bind(self), 572 | mousemove: self.onOverlayMouseMove.bind(self), 573 | }) 574 | 575 | self.overlayDragging = true 576 | self.overlayDragInfo = { 577 | mouse: { x: e.clientX, y: e.clientY }, 578 | rect: Object.assign({}, self.viewportOverlayRect), 579 | } 580 | self.viewportOverlay.classList.add('moving') 581 | 582 | self.handleOverlayDragGesture(e) 583 | 584 | e.preventDefault() 585 | } 586 | 587 | OverviewFlamechart.prototype.onOverlayMouseUp = function(e) { 588 | var self = this 589 | 590 | if (!self.overlayDragging) 591 | return 592 | 593 | releaseCapture() 594 | 595 | self.overlayDragging = false 596 | self.viewportOverlay.classList.remove('moving') 597 | 598 | self.handleOverlayDragGesture(e) 599 | 600 | e.preventDefault() 601 | } 602 | 603 | OverviewFlamechart.prototype.onOverlayMouseMove = function(e) { 604 | var self = this 605 | 606 | if (!self.overlayDragging) 607 | return 608 | 609 | self.handleOverlayDragGesture(e) 610 | 611 | e.preventDefault() 612 | } 613 | 614 | OverviewFlamechart.prototype.handleOverlayDragGesture = function(e) { 615 | var self = this 616 | 617 | var deltaX = (e.clientX - self.overlayDragInfo.mouse.x) / self.width * self.viewport.width 618 | var deltaY = (e.clientY - self.overlayDragInfo.mouse.y) / self.height * self.viewport.height 619 | 620 | var rect = Object.assign({}, self.overlayDragInfo.rect) 621 | rect.x += deltaX 622 | rect.y += deltaY 623 | rect.x = Math.max(self.viewport.x, Math.min(self.viewport.x + self.viewport.width - rect.width, rect.x)) 624 | rect.y = Math.max(self.viewport.y, Math.min(self.viewport.y + self.viewport.height - rect.height, rect.y)) 625 | 626 | self.setViewportOverlayRect(rect) 627 | self.dispatch('overlaychanged', { current: self.viewportOverlayRect }) 628 | } 629 | 630 | function FlamegraphView(data, info, sortedGems) { 631 | var self = this 632 | 633 | self.data = data 634 | self.info = info 635 | 636 | self.dataRange = self.computeDataRange() 637 | 638 | self.mainChart = new MainFlamechart(document.querySelector('.flamegraph'), data, self.dataRange, info) 639 | self.overview = new OverviewFlamechart(document.querySelector('.overview-container'), document.querySelector('.overview-viewport-overlay'), data, self.dataRange, info) 640 | self.infoElement = document.querySelector('.info') 641 | 642 | self.mainChart.on('hoveredframechanged', self.onHoveredFrameChanged.bind(self)) 643 | self.mainChart.on('viewportchanged', self.onViewportChanged.bind(self)) 644 | self.overview.on('overlaychanged', self.onOverlayChanged.bind(self)) 645 | 646 | var legend = document.querySelector('.legend') 647 | self.renderLegend(legend, sortedGems) 648 | 649 | legend.addEventListener('mousemove', self.onLegendMouseMove.bind(self)) 650 | legend.addEventListener('mouseout', self.onLegendMouseOut.bind(self)) 651 | 652 | window.addEventListener('resize', self.updateDimensions.bind(self)) 653 | 654 | self.updateDimensions() 655 | } 656 | 657 | FlamegraphView.prototype.updateDimensions = function() { 658 | var self = this 659 | 660 | var margin = {top: 10, right: 10, bottom: 10, left: 10} 661 | var width = window.innerWidth - 200 - margin.left - margin.right 662 | var mainChartHeight = Math.ceil(window.innerHeight * 0.80) - margin.top - margin.bottom 663 | var overviewHeight = Math.floor(window.innerHeight * 0.20) - 60 - margin.top - margin.bottom 664 | 665 | self.mainChart.setDimensions(width + margin.left + margin.right, mainChartHeight + margin.top + margin.bottom) 666 | self.overview.setDimensions(width + margin.left + margin.right, overviewHeight + margin.top + margin.bottom) 667 | self.overview.setViewportOverlayRect(self.mainChart.viewport) 668 | } 669 | 670 | FlamegraphView.prototype.computeDataRange = function() { 671 | var self = this 672 | 673 | var range = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } 674 | self.data.forEach(function(d) { 675 | range.minX = Math.min(range.minX, d.x) 676 | range.minY = Math.min(range.minY, d.y) 677 | range.maxX = Math.max(range.maxX, d.x + d.width) 678 | range.maxY = Math.max(range.maxY, d.y + 1) 679 | }) 680 | 681 | return range 682 | } 683 | 684 | FlamegraphView.prototype.onHoveredFrameChanged = function(data) { 685 | var self = this 686 | 687 | self.updateInfo(data.current) 688 | 689 | if (data.previous) 690 | self.repaintFrames(1, self.info[data.previous.frame_id].frames) 691 | 692 | if (data.current) 693 | self.repaintFrames(0.5, self.info[data.current.frame_id].frames) 694 | } 695 | 696 | FlamegraphView.prototype.repaintFrames = function(opacity, frames) { 697 | var self = this 698 | 699 | self.mainChart.paint(opacity, frames) 700 | self.overview.paint(opacity, frames) 701 | } 702 | 703 | FlamegraphView.prototype.updateInfo = function(frame) { 704 | var self = this 705 | 706 | if (!frame) { 707 | self.infoElement.style.backgroundColor = '' 708 | self.infoElement.querySelector('.frame').textContent = '' 709 | self.infoElement.querySelector('.file').textContent = '' 710 | self.infoElement.querySelector('.samples').textContent = '' 711 | self.infoElement.querySelector('.exclusive').textContent = '' 712 | return 713 | } 714 | 715 | var i = self.info[frame.frame_id] 716 | var shortFile = frame.file.replace(/^.+\/(gems|app|lib|config|jobs)/, '$1') 717 | var sData = self.samplePercentRaw(i.samples.length, frame.topFrame ? frame.topFrame.exclusiveCount : 0) 718 | 719 | self.infoElement.style.backgroundColor = colorString(i.color, 1) 720 | self.infoElement.querySelector('.frame').textContent = frame.frame 721 | self.infoElement.querySelector('.file').textContent = shortFile 722 | self.infoElement.querySelector('.samples').textContent = sData[0] + ' samples (' + sData[1] + '%)' 723 | if (sData[3]) 724 | self.infoElement.querySelector('.exclusive').textContent = sData[2] + ' exclusive (' + sData[3] + '%)' 725 | else 726 | self.infoElement.querySelector('.exclusive').textContent = '' 727 | } 728 | 729 | FlamegraphView.prototype.samplePercentRaw = function(samples, exclusive) { 730 | var self = this 731 | 732 | var ret = [samples, ((samples / self.dataRange.maxX) * 100).toFixed(2)] 733 | if (exclusive) 734 | ret = ret.concat([exclusive, ((exclusive / self.dataRange.maxX) * 100).toFixed(2)]) 735 | return ret 736 | } 737 | 738 | FlamegraphView.prototype.onViewportChanged = function(data) { 739 | var self = this 740 | 741 | self.overview.setViewportOverlayRect(data.current) 742 | } 743 | 744 | FlamegraphView.prototype.onOverlayChanged = function(data) { 745 | var self = this 746 | 747 | self.mainChart.setViewport(data.current) 748 | } 749 | 750 | FlamegraphView.prototype.renderLegend = function(element, sortedGems) { 751 | var self = this 752 | 753 | var fragment = document.createDocumentFragment() 754 | 755 | sortedGems.forEach(function(gem) { 756 | var sData = self.samplePercentRaw(gem.samples.length) 757 | var node = document.createElement('div') 758 | node.className = 'legend-gem' 759 | node.setAttribute('data-gem-name', gem.name) 760 | node.style.backgroundColor = colorString(gem.color, 1) 761 | 762 | var span = document.createElement('span') 763 | span.style.float = 'right' 764 | span.textContent = sData[0] + 'x' 765 | span.appendChild(document.createElement('br')) 766 | span.appendChild(document.createTextNode(sData[1] + '%')) 767 | node.appendChild(span) 768 | 769 | var name = document.createElement('div') 770 | name.className = 'name' 771 | name.textContent = gem.name 772 | name.appendChild(document.createElement('br')) 773 | name.appendChild(document.createTextNode('\u00a0')) 774 | node.appendChild(name) 775 | 776 | fragment.appendChild(node) 777 | }) 778 | 779 | element.appendChild(fragment) 780 | } 781 | 782 | FlamegraphView.prototype.onLegendMouseMove = function(e) { 783 | var self = this 784 | 785 | var gemElement = e.target.closest('.legend-gem') 786 | var gemName = gemElement.getAttribute('data-gem-name') 787 | 788 | if (self.hoveredGemName === gemName) 789 | return 790 | 791 | if (self.hoveredGemName) { 792 | self.mainChart.paint(1, null, self.hoveredGemName) 793 | self.overview.paint(1, null, self.hoveredGemName) 794 | } 795 | 796 | self.hoveredGemName = gemName 797 | 798 | self.mainChart.paint(0.5, null, self.hoveredGemName) 799 | self.overview.paint(0.5, null, self.hoveredGemName) 800 | } 801 | 802 | FlamegraphView.prototype.onLegendMouseOut = function() { 803 | var self = this 804 | 805 | if (!self.hoveredGemName) 806 | return 807 | 808 | self.mainChart.paint(1, null, self.hoveredGemName) 809 | self.overview.paint(1, null, self.hoveredGemName) 810 | self.hoveredGemName = null 811 | } 812 | 813 | var capturingListeners = null 814 | function captureMouse(listeners) { 815 | if (capturingListeners) 816 | releaseCapture() 817 | 818 | for (var name in listeners) 819 | document.addEventListener(name, listeners[name], true) 820 | capturingListeners = listeners 821 | } 822 | 823 | function releaseCapture() { 824 | if (!capturingListeners) 825 | return 826 | 827 | for (var name in capturingListeners) 828 | document.removeEventListener(name, capturingListeners[name], true) 829 | capturingListeners = null 830 | } 831 | 832 | function guessGem(frame) { 833 | var split = frame.split('/gems/') 834 | if (split.length === 1) { 835 | split = frame.split('/app/') 836 | if (split.length === 1) { 837 | split = frame.split('/lib/') 838 | } else { 839 | return split[split.length - 1].split('/')[0] 840 | } 841 | 842 | split = split[Math.max(split.length - 2, 0)].split('/') 843 | return split[split.length - 1].split(':')[0] 844 | } 845 | else 846 | { 847 | return split[split.length - 1].split('/')[0].split('-', 2)[0] 848 | } 849 | } 850 | 851 | function color() { 852 | var r = parseInt(205 + Math.random() * 50) 853 | var g = parseInt(Math.random() * 230) 854 | var b = parseInt(Math.random() * 55) 855 | return [r, g, b] 856 | } 857 | 858 | // http://stackoverflow.com/a/7419630 859 | function rainbow(numOfSteps, step) { 860 | // This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distiguishable vibrant markers in Google Maps and other apps. 861 | // Adam Cole, 2011-Sept-14 862 | // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript 863 | var r, g, b 864 | var h = step / numOfSteps 865 | var i = ~~(h * 6) 866 | var f = h * 6 - i 867 | var q = 1 - f 868 | switch (i % 6) { 869 | case 0: r = 1, g = f, b = 0; break 870 | case 1: r = q, g = 1, b = 0; break 871 | case 2: r = 0, g = 1, b = f; break 872 | case 3: r = 0, g = q, b = 1; break 873 | case 4: r = f, g = 0, b = 1; break 874 | case 5: r = 1, g = 0, b = q; break 875 | } 876 | return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)] 877 | } 878 | 879 | function colorString(color, opacity) { 880 | if (typeof opacity === 'undefined') 881 | opacity = 1 882 | return 'rgba(' + color.join(',') + ',' + opacity + ')' 883 | } 884 | 885 | // http://stackoverflow.com/questions/1960473/unique-values-in-an-array 886 | function getUnique(orig) { 887 | var o = {} 888 | for (var i = 0; i < orig.length; i++) o[orig[i]] = 1 889 | return Object.keys(o) 890 | } 891 | 892 | function centerTruncate(text, maxLength) { 893 | var charactersToKeep = maxLength - 1 894 | if (charactersToKeep <= 0) 895 | return '' 896 | if (text.length <= charactersToKeep) 897 | return text 898 | 899 | var prefixLength = Math.ceil(charactersToKeep / 2) 900 | var suffixLength = charactersToKeep - prefixLength 901 | var prefix = text.substr(0, prefixLength) 902 | var suffix = suffixLength > 0 ? text.substr(-suffixLength) : '' 903 | 904 | return [prefix, '\u2026', suffix].join('') 905 | } 906 | 907 | function flamegraph(data) { 908 | var info = {} 909 | data.forEach(function(d) { 910 | var i = info[d.frame_id] 911 | if (!i) 912 | info[d.frame_id] = i = {frames: [], samples: [], color: color()} 913 | i.frames.push(d) 914 | for (var j = 0; j < d.width; j++) { 915 | i.samples.push(d.x + j) 916 | } 917 | }) 918 | 919 | // Samples may overlap on the same line 920 | for (var r in info) { 921 | if (info[r].samples) { 922 | info[r].samples = getUnique(info[r].samples) 923 | } 924 | } 925 | 926 | // assign some colors, analyze samples per gem 927 | var gemStats = {} 928 | var topFrames = {} 929 | var lastFrame = {frame: 'd52e04d-df28-41ed-a215-b6ec840a8ea5', x: -1} 930 | 931 | data.forEach(function(d) { 932 | var gem = guessGem(d.file) 933 | var stat = gemStats[gem] 934 | d.gemName = gem 935 | 936 | if (!stat) { 937 | gemStats[gem] = stat = {name: gem, samples: [], frames: []} 938 | } 939 | 940 | stat.frames.push(d.frame_id) 941 | for (var j = 0; j < d.width; j++) { 942 | stat.samples.push(d.x + j) 943 | } 944 | // This assumes the traversal is in order 945 | if (lastFrame.x !== d.x) { 946 | var topFrame = topFrames[lastFrame.frame_id] 947 | if (!topFrame) { 948 | topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0} 949 | } 950 | topFrame.exclusiveCount += 1 951 | lastFrame.topFrame = topFrame 952 | } 953 | lastFrame = d 954 | }) 955 | 956 | var topFrame = topFrames[lastFrame.frame_id] 957 | if (!topFrame) { 958 | topFrames[lastFrame.frame_id] = topFrame = {exclusiveCount: 0} 959 | } 960 | topFrame.exclusiveCount += 1 961 | lastFrame.topFrame = topFrame 962 | 963 | var totalGems = 0 964 | for (var k in gemStats) { 965 | totalGems++ 966 | gemStats[k].samples = getUnique(gemStats[k].samples) 967 | } 968 | 969 | var gemsSorted = Object.keys(gemStats).map(function(k) { return gemStats[k] }) 970 | gemsSorted.sort(function(a, b) { return b.samples.length - a.samples.length }) 971 | 972 | var currentIndex = 0 973 | gemsSorted.forEach(function(stat) { 974 | stat.color = rainbow(totalGems, currentIndex) 975 | currentIndex += 1 976 | 977 | for (var x = 0; x < stat.frames.length; x++) { 978 | info[stat.frames[x]].color = stat.color 979 | } 980 | }) 981 | 982 | new FlamegraphView(data, info, gemsSorted) 983 | } 984 | -------------------------------------------------------------------------------- /lib/stackprof/flamegraph/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | flamegraph 4 | 63 | 64 | 65 | 66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/stackprof/middleware.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module StackProf 4 | class Middleware 5 | def initialize(app, options = {}) 6 | @app = app 7 | @options = options 8 | @num_reqs = options[:save_every] || nil 9 | 10 | Middleware.mode = options[:mode] || :cpu 11 | Middleware.interval = options[:interval] || 1000 12 | Middleware.raw = options[:raw] || false 13 | Middleware.enabled = options[:enabled] 14 | options[:path] = 'tmp/' if options[:path].to_s.empty? 15 | Middleware.path = options[:path] 16 | Middleware.metadata = options[:metadata] || {} 17 | at_exit{ Middleware.save } if options[:save_at_exit] 18 | end 19 | 20 | def call(env) 21 | enabled = Middleware.enabled?(env) 22 | StackProf.start( 23 | mode: Middleware.mode, 24 | interval: Middleware.interval, 25 | raw: Middleware.raw, 26 | metadata: Middleware.metadata, 27 | ) if enabled 28 | @app.call(env) 29 | ensure 30 | if enabled 31 | StackProf.stop 32 | if @num_reqs && (@num_reqs-=1) == 0 33 | @num_reqs = @options[:save_every] 34 | Middleware.save 35 | end 36 | end 37 | end 38 | 39 | class << self 40 | attr_accessor :enabled, :mode, :interval, :raw, :path, :metadata 41 | 42 | def enabled?(env) 43 | if enabled.respond_to?(:call) 44 | enabled.call(env) 45 | else 46 | enabled 47 | end 48 | end 49 | 50 | def save 51 | if results = StackProf.results 52 | path = Middleware.path 53 | is_directory = path != path.chomp('/') 54 | 55 | if is_directory 56 | filename = "stackprof-#{results[:mode]}-#{Process.pid}-#{Time.now.to_i}.dump" 57 | else 58 | filename = File.basename(path) 59 | path = File.dirname(path) 60 | end 61 | 62 | FileUtils.mkdir_p(path) 63 | File.open(File.join(path, filename), 'wb') do |f| 64 | f.write Marshal.dump(results) 65 | end 66 | filename 67 | end 68 | end 69 | 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/stackprof/report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pp' 4 | require 'digest/sha2' 5 | require 'json' 6 | 7 | module StackProf 8 | class Report 9 | MARSHAL_SIGNATURE = "\x04\x08" 10 | 11 | class << self 12 | def from_file(file) 13 | if (content = IO.binread(file)).start_with?(MARSHAL_SIGNATURE) 14 | new(Marshal.load(content)) 15 | else 16 | from_json(JSON.parse(content)) 17 | end 18 | end 19 | 20 | def from_json(json) 21 | new(parse_json(json)) 22 | end 23 | 24 | def parse_json(json) 25 | json.keys.each do |key| 26 | value = json.delete(key) 27 | from_json(value) if value.is_a?(Hash) 28 | 29 | new_key = case key 30 | when /\A[0-9]*\z/ 31 | key.to_i 32 | else 33 | key.to_sym 34 | end 35 | 36 | json[new_key] = value 37 | end 38 | json 39 | end 40 | end 41 | 42 | def initialize(data) 43 | @data = data 44 | end 45 | attr_reader :data 46 | 47 | def frames(sort_by_total=false) 48 | @data[:"sorted_frames_#{sort_by_total}"] ||= 49 | @data[:frames].sort_by{ |iseq, stats| -stats[sort_by_total ? :total_samples : :samples] }.inject({}){|h, (k, v)| h[k] = v; h} 50 | end 51 | 52 | def normalized_frames 53 | id2hash = {} 54 | @data[:frames].each do |frame, info| 55 | id2hash[frame.to_s] = info[:hash] = Digest::SHA256.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}") 56 | end 57 | @data[:frames].inject(Hash.new) do |hash, (frame, info)| 58 | info = hash[id2hash[frame.to_s]] = info.dup 59 | info[:edges] = info[:edges].inject(Hash.new){ |edges, (edge, weight)| edges[id2hash[edge.to_s]] = weight; edges } if info[:edges] 60 | hash 61 | end 62 | end 63 | 64 | def version 65 | @data[:version] 66 | end 67 | 68 | def modeline 69 | "#{@data[:mode]}(#{@data[:interval]})" 70 | end 71 | 72 | def overall_samples 73 | @data[:samples] 74 | end 75 | 76 | def max_samples 77 | @data[:max_samples] ||= @data[:frames].values.max_by{ |frame| frame[:samples] }[:samples] 78 | end 79 | 80 | def files 81 | @data[:files] ||= @data[:frames].inject(Hash.new) do |hash, (addr, frame)| 82 | if file = frame[:file] and lines = frame[:lines] 83 | hash[file] ||= Hash.new 84 | lines.each do |line, weight| 85 | hash[file][line] = add_lines(hash[file][line], weight) 86 | end 87 | end 88 | hash 89 | end 90 | end 91 | 92 | def add_lines(a, b) 93 | return b if a.nil? 94 | return a+b if a.is_a? Integer 95 | return [ a[0], a[1]+b ] if b.is_a? Integer 96 | [ a[0]+b[0], a[1]+b[1] ] 97 | end 98 | 99 | def print_debug 100 | pp @data 101 | end 102 | 103 | def print_dump(f=STDOUT) 104 | f.puts Marshal.dump(@data.reject{|k,v| k == :files }) 105 | end 106 | 107 | def print_json(f=STDOUT) 108 | require "json" 109 | f.puts JSON.generate(@data, max_nesting: false) 110 | end 111 | 112 | def print_stackcollapse 113 | raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw] 114 | 115 | while len = raw.shift 116 | frames = raw.slice!(0, len) 117 | weight = raw.shift 118 | 119 | print frames.map{ |a| data[:frames][a][:name] }.join(';') 120 | puts " #{weight}" 121 | end 122 | end 123 | 124 | def print_timeline_flamegraph(f=STDOUT, skip_common=true) 125 | print_flamegraph(f, skip_common, false) 126 | end 127 | 128 | def print_alphabetical_flamegraph(f=STDOUT, skip_common=true) 129 | print_flamegraph(f, skip_common, true) 130 | end 131 | 132 | def print_flamegraph(f, skip_common, alphabetical=false) 133 | raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw] 134 | 135 | stacks, max_x, max_y = flamegraph_stacks(raw) 136 | 137 | stacks.sort! if alphabetical 138 | 139 | f.puts 'flamegraph([' 140 | max_y.times do |y| 141 | row_prev = nil 142 | row_width = 0 143 | x = 0 144 | 145 | stacks.each do |stack| 146 | weight = stack.last 147 | cell = stack[y] unless y == stack.length-1 148 | 149 | if cell.nil? 150 | if row_prev 151 | flamegraph_row(f, x - row_width, y, row_width, row_prev) 152 | end 153 | 154 | row_prev = nil 155 | x += weight 156 | next 157 | end 158 | 159 | if row_prev.nil? # start new row with this cell 160 | row_width = weight 161 | row_prev = cell 162 | x += weight 163 | 164 | elsif row_prev == cell # grow current row along x-axis 165 | row_width += weight 166 | x += weight 167 | 168 | else # end current row and start new row 169 | flamegraph_row(f, x - row_width, y, row_width, row_prev) 170 | x += weight 171 | row_prev = cell 172 | row_width = weight 173 | end 174 | 175 | row_prev = cell 176 | end 177 | 178 | if row_prev 179 | next if skip_common && row_width == max_x 180 | 181 | flamegraph_row(f, x - row_width, y, row_width, row_prev) 182 | end 183 | end 184 | f.puts '])' 185 | end 186 | 187 | def flamegraph_stacks(raw) 188 | stacks = [] 189 | max_x = 0 190 | max_y = 0 191 | idx = 0 192 | 193 | while len = raw[idx] 194 | idx += 1 195 | max_y = len if len > max_y 196 | stack = raw.slice(idx, len+1) 197 | idx += len+1 198 | stacks << stack 199 | max_x += stack.last 200 | end 201 | 202 | return stacks, max_x, max_y 203 | end 204 | 205 | def flamegraph_row(f, x, y, weight, addr) 206 | frame = @data[:frames][addr] 207 | f.print ',' if @rows_started 208 | @rows_started = true 209 | f.puts %{{"x":#{x},"y":#{y},"width":#{weight},"frame_id":#{addr},"frame":#{frame[:name].dump},"file":#{frame[:file].dump}}} 210 | end 211 | 212 | def convert_to_d3_flame_graph_format(name, stacks, depth) 213 | weight = 0 214 | children = [] 215 | stacks.chunk do |stack| 216 | if depth == stack.length - 1 217 | :leaf 218 | else 219 | stack[depth] 220 | end 221 | end.each do |val, child_stacks| 222 | if val == :leaf 223 | child_stacks.each do |stack| 224 | weight += stack.last 225 | end 226 | else 227 | frame = @data[:frames][val] 228 | child_name = "#{ frame[:name] } : #{ frame[:file] } : #{ frame[:line] }" 229 | child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1) 230 | weight += child_data["value"] 231 | children << child_data 232 | end 233 | end 234 | 235 | { 236 | "name" => name, 237 | "value" => weight, 238 | "children" => children, 239 | } 240 | end 241 | 242 | def print_d3_flamegraph(f=STDOUT, skip_common=true) 243 | raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw] 244 | 245 | stacks, * = flamegraph_stacks(raw) 246 | 247 | # d3-flame-grpah supports only alphabetical flamegraph 248 | stacks.sort! 249 | 250 | require "json" 251 | json = JSON.generate(convert_to_d3_flame_graph_format("", stacks, 0), max_nesting: false) 252 | 253 | # This html code is almost copied from d3-flame-graph sample code. 254 | # (Apache License 2.0) 255 | # https://github.com/spiermar/d3-flame-graph/blob/gh-pages/index.html 256 | 257 | f.print <<-END 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 300 | 301 | stackprof (mode: #{ data[:mode] }) 302 | 303 | 304 | 308 | 309 | 310 |
311 |
312 | 324 |

stackprof (mode: #{ data[:mode] })

325 |
326 |
327 |
328 |
329 | powered by d3-flame-graph 330 |
331 |
332 |
333 |
334 |
335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 409 | 410 | 411 | END 412 | end 413 | 414 | def print_graphviz(options = {}, f = STDOUT) 415 | if filter = options[:filter] 416 | mark_stack = [] 417 | list = frames(true) 418 | list.each{ |addr, frame| mark_stack << addr if frame[:name] =~ filter } 419 | while addr = mark_stack.pop 420 | frame = list[addr] 421 | unless frame[:marked] 422 | mark_stack += frame[:edges].map{ |addr, weight| addr if list[addr][:total_samples] <= weight*1.2 }.compact if frame[:edges] 423 | frame[:marked] = true 424 | end 425 | end 426 | list = list.select{ |addr, frame| frame[:marked] } 427 | list.each{ |addr, frame| frame[:edges] && frame[:edges].delete_if{ |k,v| list[k].nil? } } 428 | list 429 | else 430 | list = frames(true) 431 | end 432 | 433 | 434 | limit = options[:limit] 435 | fraction = options[:node_fraction] 436 | 437 | included_nodes = {} 438 | node_minimum = fraction ? (fraction * overall_samples).ceil : 0 439 | 440 | f.puts "digraph profile {" 441 | f.puts "Legend [shape=box,fontsize=24,shape=plaintext,label=\"" 442 | f.print "Total samples: #{overall_samples}\\l" 443 | f.print "Showing top #{limit} nodes\\l" if limit 444 | f.print "Dropped nodes with < #{node_minimum} samples\\l" if fraction 445 | f.puts "\"];" 446 | 447 | list.each_with_index do |(frame, info), index| 448 | call, total = info.values_at(:samples, :total_samples) 449 | break if total < node_minimum || (limit && index >= limit) 450 | 451 | sample = ''.dup 452 | sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total 453 | sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples) 454 | fontsize = (1.0 * call / max_samples) * 28 + 10 455 | size = (1.0 * total / overall_samples) * 2.0 + 0.5 456 | 457 | f.puts " \"#{frame}\" [size=#{size}] [fontsize=#{fontsize}] [penwidth=\"#{size}\"] [shape=box] [label=\"#{info[:name]}\\n#{sample}\"];" 458 | included_nodes[frame] = true 459 | end 460 | 461 | list.each do |frame, info| 462 | next unless included_nodes[frame] 463 | 464 | if edges = info[:edges] 465 | edges.each do |edge, weight| 466 | next unless included_nodes[edge] 467 | 468 | size = (1.0 * weight / overall_samples) * 2.0 + 0.5 469 | f.puts " \"#{frame}\" -> \"#{edge}\" [label=\"#{weight}\"] [weight=\"#{weight}\"] [penwidth=\"#{size}\"];" 470 | end 471 | end 472 | end 473 | f.puts "}" 474 | end 475 | 476 | def print_text(sort_by_total=false, limit=nil, select_files= nil, reject_files=nil, select_names=nil, reject_names=nil, f = STDOUT) 477 | f.puts "==================================" 478 | f.printf " Mode: #{modeline}\n" 479 | f.printf " Samples: #{@data[:samples]} (%.2f%% miss rate)\n", 100.0*@data[:missed_samples]/(@data[:missed_samples]+@data[:samples]) 480 | f.printf " GC: #{@data[:gc_samples]} (%.2f%%)\n", 100.0*@data[:gc_samples]/@data[:samples] 481 | f.puts "==================================" 482 | f.printf "% 10s (pct) % 10s (pct) FRAME\n" % ["TOTAL", "SAMPLES"] 483 | list = frames(sort_by_total) 484 | list.select!{|_, info| select_files.any?{|path| info[:file].start_with?(path)}} if select_files 485 | list.select!{|_, info| select_names.any?{|reg| info[:name] =~ reg}} if select_names 486 | list.reject!{|_, info| reject_files.any?{|path| info[:file].start_with?(path)}} if reject_files 487 | list.reject!{|_, info| reject_names.any?{|reg| info[:name] =~ reg}} if reject_names 488 | list = list.first(limit) if limit 489 | list.each do |frame, info| 490 | call, total = info.values_at(:samples, :total_samples) 491 | f.printf "% 10d % 8s % 10d % 8s %s\n", total, "(%2.1f%%)" % (total*100.0/overall_samples), call, "(%2.1f%%)" % (call*100.0/overall_samples), info[:name] 492 | end 493 | end 494 | 495 | def print_callgrind(f = STDOUT) 496 | f.puts "version: 1" 497 | f.puts "creator: stackprof" 498 | f.puts "pid: 0" 499 | f.puts "cmd: ruby" 500 | f.puts "part: 1" 501 | f.puts "desc: mode: #{modeline}" 502 | f.puts "desc: missed: #{@data[:missed_samples]})" 503 | f.puts "positions: line" 504 | f.puts "events: Instructions" 505 | f.puts "summary: #{@data[:samples]}" 506 | 507 | list = frames 508 | list.each do |addr, frame| 509 | f.puts "fl=#{frame[:file]}" 510 | f.puts "fn=#{frame[:name]}" 511 | frame[:lines].each do |line, weight| 512 | f.puts "#{line} #{weight.is_a?(Array) ? weight[1] : weight}" 513 | end if frame[:lines] 514 | frame[:edges].each do |edge, weight| 515 | oframe = list[edge] 516 | f.puts "cfl=#{oframe[:file]}" unless oframe[:file] == frame[:file] 517 | f.puts "cfn=#{oframe[:name]}" 518 | f.puts "calls=#{weight} #{frame[:line] || 0}\n#{oframe[:line] || 0} #{weight}" 519 | end if frame[:edges] 520 | f.puts 521 | end 522 | 523 | f.puts "totals: #{@data[:samples]}" 524 | end 525 | 526 | def print_method(name, f = STDOUT) 527 | name = /#{name}/ unless Regexp === name 528 | frames.each do |frame, info| 529 | next unless info[:name] =~ name 530 | file, line = info.values_at(:file, :line) 531 | line ||= 1 532 | 533 | lines = info[:lines] 534 | maxline = lines ? lines.keys.max : line + 5 535 | f.printf "%s (%s:%d)\n", info[:name], file, line 536 | f.printf " samples: % 5d self (%2.1f%%) / % 5d total (%2.1f%%)\n", info[:samples], 100.0*info[:samples]/overall_samples, info[:total_samples], 100.0*info[:total_samples]/overall_samples 537 | 538 | if (callers = callers_for(frame)).any? 539 | f.puts " callers:" 540 | callers = callers.sort_by(&:last).reverse 541 | callers.each do |name, weight| 542 | f.printf " % 5d (% 8s) %s\n", weight, "%3.1f%%" % (100.0*weight/info[:total_samples]), name 543 | end 544 | end 545 | 546 | if callees = info[:edges] 547 | f.printf " callees (%d total):\n", info[:total_samples]-info[:samples] 548 | callees = callees.map{ |k, weight| [data[:frames][k][:name], weight] }.sort_by{ |k,v| -v } 549 | callees.each do |name, weight| 550 | f.printf " % 5d (% 8s) %s\n", weight, "%3.1f%%" % (100.0*weight/(info[:total_samples]-info[:samples])), name 551 | end 552 | end 553 | 554 | f.puts " code:" 555 | source_display(f, file, lines, line-1..maxline) 556 | end 557 | end 558 | 559 | # Walk up and down the stack from a given starting point (name). Loops 560 | # until `:exit` is selected 561 | def walk_method(name) 562 | method_choice = /#{Regexp.escape name}/ 563 | invalid_choice = false 564 | 565 | # Continue walking up and down the stack until the users selects "exit" 566 | while method_choice != :exit 567 | print_method method_choice unless invalid_choice 568 | STDOUT.puts "\n\n" 569 | 570 | # Determine callers and callees for the current frame 571 | new_frames = frames.select {|_, info| info[:name] =~ method_choice } 572 | new_choices = new_frames.map {|frame, info| [ 573 | callers_for(frame).sort_by(&:last).reverse.map(&:first), 574 | (info[:edges] || []).map{ |k, w| [data[:frames][k][:name], w] }.sort_by{ |k,v| -v }.map(&:first) 575 | ]}.flatten + [:exit] 576 | 577 | # Print callers and callees for selection 578 | STDOUT.puts "Select next method:" 579 | new_choices.each_with_index do |method, index| 580 | STDOUT.printf "%2d) %s\n", index + 1, method.to_s 581 | end 582 | 583 | # Pick selection 584 | STDOUT.printf "> " 585 | selection = STDIN.gets.chomp.to_i - 1 586 | STDOUT.puts "\n\n\n" 587 | 588 | # Determine if it was a valid choice 589 | # (if not, don't re-run .print_method) 590 | if new_choice = new_choices[selection] 591 | invalid_choice = false 592 | method_choice = new_choice == :exit ? :exit : %r/^#{Regexp.escape new_choice}$/ 593 | else 594 | invalid_choice = true 595 | STDOUT.puts "Invalid choice. Please select again..." 596 | end 597 | end 598 | end 599 | 600 | def print_files(sort_by_total=false, limit=nil, f = STDOUT) 601 | list = files.map{ |file, vals| [file, vals.values.inject([0,0]){ |sum, n| add_lines(sum, n) }] } 602 | list = list.sort_by{ |file, samples| -samples[1] } 603 | list = list.first(limit) if limit 604 | list.each do |file, vals| 605 | total_samples, samples = *vals 606 | f.printf "% 5d (%5.1f%%) / % 5d (%5.1f%%) %s\n", total_samples, (100.0*total_samples/overall_samples), samples, (100.0*samples/overall_samples), file 607 | end 608 | end 609 | 610 | def print_file(filter, f = STDOUT) 611 | filter = /#{Regexp.escape filter}/ unless Regexp === filter 612 | list = files.select{ |name, lines| name =~ filter } 613 | list.sort_by{ |file, vals| -vals.values.inject(0){ |sum, n| sum + (n.is_a?(Array) ? n[1] : n) } }.each do |file, lines| 614 | source_display(f, file, lines) 615 | end 616 | end 617 | 618 | def +(other) 619 | raise ArgumentError, "cannot combine #{other.class}" unless self.class == other.class 620 | raise ArgumentError, "cannot combine #{modeline} with #{other.modeline}" unless modeline == other.modeline 621 | raise ArgumentError, "cannot combine v#{version} with v#{other.version}" unless version == other.version 622 | 623 | f1, f2 = normalized_frames, other.normalized_frames 624 | frames = (f1.keys + f2.keys).uniq.inject(Hash.new) do |hash, id| 625 | if f1[id].nil? 626 | hash[id] = f2[id] 627 | elsif f2[id] 628 | hash[id] = f1[id] 629 | hash[id][:total_samples] += f2[id][:total_samples] 630 | hash[id][:samples] += f2[id][:samples] 631 | if f2[id][:edges] 632 | edges = hash[id][:edges] ||= {} 633 | f2[id][:edges].each do |edge, weight| 634 | edges[edge] ||= 0 635 | edges[edge] += weight 636 | end 637 | end 638 | if f2[id][:lines] 639 | lines = hash[id][:lines] ||= {} 640 | f2[id][:lines].each do |line, weight| 641 | lines[line] = add_lines(lines[line], weight) 642 | end 643 | end 644 | else 645 | hash[id] = f1[id] 646 | end 647 | hash 648 | end 649 | 650 | d1, d2 = data, other.data 651 | data = { 652 | version: version, 653 | mode: d1[:mode], 654 | interval: d1[:interval], 655 | samples: d1[:samples] + d2[:samples], 656 | gc_samples: d1[:gc_samples] + d2[:gc_samples], 657 | missed_samples: d1[:missed_samples] + d2[:missed_samples], 658 | frames: frames 659 | } 660 | 661 | self.class.new(data) 662 | end 663 | 664 | private 665 | def root_frames 666 | frames.select{ |addr, frame| callers_for(addr).size == 0 } 667 | end 668 | 669 | def callers_for(addr) 670 | @callers_for ||= {} 671 | @callers_for[addr] ||= data[:frames].map{ |id, other| [other[:name], other[:edges][addr]] if other[:edges] && other[:edges].include?(addr) }.compact 672 | end 673 | 674 | def source_display(f, file, lines, range=nil) 675 | File.readlines(file).each_with_index do |code, i| 676 | next unless range.nil? || range.include?(i) 677 | if lines and lineinfo = lines[i+1] 678 | total_samples, samples = lineinfo 679 | if version == 1.0 680 | samples = total_samples 681 | f.printf "% 5d % 7s | % 5d | %s", samples, "(%2.1f%%)" % (100.0*samples/overall_samples), i+1, code 682 | elsif samples > 0 683 | f.printf "% 5d % 8s / % 5d % 7s | % 5d | %s", total_samples, "(%2.1f%%)" % (100.0*total_samples/overall_samples), samples, "(%2.1f%%)" % (100.0*samples/overall_samples), i+1, code 684 | else 685 | f.printf "% 5d % 8s | % 5d | %s", total_samples, "(%3.1f%%)" % (100.0*total_samples/overall_samples), i+1, code 686 | end 687 | else 688 | if version == 1.0 689 | f.printf " | % 5d | %s", i+1, code 690 | else 691 | f.printf " | % 5d | %s", i+1, code 692 | end 693 | end 694 | end 695 | rescue SystemCallError 696 | f.puts " SOURCE UNAVAILABLE" 697 | end 698 | end 699 | end 700 | -------------------------------------------------------------------------------- /lib/stackprof/truffleruby.rb: -------------------------------------------------------------------------------- 1 | module StackProf 2 | # Define the same methods as stackprof.c 3 | class << self 4 | def running? 5 | false 6 | end 7 | 8 | def run(*args) 9 | unimplemented 10 | end 11 | 12 | def start(*args) 13 | unimplemented 14 | end 15 | 16 | def stop 17 | unimplemented 18 | end 19 | 20 | def results(*args) 21 | unimplemented 22 | end 23 | 24 | def sample 25 | unimplemented 26 | end 27 | 28 | def use_postponed_job! 29 | # noop 30 | end 31 | 32 | private def unimplemented 33 | raise "Use --cpusampler=flamegraph or --cpusampler instead of StackProf on TruffleRuby.\n" \ 34 | "See https://www.graalvm.org/tools/profiling/ and `ruby --help:cpusampler` for more details." 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /sample.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../lib', __FILE__) 2 | require 'stackprof' 3 | 4 | class A 5 | def initialize 6 | pow 7 | self.class.newobj 8 | math 9 | end 10 | 11 | def pow 12 | 2 ** 100 13 | end 14 | 15 | def self.newobj 16 | Object.new 17 | Object.new 18 | end 19 | 20 | def math 21 | 2.times do 22 | 2 + 3 * 4 ^ 5 / 6 23 | end 24 | end 25 | end 26 | 27 | #profile = StackProf.run(mode: :object, interval: 1) do 28 | #profile = StackProf.run(mode: :wall, interval: 1000) do 29 | profile = StackProf.run(mode: :cpu, interval: 1000) do 30 | 1_000_000.times do 31 | A.new 32 | end 33 | end 34 | 35 | result = StackProf::Report.new(profile) 36 | puts 37 | result.print_method(/pow|newobj|math/) 38 | puts 39 | result.print_text 40 | puts 41 | result.print_graphviz 42 | puts 43 | result.print_debug 44 | -------------------------------------------------------------------------------- /stackprof.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'stackprof' 3 | s.version = '0.2.27' 4 | s.homepage = 'http://github.com/tmm1/stackprof' 5 | 6 | s.authors = 'Aman Gupta' 7 | s.email = 'aman@tmm1.net' 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 | 16 | s.files = `git ls-files`.split("\n") 17 | s.extensions = 'ext/stackprof/extconf.rb' 18 | 19 | s.bindir = 'bin' 20 | s.executables << 'stackprof' 21 | s.executables << 'stackprof-flamegraph.pl' 22 | s.executables << 'stackprof-gprof2dot.py' 23 | 24 | s.summary = 'sampling callstack-profiler for ruby 2.2+' 25 | s.description = 'stackprof is a fast sampling profiler for ruby code, with cpu, wallclock and object allocation samplers.' 26 | 27 | s.required_ruby_version = '>= 2.2' 28 | 29 | s.license = 'MIT' 30 | 31 | s.add_development_dependency 'rake-compiler', '~> 0.9' 32 | s.add_development_dependency 'minitest', '~> 5.0' 33 | end 34 | -------------------------------------------------------------------------------- /test/fixtures/profile.dump: -------------------------------------------------------------------------------- 1 | {: modeI"cpu:ET -------------------------------------------------------------------------------- /test/fixtures/profile.json: -------------------------------------------------------------------------------- 1 | { "mode": "cpu" } 2 | -------------------------------------------------------------------------------- /test/test_middleware.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | require 'stackprof' 3 | require 'stackprof/middleware' 4 | require 'minitest/autorun' 5 | require 'tmpdir' 6 | 7 | class StackProf::MiddlewareTest < Minitest::Test 8 | 9 | def test_path_default 10 | StackProf::Middleware.new(Object.new) 11 | 12 | assert_equal 'tmp/', StackProf::Middleware.path 13 | end 14 | 15 | def test_path_custom 16 | StackProf::Middleware.new(Object.new, { path: 'foo/' }) 17 | 18 | assert_equal 'foo/', StackProf::Middleware.path 19 | end 20 | 21 | def test_save_default 22 | middleware = StackProf::Middleware.new(->(env) { 100.times { Object.new } }, 23 | save_every: 1, 24 | enabled: true) 25 | Dir.mktmpdir do |dir| 26 | Dir.chdir(dir) { middleware.call({}) } 27 | dir = File.join(dir, "tmp") 28 | assert File.directory? dir 29 | profile = Dir.entries(dir).reject { |x| File.directory?(x) }.first 30 | assert profile 31 | assert_equal "stackprof", profile.split("-")[0] 32 | assert_equal "cpu", profile.split("-")[1] 33 | assert_equal Process.pid.to_s, profile.split("-")[2] 34 | end 35 | end 36 | 37 | def test_save_custom 38 | middleware = StackProf::Middleware.new(->(env) { 100.times { Object.new } }, 39 | path: "foo/", 40 | save_every: 1, 41 | enabled: true) 42 | Dir.mktmpdir do |dir| 43 | Dir.chdir(dir) { middleware.call({}) } 44 | dir = File.join(dir, "foo") 45 | assert File.directory? dir 46 | profile = Dir.entries(dir).reject { |x| File.directory?(x) }.first 47 | assert profile 48 | assert_equal "stackprof", profile.split("-")[0] 49 | assert_equal "cpu", profile.split("-")[1] 50 | assert_equal Process.pid.to_s, profile.split("-")[2] 51 | end 52 | end 53 | 54 | def test_enabled_should_use_a_proc_if_passed 55 | env = {} 56 | 57 | StackProf::Middleware.new(Object.new, enabled: Proc.new{ false }) 58 | refute StackProf::Middleware.enabled?(env) 59 | 60 | StackProf::Middleware.new(Object.new, enabled: Proc.new{ true }) 61 | assert StackProf::Middleware.enabled?(env) 62 | end 63 | 64 | def test_enabled_should_use_a_proc_if_passed_and_use_the_request_env 65 | enable_proc = Proc.new {|env| env['PROFILE'] } 66 | 67 | env = Hash.new { false } 68 | StackProf::Middleware.new(Object.new, enabled: enable_proc) 69 | refute StackProf::Middleware.enabled?(env) 70 | 71 | env = Hash.new { true } 72 | StackProf::Middleware.new(Object.new, enabled: enable_proc) 73 | assert StackProf::Middleware.enabled?(env) 74 | end 75 | 76 | def test_raw 77 | StackProf::Middleware.new(Object.new, raw: true) 78 | assert StackProf::Middleware.raw 79 | end 80 | 81 | def test_metadata 82 | metadata = { key: 'value' } 83 | StackProf::Middleware.new(Object.new, metadata: metadata) 84 | assert_equal metadata, StackProf::Middleware.metadata 85 | end 86 | end unless RUBY_ENGINE == 'truffleruby' 87 | -------------------------------------------------------------------------------- /test/test_report.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | require 'stackprof' 3 | require 'minitest/autorun' 4 | 5 | class ReportDumpTest < Minitest::Test 6 | require 'stringio' 7 | 8 | def test_dump_to_stdout 9 | data = {} 10 | report = StackProf::Report.new(data) 11 | 12 | out, _err = capture_subprocess_io do 13 | report.print_dump 14 | end 15 | 16 | assert_dump data, out 17 | end 18 | 19 | def test_dump_to_file 20 | data = {} 21 | f = StringIO.new 22 | report = StackProf::Report.new(data) 23 | 24 | report.print_dump(f) 25 | 26 | assert_dump data, f.string 27 | end 28 | 29 | private 30 | 31 | def assert_dump(expected, marshal_data) 32 | assert_equal expected, Marshal.load(marshal_data) 33 | end 34 | end 35 | 36 | class ReportReadTest < Minitest::Test 37 | require 'pathname' 38 | 39 | def test_from_file_read_json 40 | file = fixture("profile.json") 41 | report = StackProf::Report.from_file(file) 42 | 43 | assert_equal({ mode: "cpu" }, report.data) 44 | end 45 | 46 | def test_from_file_read_marshal 47 | file = fixture("profile.dump") 48 | report = StackProf::Report.from_file(file) 49 | 50 | assert_equal({ mode: "cpu" }, report.data) 51 | end 52 | 53 | private 54 | 55 | def fixture(name) 56 | Pathname.new(__dir__).join("fixtures", name) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/test_stackprof.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | require 'stackprof' 3 | require 'minitest/autorun' 4 | require 'tempfile' 5 | require 'pathname' 6 | 7 | class StackProfTest < Minitest::Test 8 | def setup 9 | Object.new # warm some caches to avoid flakiness 10 | end 11 | 12 | def test_info 13 | profile = StackProf.run{} 14 | assert_equal 1.2, profile[:version] 15 | assert_equal :wall, profile[:mode] 16 | assert_equal 1000, profile[:interval] 17 | assert_equal 0, profile[:samples] 18 | end 19 | 20 | def test_running 21 | assert_equal false, StackProf.running? 22 | StackProf.run{ assert_equal true, StackProf.running? } 23 | end 24 | 25 | def test_start_stop_results 26 | assert_nil StackProf.results 27 | assert_equal true, StackProf.start 28 | assert_equal false, StackProf.start 29 | assert_equal true, StackProf.running? 30 | assert_nil StackProf.results 31 | assert_equal true, StackProf.stop 32 | assert_equal false, StackProf.stop 33 | assert_equal false, StackProf.running? 34 | assert_kind_of Hash, StackProf.results 35 | assert_nil StackProf.results 36 | end 37 | 38 | def test_object_allocation 39 | profile_base_line = __LINE__+1 40 | profile = StackProf.run(mode: :object) do 41 | Object.new 42 | Object.new 43 | end 44 | assert_equal :object, profile[:mode] 45 | assert_equal 1, profile[:interval] 46 | if RUBY_VERSION >= '3' 47 | assert_equal 4, profile[:samples] 48 | else 49 | assert_equal 2, profile[:samples] 50 | end 51 | 52 | frame = profile[:frames].values.first 53 | assert_includes frame[:name], "StackProfTest#test_object_allocation" 54 | assert_equal 2, frame[:samples] 55 | assert_includes [profile_base_line - 2, profile_base_line], frame[:line] 56 | if RUBY_VERSION >= '3' 57 | assert_equal [2, 1], frame[:lines][profile_base_line+1] 58 | assert_equal [2, 1], frame[:lines][profile_base_line+2] 59 | else 60 | assert_equal [1, 1], frame[:lines][profile_base_line+1] 61 | assert_equal [1, 1], frame[:lines][profile_base_line+2] 62 | end 63 | frame = profile[:frames].values[1] if RUBY_VERSION < '2.3' 64 | 65 | if RUBY_VERSION >= '3' 66 | assert_equal [4, 0], frame[:lines][profile_base_line] 67 | else 68 | assert_equal [2, 0], frame[:lines][profile_base_line] 69 | end 70 | end 71 | 72 | def test_object_allocation_interval 73 | profile = StackProf.run(mode: :object, interval: 10) do 74 | 100.times { Object.new } 75 | end 76 | assert_equal 10, profile[:samples] 77 | end 78 | 79 | def test_cputime 80 | profile = StackProf.run(mode: :cpu, interval: 500) do 81 | math 82 | end 83 | 84 | assert_operator profile[:samples], :>=, 1 85 | if RUBY_VERSION >= '3' 86 | assert profile[:frames].values.take(2).map { |f| 87 | f[:name].include? "StackProfTest#math" 88 | }.any? 89 | else 90 | frame = profile[:frames].values.first 91 | assert_includes frame[:name], "StackProfTest#math" 92 | end 93 | end 94 | 95 | def test_walltime 96 | GC.disable 97 | profile = StackProf.run(mode: :wall) do 98 | idle 99 | end 100 | 101 | frame = profile[:frames].values.first 102 | if RUBY_VERSION >= '3' 103 | assert_equal "IO.select", frame[:name] 104 | else 105 | assert_equal "StackProfTest#idle", frame[:name] 106 | end 107 | assert_in_delta 200, frame[:samples], 25 108 | ensure 109 | GC.enable 110 | end 111 | 112 | def test_custom 113 | profile_base_line = __LINE__+1 114 | profile = StackProf.run(mode: :custom) do 115 | 10.times do 116 | StackProf.sample 117 | end 118 | end 119 | 120 | assert_equal :custom, profile[:mode] 121 | assert_equal 10, profile[:samples] 122 | 123 | offset = RUBY_VERSION >= '3' ? 1 : 0 124 | frame = profile[:frames].values[offset] 125 | assert_includes frame[:name], "StackProfTest#test_custom" 126 | assert_includes [profile_base_line-2, profile_base_line+1], frame[:line] 127 | 128 | if RUBY_VERSION >= '3' 129 | assert_equal [10, 0], frame[:lines][profile_base_line+2] 130 | else 131 | assert_equal [10, 10], frame[:lines][profile_base_line+2] 132 | end 133 | end 134 | 135 | def test_raw 136 | before_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) 137 | 138 | profile = StackProf.run(mode: :custom, raw: true) do 139 | 10.times do 140 | StackProf.sample 141 | sleep 0.0001 142 | end 143 | end 144 | 145 | after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) 146 | 147 | raw = profile[:raw] 148 | raw_lines = profile[:raw_lines] 149 | assert_equal 10, raw[-1] 150 | assert_equal raw[0] + 2, raw.size 151 | assert_equal 10, raw_lines[-1] # seen 10 times 152 | 153 | offset = RUBY_VERSION >= '3' ? -3 : -2 154 | assert_equal 140, raw_lines[offset] # sample caller is on 140 155 | assert_includes profile[:frames][raw[offset]][:name], 'StackProfTest#test_raw' 156 | 157 | assert_equal 10, profile[:raw_sample_timestamps].size 158 | profile[:raw_sample_timestamps].each_cons(2) do |t1, t2| 159 | assert_operator t1, :>, before_monotonic 160 | assert_operator t2, :>=, t1 161 | assert_operator t2, :<, after_monotonic 162 | end 163 | 164 | assert_equal 10, profile[:raw_timestamp_deltas].size 165 | total_duration = after_monotonic - before_monotonic 166 | assert_operator profile[:raw_timestamp_deltas].inject(&:+), :<, total_duration 167 | 168 | profile[:raw_timestamp_deltas].each do |delta| 169 | assert_operator delta, :>, 0 170 | end 171 | end 172 | 173 | def test_metadata 174 | metadata = { 175 | path: '/foo/bar', 176 | revision: '5c0b01f1522ae8c194510977ae29377296dd236b', 177 | } 178 | profile = StackProf.run(mode: :cpu, metadata: metadata) do 179 | math 180 | end 181 | 182 | assert_equal metadata, profile[:metadata] 183 | end 184 | 185 | def test_empty_metadata 186 | profile = StackProf.run(mode: :cpu) do 187 | math 188 | end 189 | 190 | assert_equal({}, profile[:metadata]) 191 | end 192 | 193 | def test_raises_if_metadata_is_not_a_hash 194 | exception = assert_raises ArgumentError do 195 | StackProf.run(mode: :cpu, metadata: 'foobar') do 196 | math 197 | end 198 | end 199 | 200 | assert_equal 'metadata should be a hash', exception.message 201 | end 202 | 203 | def test_fork 204 | StackProf.run do 205 | pid = fork do 206 | exit! StackProf.running?? 1 : 0 207 | end 208 | Process.wait(pid) 209 | assert_equal 0, $?.exitstatus 210 | assert_equal true, StackProf.running? 211 | end 212 | end 213 | 214 | def foo(n = 10) 215 | if n == 0 216 | StackProf.sample 217 | return 218 | end 219 | foo(n - 1) 220 | end 221 | 222 | def test_recursive_total_samples 223 | profile = StackProf.run(mode: :cpu, raw: true) do 224 | 10.times do 225 | foo 226 | end 227 | end 228 | 229 | frame = profile[:frames].values.find do |frame| 230 | frame[:name] == "StackProfTest#foo" 231 | end 232 | assert_equal 10, frame[:total_samples] 233 | end 234 | 235 | def test_gc 236 | profile = StackProf.run(interval: 100, raw: true) do 237 | 5.times do 238 | GC.start 239 | end 240 | end 241 | 242 | gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" } 243 | marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" } 244 | sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" } 245 | 246 | assert gc_frame 247 | assert marking_frame 248 | assert sweeping_frame 249 | 250 | # We can't guarantee a certain number of GCs to run, so just assert 251 | # that it's within some kind of delta 252 | assert_in_delta gc_frame[:total_samples], profile[:gc_samples], 2 253 | 254 | # Lazy marking / sweeping can cause this math to not add up, so also use a delta 255 | assert_in_delta profile[:gc_samples], [gc_frame, marking_frame, sweeping_frame].map{|x| x[:samples] }.inject(:+), 2 256 | 257 | assert_operator profile[:gc_samples], :>, 0 258 | assert_operator profile[:missed_samples], :<=, 25 259 | end 260 | 261 | def test_out 262 | tmpfile = Tempfile.new('stackprof-out') 263 | ret = StackProf.run(mode: :custom, out: tmpfile) do 264 | StackProf.sample 265 | end 266 | 267 | assert_equal tmpfile, ret 268 | tmpfile.rewind 269 | profile = Marshal.load(tmpfile.read) 270 | refute_empty profile[:frames] 271 | end 272 | 273 | def test_out_to_path_string 274 | tmpfile = Tempfile.new('stackprof-out') 275 | ret = StackProf.run(mode: :custom, out: tmpfile.path) do 276 | StackProf.sample 277 | end 278 | 279 | refute_equal tmpfile, ret 280 | assert_equal tmpfile.path, ret.path 281 | tmpfile.rewind 282 | profile = Marshal.load(tmpfile.read) 283 | refute_empty profile[:frames] 284 | end 285 | 286 | def test_pathname_out 287 | tmpfile = Tempfile.new('stackprof-out') 288 | pathname = Pathname.new(tmpfile.path) 289 | ret = StackProf.run(mode: :custom, out: pathname) do 290 | StackProf.sample 291 | end 292 | 293 | assert_equal tmpfile.path, ret.path 294 | tmpfile.rewind 295 | profile = Marshal.load(tmpfile.read) 296 | refute_empty profile[:frames] 297 | end 298 | 299 | def test_min_max_interval 300 | [-1, 0, 1_000_000, 1_000_001].each do |invalid_interval| 301 | err = assert_raises(ArgumentError, "invalid interval #{invalid_interval}") do 302 | StackProf.run(interval: invalid_interval, debug: true) {} 303 | end 304 | assert_match(/microseconds/, err.message) 305 | end 306 | end 307 | 308 | def math 309 | 250_000.times do 310 | 2 ** 10 311 | end 312 | end 313 | 314 | def idle 315 | r, w = IO.pipe 316 | IO.select([r], nil, nil, 0.2) 317 | ensure 318 | r.close 319 | w.close 320 | end 321 | end unless RUBY_ENGINE == 'truffleruby' 322 | -------------------------------------------------------------------------------- /test/test_truffleruby.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | require 'stackprof' 3 | require 'minitest/autorun' 4 | 5 | if RUBY_ENGINE == 'truffleruby' 6 | class StackProfTruffleRubyTest < Minitest::Test 7 | def test_error 8 | error = assert_raises RuntimeError do 9 | StackProf.run(mode: :cpu) do 10 | unreacheable 11 | end 12 | end 13 | 14 | assert_match(/TruffleRuby/, error.message) 15 | assert_match(/--cpusampler/, error.message) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /vendor/FlameGraph/README: -------------------------------------------------------------------------------- 1 | Flame Graphs visualize profiled code-paths. 2 | 3 | Website: http://www.brendangregg.com/flamegraphs.html 4 | 5 | CPU profiling using DTrace, perf_events, SystemTap, or ktap: http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html 6 | CPU profiling using XCode Instruments: http://schani.wordpress.com/2012/11/16/flame-graphs-for-instruments/ 7 | CPU profiling using Xperf.exe: http://randomascii.wordpress.com/2013/03/26/summarizing-xperf-cpu-usage-with-flame-graphs/ 8 | Memory profiling: http://www.brendangregg.com/FlameGraphs/memoryflamegraphs.html 9 | 10 | These can be created in three steps: 11 | 12 | 1. Capture stacks 13 | 2. Fold stacks 14 | 3. flamegraph.pl 15 | 16 | 17 | 1. Capture stacks 18 | ================= 19 | Stack samples can be captured using DTrace, perf_events or SystemTap. 20 | 21 | Using DTrace to capture 60 seconds of kernel stacks at 997 Hertz: 22 | 23 | # dtrace -x stackframes=100 -n 'profile-997 /arg0/ { @[stack()] = count(); } tick-60s { exit(0); }' -o out.kern_stacks 24 | 25 | Using DTrace to capture 60 seconds of user-level stacks for PID 12345 at 97 Hertz: 26 | 27 | # dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345 && arg1/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks 28 | 29 | Using DTrace to capture 60 seconds of user-level stacks, including while time is spent in the kernel, for PID 12345 at 97 Hertz: 30 | 31 | # dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks 32 | 33 | Switch ustack() for jstack() if the application has a ustack helper to include translated frames (eg, node.js frames; see: http://dtrace.org/blogs/dap/2012/01/05/where-does-your-node-program-spend-its-time/). The rate for user-level stack collection is deliberately slower than kernel, which is especially important when using jstack() as it performs additional work to translate frames. 34 | 35 | 2. Fold stacks 36 | ============== 37 | Use the stackcollapse programs to fold stack samples into single lines. The programs provided are: 38 | 39 | - stackcollapse.pl: for DTrace stacks 40 | - stackcollapse-perf.pl: for perf_events "perf script" output 41 | - stackcollapse-stap.pl: for SystemTap stacks 42 | - stackcollapse-instruments.pl: for XCode Instruments 43 | 44 | Usage example: 45 | 46 | $ ./stackcollapse.pl out.kern_stacks > out.kern_folded 47 | 48 | The output looks like this: 49 | 50 | unix`_sys_sysenter_post_swapgs 1401 51 | unix`_sys_sysenter_post_swapgs;genunix`close 5 52 | unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf 85 53 | unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;c2audit`audit_closef 26 54 | unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;c2audit`audit_setf 5 55 | unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;genunix`audit_getstate 6 56 | unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;genunix`audit_unfalloc 2 57 | unix`_sys_sysenter_post_swapgs;genunix`close;genunix`closeandsetf;genunix`closef 48 58 | [...] 59 | 60 | 3. flamegraph.pl 61 | ================ 62 | Use flamegraph.pl to render a SVG. 63 | 64 | $ ./flamegraph.pl out.kern_folded > kernel.svg 65 | 66 | An advantage of having the folded input file (and why this is separate to flamegraph.pl) is that you can use grep for functions of interest. Eg: 67 | 68 | $ grep cpuid out.kern_folded | ./flamegraph.pl > cpuid.svg 69 | 70 | 71 | Provided Example 72 | ================ 73 | An example output from DTrace is included, both the captured stacks and 74 | the resulting Flame Graph. You can generate it yourself using: 75 | 76 | $ ./stackcollapse.pl example-stacks.txt | ./flamegraph.pl > example.svg 77 | 78 | This was from a particular performance investigation: the Flame Graph 79 | identified that CPU time was spent in the lofs module, and quantified 80 | that time. 81 | 82 | 83 | Options 84 | ======= 85 | See the USAGE message (--help) for options: 86 | 87 | USAGE: ./flamegraph.pl [options] infile > outfile.svg 88 | 89 | --titletext # change title text 90 | --width # width of image (default 1200) 91 | --height # height of each frame (default 16) 92 | --minwidth # omit smaller functions (default 0.1 pixels) 93 | --fonttype # font type (default "Verdana") 94 | --fontsize # font size (default 12) 95 | --countname # count type label (default "samples") 96 | --nametype # name type label (default "Function:") 97 | --colors # "hot", "mem", "io" palette (default "hot") 98 | --hash # colors are keyed by function name hash 99 | --cp # use consistent palette (palette.map) 100 | eg, 101 | ./flamegraph.pl --titletext="Flame Graph: malloc()" trace.txt > graph.svg 102 | 103 | As suggested in the example, flame graphs can process traces of any event, 104 | such as malloc()s, provided stack traces are gathered. 105 | 106 | 107 | Consistent Palette 108 | ================== 109 | If you use the --cp option, it will use the $colors selection and randomly 110 | generate the palette like normal. Any future flamegraphs created using the --cp 111 | option will use the same palette map. Any new symbols from future flamegraphs 112 | will have their colors randomly generated using the $colors selection. 113 | 114 | If you don't like the palette, just delete the palette.map file. 115 | 116 | This allows your to change your colorscheme between flamegraphs to make the 117 | differences REALLY stand out. 118 | 119 | Example: 120 | 121 | Say we have 2 captures, one with a problem, and one when it was working 122 | (whatever "it" is): 123 | 124 | cat working.folded | ./flamegraph.pl --cp > working.svg 125 | # this generates a palette.map, as per the normal random generated look. 126 | 127 | cat broken.folded | ./flamegraph.pl --cp --colors mem > broken.svg 128 | # this svg will use the same palette.map for the same events, but a very 129 | # different colorscheme for any new events. 130 | 131 | Take a look at the demo directory for an example: 132 | 133 | palette-example-working.svg 134 | palette-example-broken.svg 135 | -------------------------------------------------------------------------------- /vendor/FlameGraph/flamegraph.pl: -------------------------------------------------------------------------------- 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 $bgcolors = ""; # background color theme 111 | my $nameattrfile; # file holding function attributes 112 | my $timemax; # (override the) sum of the counts 113 | my $factor = 1; # factor to scale counts by 114 | my $hash = 0; # color by function name 115 | my $palette = 0; # if we use consistent palettes (default off) 116 | my %palette_map; # palette map hash 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 < 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 | } 160 | 161 | GetOptions( 162 | 'fonttype=s' => \$fonttype, 163 | 'width=i' => \$imagewidth, 164 | 'height=i' => \$frameheight, 165 | 'encoding=s' => \$encoding, 166 | 'fontsize=f' => \$fontsize, 167 | 'fontwidth=f' => \$fontwidth, 168 | 'minwidth=f' => \$minwidth, 169 | 'title=s' => \$titletext, 170 | 'subtitle=s' => \$subtitletext, 171 | 'nametype=s' => \$nametype, 172 | 'countname=s' => \$countname, 173 | 'nameattr=s' => \$nameattrfile, 174 | 'total=s' => \$timemax, 175 | 'factor=f' => \$factor, 176 | 'colors=s' => \$colors, 177 | 'bgcolors=s' => \$bgcolors, 178 | 'hash' => \$hash, 179 | 'cp' => \$palette, 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(); 188 | 189 | # internals 190 | my $ypad1 = $fontsize * 3; # pad top, include title 191 | my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels 192 | my $ypad3 = $fontsize * 2; # pad top, include subtitle (optional) 193 | my $xpad = 10; # pad lefm and right 194 | my $framepad = 1; # vertical padding for frames 195 | my $depthmax = 0; 196 | my %Events; 197 | my %nameattr; 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 | 211 | if ($nameattrfile) { 212 | # The name-attribute file format is a function name followed by a tab then 213 | # a sequence of tab separated name=value pairs. 214 | open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n"; 215 | while (<$attrfh>) { 216 | chomp; 217 | my ($funcname, $attrstr) = split /\t/, $_, 2; 218 | die "Invalid format in $nameattrfile" unless defined $attrstr; 219 | $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr }; 220 | } 221 | } 222 | 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 | } 259 | 260 | # SVG functions 261 | { package SVG; 262 | sub new { 263 | my $class = shift; 264 | my $self = {}; 265 | bless ($self, $class); 266 | return $self; 267 | } 268 | 269 | sub header { 270 | my ($self, $w, $h) = @_; 271 | my $enc_attr = ''; 272 | if (defined $encoding) { 273 | $enc_attr = qq{ encoding="$encoding"}; 274 | } 275 | $self->{svg} .= < 277 | 278 | 279 | 280 | 281 | SVG 282 | } 283 | 284 | sub include { 285 | my ($self, $content) = @_; 286 | $self->{svg} .= $content; 287 | } 288 | 289 | sub colorAllocate { 290 | my ($self, $r, $g, $b) = @_; 291 | return "rgb($r,$g,$b)"; 292 | } 293 | 294 | sub group_start { 295 | my ($self, $attr) = @_; 296 | 297 | my @g_attr = map { 298 | exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () 299 | } qw(id class); 300 | push @g_attr, $attr->{g_extra} if $attr->{g_extra}; 301 | if ($attr->{href}) { 302 | my @a_attr; 303 | push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href}; 304 | # default target=_top else links will open within SVG 305 | push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top"; 306 | push @a_attr, $attr->{a_extra} if $attr->{a_extra}; 307 | $self->{svg} .= sprintf qq/\n/, join(' ', (@a_attr, @g_attr)); 308 | } else { 309 | $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); 310 | } 311 | 312 | $self->{svg} .= sprintf qq/%s<\/title>/, $attr->{title} 313 | if $attr->{title}; # should be first element within g container 314 | } 315 | 316 | sub group_end { 317 | my ($self, $attr) = @_; 318 | $self->{svg} .= $attr->{href} ? qq/<\/a>\n/ : qq/<\/g>\n/; 319 | } 320 | 321 | sub filledRectangle { 322 | my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_; 323 | $x1 = sprintf "%0.1f", $x1; 324 | $x2 = sprintf "%0.1f", $x2; 325 | my $w = sprintf "%0.1f", $x2 - $x1; 326 | my $h = sprintf "%0.1f", $y2 - $y1; 327 | $extra = defined $extra ? $extra : ""; 328 | $self->{svg} .= qq/\n/; 329 | } 330 | 331 | sub stringTTF { 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/$str<\/text>\n/; 337 | } 338 | 339 | sub svg { 340 | my $self = shift; 341 | return "$self->{svg}\n"; 342 | } 343 | 1; 344 | } 345 | 346 | sub namehash { 347 | # Generate a vector hash for the name string, weighting early over 348 | # later characters. We want to pick the same colors for function 349 | # names across different flame graphs. 350 | my $name = shift; 351 | my $vector = 0; 352 | my $weight = 1; 353 | my $max = 1; 354 | my $mod = 10; 355 | # if module name present, trunc to 1st char 356 | $name =~ s/.(.*?)`//; 357 | foreach my $c (split //, $name) { 358 | my $i = (ord $c) % $mod; 359 | $vector += ($i / ($mod++ - 1)) * $weight; 360 | $max += 1 * $weight; 361 | $weight *= 0.70; 362 | last if $mod > 12; 363 | } 364 | return (1 - $vector / $max) 365 | } 366 | 367 | sub color { 368 | my ($type, $hash, $name) = @_; 369 | my ($v1, $v2, $v3); 370 | 371 | if ($hash) { 372 | $v1 = namehash($name); 373 | $v2 = $v3 = namehash(scalar reverse $name); 374 | } else { 375 | $v1 = rand(1); 376 | $v2 = rand(1); 377 | $v3 = rand(1); 378 | } 379 | 380 | # theme palettes 381 | if (defined $type and $type eq "hot") { 382 | my $r = 205 + int(50 * $v3); 383 | my $g = 0 + int(230 * $v1); 384 | my $b = 0 + int(55 * $v2); 385 | return "rgb($r,$g,$b)"; 386 | } 387 | if (defined $type and $type eq "mem") { 388 | my $r = 0; 389 | my $g = 190 + int(50 * $v2); 390 | my $b = 0 + int(210 * $v1); 391 | return "rgb($r,$g,$b)"; 392 | } 393 | if (defined $type and $type eq "io") { 394 | my $r = 80 + int(60 * $v1); 395 | my $g = $r; 396 | my $b = 190 + int(55 * $v2); 397 | return "rgb($r,$g,$b)"; 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 | 510 | return "rgb(0,0,0)"; 511 | } 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 | 525 | sub color_map { 526 | my ($colors, $func) = @_; 527 | if (exists $palette_map{$func}) { 528 | return $palette_map{$func}; 529 | } else { 530 | $palette_map{$func} = color($colors, $hash, $func); 531 | return $palette_map{$func}; 532 | } 533 | } 534 | 535 | sub write_palette { 536 | open(FILE, ">$pal_file"); 537 | foreach my $key (sort keys %palette_map) { 538 | print FILE $key."->".$palette_map{$key}."\n"; 539 | } 540 | close(FILE); 541 | } 542 | 543 | sub read_palette { 544 | if (-e $pal_file) { 545 | open(FILE, $pal_file) or die "can't open file $pal_file: $!"; 546 | while ( my $line = ) { 547 | chomp($line); 548 | (my $key, my $value) = split("->",$line); 549 | $palette_map{$key}=$value; 550 | } 551 | close(FILE) 552 | } 553 | } 554 | 555 | my %Node; # Hash of merged frame data 556 | my %Tmp; 557 | 558 | # flow() merges two stacks, storing the merged frames and value data in %Node. 559 | sub flow { 560 | my ($last, $this, $v, $d) = @_; 561 | 562 | my $len_a = @$last - 1; 563 | my $len_b = @$this - 1; 564 | 565 | my $i = 0; 566 | my $len_same; 567 | for (; $i <= $len_a; $i++) { 568 | last if $i > $len_b; 569 | last if $last->[$i] ne $this->[$i]; 570 | } 571 | $len_same = $i; 572 | 573 | for ($i = $len_a; $i >= $len_same; $i--) { 574 | my $k = "$last->[$i];$i"; 575 | # a unique ID is constructed from "func;depth;etime"; 576 | # func-depth isn't unique, it may be repeated later. 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 | } 581 | delete $Tmp{$k}; 582 | } 583 | 584 | for ($i = $len_same; $i <= $len_b; $i++) { 585 | my $k = "$this->[$i];$i"; 586 | $Tmp{$k}->{stime} = $v; 587 | if (defined $d) { 588 | $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; 589 | } 590 | } 591 | 592 | return $this; 593 | } 594 | 595 | # parse input 596 | my @Data; 597 | my @SortedData; 598 | my $last = []; 599 | my $time = 0; 600 | my $delta = undef; 601 | my $ignored = 0; 602 | my $line; 603 | my $maxdelta = 1; 604 | 605 | # reverse if needed 606 | foreach (<>) { 607 | chomp; 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) { 640 | ++$ignored; 641 | next; 642 | } 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 | } 679 | } 680 | flow($last, [], $time, $delta); 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 | } 694 | if ($timemax and $timemax < $time) { 695 | warn "Specified --total $timemax is less than actual total $time, so ignored\n" 696 | if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) 697 | undef $timemax; 698 | } 699 | $timemax ||= $time; 700 | 701 | my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax; 702 | my $minwidth_time = $minwidth / $widthpertime; 703 | 704 | # prune blocks that are too narrow and determine max depth 705 | while (my ($id, $node) = each %Node) { 706 | my ($func, $depth, $etime) = split ";", $id; 707 | my $stime = $node->{stime}; 708 | die "missing start for $id" if not defined $stime; 709 | 710 | if (($etime-$stime) < $minwidth_time) { 711 | delete $Node{$id}; 712 | next; 713 | } 714 | $depthmax = $depth if $depth > $depthmax; 715 | } 716 | 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; 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 | ); 727 | $im->header($imagewidth, $imageheight); 728 | my $inc = < 730 | 731 | 732 | 733 | 734 | 735 | 746 | 1060 | INC 1061 | $im->include($inc); 1062 | $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)'); 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), " "); 1069 | 1070 | if ($palette) { 1071 | read_palette(); 1072 | } 1073 | 1074 | # draw frames 1075 | $im->group_start({id => "frames"}); 1076 | while (my ($id, $node) = each %Node) { 1077 | my ($func, $depth, $etime) = split ";", $id; 1078 | my $stime = $node->{stime}; 1079 | my $delta = $node->{delta}; 1080 | 1081 | $etime = $timemax if $func eq "" and $depth == 0; 1082 | 1083 | my $x1 = $xpad + $stime * $widthpertime; 1084 | my $x2 = $xpad + $etime * $widthpertime; 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 | } 1093 | 1094 | my $samples = sprintf "%.0f", ($etime - $stime) * $factor; 1095 | (my $samples_txt = $samples) # add commas per perlfaq5 1096 | =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; 1097 | 1098 | my $info; 1099 | if ($func eq "" and $depth == 0) { 1100 | $info = "all ($samples_txt $countname, 100%)"; 1101 | } else { 1102 | my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor)); 1103 | my $escaped_func = $func; 1104 | # clean up SVG breaking characters: 1105 | $escaped_func =~ s/&/&/g; 1106 | $escaped_func =~ s//>/g; 1108 | $escaped_func =~ s/"/"/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 | } 1118 | } 1119 | 1120 | my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone 1121 | $nameattr->{title} ||= $info; 1122 | $im->group_start($nameattr); 1123 | 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); 1133 | } else { 1134 | $color = color($colors, $hash, $func); 1135 | } 1136 | $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); 1137 | 1138 | my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); 1139 | my $text = ""; 1140 | if ($chars >= 3) { # room for one char plus two dots 1141 | $func =~ s/_\[[kwij]\]$//; # strip any annotation 1142 | $text = substr $func, 0, $chars; 1143 | substr($text, -2, 2) = ".." if $chars < length $func; 1144 | $text =~ s/&/&/g; 1145 | $text =~ s//>/g; 1147 | } 1148 | $im->stringTTF(undef, $x1 + 3, 3 + ($y1 + $y2) / 2, $text); 1149 | 1150 | $im->group_end($nameattr); 1151 | } 1152 | $im->group_end(); 1153 | 1154 | print $im->svg; 1155 | 1156 | if ($palette) { 1157 | write_palette(); 1158 | } 1159 | 1160 | # vim: ts=8 sts=8 sw=8 noexpandtab 1161 | -------------------------------------------------------------------------------- /vendor/gprof2dot/hotshotmain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2007 Jose Fonseca 4 | # 5 | # This program is free software: you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published 7 | # by the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with this program. If not, see . 17 | # 18 | 19 | def run(statement, filename=None, sort=-1): 20 | import os, tempfile, hotshot, hotshot.stats 21 | logfd, logfn = tempfile.mkstemp() 22 | prof = hotshot.Profile(logfn) 23 | try: 24 | prof = prof.run(statement) 25 | except SystemExit: 26 | pass 27 | try: 28 | try: 29 | prof = prof.run(statement) 30 | except SystemExit: 31 | pass 32 | prof.close() 33 | finally: 34 | stats = hotshot.stats.load(logfn) 35 | stats.strip_dirs() 36 | stats.sort_stats(sort) 37 | if filename is not None: 38 | result = stats.dump_stats(filename) 39 | else: 40 | result = stats.print_stats() 41 | os.unlink(logfn) 42 | return result 43 | 44 | def main(): 45 | import os, sys 46 | from optparse import OptionParser 47 | usage = "hotshotmain.py [-o output_file_path] [-s sort] scriptfile [arg] ..." 48 | parser = OptionParser(usage=usage) 49 | parser.allow_interspersed_args = False 50 | parser.add_option('-o', '--outfile', dest="outfile", 51 | help="Save stats to ", default=None) 52 | parser.add_option('-s', '--sort', dest="sort", 53 | help="Sort order when printing to stdout, based on pstats.Stats class", default=-1) 54 | 55 | if not sys.argv[1:]: 56 | parser.print_usage() 57 | sys.exit(2) 58 | 59 | (options, args) = parser.parse_args() 60 | sys.argv[:] = args 61 | 62 | if (len(sys.argv) > 0): 63 | sys.path.insert(0, os.path.dirname(sys.argv[0])) 64 | run('execfile(%r)' % (sys.argv[0],), options.outfile, options.sort) 65 | else: 66 | parser.print_usage() 67 | return parser 68 | 69 | if __name__ == "__main__": 70 | main() 71 | --------------------------------------------------------------------------------