stackprof 0.2.11 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,8 @@
1
1
  require "stackprof/stackprof"
2
2
 
3
+ module StackProf
4
+ VERSION = '0.2.16'
5
+ end
6
+
3
7
  StackProf.autoload :Report, "stackprof/report.rb"
4
8
  StackProf.autoload :Middleware, "stackprof/middleware.rb"
@@ -11,13 +11,20 @@ module StackProf
11
11
  Middleware.interval = options[:interval] || 1000
12
12
  Middleware.raw = options[:raw] || false
13
13
  Middleware.enabled = options[:enabled]
14
- Middleware.path = options[:path] || 'tmp'
14
+ options[:path] = 'tmp/' if options[:path].to_s.empty?
15
+ Middleware.path = options[:path]
16
+ Middleware.metadata = options[:metadata] || {}
15
17
  at_exit{ Middleware.save } if options[:save_at_exit]
16
18
  end
17
19
 
18
20
  def call(env)
19
21
  enabled = Middleware.enabled?(env)
20
- StackProf.start(mode: Middleware.mode, interval: Middleware.interval, raw: Middleware.raw) if enabled
22
+ StackProf.start(
23
+ mode: Middleware.mode,
24
+ interval: Middleware.interval,
25
+ raw: Middleware.raw,
26
+ metadata: Middleware.metadata,
27
+ ) if enabled
21
28
  @app.call(env)
22
29
  ensure
23
30
  if enabled
@@ -30,7 +37,7 @@ module StackProf
30
37
  end
31
38
 
32
39
  class << self
33
- attr_accessor :enabled, :mode, :interval, :raw, :path
40
+ attr_accessor :enabled, :mode, :interval, :raw, :path, :metadata
34
41
 
35
42
  def enabled?(env)
36
43
  if enabled.respond_to?(:call)
@@ -40,11 +47,20 @@ module StackProf
40
47
  end
41
48
  end
42
49
 
43
- def save(filename = nil)
50
+ def save
44
51
  if results = StackProf.results
45
- FileUtils.mkdir_p(Middleware.path)
46
- filename ||= "stackprof-#{results[:mode]}-#{Process.pid}-#{Time.now.to_i}.dump"
47
- File.open(File.join(Middleware.path, filename), 'wb') do |f|
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|
48
64
  f.write Marshal.dump(results)
49
65
  end
50
66
  filename
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pp'
2
4
  require 'digest/md5'
3
5
 
@@ -38,7 +40,7 @@ module StackProf
38
40
  end
39
41
 
40
42
  def max_samples
41
- @data[:max_samples] ||= frames.max_by{ |addr, frame| frame[:samples] }.last[:samples]
43
+ @data[:max_samples] ||= @data[:frames].values.max_by{ |frame| frame[:samples] }[:samples]
42
44
  end
43
45
 
44
46
  def files
@@ -55,8 +57,8 @@ module StackProf
55
57
 
56
58
  def add_lines(a, b)
57
59
  return b if a.nil?
58
- return a+b if a.is_a? Fixnum
59
- return [ a[0], a[1]+b ] if b.is_a? Fixnum
60
+ return a+b if a.is_a? Integer
61
+ return [ a[0], a[1]+b ] if b.is_a? Integer
60
62
  [ a[0]+b[0], a[1]+b[1] ]
61
63
  end
62
64
 
@@ -68,6 +70,11 @@ module StackProf
68
70
  f.puts Marshal.dump(@data.reject{|k,v| k == :files })
69
71
  end
70
72
 
73
+ def print_json(f=STDOUT)
74
+ require "json"
75
+ f.puts JSON.generate(@data, max_nesting: false)
76
+ end
77
+
71
78
  def print_stackcollapse
72
79
  raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
73
80
 
@@ -80,19 +87,62 @@ module StackProf
80
87
  end
81
88
  end
82
89
 
83
- def print_flamegraph(f=STDOUT, skip_common=true)
90
+ def print_timeline_flamegraph(f=STDOUT, skip_common=true)
91
+ print_flamegraph(f, skip_common, false)
92
+ end
93
+
94
+ def print_alphabetical_flamegraph(f=STDOUT, skip_common=true)
95
+ print_flamegraph(f, skip_common, true)
96
+ end
97
+
98
+ StackCursor = Struct.new(:raw, :idx, :length) do
99
+ def weight
100
+ @weight ||= raw[1 + idx + length]
101
+ end
102
+
103
+ def [](i)
104
+ if i >= length
105
+ nil
106
+ else
107
+ raw[1 + idx + i]
108
+ end
109
+ end
110
+
111
+ def <=>(other)
112
+ i = 0
113
+ while i < length && i < other.length
114
+ if self[i] != other[i]
115
+ return self[i] <=> other[i]
116
+ end
117
+ i += 1
118
+ end
119
+
120
+ return length <=> other.length
121
+ end
122
+ end
123
+
124
+ def print_flamegraph(f, skip_common, alphabetical=false)
84
125
  raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
