stackprof 0.2.12 → 0.2.17
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/.github/workflows/ci.yml +43 -0
- data/.gitignore +1 -1
- data/CHANGELOG.md +13 -2
- data/README.md +66 -51
- data/Rakefile +11 -25
- data/bin/stackprof +14 -4
- data/ext/stackprof/extconf.rb +9 -0
- data/ext/stackprof/stackprof.c +788 -0
- data/lib/stackprof.rb +4 -0
- data/lib/stackprof/middleware.rb +8 -2
- data/lib/stackprof/report.rb +270 -9
- data/stackprof.gemspec +11 -2
- data/test/test_middleware.rb +6 -0
- data/test/test_stackprof.rb +112 -11
- data/vendor/FlameGraph/flamegraph.pl +751 -85
- metadata +14 -12
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -27
data/lib/stackprof.rb
CHANGED
data/lib/stackprof/middleware.rb
CHANGED
@@ -13,12 +13,18 @@ module StackProf
|
|
13
13
|
Middleware.enabled = options[:enabled]
|
14
14
|
options[:path] = 'tmp/' if options[:path].to_s.empty?
|
15
15
|
Middleware.path = options[:path]
|
16
|
+
Middleware.metadata = options[:metadata] || {}
|
16
17
|
at_exit{ Middleware.save } if options[:save_at_exit]
|
17
18
|
end
|
18
19
|
|
19
20
|
def call(env)
|
20
21
|
enabled = Middleware.enabled?(env)
|
21
|
-
StackProf.start(
|
22
|
+
StackProf.start(
|
23
|
+
mode: Middleware.mode,
|
24
|
+
interval: Middleware.interval,
|
25
|
+
raw: Middleware.raw,
|
26
|
+
metadata: Middleware.metadata,
|
27
|
+
) if enabled
|
22
28
|
@app.call(env)
|
23
29
|
ensure
|
24
30
|
if enabled
|
@@ -31,7 +37,7 @@ module StackProf
|
|
31
37
|
end
|
32
38
|
|
33
39
|
class << self
|
34
|
-
attr_accessor :enabled, :mode, :interval, :raw, :path
|
40
|
+
attr_accessor :enabled, :mode, :interval, :raw, :path, :metadata
|
35
41
|
|
36
42
|
def enabled?(env)
|
37
43
|
if enabled.respond_to?(:call)
|
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
|
@@ -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 = []
|
@@ -185,7 +445,7 @@ module StackProf
|
|
185
445
|
call, total = info.values_at(:samples, :total_samples)
|
186
446
|
break if total < node_minimum || (limit && index >= limit)
|
187
447
|
|
188
|
-
sample = ''
|
448
|
+
sample = ''.dup
|
189
449
|
sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total
|
190
450
|
sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples)
|
191
451
|
fontsize = (1.0 * call / max_samples) * 28 + 10
|
@@ -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.17'
|
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
@@ -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
|
data/test/test_stackprof.rb
CHANGED
@@ -2,6 +2,7 @@ $:.unshift File.expand_path('../../lib', __FILE__)
|
|
2
2
|
require 'stackprof'
|
3
3
|
require 'minitest/autorun'
|
4
4
|
require 'tempfile'
|
5
|
+
require 'pathname'
|
5
6
|
|
6
7
|
class StackProfTest < MiniTest::Test
|
7
8
|
def test_info
|
@@ -38,16 +39,30 @@ class StackProfTest < MiniTest::Test
|
|
38
39
|
end
|
39
40
|
assert_equal :object, profile[:mode]
|
40
41
|
assert_equal 1, profile[:interval]
|
41
|
-
|
42
|
+
if RUBY_VERSION >= '3'
|
43
|
+
assert_equal 4, profile[:samples]
|
44
|
+
else
|
45
|
+
assert_equal 2, profile[:samples]
|
46
|
+
end
|
42
47
|
|
43
48
|
frame = profile[:frames].values.first
|
44
49
|
assert_includes frame[:name], "StackProfTest#test_object_allocation"
|
45
50
|
assert_equal 2, frame[:samples]
|
46
51
|
assert_includes [profile_base_line - 2, profile_base_line], frame[:line]
|
47
|
-
|
48
|
-
|
52
|
+
if RUBY_VERSION >= '3'
|
53
|
+
assert_equal [2, 1], frame[:lines][profile_base_line+1]
|
54
|
+
assert_equal [2, 1], frame[:lines][profile_base_line+2]
|
55
|
+
else
|
56
|
+
assert_equal [1, 1], frame[:lines][profile_base_line+1]
|
57
|
+
assert_equal [1, 1], frame[:lines][profile_base_line+2]
|
58
|
+
end
|
49
59
|
frame = profile[:frames].values[1] if RUBY_VERSION < '2.3'
|
50
|
-
|
60
|
+
|
61
|
+
if RUBY_VERSION >= '3'
|
62
|
+
assert_equal [4, 0], frame[:lines][profile_base_line]
|
63
|
+
else
|
64
|
+
assert_equal [2, 0], frame[:lines][profile_base_line]
|
65
|
+
end
|
51
66
|
end
|
52
67
|
|
53
68
|
def test_object_allocation_interval
|
@@ -63,7 +78,8 @@ class StackProfTest < MiniTest::Test
|
|
63
78
|
end
|
64
79
|
|
65
80
|
assert_operator profile[:samples], :>=, 1
|
66
|
-
|
81
|
+
offset = RUBY_VERSION >= '3' ? 1 : 0
|
82
|
+
frame = profile[:frames].values[offset]
|
67
83
|
assert_includes frame[:name], "StackProfTest#math"
|
68
84
|
end
|
69
85
|
|
@@ -73,7 +89,11 @@ class StackProfTest < MiniTest::Test
|
|
73
89
|
end
|
74
90
|
|
75
91
|
frame = profile[:frames].values.first
|
76
|
-
|
92
|
+
if RUBY_VERSION >= '3'
|
93
|
+
assert_equal "IO.select", frame[:name]
|
94
|
+
else
|
95
|
+
assert_equal "StackProfTest#idle", frame[:name]
|
96
|
+
end
|
77
97
|
assert_in_delta 200, frame[:samples], 25
|
78
98
|
end
|
79
99
|
|
@@ -88,10 +108,16 @@ class StackProfTest < MiniTest::Test
|
|
88
108
|
assert_equal :custom, profile[:mode]
|
89
109
|
assert_equal 10, profile[:samples]
|
90
110
|
|
91
|
-
|
111
|
+
offset = RUBY_VERSION >= '3' ? 1 : 0
|
112
|
+
frame = profile[:frames].values[offset]
|
92
113
|
assert_includes frame[:name], "StackProfTest#test_custom"
|
93
114
|
assert_includes [profile_base_line-2, profile_base_line+1], frame[:line]
|
94
|
-
|
115
|
+
|
116
|
+
if RUBY_VERSION >= '3'
|
117
|
+
assert_equal [10, 0], frame[:lines][profile_base_line+2]
|
118
|
+
else
|
119
|
+
assert_equal [10, 10], frame[:lines][profile_base_line+2]
|
120
|
+
end
|
95
121
|
end
|
96
122
|
|
97
123
|
def test_raw
|
@@ -104,10 +130,42 @@ class StackProfTest < MiniTest::Test
|
|
104
130
|
raw = profile[:raw]
|
105
131
|
assert_equal 10, raw[-1]
|
106
132
|
assert_equal raw[0] + 2, raw.size
|
107
|
-
|
133
|
+
|
134
|
+
offset = RUBY_VERSION >= '3' ? -3 : -2
|
135
|
+
assert_includes profile[:frames][raw[offset]][:name], 'StackProfTest#test_raw'
|
108
136
|
assert_equal 10, profile[:raw_timestamp_deltas].size
|
109
137
|
end
|
110
138
|
|
139
|
+
def test_metadata
|
140
|
+
metadata = {
|
141
|
+
path: '/foo/bar',
|
142
|
+
revision: '5c0b01f1522ae8c194510977ae29377296dd236b',
|
143
|
+
}
|
144
|
+
profile = StackProf.run(mode: :cpu, metadata: metadata) do
|
145
|
+
math
|
146
|
+
end
|
147
|
+
|
148
|
+
assert_equal metadata, profile[:metadata]
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_empty_metadata
|
152
|
+
profile = StackProf.run(mode: :cpu) do
|
153
|
+
math
|
154
|
+
end
|
155
|
+
|
156
|
+
assert_equal({}, profile[:metadata])
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_raises_if_metadata_is_not_a_hash
|
160
|
+
exception = assert_raises ArgumentError do
|
161
|
+
StackProf.run(mode: :cpu, metadata: 'foobar') do
|
162
|
+
math
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
assert_equal 'metadata should be a hash', exception.message
|
167
|
+
end
|
168
|
+
|
111
169
|
def test_fork
|
112
170
|
StackProf.run do
|
113
171
|
pid = fork do
|
@@ -149,10 +207,18 @@ class StackProfTest < MiniTest::Test
|
|
149
207
|
|
150
208
|
raw = profile[:raw]
|
151
209
|
gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" }
|
210
|
+
marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" }
|
211
|
+
sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" }
|
212
|
+
|
152
213
|
assert gc_frame
|
153
|
-
|
214
|
+
assert marking_frame
|
215
|
+
assert sweeping_frame
|
216
|
+
|
217
|
+
assert_equal gc_frame[:total_samples], profile[:gc_samples]
|
218
|
+
assert_equal profile[:gc_samples], [gc_frame, marking_frame, sweeping_frame].map{|x| x[:samples] }.inject(:+)
|
219
|
+
|
154
220
|
assert_operator profile[:gc_samples], :>, 0
|
155
|
-
assert_operator profile[:missed_samples], :<=,
|
221
|
+
assert_operator profile[:missed_samples], :<=, 25
|
156
222
|
end
|
157
223
|
|
158
224
|
def test_out
|
@@ -167,6 +233,41 @@ class StackProfTest < MiniTest::Test
|
|
167
233
|
refute_empty profile[:frames]
|
168
234
|
end
|
169
235
|
|
236
|
+
def test_out_to_path_string
|
237
|
+
tmpfile = Tempfile.new('stackprof-out')
|
238
|
+
ret = StackProf.run(mode: :custom, out: tmpfile.path) do
|
239
|
+
StackProf.sample
|
240
|
+
end
|
241
|
+
|
242
|
+
refute_equal tmpfile, ret
|
243
|
+
assert_equal tmpfile.path, ret.path
|
244
|
+
tmpfile.rewind
|
245
|
+
profile = Marshal.load(tmpfile.read)
|
246
|
+
refute_empty profile[:frames]
|
247
|
+
end
|
248
|
+
|
249
|
+
def test_pathname_out
|
250
|
+
tmpfile = Tempfile.new('stackprof-out')
|
251
|
+
pathname = Pathname.new(tmpfile.path)
|
252
|
+
ret = StackProf.run(mode: :custom, out: pathname) do
|
253
|
+
StackProf.sample
|
254
|
+
end
|
255
|
+
|
256
|
+
assert_equal tmpfile.path, ret.path
|
257
|
+
tmpfile.rewind
|
258
|
+
profile = Marshal.load(tmpfile.read)
|
259
|
+
refute_empty profile[:frames]
|
260
|
+
end
|
261
|
+
|
262
|
+
def test_min_max_interval
|
263
|
+
[-1, 0, 1_000_000, 1_000_001].each do |invalid_interval|
|
264
|
+
err = assert_raises(ArgumentError, "invalid interval #{invalid_interval}") do
|
265
|
+
StackProf.run(interval: invalid_interval, debug: true) {}
|
266
|
+
end
|
267
|
+
assert_match(/microseconds/, err.message)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
170
271
|
def math
|
171
272
|
250_000.times do
|
172
273
|
2 ** 10
|