stackprof 0.2.12 → 0.2.26
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 +17 -2
- data/README.md +66 -51
- data/Rakefile +21 -25
- data/bin/stackprof +115 -71
- data/ext/stackprof/extconf.rb +6 -0
- data/ext/stackprof/stackprof.c +392 -84
- data/lib/stackprof/autorun.rb +19 -0
- data/lib/stackprof/middleware.rb +8 -2
- data/lib/stackprof/report.rb +280 -16
- data/lib/stackprof/truffleruby.rb +37 -0
- data/lib/stackprof.rb +22 -1
- data/stackprof.gemspec +11 -3
- data/test/fixtures/profile.dump +1 -0
- data/test/fixtures/profile.json +1 -0
- data/test/test_middleware.rb +36 -17
- data/test/test_report.rb +25 -1
- data/test/test_stackprof.rb +153 -15
- data/test/test_truffleruby.rb +18 -0
- data/vendor/FlameGraph/flamegraph.pl +751 -85
- metadata +16 -23
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -27
data/test/test_stackprof.rb
CHANGED
@@ -2,8 +2,13 @@ $:.unshift File.expand_path('../../lib', __FILE__)
|
|
2
2
|
require 'stackprof'
|
3
3
|
require 'minitest/autorun'
|
4
4
|
require 'tempfile'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
class StackProfTest < Minitest::Test
|
8
|
+
def setup
|
9
|
+
Object.new # warm some caches to avoid flakiness
|
10
|
+
end
|
5
11
|
|
6
|
-
class StackProfTest < MiniTest::Test
|
7
12
|
def test_info
|
8
13
|
profile = StackProf.run{}
|
9
14
|
assert_equal 1.2, profile[:version]
|
@@ -38,16 +43,30 @@ class StackProfTest < MiniTest::Test
|
|
38
43
|
end
|
39
44
|
assert_equal :object, profile[:mode]
|
40
45
|
assert_equal 1, profile[:interval]
|
41
|
-
|
46
|
+
if RUBY_VERSION >= '3'
|
47
|
+
assert_equal 4, profile[:samples]
|
48
|
+
else
|
49
|
+
assert_equal 2, profile[:samples]
|
50
|
+
end
|
42
51
|
|
43
52
|
frame = profile[:frames].values.first
|
44
53
|
assert_includes frame[:name], "StackProfTest#test_object_allocation"
|
45
54
|
assert_equal 2, frame[:samples]
|
46
55
|
assert_includes [profile_base_line - 2, profile_base_line], frame[:line]
|
47
|
-
|
48
|
-
|
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
|
49
63
|
frame = profile[:frames].values[1] if RUBY_VERSION < '2.3'
|
50
|
-
|
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
|
51
70
|
end
|
52
71
|
|
53
72
|
def test_object_allocation_interval
|
@@ -63,18 +82,31 @@ class StackProfTest < MiniTest::Test
|
|
63
82
|
end
|
64
83
|
|
65
84
|
assert_operator profile[:samples], :>=, 1
|
66
|
-
|
67
|
-
|
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
|
68
93
|
end
|
69
94
|
|
70
95
|
def test_walltime
|
96
|
+
GC.disable
|
71
97
|
profile = StackProf.run(mode: :wall) do
|
72
98
|
idle
|
73
99
|
end
|
74
100
|
|
75
101
|
frame = profile[:frames].values.first
|
76
|
-
|
102
|
+
if RUBY_VERSION >= '3'
|
103
|
+
assert_equal "IO.select", frame[:name]
|
104
|
+
else
|
105
|
+
assert_equal "StackProfTest#idle", frame[:name]
|
106
|
+
end
|
77
107
|
assert_in_delta 200, frame[:samples], 25
|
108
|
+
ensure
|
109
|
+
GC.enable
|
78
110
|
end
|
79
111
|
|
80
112
|
def test_custom
|
@@ -88,24 +120,84 @@ class StackProfTest < MiniTest::Test
|
|
88
120
|
assert_equal :custom, profile[:mode]
|
89
121
|
assert_equal 10, profile[:samples]
|
90
122
|
|
91
|
-
|
123
|
+
offset = RUBY_VERSION >= '3' ? 1 : 0
|
124
|
+
frame = profile[:frames].values[offset]
|
92
125
|
assert_includes frame[:name], "StackProfTest#test_custom"
|
93
126
|
assert_includes [profile_base_line-2, profile_base_line+1], frame[:line]
|
94
|
-
|
127
|
+
|
128
|
+
if RUBY_VERSION >= '3'
|
129
|
+
assert_equal [10, 0], frame[:lines][profile_base_line+2]
|
130
|
+
else
|
131
|
+
assert_equal [10, 10], frame[:lines][profile_base_line+2]
|
132
|
+
end
|
95
133
|
end
|
96
134
|
|
97
135
|
def test_raw
|
136
|
+
before_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
137
|
+
|
98
138
|
profile = StackProf.run(mode: :custom, raw: true) do
|
99
139
|
10.times do
|
100
140
|
StackProf.sample
|
141
|
+
sleep 0.0001
|
101
142
|
end
|
102
143
|
end
|
103
144
|
|
145
|
+
after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
146
|
+
|
104
147
|
raw = profile[:raw]
|
148
|
+
raw_lines = profile[:raw_lines]
|
105
149
|
assert_equal 10, raw[-1]
|
106
150
|
assert_equal raw[0] + 2, raw.size
|
107
|
-
|
151
|
+
assert_equal 10, raw_lines[-1] # seen 10 times
|
152
|
+
|
153
|
+
offset = RUBY_VERSION >= '3' ? -3 : -2
|
154
|
+
assert_equal 140, raw_lines[offset] # sample caller is on 140
|
155
|
+
assert_includes profile[:frames][raw[offset]][:name], 'StackProfTest#test_raw'
|
156
|
+
|
157
|
+
assert_equal 10, profile[:raw_sample_timestamps].size
|
158
|
+
profile[:raw_sample_timestamps].each_cons(2) do |t1, t2|
|
159
|
+
assert_operator t1, :>, before_monotonic
|
160
|
+
assert_operator t2, :>=, t1
|
161
|
+
assert_operator t2, :<, after_monotonic
|
162
|
+
end
|
163
|
+
|
108
164
|
assert_equal 10, profile[:raw_timestamp_deltas].size
|
165
|
+
total_duration = after_monotonic - before_monotonic
|
166
|
+
assert_operator profile[:raw_timestamp_deltas].inject(&:+), :<, total_duration
|
167
|
+
|
168
|
+
profile[:raw_timestamp_deltas].each do |delta|
|
169
|
+
assert_operator delta, :>, 0
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_metadata
|
174
|
+
metadata = {
|
175
|
+
path: '/foo/bar',
|
176
|
+
revision: '5c0b01f1522ae8c194510977ae29377296dd236b',
|
177
|
+
}
|
178
|
+
profile = StackProf.run(mode: :cpu, metadata: metadata) do
|
179
|
+
math
|
180
|
+
end
|
181
|
+
|
182
|
+
assert_equal metadata, profile[:metadata]
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_empty_metadata
|
186
|
+
profile = StackProf.run(mode: :cpu) do
|
187
|
+
math
|
188
|
+
end
|
189
|
+
|
190
|
+
assert_equal({}, profile[:metadata])
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_raises_if_metadata_is_not_a_hash
|
194
|
+
exception = assert_raises ArgumentError do
|
195
|
+
StackProf.run(mode: :cpu, metadata: 'foobar') do
|
196
|
+
math
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
assert_equal 'metadata should be a hash', exception.message
|
109
201
|
end
|
110
202
|
|
111
203
|
def test_fork
|
@@ -147,12 +239,23 @@ class StackProfTest < MiniTest::Test
|
|
147
239
|
end
|
148
240
|
end
|
149
241
|
|
150
|
-
raw = profile[:raw]
|
151
242
|
gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" }
|
243
|
+
marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" }
|
244
|
+
sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" }
|
245
|
+
|
152
246
|
assert gc_frame
|
153
|
-
|
247
|
+
assert marking_frame
|
248
|
+
assert sweeping_frame
|
249
|
+
|
250
|
+
# We can't guarantee a certain number of GCs to run, so just assert
|
251
|
+
# that it's within some kind of delta
|
252
|
+
assert_in_delta gc_frame[:total_samples], profile[:gc_samples], 2
|
253
|
+
|
254
|
+
# Lazy marking / sweeping can cause this math to not add up, so also use a delta
|
255
|
+
assert_in_delta profile[:gc_samples], [gc_frame, marking_frame, sweeping_frame].map{|x| x[:samples] }.inject(:+), 2
|
256
|
+
|
154
257
|
assert_operator profile[:gc_samples], :>, 0
|
155
|
-
assert_operator profile[:missed_samples], :<=,
|
258
|
+
assert_operator profile[:missed_samples], :<=, 25
|
156
259
|
end
|
157
260
|
|
158
261
|
def test_out
|
@@ -167,6 +270,41 @@ class StackProfTest < MiniTest::Test
|
|
167
270
|
refute_empty profile[:frames]
|
168
271
|
end
|
169
272
|
|
273
|
+
def test_out_to_path_string
|
274
|
+
tmpfile = Tempfile.new('stackprof-out')
|
275
|
+
ret = StackProf.run(mode: :custom, out: tmpfile.path) do
|
276
|
+
StackProf.sample
|
277
|
+
end
|
278
|
+
|
279
|
+
refute_equal tmpfile, ret
|
280
|
+
assert_equal tmpfile.path, ret.path
|
281
|
+
tmpfile.rewind
|
282
|
+
profile = Marshal.load(tmpfile.read)
|
283
|
+
refute_empty profile[:frames]
|
284
|
+
end
|
285
|
+
|
286
|
+
def test_pathname_out
|
287
|
+
tmpfile = Tempfile.new('stackprof-out')
|
288
|
+
pathname = Pathname.new(tmpfile.path)
|
289
|
+
ret = StackProf.run(mode: :custom, out: pathname) do
|
290
|
+
StackProf.sample
|
291
|
+
end
|
292
|
+
|
293
|
+
assert_equal tmpfile.path, ret.path
|
294
|
+
tmpfile.rewind
|
295
|
+
profile = Marshal.load(tmpfile.read)
|
296
|
+
refute_empty profile[:frames]
|
297
|
+
end
|
298
|
+
|
299
|
+
def test_min_max_interval
|
300
|
+
[-1, 0, 1_000_000, 1_000_001].each do |invalid_interval|
|
301
|
+
err = assert_raises(ArgumentError, "invalid interval #{invalid_interval}") do
|
302
|
+
StackProf.run(interval: invalid_interval, debug: true) {}
|
303
|
+
end
|
304
|
+
assert_match(/microseconds/, err.message)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
170
308
|
def math
|
171
309
|
250_000.times do
|
172
310
|
2 ** 10
|
@@ -180,4 +318,4 @@ class StackProfTest < MiniTest::Test
|
|
180
318
|
r.close
|
181
319
|
w.close
|
182
320
|
end
|
183
|
-
end
|
321
|
+
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
|