stackprof 0.2.19 → 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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +4 -0
- data/Rakefile +14 -4
- data/bin/stackprof +115 -81
- data/ext/stackprof/extconf.rb +6 -0
- data/ext/stackprof/stackprof.c +70 -24
- data/lib/stackprof/autorun.rb +19 -0
- data/lib/stackprof/report.rb +37 -3
- data/lib/stackprof/truffleruby.rb +37 -0
- data/lib/stackprof.rb +15 -2
- data/stackprof.gemspec +1 -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 +5 -1
- data/test/test_truffleruby.rb +18 -0
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b299eb696cf0c2748e931532afab3f90bab1c94447ff1c844bce0eda878a93c6
|
4
|
+
data.tar.gz: bd2c389baa8253fc06beda425abc87762b9bca425c6bc5046655610fb9852e79
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 322e506fc77bd964f39e7b3f0c5584e57efdec8e1dcc432cdea924536e334234d916a1d5e1ff085e2e5210a49751897ca36a4b1bd007468a5b16184151129be3
|
7
|
+
data.tar.gz: 3234d4159c78119a9238200d1f4516280cbd1e0cfe3affca1c4fa4827b805ecf154e38857c917fafe7aed042d67525bb7e6c5fce85a039e30cde4672ae08f97e
|
data/.github/workflows/ci.yml
CHANGED
@@ -8,7 +8,7 @@ jobs:
|
|
8
8
|
strategy:
|
9
9
|
fail-fast: false
|
10
10
|
matrix:
|
11
|
-
ruby: [ ruby-head, '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2' ]
|
11
|
+
ruby: [ ruby-head, '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2', truffleruby ]
|
12
12
|
steps:
|
13
13
|
- name: Checkout
|
14
14
|
uses: actions/checkout@v2
|
data/CHANGELOG.md
CHANGED
data/Rakefile
CHANGED
@@ -7,11 +7,21 @@ Rake::TestTask.new(:test) do |t|
|
|
7
7
|
t.test_files = FileList["test/**/test_*.rb"]
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
if RUBY_ENGINE == "truffleruby"
|
11
|
+
task :compile do
|
12
|
+
# noop
|
13
|
+
end
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
task :clean do
|
16
|
+
# noop
|
17
|
+
end
|
18
|
+
else
|
19
|
+
require "rake/extensiontask"
|
20
|
+
|
21
|
+
Rake::ExtensionTask.new("stackprof") do |ext|
|
22
|
+
ext.ext_dir = "ext/stackprof"
|
23
|
+
ext.lib_dir = "lib/stackprof"
|
24
|
+
end
|
15
25
|
end
|
16
26
|
|
17
27
|
task default: %i(compile test)
|
data/bin/stackprof
CHANGED
@@ -2,94 +2,128 @@
|
|
2
2
|
require 'optparse'
|
3
3
|
require 'stackprof'
|
4
4
|
|
5
|
-
|
5
|
+
banner = <<-END
|
6
|
+
Usage: stackprof run [--mode=MODE|--out=FILE|--interval=INTERVAL|--format=FORMAT] -- COMMAND
|
7
|
+
Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]
|
8
|
+
END
|
6
9
|
|
7
|
-
|
8
|
-
|
10
|
+
if ARGV.first == "run"
|
11
|
+
ARGV.shift
|
12
|
+
env = {}
|
13
|
+
parser = OptionParser.new(banner) do |o|
|
14
|
+
o.on('--mode [MODE]', String, 'Mode of sampling: cpu, wall, object, default to wall') do |mode|
|
15
|
+
env["STACKPROF_MODE"] = mode
|
16
|
+
end
|
9
17
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
o.on('--limit [num]', Integer, 'Limit --text, --files, or --graphviz output to N entries'){ |n| options[:limit] = n }
|
14
|
-
o.on('--sort-total', "Sort --text or --files output on total samples\n\n"){ options[:sort] = true }
|
15
|
-
o.on('--method [grep]', 'Zoom into specified method'){ |f| options[:format] = :method; options[:filter] = f }
|
16
|
-
o.on('--file [grep]', "Show annotated code for specified file"){ |f| options[:format] = :file; options[:filter] = f }
|
17
|
-
o.on('--walk', "Walk the stacktrace interactively\n\n"){ |f| options[:walk] = true }
|
18
|
-
o.on('--callgrind', 'Callgrind output (use with kcachegrind, stackprof-gprof2dot.py)'){ options[:format] = :callgrind }
|
19
|
-
o.on('--graphviz', "Graphviz output (use with dot)"){ options[:format] = :graphviz }
|
20
|
-
o.on('--node-fraction [frac]', OptionParser::DecimalNumeric, 'Drop nodes representing less than [frac] fraction of samples'){ |n| options[:node_fraction] = n }
|
21
|
-
o.on('--stackcollapse', 'stackcollapse.pl compatible output (use with stackprof-flamegraph.pl)'){ options[:format] = :stackcollapse }
|
22
|
-
o.on('--timeline-flamegraph', "timeline-flamegraph output (js)"){ options[:format] = :timeline_flamegraph }
|
23
|
-
o.on('--alphabetical-flamegraph', "alphabetical-flamegraph output (js)"){ options[:format] = :alphabetical_flamegraph }
|
24
|
-
o.on('--flamegraph', "alias to --timeline-flamegraph"){ options[:format] = :timeline_flamegraph }
|
25
|
-
o.on('--flamegraph-viewer [f.js]', String, "open html viewer for flamegraph output"){ |file|
|
26
|
-
puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}")
|
27
|
-
exit
|
28
|
-
}
|
29
|
-
o.on('--d3-flamegraph', "flamegraph output (html using d3-flame-graph)\n\n"){ options[:format] = :d3_flamegraph }
|
30
|
-
o.on('--select-files []', String, 'Show results of matching files'){ |path| (options[:select_files] ||= []) << File.expand_path(path) }
|
31
|
-
o.on('--reject-files []', String, 'Exclude results of matching files'){ |path| (options[:reject_files] ||= []) << File.expand_path(path) }
|
32
|
-
o.on('--select-names []', Regexp, 'Show results of matching method names'){ |regexp| (options[:select_names] ||= []) << regexp }
|
33
|
-
o.on('--reject-names []', Regexp, 'Exclude results of matching method names'){ |regexp| (options[:reject_names] ||= []) << regexp }
|
34
|
-
o.on('--dump', 'Print marshaled profile dump (combine multiple profiles)'){ options[:format] = :dump }
|
35
|
-
o.on('--debug', 'Pretty print raw profile data'){ options[:format] = :debug }
|
36
|
-
end
|
18
|
+
o.on('--out [FILENAME]', String, 'The target file, which will be overwritten. Defaults to a random temporary file') do |out|
|
19
|
+
env['STACKPROF_OUT'] = out
|
20
|
+
end
|
37
21
|
|
38
|
-
|
39
|
-
|
22
|
+
o.on('--interval [MILLISECONDS]', Integer, 'Mode-relative sample rate') do |interval|
|
23
|
+
env['STACKPROF_INTERVAL'] = interval.to_s
|
24
|
+
end
|
40
25
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
26
|
+
o.on('--raw', 'collects the extra data required by the --flamegraph and --stackcollapse report types') do |raw|
|
27
|
+
env['STACKPROF_RAW'] = raw.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
o.on('--ignore-gc', 'Ignore garbage collection frames') do |gc|
|
31
|
+
env['STACKPROF_IGNORE_GC'] = gc.to_s
|
32
|
+
end
|
48
33
|
end
|
49
|
-
|
50
|
-
|
34
|
+
parser.parse!
|
35
|
+
parser.abort(parser.help) if ARGV.empty?
|
36
|
+
stackprof_path = File.expand_path('../lib', __dir__)
|
37
|
+
env['RUBYOPT'] = "-I #{stackprof_path} -r stackprof/autorun #{ENV['RUBYOPT']}"
|
38
|
+
Kernel.exec(env, *ARGV)
|
39
|
+
else
|
40
|
+
options = {}
|
51
41
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
}
|
42
|
+
parser = OptionParser.new(banner) do |o|
|
43
|
+
o.on('--text', 'Text summary per method (default)'){ options[:format] = :text }
|
44
|
+
o.on('--json', 'JSON output (use with web viewers)'){ options[:format] = :json }
|
45
|
+
o.on('--files', 'List of files'){ |f| options[:format] = :files }
|
46
|
+
o.on('--limit [num]', Integer, 'Limit --text, --files, or --graphviz output to N entries'){ |n| options[:limit] = n }
|
47
|
+
o.on('--sort-total', "Sort --text or --files output on total samples\n\n"){ options[:sort] = true }
|
48
|
+
o.on('--method [grep]', 'Zoom into specified method'){ |f| options[:format] = :method; options[:filter] = f }
|
49
|
+
o.on('--file [grep]', "Show annotated code for specified file"){ |f| options[:format] = :file; options[:filter] = f }
|
50
|
+
o.on('--walk', "Walk the stacktrace interactively\n\n"){ |f| options[:walk] = true }
|
51
|
+
o.on('--callgrind', 'Callgrind output (use with kcachegrind, stackprof-gprof2dot.py)'){ options[:format] = :callgrind }
|
52
|
+
o.on('--graphviz', "Graphviz output (use with dot)"){ options[:format] = :graphviz }
|
53
|
+
o.on('--node-fraction [frac]', OptionParser::DecimalNumeric, 'Drop nodes representing less than [frac] fraction of samples'){ |n| options[:node_fraction] = n }
|
54
|
+
o.on('--stackcollapse', 'stackcollapse.pl compatible output (use with stackprof-flamegraph.pl)'){ options[:format] = :stackcollapse }
|
55
|
+
o.on('--timeline-flamegraph', "timeline-flamegraph output (js)"){ options[:format] = :timeline_flamegraph }
|
56
|
+
o.on('--alphabetical-flamegraph', "alphabetical-flamegraph output (js)"){ options[:format] = :alphabetical_flamegraph }
|
57
|
+
o.on('--flamegraph', "alias to --timeline-flamegraph"){ options[:format] = :timeline_flamegraph }
|
58
|
+
o.on('--flamegraph-viewer [f.js]', String, "open html viewer for flamegraph output"){ |file|
|
59
|
+
puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}")
|
60
|
+
exit
|
61
|
+
}
|
62
|
+
o.on('--d3-flamegraph', "flamegraph output (html using d3-flame-graph)\n\n"){ options[:format] = :d3_flamegraph }
|
63
|
+
o.on('--select-files []', String, 'Show results of matching files'){ |path| (options[:select_files] ||= []) << File.expand_path(path) }
|
64
|
+
o.on('--reject-files []', String, 'Exclude results of matching files'){ |path| (options[:reject_files] ||= []) << File.expand_path(path) }
|
65
|
+
o.on('--select-names []', Regexp, 'Show results of matching method names'){ |regexp| (options[:select_names] ||= []) << regexp }
|
66
|
+
o.on('--reject-names []', Regexp, 'Exclude results of matching method names'){ |regexp| (options[:reject_names] ||= []) << regexp }
|
67
|
+
o.on('--dump', 'Print marshaled profile dump (combine multiple profiles)'){ options[:format] = :dump }
|
68
|
+
o.on('--debug', 'Pretty print raw profile data'){ options[:format] = :debug }
|
69
|
+
end
|
57
70
|
|
58
|
-
|
59
|
-
|
60
|
-
default_options[:node_fraction] = 0.005
|
61
|
-
end
|
71
|
+
parser.parse!
|
72
|
+
parser.abort(parser.help) if ARGV.empty?
|
62
73
|
|
63
|
-
|
64
|
-
|
74
|
+
reports = []
|
75
|
+
while ARGV.size > 0
|
76
|
+
begin
|
77
|
+
file = ARGV.pop
|
78
|
+
reports << StackProf::Report.from_file(file)
|
79
|
+
rescue TypeError => e
|
80
|
+
STDERR.puts "** error parsing #{file}: #{e.inspect}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
report = reports.inject(:+)
|
65
84
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
when :
|
82
|
-
|
83
|
-
when :
|
84
|
-
|
85
|
-
when :
|
86
|
-
|
87
|
-
when :
|
88
|
-
|
89
|
-
when :
|
90
|
-
|
91
|
-
when :
|
92
|
-
|
93
|
-
|
94
|
-
|
85
|
+
default_options = {
|
86
|
+
:format => :text,
|
87
|
+
:sort => false,
|
88
|
+
:limit => 30
|
89
|
+
}
|
90
|
+
|
91
|
+
if options[:format] == :graphviz
|
92
|
+
default_options[:limit] = 120
|
93
|
+
default_options[:node_fraction] = 0.005
|
94
|
+
end
|
95
|
+
|
96
|
+
options = default_options.merge(options)
|
97
|
+
options.delete(:limit) if options[:limit] == 0
|
98
|
+
|
99
|
+
case options[:format]
|
100
|
+
when :text
|
101
|
+
report.print_text(options[:sort], options[:limit], options[:select_files], options[:reject_files], options[:select_names], options[:reject_names])
|
102
|
+
when :json
|
103
|
+
report.print_json
|
104
|
+
when :debug
|
105
|
+
report.print_debug
|
106
|
+
when :dump
|
107
|
+
report.print_dump
|
108
|
+
when :callgrind
|
109
|
+
report.print_callgrind
|
110
|
+
when :graphviz
|
111
|
+
report.print_graphviz(options)
|
112
|
+
when :stackcollapse
|
113
|
+
report.print_stackcollapse
|
114
|
+
when :timeline_flamegraph
|
115
|
+
report.print_timeline_flamegraph
|
116
|
+
when :alphabetical_flamegraph
|
117
|
+
report.print_alphabetical_flamegraph
|
118
|
+
when :d3_flamegraph
|
119
|
+
report.print_d3_flamegraph
|
120
|
+
when :method
|
121
|
+
options[:walk] ? report.walk_method(options[:filter]) : report.print_method(options[:filter])
|
122
|
+
when :file
|
123
|
+
report.print_file(options[:filter])
|
124
|
+
when :files
|
125
|
+
report.print_files(options[:sort], options[:limit])
|
126
|
+
else
|
127
|
+
raise ArgumentError, "unknown format: #{options[:format]}"
|
128
|
+
end
|
95
129
|
end
|
data/ext/stackprof/extconf.rb
CHANGED
data/ext/stackprof/stackprof.c
CHANGED
@@ -12,6 +12,7 @@
|
|
12
12
|
#include <ruby/st.h>
|
13
13
|
#include <ruby/io.h>
|
14
14
|
#include <ruby/intern.h>
|
15
|
+
#include <ruby/vm.h>
|
15
16
|
#include <signal.h>
|
16
17
|
#include <sys/time.h>
|
17
18
|
#include <time.h>
|
@@ -25,20 +26,15 @@
|
|
25
26
|
#define FAKE_FRAME_MARK INT2FIX(1)
|
26
27
|
#define FAKE_FRAME_SWEEP INT2FIX(2)
|
27
28
|
|
28
|
-
/*
|
29
|
-
* As of Ruby 3.0, it should be safe to read stack frames at any time
|
30
|
-
* See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21
|
31
|
-
*/
|
32
|
-
#if RUBY_API_VERSION_MAJOR < 3
|
33
|
-
#define USE_POSTPONED_JOB
|
34
|
-
#endif
|
35
|
-
|
36
29
|
static const char *fake_frame_cstrs[] = {
|
37
30
|
"(garbage collection)",
|
38
31
|
"(marking)",
|
39
32
|
"(sweeping)",
|
40
33
|
};
|
41
34
|
|
35
|
+
static int stackprof_use_postponed_job = 1;
|
36
|
+
static int ruby_vm_running = 0;
|
37
|
+
|
42
38
|
#define TOTAL_FAKE_FRAMES (sizeof(fake_frame_cstrs) / sizeof(char *))
|
43
39
|
|
44
40
|
#ifdef _POSIX_MONOTONIC_CLOCK
|
@@ -131,6 +127,8 @@ static struct {
|
|
131
127
|
sample_time_t buffer_time;
|
132
128
|
VALUE frames_buffer[BUF_SIZE];
|
133
129
|
int lines_buffer[BUF_SIZE];
|
130
|
+
|
131
|
+
pthread_t target_thread;
|
134
132
|
} _stackprof;
|
135
133
|
|
136
134
|
static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
|
@@ -152,6 +150,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
|
|
152
150
|
VALUE opts = Qnil, mode = Qnil, interval = Qnil, metadata = rb_hash_new(), out = Qfalse;
|
153
151
|
int ignore_gc = 0;
|
154
152
|
int raw = 0, aggregate = 1;
|
153
|
+
VALUE metadata_val;
|
155
154
|
|
156
155
|
if (_stackprof.running)
|
157
156
|
return Qfalse;
|
@@ -166,7 +165,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
|
|
166
165
|
ignore_gc = 1;
|
167
166
|
}
|
168
167
|
|
169
|
-
|
168
|
+
metadata_val = rb_hash_aref(opts, sym_metadata);
|
170
169
|
if (RTEST(metadata_val)) {
|
171
170
|
if (!RB_TYPE_P(metadata_val, T_HASH))
|
172
171
|
rb_raise(rb_eArgError, "metadata should be a hash");
|
@@ -224,6 +223,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
|
|
224
223
|
_stackprof.ignore_gc = ignore_gc;
|
225
224
|
_stackprof.metadata = metadata;
|
226
225
|
_stackprof.out = out;
|
226
|
+
_stackprof.target_thread = pthread_self();
|
227
227
|
|
228
228
|
if (raw) {
|
229
229
|
capture_timestamp(&_stackprof.last_sample_at);
|
@@ -603,14 +603,15 @@ stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t ti
|
|
603
603
|
void
|
604
604
|
stackprof_buffer_sample(void)
|
605
605
|
{
|
606
|
+
uint64_t start_timestamp = 0;
|
607
|
+
int64_t timestamp_delta = 0;
|
608
|
+
int num;
|
609
|
+
|
606
610
|
if (_stackprof.buffer_count > 0) {
|
607
611
|
// Another sample is already pending
|
608
612
|
return;
|
609
613
|
}
|
610
614
|
|
611
|
-
uint64_t start_timestamp = 0;
|
612
|
-
int64_t timestamp_delta = 0;
|
613
|
-
int num;
|
614
615
|
if (_stackprof.raw) {
|
615
616
|
struct timestamp_t t;
|
616
617
|
capture_timestamp(&t);
|
@@ -701,7 +702,6 @@ stackprof_job_record_gc(void *data)
|
|
701
702
|
stackprof_record_gc_samples();
|
702
703
|
}
|
703
704
|
|
704
|
-
#ifdef USE_POSTPONED_JOB
|
705
705
|
static void
|
706
706
|
stackprof_job_sample_and_record(void *data)
|
707
707
|
{
|
@@ -709,7 +709,6 @@ stackprof_job_sample_and_record(void *data)
|
|
709
709
|
|
710
710
|
stackprof_sample_and_record();
|
711
711
|
}
|
712
|
-
#endif
|
713
712
|
|
714
713
|
static void
|
715
714
|
stackprof_job_record_buffer(void *data)
|
@@ -727,7 +726,27 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
|
|
727
726
|
_stackprof.overall_signals++;
|
728
727
|
|
729
728
|
if (!_stackprof.running) return;
|
730
|
-
|
729
|
+
|
730
|
+
// There's a possibility that the signal handler is invoked *after* the Ruby
|
731
|
+
// VM has been shut down (e.g. after ruby_cleanup(0)). In this case, things
|
732
|
+
// that rely on global VM state (e.g. rb_during_gc) will segfault.
|
733
|
+
if (!ruby_vm_running) return;
|
734
|
+
|
735
|
+
if (_stackprof.mode == sym_wall) {
|
736
|
+
// In "wall" mode, the SIGALRM signal will arrive at an arbitrary thread.
|
737
|
+
// In order to provide more useful results, especially under threaded web
|
738
|
+
// servers, we want to forward this signal to the original thread
|
739
|
+
// StackProf was started from.
|
740
|
+
// According to POSIX.1-2008 TC1 pthread_kill and pthread_self should be
|
741
|
+
// async-signal-safe.
|
742
|
+
if (pthread_self() != _stackprof.target_thread) {
|
743
|
+
pthread_kill(_stackprof.target_thread, sig);
|
744
|
+
return;
|
745
|
+
}
|
746
|
+
} else {
|
747
|
+
if (!ruby_native_thread_p()) return;
|
748
|
+
}
|
749
|
+
|
731
750
|
if (pthread_mutex_trylock(&lock)) return;
|
732
751
|
|
733
752
|
if (!_stackprof.ignore_gc && rb_during_gc()) {
|
@@ -740,15 +759,15 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
|
|
740
759
|
_stackprof.unrecorded_gc_samples++;
|
741
760
|
rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0);
|
742
761
|
} else {
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
762
|
+
if (stackprof_use_postponed_job) {
|
763
|
+
rb_postponed_job_register_one(0, stackprof_job_sample_and_record, (void*)0);
|
764
|
+
} else {
|
765
|
+
// Buffer a sample immediately, if an existing sample exists this will
|
766
|
+
// return immediately
|
767
|
+
stackprof_buffer_sample();
|
768
|
+
// Enqueue a job to record the sample
|
769
|
+
rb_postponed_job_register_one(0, stackprof_job_record_buffer, (void*)0);
|
770
|
+
}
|
752
771
|
}
|
753
772
|
pthread_mutex_unlock(&lock);
|
754
773
|
}
|
@@ -792,6 +811,10 @@ stackprof_gc_mark(void *data)
|
|
792
811
|
|
793
812
|
if (_stackprof.frames)
|
794
813
|
st_foreach(_stackprof.frames, frame_mark_i, 0);
|
814
|
+
|
815
|
+
for (int i = 0; i < _stackprof.buffer_count; i++) {
|
816
|
+
rb_gc_mark(_stackprof.frames_buffer[i]);
|
817
|
+
}
|
795
818
|
}
|
796
819
|
|
797
820
|
static void
|
@@ -826,10 +849,32 @@ stackprof_atfork_child(void)
|
|
826
849
|
stackprof_stop(rb_mStackProf);
|
827
850
|
}
|
828
851
|
|
852
|
+
static VALUE
|
853
|
+
stackprof_use_postponed_job_l(VALUE self)
|
854
|
+
{
|
855
|
+
stackprof_use_postponed_job = 1;
|
856
|
+
return Qnil;
|
857
|
+
}
|
858
|
+
|
859
|
+
static void
|
860
|
+
stackprof_at_exit(ruby_vm_t* vm)
|
861
|
+
{
|
862
|
+
ruby_vm_running = 0;
|
863
|
+
}
|
864
|
+
|
829
865
|
void
|
830
866
|
Init_stackprof(void)
|
831
867
|
{
|
832
868
|
size_t i;
|
869
|
+
/*
|
870
|
+
* As of Ruby 3.0, it should be safe to read stack frames at any time, unless YJIT is enabled
|
871
|
+
* See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21
|
872
|
+
*/
|
873
|
+
stackprof_use_postponed_job = RUBY_API_VERSION_MAJOR < 3;
|
874
|
+
|
875
|
+
ruby_vm_running = 1;
|
876
|
+
ruby_vm_at_exit(stackprof_at_exit);
|
877
|
+
|
833
878
|
#define S(name) sym_##name = ID2SYM(rb_intern(#name));
|
834
879
|
S(object);
|
835
880
|
S(custom);
|
@@ -890,6 +935,7 @@ Init_stackprof(void)
|
|
890
935
|
rb_define_singleton_method(rb_mStackProf, "stop", stackprof_stop, 0);
|
891
936
|
rb_define_singleton_method(rb_mStackProf, "results", stackprof_results, -1);
|
892
937
|
rb_define_singleton_method(rb_mStackProf, "sample", stackprof_sample, 0);
|
938
|
+
rb_define_singleton_method(rb_mStackProf, "use_postponed_job!", stackprof_use_postponed_job_l, 0);
|
893
939
|
|
894
940
|
pthread_atfork(stackprof_atfork_prepare, stackprof_atfork_parent, stackprof_atfork_child);
|
895
941
|
}
|
@@ -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,10 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'pp'
|
4
|
-
require 'digest/
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'json'
|
5
6
|
|
6
7
|
module StackProf
|
7
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
|
+
|
8
42
|
def initialize(data)
|
9
43
|
@data = data
|
10
44
|
end
|
@@ -18,7 +52,7 @@ module StackProf
|
|
18
52
|
def normalized_frames
|
19
53
|
id2hash = {}
|
20
54
|
@data[:frames].each do |frame, info|
|
21
|
-
id2hash[frame.to_s] = info[:hash] = Digest::
|
55
|
+
id2hash[frame.to_s] = info[:hash] = Digest::SHA256.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}")
|
22
56
|
end
|
23
57
|
@data[:frames].inject(Hash.new) do |hash, (frame, info)|
|
24
58
|
info = hash[id2hash[frame.to_s]] = info.dup
|
@@ -191,7 +225,7 @@ module StackProf
|
|
191
225
|
end
|
192
226
|
else
|
193
227
|
frame = @data[:frames][val]
|
194
|
-
child_name = "#{ frame[:name] } : #{ frame[:file] }"
|
228
|
+
child_name = "#{ frame[:name] } : #{ frame[:file] } : #{ frame[:line] }"
|
195
229
|
child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
|
196
230
|
weight += child_data["value"]
|
197
231
|
children << child_data
|
@@ -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,20 @@
|
|
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
|
2
15
|
|
3
16
|
module StackProf
|
4
|
-
VERSION = '0.2.
|
17
|
+
VERSION = '0.2.25'
|
5
18
|
end
|
6
19
|
|
7
20
|
StackProf.autoload :Report, "stackprof/report.rb"
|
data/stackprof.gemspec
CHANGED
@@ -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]
|
@@ -304,4 +308,4 @@ class StackProfTest < MiniTest::Test
|
|
304
308
|
r.close
|
305
309
|
w.close
|
306
310
|
end
|
307
|
-
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
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stackprof
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.25
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aman Gupta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -76,15 +76,20 @@ files:
|
|
76
76
|
- ext/stackprof/extconf.rb
|
77
77
|
- ext/stackprof/stackprof.c
|
78
78
|
- lib/stackprof.rb
|
79
|
+
- lib/stackprof/autorun.rb
|
79
80
|
- lib/stackprof/flamegraph/flamegraph.js
|
80
81
|
- lib/stackprof/flamegraph/viewer.html
|
81
82
|
- lib/stackprof/middleware.rb
|
82
83
|
- lib/stackprof/report.rb
|
84
|
+
- lib/stackprof/truffleruby.rb
|
83
85
|
- sample.rb
|
84
86
|
- stackprof.gemspec
|
87
|
+
- test/fixtures/profile.dump
|
88
|
+
- test/fixtures/profile.json
|
85
89
|
- test/test_middleware.rb
|
86
90
|
- test/test_report.rb
|
87
91
|
- test/test_stackprof.rb
|
92
|
+
- test/test_truffleruby.rb
|
88
93
|
- vendor/FlameGraph/README
|
89
94
|
- vendor/FlameGraph/flamegraph.pl
|
90
95
|
- vendor/gprof2dot/gprof2dot.py
|
@@ -94,9 +99,9 @@ licenses:
|
|
94
99
|
- MIT
|
95
100
|
metadata:
|
96
101
|
bug_tracker_uri: https://github.com/tmm1/stackprof/issues
|
97
|
-
changelog_uri: https://github.com/tmm1/stackprof/blob/v0.2.
|
98
|
-
documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.
|
99
|
-
source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.
|
102
|
+
changelog_uri: https://github.com/tmm1/stackprof/blob/v0.2.25/CHANGELOG.md
|
103
|
+
documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.25
|
104
|
+
source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.25
|
100
105
|
post_install_message:
|
101
106
|
rdoc_options: []
|
102
107
|
require_paths:
|