85
126
 
86
127
  stacks = []
87
128
  max_x = 0
88
129
  max_y = 0
89
- while len = raw.shift
130
+
131
+ idx = 0
132
+ loop do
133
+ len = raw[idx]
134
+ break unless len
90
135
  max_y = len if len > max_y
91
- stack = raw.slice!(0, len+1)
136
+
137
+ stack = StackCursor.new(raw, idx, len)
92
138
  stacks << stack
93
- max_x += stack.last
139
+ max_x += stack.weight
140
+
141
+ idx += len + 2
94
142
  end
95
143
 
144
+ stacks.sort! if alphabetical
145
+
96
146
  f.puts 'flamegraph(['
97
147
  max_y.times do |y|
98
148
  row_prev = nil
@@ -100,7 +150,7 @@ module StackProf
100
150
  x = 0
101
151
 
102
152
  stacks.each do |stack|
103
- weight = stack.last
153
+ weight = stack.weight
104
154
  cell = stack[y] unless y == stack.length-1
105
155
 
106
156
  if cell.nil?
@@ -142,12 +192,222 @@ module StackProf
142
192
  end
143
193
 
144
194
  def flamegraph_row(f, x, y, weight, addr)
145
- frame = frames[addr]
195
+ frame = @data[:frames][addr]
146
196
  f.print ',' if @rows_started
147
197
  @rows_started = true
