stackprof 0.2.17 → 0.2.24
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/Rakefile +14 -4
- data/bin/stackprof +115 -81
- data/ext/stackprof/extconf.rb +6 -0
- data/ext/stackprof/stackprof.c +223 -74
- data/lib/stackprof/autorun.rb +19 -0
- data/lib/stackprof/report.rb +58 -55
- 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 +32 -5
- data/test/test_truffleruby.rb +18 -0
- metadata +14 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90e33127951d53147b6a65704446ec812999df9525d868865029ee1c7450445d
|
4
|
+
data.tar.gz: 19a5f104482f355cb1957b71b18b15b9ca9bb981cfcc109688366e53704c5711
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17610238200852f97afcc730efd9171c46c3a001ff7d5b82cb65fc92f96d9d2b206474bcecc7eeee1981365d2a07fd4dad460edc54a8bc9c8b1e6c7412f7cde3
|
7
|
+
data.tar.gz: 35cc1394b526edd0782468b06d54f66eb1ff10e5741becf486f68f0b141511fad9db622816004a4ae2e1a59cff048abe2d5697848eff127640663e9246f33cca
|
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/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
@@ -7,37 +7,77 @@
|
|
7
7
|
**********************************************************************/
|
8
8
|
|
9
9
|
#include <ruby/ruby.h>
|
10
|
+
#include <ruby/version.h>
|
10
11
|
#include <ruby/debug.h>
|
11
12
|
#include <ruby/st.h>
|
12
13
|
#include <ruby/io.h>
|
13
14
|
#include <ruby/intern.h>
|
15
|
+
#include <ruby/vm.h>
|
14
16
|
#include <signal.h>
|
15
17
|
#include <sys/time.h>
|
18
|
+
#include <time.h>
|
16
19
|
#include <pthread.h>
|
17
20
|
|
18
21
|
#define BUF_SIZE 2048
|
19
22
|
#define MICROSECONDS_IN_SECOND 1000000
|
23
|
+
#define NANOSECONDS_IN_SECOND 1000000000
|
20
24
|
|
21
25
|
#define FAKE_FRAME_GC INT2FIX(0)
|
22
26
|
#define FAKE_FRAME_MARK INT2FIX(1)
|
23
27
|
#define FAKE_FRAME_SWEEP INT2FIX(2)
|
24
28
|
|
25
|
-
/*
|
26
|
-
* As of Ruby 3.0, it should be safe to read stack frames at any time
|
27
|
-
* See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21
|
28
|
-
*/
|
29
|
-
#if RUBY_API_VERSION_MAJOR < 3
|
30
|
-
#define USE_POSTPONED_JOB
|
31
|
-
#endif
|
32
|
-
|
33
29
|
static const char *fake_frame_cstrs[] = {
|
34
30
|
"(garbage collection)",
|
35
31
|
"(marking)",
|
36
32
|
"(sweeping)",
|
37
33
|
};
|
38
34
|
|
35
|
+
static int stackprof_use_postponed_job = 1;
|
36
|
+
static int ruby_vm_running = 0;
|
37
|
+
|
39
38
|
#define TOTAL_FAKE_FRAMES (sizeof(fake_frame_cstrs) / sizeof(char *))
|
40
39
|
|
40
|
+
#ifdef _POSIX_MONOTONIC_CLOCK
|
41
|
+
#define timestamp_t timespec
|
42
|
+
typedef struct timestamp_t timestamp_t;
|
43
|
+
|
44
|
+
static void capture_timestamp(timestamp_t *ts) {
|
45
|
+
clock_gettime(CLOCK_MONOTONIC, ts);
|
46
|
+
}
|
47
|
+
|
48
|
+
static int64_t delta_usec(timestamp_t *start, timestamp_t *end) {
|
49
|
+
int64_t result = MICROSECONDS_IN_SECOND * (end->tv_sec - start->tv_sec);
|
50
|
+
if (end->tv_nsec < start->tv_nsec) {
|
51
|
+
result -= MICROSECONDS_IN_SECOND;
|
52
|
+
result += (NANOSECONDS_IN_SECOND + end->tv_nsec - start->tv_nsec) / 1000;
|
53
|
+
} else {
|
54
|
+
result += (end->tv_nsec - start->tv_nsec) / 1000;
|
55
|
+
}
|
56
|
+
return result;
|
57
|
+
}
|
58
|
+
|
59
|
+
static uint64_t timestamp_usec(timestamp_t *ts) {
|
60
|
+
return (MICROSECONDS_IN_SECOND * ts->tv_sec) + (ts->tv_nsec / 1000);
|
61
|
+
}
|
62
|
+
#else
|
63
|
+
#define timestamp_t timeval
|
64
|
+
typedef struct timestamp_t timestamp_t;
|
65
|
+
|
66
|
+
static void capture_timestamp(timestamp_t *ts) {
|
67
|
+
gettimeofday(ts, NULL);
|
68
|
+
}
|
69
|
+
|
70
|
+
static int64_t delta_usec(timestamp_t *start, timestamp_t *end) {
|
71
|
+
struct timeval diff;
|
72
|
+
timersub(end, start, &diff);
|
73
|
+
return (MICROSECONDS_IN_SECOND * diff.tv_sec) + diff.tv_usec;
|
74
|
+
}
|
75
|
+
|
76
|
+
static uint64_t timestamp_usec(timestamp_t *ts) {
|
77
|
+
return (MICROSECONDS_IN_SECOND * ts.tv_sec) + diff.tv_usec
|
78
|
+
}
|
79
|
+
#endif
|
80
|
+
|
41
81
|
typedef struct {
|
42
82
|
size_t total_samples;
|
43
83
|
size_t caller_samples;
|
@@ -46,6 +86,11 @@ typedef struct {
|
|
46
86
|
st_table *lines;
|
47
87
|
} frame_data_t;
|
48
88
|
|
89
|
+
typedef struct {
|
90
|
+
uint64_t timestamp_usec;
|
91
|
+
int64_t delta_usec;
|
92
|
+
} sample_time_t;
|
93
|
+
|
49
94
|
static struct {
|
50
95
|
int running;
|
51
96
|
int raw;
|
@@ -62,10 +107,10 @@ static struct {
|
|
62
107
|
size_t raw_samples_capa;
|
63
108
|
size_t raw_sample_index;
|
64
109
|
|
65
|
-
struct
|
66
|
-
|
67
|
-
size_t
|
68
|
-
size_t
|
110
|
+
struct timestamp_t last_sample_at;
|
111
|
+
sample_time_t *raw_sample_times;
|
112
|
+
size_t raw_sample_times_len;
|
113
|
+
size_t raw_sample_times_capa;
|
69
114
|
|
70
115
|
size_t overall_signals;
|
71
116
|
size_t overall_samples;
|
@@ -77,14 +122,19 @@ static struct {
|
|
77
122
|
|
78
123
|
VALUE fake_frame_names[TOTAL_FAKE_FRAMES];
|
79
124
|
VALUE empty_string;
|
125
|
+
|
126
|
+
int buffer_count;
|
127
|
+
sample_time_t buffer_time;
|
80
128
|
VALUE frames_buffer[BUF_SIZE];
|
81
129
|
int lines_buffer[BUF_SIZE];
|
130
|
+
|
131
|
+
pthread_t target_thread;
|
82
132
|
} _stackprof;
|
83
133
|
|
84
134
|
static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
|
85
135
|
static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines;
|
86
136
|
static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_metadata, sym_frames, sym_ignore_gc, sym_out;
|
87
|
-
static VALUE sym_aggregate, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping;
|
137
|
+
static VALUE sym_aggregate, sym_raw_sample_timestamps, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping;
|
88
138
|
static VALUE sym_gc_samples, objtracer;
|
89
139
|
static VALUE gc_hook;
|
90
140
|
static VALUE rb_mStackProf;
|
@@ -100,6 +150,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
|
|
100
150
|
VALUE opts = Qnil, mode = Qnil, interval = Qnil, metadata = rb_hash_new(), out = Qfalse;
|
101
151
|
int ignore_gc = 0;
|
102
152
|
int raw = 0, aggregate = 1;
|
153
|
+
VALUE metadata_val;
|
103
154
|
|
104
155
|
if (_stackprof.running)
|
105
156
|
return Qfalse;
|
@@ -114,7 +165,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
|
|
114
165
|
ignore_gc = 1;
|
115
166
|
}
|
116
167
|
|
117
|
-
|
168
|
+
metadata_val = rb_hash_aref(opts, sym_metadata);
|
118
169
|
if (RTEST(metadata_val)) {
|
119
170
|
if (!RB_TYPE_P(metadata_val, T_HASH))
|
120
171
|
rb_raise(rb_eArgError, "metadata should be a hash");
|
@@ -172,9 +223,10 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
|
|
172
223
|
_stackprof.ignore_gc = ignore_gc;
|
173
224
|
_stackprof.metadata = metadata;
|
174
225
|
_stackprof.out = out;
|
226
|
+
_stackprof.target_thread = pthread_self();
|
175
227
|
|
176
228
|
if (raw) {
|
177
|
-
|
229
|
+
capture_timestamp(&_stackprof.last_sample_at);
|
178
230
|
}
|
179
231
|
|
180
232
|
return Qtrue;
|
@@ -209,13 +261,19 @@ stackprof_stop(VALUE self)
|
|
209
261
|
return Qtrue;
|
210
262
|
}
|
211
263
|
|
264
|
+
#if SIZEOF_VOIDP == SIZEOF_LONG
|
265
|
+
# define PTR2NUM(x) (LONG2NUM((long)(x)))
|
266
|
+
#else
|
267
|
+
# define PTR2NUM(x) (LL2NUM((LONG_LONG)(x)))
|
268
|
+
#endif
|
269
|
+
|
212
270
|
static int
|
213
271
|
frame_edges_i(st_data_t key, st_data_t val, st_data_t arg)
|
214
272
|
{
|
215
273
|
VALUE edges = (VALUE)arg;
|
216
274
|
|
217
275
|
intptr_t weight = (intptr_t)val;
|
218
|
-
rb_hash_aset(edges,
|
276
|
+
rb_hash_aset(edges, PTR2NUM(key), INT2FIX(weight));
|
219
277
|
return ST_CONTINUE;
|
220
278
|
}
|
221
279
|
|
@@ -242,7 +300,7 @@ frame_i(st_data_t key, st_data_t val, st_data_t arg)
|
|
242
300
|
VALUE name, file, edges, lines;
|
243
301
|
VALUE line;
|
244
302
|
|
245
|
-
rb_hash_aset(results,
|
303
|
+
rb_hash_aset(results, PTR2NUM(frame), details);
|
246
304
|
|
247
305
|
if (FIXNUM_P(frame)) {
|
248
306
|
name = _stackprof.fake_frame_names[FIX2INT(frame)];
|
@@ -314,7 +372,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
|
|
314
372
|
|
315
373
|
if (_stackprof.raw && _stackprof.raw_samples_len) {
|
316
374
|
size_t len, n, o;
|
317
|
-
VALUE raw_timestamp_deltas;
|
375
|
+
VALUE raw_sample_timestamps, raw_timestamp_deltas;
|
318
376
|
VALUE raw_samples = rb_ary_new_capa(_stackprof.raw_samples_len);
|
319
377
|
|
320
378
|
for (n = 0; n < _stackprof.raw_samples_len; n++) {
|
@@ -322,7 +380,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
|
|
322
380
|
rb_ary_push(raw_samples, SIZET2NUM(len));
|
323
381
|
|
324
382
|
for (o = 0, n++; o < len; n++, o++)
|
325
|
-
rb_ary_push(raw_samples,
|
383
|
+
rb_ary_push(raw_samples, PTR2NUM(_stackprof.raw_samples[n]));
|
326
384
|
rb_ary_push(raw_samples, SIZET2NUM((size_t)_stackprof.raw_samples[n]));
|
327
385
|
}
|
328
386
|
|
@@ -334,17 +392,20 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
|
|
334
392
|
|
335
393
|
rb_hash_aset(results, sym_raw, raw_samples);
|
336
394
|
|
337
|
-
|
395
|
+
raw_sample_timestamps = rb_ary_new_capa(_stackprof.raw_sample_times_len);
|
396
|
+
raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_sample_times_len);
|
338
397
|
|
339
|
-
for (n = 0; n < _stackprof.
|
340
|
-
rb_ary_push(
|
398
|
+
for (n = 0; n < _stackprof.raw_sample_times_len; n++) {
|
399
|
+
rb_ary_push(raw_sample_timestamps, ULL2NUM(_stackprof.raw_sample_times[n].timestamp_usec));
|
400
|
+
rb_ary_push(raw_timestamp_deltas, LL2NUM(_stackprof.raw_sample_times[n].delta_usec));
|
341
401
|
}
|
342
402
|
|
343
|
-
free(_stackprof.
|
344
|
-
_stackprof.
|
345
|
-
_stackprof.
|
346
|
-
_stackprof.
|
403
|
+
free(_stackprof.raw_sample_times);
|
404
|
+
_stackprof.raw_sample_times = NULL;
|
405
|
+
_stackprof.raw_sample_times_len = 0;
|
406
|
+
_stackprof.raw_sample_times_capa = 0;
|
347
407
|
|
408
|
+
rb_hash_aset(results, sym_raw_sample_timestamps, raw_sample_timestamps);
|
348
409
|
rb_hash_aset(results, sym_raw_timestamp_deltas, raw_timestamp_deltas);
|
349
410
|
|
350
411
|
_stackprof.raw = 0;
|
@@ -424,14 +485,14 @@ st_numtable_increment(st_table *table, st_data_t key, size_t increment)
|
|
424
485
|
}
|
425
486
|
|
426
487
|
void
|
427
|
-
stackprof_record_sample_for_stack(int num,
|
488
|
+
stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t timestamp_delta)
|
428
489
|
{
|
429
490
|
int i, n;
|
430
491
|
VALUE prev_frame = Qnil;
|
431
492
|
|
432
493
|
_stackprof.overall_samples++;
|
433
494
|
|
434
|
-
if (_stackprof.raw) {
|
495
|
+
if (_stackprof.raw && num > 0) {
|
435
496
|
int found = 0;
|
436
497
|
|
437
498
|
/* If there's no sample buffer allocated, then allocate one. The buffer
|
@@ -483,20 +544,23 @@ stackprof_record_sample_for_stack(int num, int timestamp_delta)
|
|
483
544
|
}
|
484
545
|
|
485
546
|
/* If there's no timestamp delta buffer, allocate one */
|
486
|
-
if (!_stackprof.
|
487
|
-
_stackprof.
|
488
|
-
_stackprof.
|
489
|
-
_stackprof.
|
547
|
+
if (!_stackprof.raw_sample_times) {
|
548
|
+
_stackprof.raw_sample_times_capa = 100;
|
549
|
+
_stackprof.raw_sample_times = malloc(sizeof(sample_time_t) * _stackprof.raw_sample_times_capa);
|
550
|
+
_stackprof.raw_sample_times_len = 0;
|
490
551
|
}
|
491
552
|
|
492
553
|
/* Double the buffer size if it's too small */
|
493
|
-
while (_stackprof.
|
494
|
-
_stackprof.
|
495
|
-
_stackprof.
|
554
|
+
while (_stackprof.raw_sample_times_capa <= _stackprof.raw_sample_times_len + 1) {
|
555
|
+
_stackprof.raw_sample_times_capa *= 2;
|
556
|
+
_stackprof.raw_sample_times = realloc(_stackprof.raw_sample_times, sizeof(sample_time_t) * _stackprof.raw_sample_times_capa);
|
496
557
|
}
|
497
558
|
|
498
|
-
/* Store the time delta (which is the amount of
|
499
|
-
_stackprof.
|
559
|
+
/* Store the time delta (which is the amount of microseconds between samples). */
|
560
|
+
_stackprof.raw_sample_times[_stackprof.raw_sample_times_len++] = (sample_time_t) {
|
561
|
+
.timestamp_usec = sample_timestamp,
|
562
|
+
.delta_usec = timestamp_delta,
|
563
|
+
};
|
500
564
|
}
|
501
565
|
|
502
566
|
for (i = 0; i < num; i++) {
|
@@ -529,48 +593,60 @@ stackprof_record_sample_for_stack(int num, int timestamp_delta)
|
|
529
593
|
}
|
530
594
|
|
531
595
|
if (_stackprof.raw) {
|
532
|
-
|
596
|
+
capture_timestamp(&_stackprof.last_sample_at);
|
533
597
|
}
|
534
598
|
}
|
535
599
|
|
600
|
+
// buffer the current profile frames
|
601
|
+
// This must be async-signal-safe
|
602
|
+
// Returns immediately if another set of frames are already in the buffer
|
536
603
|
void
|
537
|
-
|
604
|
+
stackprof_buffer_sample(void)
|
538
605
|
{
|
539
|
-
|
606
|
+
uint64_t start_timestamp = 0;
|
607
|
+
int64_t timestamp_delta = 0;
|
540
608
|
int num;
|
609
|
+
|
610
|
+
if (_stackprof.buffer_count > 0) {
|
611
|
+
// Another sample is already pending
|
612
|
+
return;
|
613
|
+
}
|
614
|
+
|
541
615
|
if (_stackprof.raw) {
|
542
|
-
struct
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
timestamp_delta = (1000 * diff.tv_sec) + diff.tv_usec;
|
616
|
+
struct timestamp_t t;
|
617
|
+
capture_timestamp(&t);
|
618
|
+
start_timestamp = timestamp_usec(&t);
|
619
|
+
timestamp_delta = delta_usec(&_stackprof.last_sample_at, &t);
|
547
620
|
}
|
621
|
+
|
548
622
|
num = rb_profile_frames(0, sizeof(_stackprof.frames_buffer) / sizeof(VALUE), _stackprof.frames_buffer, _stackprof.lines_buffer);
|
549
|
-
|
623
|
+
|
624
|
+
_stackprof.buffer_count = num;
|
625
|
+
_stackprof.buffer_time.timestamp_usec = start_timestamp;
|
626
|
+
_stackprof.buffer_time.delta_usec = timestamp_delta;
|
550
627
|
}
|
551
628
|
|
552
629
|
void
|
553
|
-
stackprof_record_gc_samples()
|
630
|
+
stackprof_record_gc_samples(void)
|
554
631
|
{
|
555
|
-
|
556
|
-
|
632
|
+
int64_t delta_to_first_unrecorded_gc_sample = 0;
|
633
|
+
uint64_t start_timestamp = 0;
|
634
|
+
size_t i;
|
557
635
|
if (_stackprof.raw) {
|
558
|
-
struct
|
559
|
-
|
560
|
-
|
561
|
-
timersub(&t, &_stackprof.last_sample_at, &diff);
|
636
|
+
struct timestamp_t t;
|
637
|
+
capture_timestamp(&t);
|
638
|
+
start_timestamp = timestamp_usec(&t);
|
562
639
|
|
563
640
|
// We don't know when the GC samples were actually marked, so let's
|
564
641
|
// assume that they were marked at a perfectly regular interval.
|
565
|
-
delta_to_first_unrecorded_gc_sample = (
|
642
|
+
delta_to_first_unrecorded_gc_sample = delta_usec(&_stackprof.last_sample_at, &t) - (_stackprof.unrecorded_gc_samples - 1) * NUM2LONG(_stackprof.interval);
|
566
643
|
if (delta_to_first_unrecorded_gc_sample < 0) {
|
567
644
|
delta_to_first_unrecorded_gc_sample = 0;
|
568
645
|
}
|
569
646
|
}
|
570
647
|
|
571
|
-
|
572
648
|
for (i = 0; i < _stackprof.unrecorded_gc_samples; i++) {
|
573
|
-
|
649
|
+
int64_t timestamp_delta = i == 0 ? delta_to_first_unrecorded_gc_sample : NUM2LONG(_stackprof.interval);
|
574
650
|
|
575
651
|
if (_stackprof.unrecorded_gc_marking_samples) {
|
576
652
|
_stackprof.frames_buffer[0] = FAKE_FRAME_MARK;
|
@@ -579,7 +655,7 @@ stackprof_record_gc_samples()
|
|
579
655
|
_stackprof.lines_buffer[1] = 0;
|
580
656
|
_stackprof.unrecorded_gc_marking_samples--;
|
581
657
|
|
582
|
-
stackprof_record_sample_for_stack(2, timestamp_delta);
|
658
|
+
stackprof_record_sample_for_stack(2, start_timestamp, timestamp_delta);
|
583
659
|
} else if (_stackprof.unrecorded_gc_sweeping_samples) {
|
584
660
|
_stackprof.frames_buffer[0] = FAKE_FRAME_SWEEP;
|
585
661
|
_stackprof.lines_buffer[0] = 0;
|
@@ -588,11 +664,11 @@ stackprof_record_gc_samples()
|
|
588
664
|
|
589
665
|
_stackprof.unrecorded_gc_sweeping_samples--;
|
590
666
|
|
591
|
-
stackprof_record_sample_for_stack(2, timestamp_delta);
|
667
|
+
stackprof_record_sample_for_stack(2, start_timestamp, timestamp_delta);
|
592
668
|
} else {
|
593
669
|
_stackprof.frames_buffer[0] = FAKE_FRAME_GC;
|
594
670
|
_stackprof.lines_buffer[0] = 0;
|
595
|
-
stackprof_record_sample_for_stack(1, timestamp_delta);
|
671
|
+
stackprof_record_sample_for_stack(1, start_timestamp, timestamp_delta);
|
596
672
|
}
|
597
673
|
}
|
598
674
|
_stackprof.during_gc += _stackprof.unrecorded_gc_samples;
|
@@ -601,8 +677,25 @@ stackprof_record_gc_samples()
|
|
601
677
|
_stackprof.unrecorded_gc_sweeping_samples = 0;
|
602
678
|
}
|
603
679
|
|
680
|
+
// record the sample previously buffered by stackprof_buffer_sample
|
681
|
+
static void
|
682
|
+
stackprof_record_buffer(void)
|
683
|
+
{
|
684
|
+
stackprof_record_sample_for_stack(_stackprof.buffer_count, _stackprof.buffer_time.timestamp_usec, _stackprof.buffer_time.delta_usec);
|
685
|
+
|
686
|
+
// reset the buffer
|
687
|
+
_stackprof.buffer_count = 0;
|
688
|
+
}
|
689
|
+
|
690
|
+
static void
|
691
|
+
stackprof_sample_and_record(void)
|
692
|
+
{
|
693
|
+
stackprof_buffer_sample();
|
694
|
+
stackprof_record_buffer();
|
695
|
+
}
|
696
|
+
|
604
697
|
static void
|
605
|
-
|
698
|
+
stackprof_job_record_gc(void *data)
|
606
699
|
{
|
607
700
|
if (!_stackprof.running) return;
|
608
701
|
|
@@ -610,11 +703,19 @@ stackprof_gc_job_handler(void *data)
|
|
610
703
|
}
|
611
704
|
|
612
705
|
static void
|
613
|
-
|
706
|
+
stackprof_job_sample_and_record(void *data)
|
614
707
|
{
|
615
708
|
if (!_stackprof.running) return;
|
616
709
|
|
617
|
-
|
710
|
+
stackprof_sample_and_record();
|
711
|
+
}
|
712
|
+
|
713
|
+
static void
|
714
|
+
stackprof_job_record_buffer(void *data)
|
715
|
+
{
|
716
|
+
if (!_stackprof.running) return;
|
717
|
+
|
718
|
+
stackprof_record_buffer();
|
618
719
|
}
|
619
720
|
|
620
721
|
static void
|
@@ -625,7 +726,27 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
|
|
625
726
|
_stackprof.overall_signals++;
|
626
727
|
|
627
728
|
if (!_stackprof.running) return;
|
628
|
-
|
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
|
+
|
629
750
|
if (pthread_mutex_trylock(&lock)) return;
|
630
751
|
|
631
752
|
if (!_stackprof.ignore_gc && rb_during_gc()) {
|
@@ -636,13 +757,17 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
|
|
636
757
|
_stackprof.unrecorded_gc_sweeping_samples++;
|
637
758
|
}
|
638
759
|
_stackprof.unrecorded_gc_samples++;
|
639
|
-
rb_postponed_job_register_one(0,
|
760
|
+
rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0);
|
640
761
|
} else {
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
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
|
+
}
|
646
771
|
}
|
647
772
|
pthread_mutex_unlock(&lock);
|
648
773
|
}
|
@@ -653,7 +778,7 @@ stackprof_newobj_handler(VALUE tpval, void *data)
|
|
653
778
|
_stackprof.overall_signals++;
|
654
779
|
if (RTEST(_stackprof.interval) && _stackprof.overall_signals % NUM2LONG(_stackprof.interval))
|
655
780
|
return;
|
656
|
-
|
781
|
+
stackprof_sample_and_record();
|
657
782
|
}
|
658
783
|
|
659
784
|
static VALUE
|
@@ -663,7 +788,7 @@ stackprof_sample(VALUE self)
|
|
663
788
|
return Qfalse;
|
664
789
|
|
665
790
|
_stackprof.overall_signals++;
|
666
|
-
|
791
|
+
stackprof_sample_and_record();
|
667
792
|
return Qtrue;
|
668
793
|
}
|
669
794
|
|
@@ -720,10 +845,32 @@ stackprof_atfork_child(void)
|
|
720
845
|
stackprof_stop(rb_mStackProf);
|
721
846
|
}
|
722
847
|
|
848
|
+
static VALUE
|
849
|
+
stackprof_use_postponed_job_l(VALUE self)
|
850
|
+
{
|
851
|
+
stackprof_use_postponed_job = 1;
|
852
|
+
return Qnil;
|
853
|
+
}
|
854
|
+
|
855
|
+
static void
|
856
|
+
stackprof_at_exit(ruby_vm_t* vm)
|
857
|
+
{
|
858
|
+
ruby_vm_running = 0;
|
859
|
+
}
|
860
|
+
|
723
861
|
void
|
724
862
|
Init_stackprof(void)
|
725
863
|
{
|
726
864
|
size_t i;
|
865
|
+
/*
|
866
|
+
* As of Ruby 3.0, it should be safe to read stack frames at any time, unless YJIT is enabled
|
867
|
+
* See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21
|
868
|
+
*/
|
869
|
+
stackprof_use_postponed_job = RUBY_API_VERSION_MAJOR < 3;
|
870
|
+
|
871
|
+
ruby_vm_running = 1;
|
872
|
+
ruby_vm_at_exit(stackprof_at_exit);
|
873
|
+
|
727
874
|
#define S(name) sym_##name = ID2SYM(rb_intern(#name));
|
728
875
|
S(object);
|
729
876
|
S(custom);
|
@@ -742,6 +889,7 @@ Init_stackprof(void)
|
|
742
889
|
S(mode);
|
743
890
|
S(interval);
|
744
891
|
S(raw);
|
892
|
+
S(raw_sample_timestamps);
|
745
893
|
S(raw_timestamp_deltas);
|
746
894
|
S(out);
|
747
895
|
S(metadata);
|
@@ -764,9 +912,9 @@ Init_stackprof(void)
|
|
764
912
|
_stackprof.raw_samples_capa = 0;
|
765
913
|
_stackprof.raw_sample_index = 0;
|
766
914
|
|
767
|
-
_stackprof.
|
768
|
-
_stackprof.
|
769
|
-
_stackprof.
|
915
|
+
_stackprof.raw_sample_times = NULL;
|
916
|
+
_stackprof.raw_sample_times_len = 0;
|
917
|
+
_stackprof.raw_sample_times_capa = 0;
|
770
918
|
|
771
919
|
_stackprof.empty_string = rb_str_new_cstr("");
|
772
920
|
rb_global_variable(&_stackprof.empty_string);
|
@@ -783,6 +931,7 @@ Init_stackprof(void)
|
|
783
931
|
rb_define_singleton_method(rb_mStackProf, "stop", stackprof_stop, 0);
|
784
932
|
rb_define_singleton_method(rb_mStackProf, "results", stackprof_results, -1);
|
785
933
|
rb_define_singleton_method(rb_mStackProf, "sample", stackprof_sample, 0);
|
934
|
+
rb_define_singleton_method(rb_mStackProf, "use_postponed_job!", stackprof_use_postponed_job_l, 0);
|
786
935
|
|
787
936
|
pthread_atfork(stackprof_atfork_prepare, stackprof_atfork_parent, stackprof_atfork_child);
|
788
937
|
}
|
@@ -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
|
@@ -95,51 +129,10 @@ module StackProf
|
|
95
129
|
print_flamegraph(f, skip_common, true)
|
96
130
|
end
|
97
131
|
|
98
|
-
StackCursor = Struct.new(:raw, :idx, :length) do
|
99
|
-
def weight
|
100
|
-
@weight ||= raw[1 + idx + length]
|
101
|
-
end
|
102
|
-
|
103
|
-
def [](i)
|
104
|
-
if i >= length
|
105
|
-
nil
|
106
|
-
else
|
107
|
-
raw[1 + idx + i]
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def <=>(other)
|
112
|
-
i = 0
|
113
|
-
while i < length && i < other.length
|
114
|
-
if self[i] != other[i]
|
115
|
-
return self[i] <=> other[i]
|
116
|
-
end
|
117
|
-
i += 1
|
118
|
-
end
|
119
|
-
|
120
|
-
return length <=> other.length
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
132
|
def print_flamegraph(f, skip_common, alphabetical=false)
|
125
133
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
126
134
|
|
127
|
-
stacks =
|
128
|
-
max_x = 0
|
129
|
-
max_y = 0
|
130
|
-
|
131
|
-
idx = 0
|
132
|
-
loop do
|
133
|
-
len = raw[idx]
|
134
|
-
break unless len
|
135
|
-
max_y = len if len > max_y
|
136
|
-
|
137
|
-
stack = StackCursor.new(raw, idx, len)
|
138
|
-
stacks << stack
|
139
|
-
max_x += stack.weight
|
140
|
-
|
141
|
-
idx += len + 2
|
142
|
-
end
|
135
|
+
stacks, max_x, max_y = flamegraph_stacks(raw)
|
143
136
|
|
144
137
|
stacks.sort! if alphabetical
|
145
138
|
|
@@ -150,7 +143,7 @@ module StackProf
|
|
150
143
|
x = 0
|
151
144
|
|
152
145
|
stacks.each do |stack|
|
153
|
-
weight = stack.
|
146
|
+
weight = stack.last
|
154
147
|
cell = stack[y] unless y == stack.length-1
|
155
148
|
|
156
149
|
if cell.nil?
|
@@ -191,6 +184,24 @@ module StackProf
|
|
191
184
|
f.puts '])'
|
192
185
|
end
|
193
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
|
+
|
194
205
|
def flamegraph_row(f, x, y, weight, addr)
|
195
206
|
frame = @data[:frames][addr]
|
196
207
|
f.print ',' if @rows_started
|
@@ -214,7 +225,7 @@ module StackProf
|
|
214
225
|
end
|
215
226
|
else
|
216
227
|
frame = @data[:frames][val]
|
217
|
-
child_name = "#{ frame[:name] } : #{ frame[:file] }"
|
228
|
+
child_name = "#{ frame[:name] } : #{ frame[:file] } : #{ frame[:line] }"
|
218
229
|
child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
|
219
230
|
weight += child_data["value"]
|
220
231
|
children << child_data
|
@@ -231,15 +242,7 @@ module StackProf
|
|
231
242
|
def print_d3_flamegraph(f=STDOUT, skip_common=true)
|
232
243
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
233
244
|
|
234
|
-
stacks =
|
235
|
-
max_x = 0
|
236
|
-
max_y = 0
|
237
|
-
while len = raw.shift
|
238
|
-
max_y = len if len > max_y
|
239
|
-
stack = raw.slice!(0, len+1)
|
240
|
-
stacks << stack
|
241
|
-
max_x += stack.last
|
242
|
-
end
|
245
|
+
stacks, * = flamegraph_stacks(raw)
|
243
246
|
|
244
247
|
# d3-flame-grpah supports only alphabetical flamegraph
|
245
248
|
stacks.sort!
|
@@ -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.24'
|
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]
|
@@ -78,9 +82,14 @@ class StackProfTest < MiniTest::Test
|
|
78
82
|
end
|
79
83
|
|
80
84
|
assert_operator profile[:samples], :>=, 1
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
84
93
|
end
|
85
94
|
|
86
95
|
def test_walltime
|
@@ -121,19 +130,38 @@ class StackProfTest < MiniTest::Test
|
|
121
130
|
end
|
122
131
|
|
123
132
|
def test_raw
|
133
|
+
before_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
134
|
+
|
124
135
|
profile = StackProf.run(mode: :custom, raw: true) do
|
125
136
|
10.times do
|
126
137
|
StackProf.sample
|
138
|
+
sleep 0.0001
|
127
139
|
end
|
128
140
|
end
|
129
141
|
|
142
|
+
after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
143
|
+
|
130
144
|
raw = profile[:raw]
|
131
145
|
assert_equal 10, raw[-1]
|
132
146
|
assert_equal raw[0] + 2, raw.size
|
133
147
|
|
134
148
|
offset = RUBY_VERSION >= '3' ? -3 : -2
|
135
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
|
+
|
136
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
|
137
165
|
end
|
138
166
|
|
139
167
|
def test_metadata
|
@@ -205,7 +233,6 @@ class StackProfTest < MiniTest::Test
|
|
205
233
|
end
|
206
234
|
end
|
207
235
|
|
208
|
-
raw = profile[:raw]
|
209
236
|
gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" }
|
210
237
|
marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" }
|
211
238
|
sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" }
|
@@ -281,4 +308,4 @@ class StackProfTest < MiniTest::Test
|
|
281
308
|
r.close
|
282
309
|
w.close
|
283
310
|
end
|
284
|
-
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.24
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aman Gupta
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-20 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,10 +99,10 @@ 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.
|
100
|
-
post_install_message:
|
102
|
+
changelog_uri: https://github.com/tmm1/stackprof/blob/v0.2.24/CHANGELOG.md
|
103
|
+
documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.24
|
104
|
+
source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.24
|
105
|
+
post_install_message:
|
101
106
|
rdoc_options: []
|
102
107
|
require_paths:
|
103
108
|
- lib
|
@@ -112,8 +117,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
117
|
- !ruby/object:Gem::Version
|
113
118
|
version: '0'
|
114
119
|
requirements: []
|
115
|
-
rubygems_version: 3.1
|
116
|
-
signing_key:
|
120
|
+
rubygems_version: 3.0.3.1
|
121
|
+
signing_key:
|
117
122
|
specification_version: 4
|
118
123
|
summary: sampling callstack-profiler for ruby 2.2+
|
119
124
|
test_files: []
|