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
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
|
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
@@ -2,11 +2,16 @@ $:.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
|
8
|
+
def setup
|
9
|
+
Object.new # warm some caches to avoid flakiness
|
10
|
+
end
|
11
|
+
|
7
12
|
def test_info
|
8
13
|
profile = StackProf.run{}
|
9
|
-
assert_equal 1.
|
14
|
+
assert_equal 1.2, profile[:version]
|
10
15
|
assert_equal :wall, profile[:mode]
|
11
16
|
assert_equal 1000, profile[:interval]
|
12
17
|
assert_equal 0, profile[:samples]
|
@@ -18,16 +23,16 @@ class StackProfTest < MiniTest::Test
|
|
18
23
|
end
|
19
24
|
|
20
25
|
def test_start_stop_results
|
21
|
-
|
26
|
+
assert_nil StackProf.results
|
22
27
|
assert_equal true, StackProf.start
|
23
28
|
assert_equal false, StackProf.start
|
24
29
|
assert_equal true, StackProf.running?
|
25
|
-
|
30
|
+
assert_nil StackProf.results
|
26
31
|
assert_equal true, StackProf.stop
|
27
32
|
assert_equal false, StackProf.stop
|
28
33
|
assert_equal false, StackProf.running?
|
29
34
|
assert_kind_of Hash, StackProf.results
|
30
|
-
|
35
|
+
assert_nil StackProf.results
|
31
36
|
end
|
32
37
|
|
33
38
|
def test_object_allocation
|
@@ -38,17 +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
|
-
|
47
|
-
|
48
|
-
|
55
|
+
assert_includes [profile_base_line - 2, profile_base_line], frame[:line]
|
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
|
63
|
+
frame = profile[:frames].values[1] if RUBY_VERSION < '2.3'
|
49
64
|
|
50
|
-
|
51
|
-
|
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
|
@@ -63,9 +81,15 @@ class StackProfTest < MiniTest::Test
|
|
63
81
|
math
|
64
82
|
end
|
65
83
|
|
66
|
-
assert_operator profile[:samples],
|
67
|
-
|
68
|
-
|
84
|
+
assert_operator profile[:samples], :>=, 1
|
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,8 +98,12 @@ class StackProfTest < MiniTest::Test
|
|
74
98
|
end
|
75
99
|
|
76
100
|
frame = profile[:frames].values.first
|
77
|
-
|
78
|
-
|
101
|
+
if RUBY_VERSION >= '3'
|
102
|
+
assert_equal "IO.select", frame[:name]
|
103
|
+
else
|
104
|
+
assert_equal "StackProfTest#idle", frame[:name]
|
105
|
+
end
|
106
|
+
assert_in_delta 200, frame[:samples], 25
|
79
107
|
end
|
80
108
|
|
81
109
|
def test_custom
|
@@ -89,23 +117,81 @@ class StackProfTest < MiniTest::Test
|
|
89
117
|
assert_equal :custom, profile[:mode]
|
90
118
|
assert_equal 10, profile[:samples]
|
91
119
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
120
|
+
offset = RUBY_VERSION >= '3' ? 1 : 0
|
121
|
+
frame = profile[:frames].values[offset]
|
122
|
+
assert_includes frame[:name], "StackProfTest#test_custom"
|
123
|
+
assert_includes [profile_base_line-2, profile_base_line+1], frame[:line]
|
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
|
+
|
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
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_metadata
|
168
|
+
metadata = {
|
169
|
+
path: '/foo/bar',
|
170
|
+
revision: '5c0b01f1522ae8c194510977ae29377296dd236b',
|
171
|
+
}
|
172
|
+
profile = StackProf.run(mode: :cpu, metadata: metadata) do
|
173
|
+
math
|
174
|
+
end
|
175
|
+
|
176
|
+
assert_equal metadata, profile[:metadata]
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_empty_metadata
|
180
|
+
profile = StackProf.run(mode: :cpu) do
|
181
|
+
math
|
182
|
+
end
|
183
|
+
|
184
|
+
assert_equal({}, profile[:metadata])
|
185
|
+
end
|
186
|
+
|
187
|
+
def test_raises_if_metadata_is_not_a_hash
|
188
|
+
exception = assert_raises ArgumentError do
|
189
|
+
StackProf.run(mode: :cpu, metadata: 'foobar') do
|
190
|
+
math
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
assert_equal 'metadata should be a hash', exception.message
|
109
195
|
end
|
110
196
|
|
111
197
|
def test_fork
|
@@ -119,16 +205,47 @@ class StackProfTest < MiniTest::Test
|
|
119
205
|
end
|
120
206
|
end
|
121
207
|
|
208
|
+
def foo(n = 10)
|
209
|
+
if n == 0
|
210
|
+
StackProf.sample
|
211
|
+
return
|
212
|
+
end
|
213
|
+
foo(n - 1)
|
214
|
+
end
|
215
|
+
|
216
|
+
def test_recursive_total_samples
|
217
|
+
profile = StackProf.run(mode: :cpu, raw: true) do
|
218
|
+
10.times do
|
219
|
+
foo
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
frame = profile[:frames].values.find do |frame|
|
224
|
+
frame[:name] == "StackProfTest#foo"
|
225
|
+
end
|
226
|
+
assert_equal 10, frame[:total_samples]
|
227
|
+
end
|
228
|
+
|
122
229
|
def test_gc
|
123
|
-
profile = StackProf.run(interval: 100) do
|
230
|
+
profile = StackProf.run(interval: 100, raw: true) do
|
124
231
|
5.times do
|
125
232
|
GC.start
|
126
233
|
end
|
127
234
|
end
|
128
235
|
|
129
|
-
|
236
|
+
gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" }
|
237
|
+
marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" }
|
238
|
+
sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" }
|
239
|
+
|
240
|
+
assert gc_frame
|
241
|
+
assert marking_frame
|
242
|
+
assert sweeping_frame
|
243
|
+
|
244
|
+
assert_equal gc_frame[:total_samples], profile[:gc_samples]
|
245
|
+
assert_equal profile[:gc_samples], [gc_frame, marking_frame, sweeping_frame].map{|x| x[:samples] }.inject(:+)
|
246
|
+
|
130
247
|
assert_operator profile[:gc_samples], :>, 0
|
131
|
-
|
248
|
+
assert_operator profile[:missed_samples], :<=, 25
|
132
249
|
end
|
133
250
|
|
134
251
|
def test_out
|
@@ -143,6 +260,41 @@ class StackProfTest < MiniTest::Test
|
|
143
260
|
refute_empty profile[:frames]
|
144
261
|
end
|
145
262
|
|
263
|
+
def test_out_to_path_string
|
264
|
+
tmpfile = Tempfile.new('stackprof-out')
|
265
|
+
ret = StackProf.run(mode: :custom, out: tmpfile.path) do
|
266
|
+
StackProf.sample
|
267
|
+
end
|
268
|
+
|
269
|
+
refute_equal tmpfile, ret
|
270
|
+
assert_equal tmpfile.path, ret.path
|
271
|
+
tmpfile.rewind
|
272
|
+
profile = Marshal.load(tmpfile.read)
|
273
|
+
refute_empty profile[:frames]
|
274
|
+
end
|
275
|
+
|
276
|
+
def test_pathname_out
|
277
|
+
tmpfile = Tempfile.new('stackprof-out')
|
278
|
+
pathname = Pathname.new(tmpfile.path)
|
279
|
+
ret = StackProf.run(mode: :custom, out: pathname) do
|
280
|
+
StackProf.sample
|
281
|
+
end
|
282
|
+
|
283
|
+
assert_equal tmpfile.path, ret.path
|
284
|
+
tmpfile.rewind
|
285
|
+
profile = Marshal.load(tmpfile.read)
|
286
|
+
refute_empty profile[:frames]
|
287
|
+
end
|
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
|
+
|
146
298
|
def math
|
147
299
|
250_000.times do
|
148
300
|
2 ** 10
|
@@ -156,4 +308,4 @@ class StackProfTest < MiniTest::Test
|
|
156
308
|
r.close
|
157
309
|
w.close
|
158
310
|
end
|
159
|
-
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
|