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.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +21 -8
- data/CHANGELOG.md +14 -0
- data/Dockerfile +21 -0
- data/README.md +39 -19
- data/bin/stackprof +14 -4
- data/ext/stackprof/stackprof.c +131 -34
- data/lib/stackprof.rb +4 -0
- data/lib/stackprof/middleware.rb +23 -7
- data/lib/stackprof/report.rb +271 -10
- data/stackprof.gemspec +11 -2
- data/test/test_middleware.rb +13 -7
- data/test/test_stackprof.rb +97 -2
- data/vendor/FlameGraph/flamegraph.pl +751 -85
- metadata +15 -11
- data/Gemfile.lock +0 -27
data/lib/stackprof.rb
CHANGED
data/lib/stackprof/middleware.rb
CHANGED
@@ -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
|
-
|
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(
|
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
|
50
|
+
def save
|
44
51
|
if results = StackProf.results
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
data/lib/stackprof/report.rb
CHANGED
@@ -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{ |
|
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?
|
59
|
-
return [ a[0], a[1]+b ] if b.is_a?
|
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
|
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
|
-
|
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
|
-
|
136
|
+
|
137
|
+
stack = StackCursor.new(raw, idx, len)
|
92
138
|
stacks << stack
|
93
|
-
max_x += stack.
|
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.
|
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
|
data/stackprof.gemspec
CHANGED
@@ -1,11 +1,18 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'stackprof'
|
3
|
-
s.version = '0.2.
|
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.
|
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'
|
data/test/test_middleware.rb
CHANGED
@@ -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: '/
|
16
|
+
StackProf::Middleware.new(Object.new, { path: 'foo/' })
|
17
17
|
|
18
|
-
assert_equal '/
|
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: '/
|
32
|
+
StackProf::Middleware.new(Object.new, { path: 'foo/' })
|
33
33
|
|
34
34
|
StackProf.stubs(:results).returns({ mode: 'foo' })
|
35
|
-
FileUtils.expects(:mkdir_p).with('/
|
36
|
-
File.expects(:open).with(regexp_matches(
|
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
|