stackprof 0.2.15 → 0.2.21
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +43 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +12 -1
- data/README.md +57 -51
- data/Rakefile +21 -25
- data/bin/stackprof +1 -1
- data/ext/stackprof/extconf.rb +6 -0
- data/ext/stackprof/stackprof.c +210 -71
- data/lib/stackprof/report.rb +65 -26
- data/lib/stackprof/truffleruby.rb +37 -0
- data/lib/stackprof.rb +10 -2
- data/stackprof.gemspec +8 -1
- data/test/fixtures/profile.dump +1 -0
- data/test/fixtures/profile.json +1 -0
- data/test/test_report.rb +24 -0
- data/test/test_stackprof.rb +75 -12
- data/test/test_truffleruby.rb +18 -0
- data/vendor/FlameGraph/flamegraph.pl +751 -85
- metadata +16 -10
- data/.travis.yml +0 -21
- data/Dockerfile +0 -21
- data/Gemfile.lock +0 -27
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
|
@@ -96,15 +132,7 @@ module StackProf
|
|
96
132
|
def print_flamegraph(f, skip_common, alphabetical=false)
|
97
133
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
98
134
|
|
99
|
-
stacks =
|
100
|
-
max_x = 0
|
101
|
-
max_y = 0
|
102
|
-
while len = raw.shift
|
103
|
-
max_y = len if len > max_y
|
104
|
-
stack = raw.slice!(0, len+1)
|
105
|
-
stacks << stack
|
106
|
-
max_x += stack.last
|
107
|
-
end
|
135
|
+
stacks, max_x, max_y = flamegraph_stacks(raw)
|
108
136
|
|
109
137
|
stacks.sort! if alphabetical
|
110
138
|
|
@@ -156,8 +184,26 @@ module StackProf
|
|
156
184
|
f.puts '])'
|
157
185
|
end
|
158
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
|
+
|
159
205
|
def flamegraph_row(f, x, y, weight, addr)
|
160
|
-
frame = frames[addr]
|
206
|
+
frame = @data[:frames][addr]
|
161
207
|
f.print ',' if @rows_started
|
162
208
|
@rows_started = true
|
163
209
|
f.puts %{{"x":#{x},"y":#{y},"width":#{weight},"frame_id":#{addr},"frame":#{frame[:name].dump},"file":#{frame[:file].dump}}}
|
@@ -178,8 +224,8 @@ module StackProf
|
|
178
224
|
weight += stack.last
|
179
225
|
end
|
180
226
|
else
|
181
|
-
frame = frames[val]
|
182
|
-
child_name = "#{ frame[:name] } : #{ frame[:file] }"
|
227
|
+
frame = @data[:frames][val]
|
228
|
+
child_name = "#{ frame[:name] } : #{ frame[:file] } : #{ frame[:line] }"
|
183
229
|
child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
|
184
230
|
weight += child_data["value"]
|
185
231
|
children << child_data
|
@@ -196,15 +242,7 @@ module StackProf
|
|
196
242
|
def print_d3_flamegraph(f=STDOUT, skip_common=true)
|
197
243
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
198
244
|
|
199
|
-
stacks =
|
200
|
-
max_x = 0
|
201
|
-
max_y = 0
|
202
|
-
while len = raw.shift
|
203
|
-
max_y = len if len > max_y
|
204
|
-
stack = raw.slice!(0, len+1)
|
205
|
-
stacks << stack
|
206
|
-
max_x += stack.last
|
207
|
-
end
|
245
|
+
stacks, * = flamegraph_stacks(raw)
|
208
246
|
|
209
247
|
# d3-flame-grpah supports only alphabetical flamegraph
|
210
248
|
stacks.sort!
|
@@ -410,7 +448,7 @@ module StackProf
|
|
410
448
|
call, total = info.values_at(:samples, :total_samples)
|
411
449
|
break if total < node_minimum || (limit && index >= limit)
|
412
450
|
|
413
|
-
sample = ''
|
451
|
+
sample = ''.dup
|
414
452
|
sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total
|
415
453
|
sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples)
|
416
454
|
fontsize = (1.0 * call / max_samples) * 28 + 10
|
@@ -654,7 +692,8 @@ module StackProf
|
|
654
692
|
end
|
655
693
|
end
|
656
694
|
end
|
695
|
+
rescue SystemCallError
|
696
|
+
f.puts " SOURCE UNAVAILABLE"
|
657
697
|
end
|
658
|
-
|
659
698
|
end
|
660
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,7 +1,15 @@
|
|
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
|
+
end
|
2
10
|
|
3
11
|
module StackProf
|
4
|
-
VERSION = '0.2.
|
12
|
+
VERSION = '0.2.21'
|
5
13
|
end
|
6
14
|
|
7
15
|
StackProf.autoload :Report, "stackprof/report.rb"
|
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.21'
|
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
|
|
@@ -0,0 +1 @@
|
|
1
|
+
{: modeI"cpu:ET
|
@@ -0,0 +1 @@
|
|
1
|
+
{ "mode": "cpu" }
|
data/test/test_report.rb
CHANGED
@@ -32,3 +32,27 @@ class ReportDumpTest < MiniTest::Test
|
|
32
32
|
assert_equal expected, Marshal.load(marshal_data)
|
33
33
|
end
|
34
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
|
data/test/test_stackprof.rb
CHANGED
@@ -5,6 +5,10 @@ require 'tempfile'
|
|
5
5
|
require 'pathname'
|
6
6
|
|
7
7
|
class StackProfTest < MiniTest::Test
|
8
|
+
def setup
|
9
|
+
Object.new # warm some caches to avoid flakiness
|
10
|
+
end
|
11
|
+
|
8
12
|
def test_info
|
9
13
|
profile = StackProf.run{}
|
10
14
|
assert_equal 1.2, profile[:version]
|
@@ -39,16 +43,30 @@ class StackProfTest < MiniTest::Test
|
|
39
43
|
end
|
40
44
|
assert_equal :object, profile[:mode]
|
41
45
|
assert_equal 1, profile[:interval]
|
42
|
-
|
46
|
+
if RUBY_VERSION >= '3'
|
47
|
+
assert_equal 4, profile[:samples]
|
48
|
+
else
|
49
|
+
assert_equal 2, profile[:samples]
|
50
|
+
end
|
43
51
|
|
44
52
|
frame = profile[:frames].values.first
|
45
53
|
assert_includes frame[:name], "StackProfTest#test_object_allocation"
|
46
54
|
assert_equal 2, frame[:samples]
|
47
55
|
assert_includes [profile_base_line - 2, profile_base_line], frame[:line]
|
48
|
-
|
49
|
-
|
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
|
50
63
|
frame = profile[:frames].values[1] if RUBY_VERSION < '2.3'
|
51
|
-
|
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
|
52
70
|
end
|
53
71
|
|
54
72
|
def test_object_allocation_interval
|
@@ -64,8 +82,14 @@ class StackProfTest < MiniTest::Test
|
|
64
82
|
end
|
65
83
|
|
66
84
|
assert_operator profile[:samples], :>=, 1
|
67
|
-
|
68
|
-
|
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
|
69
93
|
end
|
70
94
|
|
71
95
|
def test_walltime
|
@@ -74,7 +98,11 @@ class StackProfTest < MiniTest::Test
|
|
74
98
|
end
|
75
99
|
|
76
100
|
frame = profile[:frames].values.first
|
77
|
-
|
101
|
+
if RUBY_VERSION >= '3'
|
102
|
+
assert_equal "IO.select", frame[:name]
|
103
|
+
else
|
104
|
+
assert_equal "StackProfTest#idle", frame[:name]
|
105
|
+
end
|
78
106
|
assert_in_delta 200, frame[:samples], 25
|
79
107
|
end
|
80
108
|
|
@@ -89,24 +117,51 @@ class StackProfTest < MiniTest::Test
|
|
89
117
|
assert_equal :custom, profile[:mode]
|
90
118
|
assert_equal 10, profile[:samples]
|
91
119
|
|
92
|
-
|
120
|
+
offset = RUBY_VERSION >= '3' ? 1 : 0
|
121
|
+
frame = profile[:frames].values[offset]
|
93
122
|
assert_includes frame[:name], "StackProfTest#test_custom"
|
94
123
|
assert_includes [profile_base_line-2, profile_base_line+1], frame[:line]
|
95
|
-
|
124
|
+
|
125
|
+
if RUBY_VERSION >= '3'
|
126
|
+
assert_equal [10, 0], frame[:lines][profile_base_line+2]
|
127
|
+
else
|
128
|
+
assert_equal [10, 10], frame[:lines][profile_base_line+2]
|
129
|
+
end
|
96
130
|
end
|
97
131
|
|
98
132
|
def test_raw
|
133
|
+
before_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
134
|
+
|
99
135
|
profile = StackProf.run(mode: :custom, raw: true) do
|
100
136
|
10.times do
|
101
137
|
StackProf.sample
|
138
|
+
sleep 0.0001
|
102
139
|
end
|
103
140
|
end
|
104
141
|
|
142
|
+
after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
143
|
+
|
105
144
|
raw = profile[:raw]
|
106
145
|
assert_equal 10, raw[-1]
|
107
146
|
assert_equal raw[0] + 2, raw.size
|
108
|
-
|
147
|
+
|
148
|
+
offset = RUBY_VERSION >= '3' ? -3 : -2
|
149
|
+
assert_includes profile[:frames][raw[offset]][:name], 'StackProfTest#test_raw'
|
150
|
+
|
151
|
+
assert_equal 10, profile[:raw_sample_timestamps].size
|
152
|
+
profile[:raw_sample_timestamps].each_cons(2) do |t1, t2|
|
153
|
+
assert_operator t1, :>, before_monotonic
|
154
|
+
assert_operator t2, :>=, t1
|
155
|
+
assert_operator t2, :<, after_monotonic
|
156
|
+
end
|
157
|
+
|
109
158
|
assert_equal 10, profile[:raw_timestamp_deltas].size
|
159
|
+
total_duration = after_monotonic - before_monotonic
|
160
|
+
assert_operator profile[:raw_timestamp_deltas].inject(&:+), :<, total_duration
|
161
|
+
|
162
|
+
profile[:raw_timestamp_deltas].each do |delta|
|
163
|
+
assert_operator delta, :>, 0
|
164
|
+
end
|
110
165
|
end
|
111
166
|
|
112
167
|
def test_metadata
|
@@ -178,7 +233,6 @@ class StackProfTest < MiniTest::Test
|
|
178
233
|
end
|
179
234
|
end
|
180
235
|
|
181
|
-
raw = profile[:raw]
|
182
236
|
gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" }
|
183
237
|
marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" }
|
184
238
|
sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" }
|
@@ -232,6 +286,15 @@ class StackProfTest < MiniTest::Test
|
|
232
286
|
refute_empty profile[:frames]
|
233
287
|
end
|
234
288
|
|
289
|
+
def test_min_max_interval
|
290
|
+
[-1, 0, 1_000_000, 1_000_001].each do |invalid_interval|
|
291
|
+
err = assert_raises(ArgumentError, "invalid interval #{invalid_interval}") do
|
292
|
+
StackProf.run(interval: invalid_interval, debug: true) {}
|
293
|
+
end
|
294
|
+
assert_match(/microseconds/, err.message)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
235
298
|
def math
|
236
299
|
250_000.times do
|
237
300
|
2 ** 10
|
@@ -245,4 +308,4 @@ class StackProfTest < MiniTest::Test
|
|
245
308
|
r.close
|
246
309
|
w.close
|
247
310
|
end
|
248
|
-
end
|
311
|
+
end unless RUBY_ENGINE == 'truffleruby'
|
@@ -0,0 +1,18 @@
|
|
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
|