148
198
  f.puts %{{"x":#{x},"y":#{y},"width":#{weight},"frame_id":#{addr},"frame":#{frame[:name].dump},"file":#{frame[:file].dump}}}
149
199
  end
150
200
 
201
+ def convert_to_d3_flame_graph_format(name, stacks, depth)
202
+ weight = 0
203
+ children = []
204
+ stacks.chunk do |stack|
205
+ if depth == stack.length - 1
206
+ :leaf
207
+ else
208
+ stack[depth]
209
+ end
210
+ end.each do |val, child_stacks|
211
+ if val == :leaf
212
+ child_stacks.each do |stack|
213
+ weight += stack.last
214
+ end
215
+ else
216
+ frame = @data[:frames][val]
217
+ child_name = "#{ frame[:name] } : #{ frame[:file] }"
218
+ child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
219
+ weight += child_data["value"]
220
+ children << child_data
221
+ end
222
+ end
223
+
224
+ {
225
+ "name" => name,
226
+ "value" => weight,
227
+ "children" => children,
228
+ }
229
+ end
230
+
231
+ def print_d3_flamegraph(f=STDOUT, skip_common=true)
232
+ raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
233
+
234
+ stacks = []
235
+ max_x = 0
236
+ max_y = 0
237
+ while len = raw.shift
238
+ max_y = len if len > max_y
239
+ stack = raw.slice!(0, len+1)
240
+ stacks << stack
241
+ max_x += stack.last
242
+ end
243
+
244
+ # d3-flame-grpah supports only alphabetical flamegraph
245
+ stacks.sort!
246
+
247
+ require "json"
248
+ json = JSON.generate(convert_to_d3_flame_graph_format("<root>", stacks, 0), max_nesting: false)
249
+
250
+ # This html code is almost copied from d3-flame-graph sample code.
251
+ # (Apache License 2.0)
252
+ # https://github.com/spiermar/d3-flame-graph/blob/gh-pages/index.html
253
+
254
+ f.print <<-END
255
+ <!DOCTYPE html>
256
+ <html lang="en">
257
+ <head>
258
+ <meta charset="utf-8">
259
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
260
+ <meta name="viewport" content="width=device-width, initial-scale=1">
261
+
262
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
263
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.css">
264
+
265
+ <style>
266
+
267
+ /* Space out content a bit */
268
+ body {
269
+ padding-top: 20px;
270
+ padding-bottom: 20px;
271
+ }
272
+
273
+ /* Custom page header */
274
+ .header {
275
+ padding-bottom: 20px;
276
+ padding-right: 15px;
277
+ padding-left: 15px;
278
+ border-bottom: 1px solid #e5e5e5;
279
+ }
280
+
281
+ /* Make the masthead heading the same height as the navigation */
282
+ .header h3 {
283
+ margin-top: 0;
284
+ margin-bottom: 0;
285
+ line-height: 40px;
286
+ }
287
+
288
+ /* Customize container */
289
+ .container {
290
+ max-width: 990px;
291
+ }
292
+
293
+ address {
294
+ text-align: right;
295
+ }
296
+ </style>
297
+
298
+ <title>stackprof (mode: #{ data[:mode] })</title>
299
+
300
+ <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
301
+ <!--[if lt IE 9]>
302
+ <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
303
+ <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
304
+ <![endif]-->
305
+ </head>
306
+ <body>
307
+ <div class="container">
308
+ <div class="header clearfix">
309
+ <nav>
310
+ <div class="pull-right">
311
+ <form class="form-inline" id="form">
312
+ <a class="btn" href="javascript: resetZoom();">Reset zoom</a>
313
+ <a class="btn" href="javascript: clear();">Clear</a>
314
+ <div class="form-group">
315
+ <input type="text" class="form-control" id="term">
316
+ </div>
317
+ <a class="btn btn-primary" href="javascript: search();">Search</a>
318
+ </form>
319
+ </div>
320
+ </nav>
321
+ <h3 class="text-muted">stackprof (mode: #{ data[:mode] })</h3>
322
+ </div>
323
+ <div id="chart">
324
+ </div>
325
+ <address>
326
+ powered by <a href="https://github.com/spiermar/d3-flame-graph">d3-flame-graph</a>
327
+ </address>
328
+ <hr>
329
+ <div id="details">
330
+ </div>
331
+ </div>
332
+
333
+ <!-- D3.js -->
334
+ <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
335
+
336
+ <!-- d3-tip -->
337
+ <script type="text/javascript" src=https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js></script>
338
+
339
+ <!-- d3-flamegraph -->
340
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.min.js"></script>
341
+
342
+ <script type="text/javascript">
343
+ var flameGraph = d3.flamegraph()
344
+ .width(960)
345
+ .cellHeight(18)
346
+ .transitionDuration(750)
347
+ .minFrameSize(5)
348
+ .transitionEase(d3.easeCubic)
349
+ .sort(true)
350
+ //Example to sort in reverse order
351
+ //.sort(function(a,b){ return d3.descending(a.name, b.name);})
352
+ .title("")
353
+ .onClick(onClick)
354
+ .differential(false)
355
+ .selfValue(false);
356
+
357
+
358
+ // Example on how to use custom tooltips using d3-tip.
359
+ // var tip = d3.tip()
360
+ // .direction("s")
361
+ // .offset([8, 0])
362
+ // .attr('class', 'd3-flame-graph-tip')
363
+ // .html(function(d) { return "name: " + d.data.name + ", value: " + d.data.value; });
364
+
365
+ // flameGraph.tooltip(tip);
366
+
367
+ var details = document.getElementById("details");
368
+ flameGraph.setDetailsElement(details);
369
+
370
+ // Example on how to use custom labels
371
+ // var label = function(d) {
372
+ // return "name: " + d.name + ", value: " + d.value;
373
+ // }
374
+ // flameGraph.label(label);
375
+
376
+ // Example of how to set fixed chart height
377
+ // flameGraph.height(540);
378
+
379
+ d3.select("#chart")
380
+ .datum(#{ json })
381
+ .call(flameGraph);
382
+
383
+ document.getElementById("form").addEventListener("submit", function(event){
384
+ event.preventDefault();
385
+ search();
386
+ });
387
+
388
+ function search() {
389
+ var term = document.getElementById("term").value;
390
+ flameGraph.search(term);
391
+ }
392
+
393
+ function clear() {
394
+ document.getElementById('term').value = '';
395
+ flameGraph.clear();
396
+ }
397
+
398
+ function resetZoom() {
399
+ flameGraph.resetZoom();
400
+ }
401
+
402
+ function onClick(d) {
403
+ console.info("Clicked on " + d.data.name);
404
+ }
405
+ </script>
406
+ </body>
407
+ </html>
408
+ END
409
+ end
410
+
151
411
  def print_graphviz(options = {}, f = STDOUT)
152
412
  if filter = options[:filter]
153
413
  mark_stack = []
@@ -429,7 +689,8 @@ module StackProf
429
689
  end
430
690
  end
431
691
  end
692
+ rescue SystemCallError
693
+ f.puts " SOURCE UNAVAILABLE"
432
694
  end
433
-
434
695
  end
435
696
  end
@@ -1,11 +1,18 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'stackprof'
3
- s.version = '0.2.11'
3
+ s.version = '0.2.16'
4
4
  s.homepage = 'http://github.com/tmm1/stackprof'
5
5
 
6
6
  s.authors = 'Aman Gupta'
7
7
  s.email = 'aman@tmm1.net'
8
8
 
9
+ s.metadata = {
10
+ 'bug_tracker_uri' => 'https://github.com/tmm1/stackprof/issues',
11
+ 'changelog_uri' => "https://github.com/tmm1/stackprof/blob/v#{s.version}/CHANGELOG.md",
12
+ 'documentation_uri' => "https://www.rubydoc.info/gems/stackprof/#{s.version}",
13
+ 'source_code_uri' => "https://github.com/tmm1/stackprof/tree/v#{s.version}"
14
+ }
15
+
9
16
  s.files = `git ls-files`.split("\n")
10
17
  s.extensions = 'ext/stackprof/extconf.rb'
11
18
 
@@ -14,9 +21,11 @@ Gem::Specification.new do |s|
14
21
  s.executables << 'stackprof-flamegraph.pl'
15
22
  s.executables << 'stackprof-gprof2dot.py'
16
23
 
17
- s.summary = 'sampling callstack-profiler for ruby 2.1+'
24
+ s.summary = 'sampling callstack-profiler for ruby 2.2+'
18
25
  s.description = 'stackprof is a fast sampling profiler for ruby code, with cpu, wallclock and object allocation samplers.'
19
26
 
27
+ s.required_ruby_version = '>= 2.2'
28
+
20
29
  s.license = 'MIT'
21
30
 
22
31
  s.add_development_dependency 'rake-compiler', '~> 0.9'
@@ -9,31 +9,31 @@ class StackProf::MiddlewareTest < MiniTest::Test
9
9
  def test_path_default
10
10
  StackProf::Middleware.new(Object.new)
11
11
 
12
- assert_equal 'tmp', StackProf::Middleware.path
12
+ assert_equal 'tmp/', StackProf::Middleware.path
13
13
  end
14
14
 
15
15
  def test_path_custom
16
- StackProf::Middleware.new(Object.new, { path: '/foo' })
16
+ StackProf::Middleware.new(Object.new, { path: 'foo/' })
17
17
 
18
- assert_equal '/foo', StackProf::Middleware.path
18
+ assert_equal 'foo/', StackProf::Middleware.path
19
19
  end
20
20
 
21
21
  def test_save_default
22
22
  StackProf::Middleware.new(Object.new)
23
23
 
24
24
  StackProf.stubs(:results).returns({ mode: 'foo' })
25
- FileUtils.expects(:mkdir_p).with('tmp')
25
+ FileUtils.expects(:mkdir_p).with('tmp/')
26
26
  File.expects(:open).with(regexp_matches(/^tmp\/stackprof-foo/), 'wb')
27
27
 
28
28
  StackProf::Middleware.save
29
29
  end
30
30
 
31
31
  def test_save_custom
32
- StackProf::Middleware.new(Object.new, { path: '/foo' })
32
+ StackProf::Middleware.new(Object.new, { path: 'foo/' })
33
33
 
34
34
  StackProf.stubs(:results).returns({ mode: 'foo' })
35
- FileUtils.expects(:mkdir_p).with('/foo')
36
- File.expects(:open).with(regexp_matches(/^\/foo\/stackprof-foo/), 'wb')
35
+ FileUtils.expects(:mkdir_p).with('foo/')
36
+ File.expects(:open).with(regexp_matches(/^foo\/stackprof-foo/), 'wb')
37
37
 
38
38
  StackProf::Middleware.save
39
39
  end
@@ -64,4 +64,10 @@ class StackProf::MiddlewareTest < MiniTest::Test
64
64
  StackProf::Middleware.new(Object.new, raw: true)
65
65
  assert StackProf::Middleware.raw
66
66
  end
67
+
68
+ def test_metadata
69
+ metadata = { key: 'value' }
70
+ StackProf::Middleware.new(Object.new, metadata: metadata)
71
+ assert_equal metadata, StackProf::Middleware.metadata
72
+ end
67
73
  end