stackprof 0.2.15 → 0.2.27
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 +4 -4
- data/.github/workflows/ci.yml +43 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +16 -1
- data/README.md +57 -51
- data/Rakefile +21 -25
- data/bin/stackprof +115 -81
- data/ext/stackprof/extconf.rb +6 -0
- data/ext/stackprof/stackprof.c +335 -85
- data/lib/stackprof/autorun.rb +19 -0
- data/lib/stackprof/report.rb +65 -26
- data/lib/stackprof/truffleruby.rb +37 -0
- data/lib/stackprof.rb +19 -2
- data/stackprof.gemspec +8 -2
- data/test/fixtures/profile.dump +1 -0
- data/test/fixtures/profile.json +1 -0
- data/test/test_middleware.rb +30 -17
- data/test/test_report.rb +25 -1
- data/test/test_stackprof.rb +88 -15
- data/test/test_truffleruby.rb +18 -0
- data/vendor/FlameGraph/flamegraph.pl +751 -85
- metadata +14 -24
- data/.travis.yml +0 -21
- data/Dockerfile +0 -21
- data/Gemfile.lock +0 -27
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "stackprof"
|
|
2
|
+
|
|
3
|
+
options = {}
|
|
4
|
+
options[:mode] = ENV["STACKPROF_MODE"].to_sym if ENV.key?("STACKPROF_MODE")
|
|
5
|
+
options[:interval] = Integer(ENV["STACKPROF_INTERVAL"]) if ENV.key?("STACKPROF_INTERVAL")
|
|
6
|
+
options[:raw] = true if ENV["STACKPROF_RAW"]
|
|
7
|
+
options[:ignore_gc] = true if ENV["STACKPROF_IGNORE_GC"]
|
|
8
|
+
|
|
9
|
+
at_exit do
|
|
10
|
+
StackProf.stop
|
|
11
|
+
output_path = ENV.fetch("STACKPROF_OUT") do
|
|
12
|
+
require "tempfile"
|
|
13
|
+
Tempfile.create(["stackprof", ".dump"]).path
|
|
14
|
+
end
|
|
15
|
+
StackProf.results(output_path)
|
|
16
|
+
$stderr.puts("StackProf results dumped at: #{output_path}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
StackProf.start(**options)
|
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,24 @@
|
|
|
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
|
+
if RUBY_VERSION < "3.3"
|
|
9
|
+
# On 3.3 we don't need postponed jobs:
|
|
10
|
+
# https://github.com/ruby/ruby/commit/a1dc1a3de9683daf5a543d6f618e17aabfcb8708
|
|
11
|
+
StackProf.use_postponed_job!
|
|
12
|
+
end
|
|
13
|
+
elsif RUBY_VERSION == "3.2.0"
|
|
14
|
+
# 3.2.0 crash is the signal is received at the wrong time.
|
|
15
|
+
# Fixed in https://github.com/ruby/ruby/pull/7116
|
|
16
|
+
# The fix is backported in 3.2.1: https://bugs.ruby-lang.org/issues/19336
|
|
17
|
+
StackProf.use_postponed_job!
|
|
18
|
+
end
|
|
2
19
|
|
|
3
20
|
module StackProf
|
|
4
|
-
VERSION = '0.2.
|
|
21
|
+
VERSION = '0.2.27'
|
|
5
22
|
end
|
|
6
23
|
|
|
7
24
|
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.27'
|
|
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
|
|
|
@@ -22,6 +29,5 @@ Gem::Specification.new do |s|
|
|
|
22
29
|
s.license = 'MIT'
|
|
23
30
|
|
|
24
31
|
s.add_development_dependency 'rake-compiler', '~> 0.9'
|
|
25
|
-
s.add_development_dependency 'mocha', '~> 0.14'
|
|
26
32
|
s.add_development_dependency 'minitest', '~> 5.0'
|
|
27
33
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{: modeI"cpu:ET
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "mode": "cpu" }
|
data/test/test_middleware.rb
CHANGED
|
@@ -2,9 +2,9 @@ $:.unshift File.expand_path('../../lib', __FILE__)
|
|
|
2
2
|
require 'stackprof'
|
|
3
3
|
require 'stackprof/middleware'
|
|
4
4
|
require 'minitest/autorun'
|
|
5
|
-
require '
|
|
5
|
+
require 'tmpdir'
|
|
6
6
|
|
|
7
|
-
class StackProf::MiddlewareTest <
|
|
7
|
+
class StackProf::MiddlewareTest < Minitest::Test
|
|
8
8
|
|
|
9
9
|
def test_path_default
|
|
10
10
|
StackProf::Middleware.new(Object.new)
|
|
@@ -19,23 +19,36 @@ class StackProf::MiddlewareTest < MiniTest::Test
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def test_save_default
|
|
22
|
-
StackProf::Middleware.new(Object.new
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
middleware = StackProf::Middleware.new(->(env) { 100.times { Object.new } },
|
|
23
|
+
save_every: 1,
|
|
24
|
+
enabled: true)
|
|
25
|
+
Dir.mktmpdir do |dir|
|
|
26
|
+
Dir.chdir(dir) { middleware.call({}) }
|
|
27
|
+
dir = File.join(dir, "tmp")
|
|
28
|
+
assert File.directory? dir
|
|
29
|
+
profile = Dir.entries(dir).reject { |x| File.directory?(x) }.first
|
|
30
|
+
assert profile
|
|
31
|
+
assert_equal "stackprof", profile.split("-")[0]
|
|
32
|
+
assert_equal "cpu", profile.split("-")[1]
|
|
33
|
+
assert_equal Process.pid.to_s, profile.split("-")[2]
|
|
34
|
+
end
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
def test_save_custom
|
|
32
|
-
StackProf::Middleware.new(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
middleware = StackProf::Middleware.new(->(env) { 100.times { Object.new } },
|
|
39
|
+
path: "foo/",
|
|
40
|
+
save_every: 1,
|
|
41
|
+
enabled: true)
|
|
42
|
+
Dir.mktmpdir do |dir|
|
|
43
|
+
Dir.chdir(dir) { middleware.call({}) }
|
|
44
|
+
dir = File.join(dir, "foo")
|
|
45
|
+
assert File.directory? dir
|
|
46
|
+
profile = Dir.entries(dir).reject { |x| File.directory?(x) }.first
|
|
47
|
+
assert profile
|
|
48
|
+
assert_equal "stackprof", profile.split("-")[0]
|
|
49
|
+
assert_equal "cpu", profile.split("-")[1]
|
|
50
|
+
assert_equal Process.pid.to_s, profile.split("-")[2]
|
|
51
|
+
end
|
|
39
52
|
end
|
|
40
53
|
|
|
41
54
|
def test_enabled_should_use_a_proc_if_passed
|
|
@@ -70,4 +83,4 @@ class StackProf::MiddlewareTest < MiniTest::Test
|
|
|
70
83
|
StackProf::Middleware.new(Object.new, metadata: metadata)
|
|
71
84
|
assert_equal metadata, StackProf::Middleware.metadata
|
|
72
85
|
end
|
|
73
|
-
end
|
|
86
|
+
end unless RUBY_ENGINE == 'truffleruby'
|
data/test/test_report.rb
CHANGED
|
@@ -2,7 +2,7 @@ $:.unshift File.expand_path('../../lib', __FILE__)
|
|
|
2
2
|
require 'stackprof'
|
|
3
3
|
require 'minitest/autorun'
|
|
4
4
|
|
|
5
|
-
class ReportDumpTest <
|
|
5
|
+
class ReportDumpTest < Minitest::Test
|
|
6
6
|
require 'stringio'
|
|
7
7
|
|
|
8
8
|
def test_dump_to_stdout
|
|
@@ -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
|
@@ -4,7 +4,11 @@ require 'minitest/autorun'
|
|
|
4
4
|
require 'tempfile'
|
|
5
5
|
require 'pathname'
|
|
6
6
|
|
|
7
|
-
class StackProfTest <
|
|
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,18 +82,31 @@ 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
|
|
96
|
+
GC.disable
|
|
72
97
|
profile = StackProf.run(mode: :wall) do
|
|
73
98
|
idle
|
|
74
99
|
end
|
|
75
100
|
|
|
76
101
|
frame = profile[:frames].values.first
|
|
77
|
-
|
|
102
|
+
if RUBY_VERSION >= '3'
|
|
103
|
+
assert_equal "IO.select", frame[:name]
|
|
104
|
+
else
|
|
105
|
+
assert_equal "StackProfTest#idle", frame[:name]
|
|
106
|
+
end
|
|
78
107
|
assert_in_delta 200, frame[:samples], 25
|
|
108
|
+
ensure
|
|
109
|
+
GC.enable
|
|
79
110
|
end
|
|
80
111
|
|
|
81
112
|
def test_custom
|
|
@@ -89,24 +120,54 @@ class StackProfTest < MiniTest::Test
|
|
|
89
120
|
assert_equal :custom, profile[:mode]
|
|
90
121
|
assert_equal 10, profile[:samples]
|
|
91
122
|
|
|
92
|
-
|
|
123
|
+
offset = RUBY_VERSION >= '3' ? 1 : 0
|
|
124
|
+
frame = profile[:frames].values[offset]
|
|
93
125
|
assert_includes frame[:name], "StackProfTest#test_custom"
|
|
94
126
|
assert_includes [profile_base_line-2, profile_base_line+1], frame[:line]
|
|
95
|
-
|
|
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
|
|
96
133
|
end
|
|
97
134
|
|
|
98
135
|
def test_raw
|
|
136
|
+
before_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
137
|
+
|
|
99
138
|
profile = StackProf.run(mode: :custom, raw: true) do
|
|
100
139
|
10.times do
|
|
101
140
|
StackProf.sample
|
|
141
|
+
sleep 0.0001
|
|
102
142
|
end
|
|
103
143
|
end
|
|
104
144
|
|
|
145
|
+
after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
146
|
+
|
|
105
147
|
raw = profile[:raw]
|
|
148
|
+
raw_lines = profile[:raw_lines]
|
|
106
149
|
assert_equal 10, raw[-1]
|
|
107
150
|
assert_equal raw[0] + 2, raw.size
|
|
108
|
-
|
|
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
|
+
|
|
109
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
|
|
110
171
|
end
|
|
111
172
|
|
|
112
173
|
def test_metadata
|
|
@@ -178,7 +239,6 @@ class StackProfTest < MiniTest::Test
|
|
|
178
239
|
end
|
|
179
240
|
end
|
|
180
241
|
|
|
181
|
-
raw = profile[:raw]
|
|
182
242
|
gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" }
|
|
183
243
|
marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" }
|
|
184
244
|
sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" }
|
|
@@ -187,8 +247,12 @@ class StackProfTest < MiniTest::Test
|
|
|
187
247
|
assert marking_frame
|
|
188
248
|
assert sweeping_frame
|
|
189
249
|
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
192
256
|
|
|
193
257
|
assert_operator profile[:gc_samples], :>, 0
|
|
194
258
|
assert_operator profile[:missed_samples], :<=, 25
|
|
@@ -232,6 +296,15 @@ class StackProfTest < MiniTest::Test
|
|
|
232
296
|
refute_empty profile[:frames]
|
|
233
297
|
end
|
|
234
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
|
+
|
|
235
308
|
def math
|
|
236
309
|
250_000.times do
|
|
237
310
|
2 ** 10
|
|
@@ -245,4 +318,4 @@ class StackProfTest < MiniTest::Test
|
|
|
245
318
|
r.close
|
|
246
319
|
w.close
|
|
247
320
|
end
|
|
248
|
-
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
|