stackprof 0.2.10 → 0.2.25
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 +2 -0
- data/CHANGELOG.md +18 -0
- data/README.md +87 -67
- data/Rakefile +21 -25
- data/bin/stackprof +115 -70
- data/ext/stackprof/extconf.rb +6 -0
- data/ext/stackprof/stackprof.c +434 -37
- data/lib/stackprof/autorun.rb +19 -0
- data/lib/stackprof/flamegraph/flamegraph.js +926 -300
- data/lib/stackprof/flamegraph/viewer.html +29 -23
- data/lib/stackprof/middleware.rb +23 -7
- data/lib/stackprof/report.rb +323 -18
- data/lib/stackprof/truffleruby.rb +37 -0
- data/lib/stackprof.rb +18 -1
- data/sample.rb +3 -3
- data/stackprof.gemspec +11 -2
- data/test/fixtures/profile.dump +1 -0
- data/test/fixtures/profile.json +1 -0
- data/test/test_middleware.rb +13 -7
- data/test/test_report.rb +24 -0
- data/test/test_stackprof.rb +177 -25
- data/test/test_truffleruby.rb +18 -0
- data/vendor/FlameGraph/flamegraph.pl +751 -85
- metadata +17 -10
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -24
@@ -5,9 +5,32 @@
|
|
5
5
|
body {
|
6
6
|
margin: 0;
|
7
7
|
padding: 0;
|
8
|
-
font-family:
|
8
|
+
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
9
9
|
font-size: 10pt;
|
10
10
|
}
|
11
|
+
.overview-container {
|
12
|
+
position: relative;
|
13
|
+
}
|
14
|
+
.overview {
|
15
|
+
cursor: col-resize;
|
16
|
+
}
|
17
|
+
.overview-viewport-overlay {
|
18
|
+
position: absolute;
|
19
|
+
top: 0;
|
20
|
+
left: 0;
|
21
|
+
width: 1;
|
22
|
+
height: 1;
|
23
|
+
background-color: rgba(0, 0, 0, 0.25);
|
24
|
+
transform-origin: top left;
|
25
|
+
cursor: -moz-grab;
|
26
|
+
cursor: -webkit-grab;
|
27
|
+
cursor: grab;
|
28
|
+
}
|
29
|
+
.moving {
|
30
|
+
cursor: -moz-grabbing;
|
31
|
+
cursor: -webkit-grabbing;
|
32
|
+
cursor: grabbing;
|
33
|
+
}
|
11
34
|
.info {
|
12
35
|
display: block;
|
13
36
|
height: 40px;
|
@@ -36,32 +59,15 @@
|
|
36
59
|
max-width: 70%;
|
37
60
|
word-wrap: break-word;
|
38
61
|
}
|
39
|
-
.legend:hover + .flamegraph .flames:not(.highlighted) {
|
40
|
-
opacity: 0.25;
|
41
|
-
}
|
42
|
-
.legend:hover ~ .zoom .flames:not(.highlighted) {
|
43
|
-
opacity: 0.25;
|
44
|
-
}
|
45
|
-
.brush .extent {
|
46
|
-
stroke: #999;
|
47
|
-
fill-opacity: .125;
|
48
|
-
shape-rendering: crispEdges;
|
49
|
-
}
|
50
|
-
.label {
|
51
|
-
white-space: nowrap;
|
52
|
-
display: inline-flex;
|
53
|
-
align-items: center;
|
54
|
-
vertical-align: middle;
|
55
|
-
padding-left: 1px;
|
56
|
-
}
|
57
62
|
</style>
|
58
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
59
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.8/d3.min.js"></script>
|
60
63
|
<script src="flamegraph.js"></script>
|
61
64
|
</head>
|
62
65
|
<body>
|
63
66
|
<div class="legend"></div>
|
64
|
-
<div class="
|
67
|
+
<div class="overview-container">
|
68
|
+
<canvas class="overview"></canvas>
|
69
|
+
<div class="overview-viewport-overlay"></div>
|
70
|
+
</div>
|
65
71
|
<div class="info">
|
66
72
|
<div style="float: right; text-align: right">
|
67
73
|
<div class="samples"></div>
|
@@ -70,7 +76,7 @@
|
|
70
76
|
<div class="frame"></div>
|
71
77
|
<div class="file"></div>
|
72
78
|
</div>
|
73
|
-
<
|
79
|
+
<canvas class="flamegraph"></canvas>
|
74
80
|
<script type="text/javascript">
|
75
81
|
var queryDict = {}
|
76
82
|
location.search.substr(1).split("&").forEach(function(item) {queryDict[item.split("=")[0]] = decodeURIComponent(item.split("=")[1])})
|
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,8 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pp'
|
2
|
-
require 'digest/
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'json'
|
3
6
|
|
4
7
|
module StackProf
|
5
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
|
+
|
6
42
|
def initialize(data)
|
7
43
|
@data = data
|
8
44
|
end
|
@@ -16,7 +52,7 @@ module StackProf
|
|
16
52
|
def normalized_frames
|
17
53
|
id2hash = {}
|
18
54
|
@data[:frames].each do |frame, info|
|
19
|
-
id2hash[frame.to_s] = info[:hash] = Digest::
|
55
|
+
id2hash[frame.to_s] = info[:hash] = Digest::SHA256.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}")
|
20
56
|
end
|
21
57
|
@data[:frames].inject(Hash.new) do |hash, (frame, info)|
|
22
58
|
info = hash[id2hash[frame.to_s]] = info.dup
|
@@ -38,7 +74,7 @@ module StackProf
|
|
38
74
|
end
|
39
75
|
|
40
76
|
def max_samples
|
41
|
-
@data[:max_samples] ||= frames.max_by{ |
|
77
|
+
@data[:max_samples] ||= @data[:frames].values.max_by{ |frame| frame[:samples] }[:samples]
|
42
78
|
end
|
43
79
|
|
44
80
|
def files
|
@@ -55,8 +91,8 @@ module StackProf
|
|
55
91
|
|
56
92
|
def add_lines(a, b)
|
57
93
|
return b if a.nil?
|
58
|
-
return a+b if a.is_a?
|
59
|
-
return [ a[0], a[1]+b ] if b.is_a?
|
94
|
+
return a+b if a.is_a? Integer
|
95
|
+
return [ a[0], a[1]+b ] if b.is_a? Integer
|
60
96
|
[ a[0]+b[0], a[1]+b[1] ]
|
61
97
|
end
|
62
98
|
|
@@ -68,6 +104,11 @@ module StackProf
|
|
68
104
|
f.puts Marshal.dump(@data.reject{|k,v| k == :files })
|
69
105
|
end
|
70
106
|
|
107
|
+
def print_json(f=STDOUT)
|
108
|
+
require "json"
|
109
|
+
f.puts JSON.generate(@data, max_nesting: false)
|
110
|
+
end
|
111
|
+
|
71
112
|
def print_stackcollapse
|
72
113
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
73
114
|
|
@@ -80,18 +121,20 @@ module StackProf
|
|
80
121
|
end
|
81
122
|
end
|
82
123
|
|
83
|
-
def
|
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)
|
84
133
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
85
134
|
|
86
|
-
stacks =
|
87
|
-
|
88
|
-
|
89
|
-
while len = raw.shift
|
90
|
-
max_y = len if len > max_y
|
91
|
-
stack = raw.slice!(0, len+1)
|
92
|
-
stacks << stack
|
93
|
-
max_x += stack.last
|
94
|
-
end
|
135
|
+
stacks, max_x, max_y = flamegraph_stacks(raw)
|
136
|
+
|
137
|
+
stacks.sort! if alphabetical
|
95
138
|
|
96
139
|
f.puts 'flamegraph(['
|
97
140
|
max_y.times do |y|
|
@@ -141,13 +184,233 @@ module StackProf
|
|
141
184
|
f.puts '])'
|
142
185
|
end
|
143
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
|
+
|
144
205
|
def flamegraph_row(f, x, y, weight, addr)
|
145
|
-
frame = frames[addr]
|
206
|
+
frame = @data[:frames][addr]
|
146
207
|
f.print ',' if @rows_started
|
147
208
|
@rows_started = true
|
148
209
|
f.puts %{{"x":#{x},"y":#{y},"width":#{weight},"frame_id":#{addr},"frame":#{frame[:name].dump},"file":#{frame[:file].dump}}}
|
149
210
|
end
|
150
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("<root>", 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
|
+
<!DOCTYPE html>
|
259
|
+
<html lang="en">
|
260
|
+
<head>
|
261
|
+
<meta charset="utf-8">
|
262
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
263
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
264
|
+
|
265
|
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
266
|
+
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.css">
|
267
|
+
|
268
|
+
<style>
|
269
|
+
|
270
|
+
/* Space out content a bit */
|
271
|
+
body {
|
272
|
+
padding-top: 20px;
|
273
|
+
padding-bottom: 20px;
|
274
|
+
}
|
275
|
+
|
276
|
+
/* Custom page header */
|
277
|
+
.header {
|
278
|
+
padding-bottom: 20px;
|
279
|
+
padding-right: 15px;
|
280
|
+
padding-left: 15px;
|
281
|
+
border-bottom: 1px solid #e5e5e5;
|
282
|
+
}
|
283
|
+
|
284
|
+
/* Make the masthead heading the same height as the navigation */
|
285
|
+
.header h3 {
|
286
|
+
margin-top: 0;
|
287
|
+
margin-bottom: 0;
|
288
|
+
line-height: 40px;
|
289
|
+
}
|
290
|
+
|
291
|
+
/* Customize container */
|
292
|
+
.container {
|
293
|
+
max-width: 990px;
|
294
|
+
}
|
295
|
+
|
296
|
+
address {
|
297
|
+
text-align: right;
|
298
|
+
}
|
299
|
+
</style>
|
300
|
+
|
301
|
+
<title>stackprof (mode: #{ data[:mode] })</title>
|
302
|
+
|
303
|
+
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
304
|
+
<!--[if lt IE 9]>
|
305
|
+
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
306
|
+
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
307
|
+
<![endif]-->
|
308
|
+
</head>
|
309
|
+
<body>
|
310
|
+
<div class="container">
|
311
|
+
<div class="header clearfix">
|
312
|
+
<nav>
|
313
|
+
<div class="pull-right">
|
314
|
+
<form class="form-inline" id="form">
|
315
|
+
<a class="btn" href="javascript: resetZoom();">Reset zoom</a>
|
316
|
+
<a class="btn" href="javascript: clear();">Clear</a>
|
317
|
+
<div class="form-group">
|
318
|
+
<input type="text" class="form-control" id="term">
|
319
|
+
</div>
|
320
|
+
<a class="btn btn-primary" href="javascript: search();">Search</a>
|
321
|
+
</form>
|
322
|
+
</div>
|
323
|
+
</nav>
|
324
|
+
<h3 class="text-muted">stackprof (mode: #{ data[:mode] })</h3>
|
325
|
+
</div>
|
326
|
+
<div id="chart">
|
327
|
+
</div>
|
328
|
+
<address>
|
329
|
+
powered by <a href="https://github.com/spiermar/d3-flame-graph">d3-flame-graph</a>
|
330
|
+
</address>
|
331
|
+
<hr>
|
332
|
+
<div id="details">
|
333
|
+
</div>
|
334
|
+
</div>
|
335
|
+
|
336
|
+
<!-- D3.js -->
|
337
|
+
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
|
338
|
+
|
339
|
+
<!-- d3-tip -->
|
340
|
+
<script type="text/javascript" src=https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js></script>
|
341
|
+
|
342
|
+
<!-- d3-flamegraph -->
|
343
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.min.js"></script>
|
344
|
+
|
345
|
+
<script type="text/javascript">
|
346
|
+
var flameGraph = d3.flamegraph()
|
347
|
+
.width(960)
|
348
|
+
.cellHeight(18)
|
349
|
+
.transitionDuration(750)
|
350
|
+
.minFrameSize(5)
|
351
|
+
.transitionEase(d3.easeCubic)
|
352
|
+
.sort(true)
|
353
|
+
//Example to sort in reverse order
|
354
|
+
//.sort(function(a,b){ return d3.descending(a.name, b.name);})
|
355
|
+
.title("")
|
356
|
+
.onClick(onClick)
|
357
|
+
.differential(false)
|
358
|
+
.selfValue(false);
|
359
|
+
|
360
|
+
|
361
|
+
// Example on how to use custom tooltips using d3-tip.
|
362
|
+
// var tip = d3.tip()
|
363
|
+
// .direction("s")
|
364
|
+
// .offset([8, 0])
|
365
|
+
// .attr('class', 'd3-flame-graph-tip')
|
366
|
+
// .html(function(d) { return "name: " + d.data.name + ", value: " + d.data.value; });
|
367
|
+
|
368
|
+
// flameGraph.tooltip(tip);
|
369
|
+
|
370
|
+
var details = document.getElementById("details");
|
371
|
+
flameGraph.setDetailsElement(details);
|
372
|
+
|
373
|
+
// Example on how to use custom labels
|
374
|
+
// var label = function(d) {
|
375
|
+
// return "name: " + d.name + ", value: " + d.value;
|
376
|
+
// }
|
377
|
+
// flameGraph.label(label);
|
378
|
+
|
379
|
+
// Example of how to set fixed chart height
|
380
|
+
// flameGraph.height(540);
|
381
|
+
|
382
|
+
d3.select("#chart")
|
383
|
+
.datum(#{ json })
|
384
|
+
.call(flameGraph);
|
385
|
+
|
386
|
+
document.getElementById("form").addEventListener("submit", function(event){
|
387
|
+
event.preventDefault();
|
388
|
+
search();
|
389
|
+
});
|
390
|
+
|
391
|
+
function search() {
|
392
|
+
var term = document.getElementById("term").value;
|
393
|
+
flameGraph.search(term);
|
394
|
+
}
|
395
|
+
|
396
|
+
function clear() {
|
397
|
+
document.getElementById('term').value = '';
|
398
|
+
flameGraph.clear();
|
399
|
+
}
|
400
|
+
|
401
|
+
function resetZoom() {
|
402
|
+
flameGraph.resetZoom();
|
403
|
+
}
|
404
|
+
|
405
|
+
function onClick(d) {
|
406
|
+
console.info("Clicked on " + d.data.name);
|
407
|
+
}
|
408
|
+
</script>
|
409
|
+
</body>
|
410
|
+
</html>
|
411
|
+
END
|
412
|
+
end
|
413
|
+
|
151
414
|
def print_graphviz(options = {}, f = STDOUT)
|
152
415
|
if filter = options[:filter]
|
153
416
|
mark_stack = []
|
@@ -185,7 +448,7 @@ module StackProf
|
|
185
448
|
call, total = info.values_at(:samples, :total_samples)
|
186
449
|
break if total < node_minimum || (limit && index >= limit)
|
187
450
|
|
188
|
-
sample = ''
|
451
|
+
sample = ''.dup
|
189
452
|
sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total
|
190
453
|
sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples)
|
191
454
|
fontsize = (1.0 * call / max_samples) * 28 + 10
|
@@ -293,6 +556,47 @@ module StackProf
|
|
293
556
|
end
|
294
557
|
end
|
295
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
|
+
|
296
600
|
def print_files(sort_by_total=false, limit=nil, f = STDOUT)
|
297
601
|
list = files.map{ |file, vals| [file, vals.values.inject([0,0]){ |sum, n| add_lines(sum, n) }] }
|
298
602
|
list = list.sort_by{ |file, samples| -samples[1] }
|
@@ -388,7 +692,8 @@ module StackProf
|
|
388
692
|
end
|
389
693
|
end
|
390
694
|
end
|
695
|
+
rescue SystemCallError
|
696
|
+
f.puts " SOURCE UNAVAILABLE"
|
391
697
|
end
|
392
|
-
|
393
698
|
end
|
394
699
|
end
|
@@ -0,0 +1,37 @@
|
|
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
|
data/lib/stackprof.rb
CHANGED
@@ -1,4 +1,21 @@
|
|
1
|
-
|
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
|
+
StackProf.use_postponed_job!
|
9
|
+
elsif RUBY_VERSION == "3.2.0"
|
10
|
+
# 3.2.0 crash is the signal is received at the wrong time.
|
11
|
+
# Fixed in https://github.com/ruby/ruby/pull/7116
|
12
|
+
# The fix is backported in 3.2.1: https://bugs.ruby-lang.org/issues/19336
|
13
|
+
StackProf.use_postponed_job!
|
14
|
+
end
|
15
|
+
|
16
|
+
module StackProf
|
17
|
+
VERSION = '0.2.25'
|
18
|
+
end
|
2
19
|
|
3
20
|
StackProf.autoload :Report, "stackprof/report.rb"
|
4
21
|
StackProf.autoload :Middleware, "stackprof/middleware.rb"
|
data/sample.rb
CHANGED
@@ -24,9 +24,9 @@ class A
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
#profile = StackProf.run(:object, 1) do
|
28
|
-
#profile = StackProf.run(:wall, 1000) do
|
29
|
-
profile = StackProf.run(:cpu, 1000) do
|
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
30
|
1_000_000.times do
|
31
31
|
A.new
|
32
32
|
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.25'
|
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'
|
@@ -0,0 +1 @@
|
|
1
|
+
{: modeI"cpu:ET
|
@@ -0,0 +1 @@
|
|
1
|
+
{ "mode": "cpu" }
|