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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b55691b8d1966ba4b2b2458a4908b2a2d5b65f2074dfe3b3b1b6350f752704ec
4
- data.tar.gz: 79e2a0508a1c722f39cc61d39b0577cfb5520669a7a2db4cadac6c49dcb1267a
3
+ metadata.gz: 90e33127951d53147b6a65704446ec812999df9525d868865029ee1c7450445d
4
+ data.tar.gz: 19a5f104482f355cb1957b71b18b15b9ca9bb981cfcc109688366e53704c5711
5
5
  SHA512:
6
- metadata.gz: 2fa22779f03c332a3680f526bf1df29553588773fabeb00da327af3525018e535e973bafd990254c6ad50516faf5e8b1d087bb7c208c99d0b512d99ccdef53bb
7
- data.tar.gz: 73ba1328c793b0c0c4657e7826f4bf2cd52102c61a2ca2e3e0b1c5240ffe96ee0ec328ea831b1592c10b4e13c6aec2bb9d28fd05e93ddef999d5131e55124362
6
+ metadata.gz: 17610238200852f97afcc730efd9171c46c3a001ff7d5b82cb65fc92f96d9d2b206474bcecc7eeee1981365d2a07fd4dad460edc54a8bc9c8b1e6c7412f7cde3
7
+ data.tar.gz: 35cc1394b526edd0782468b06d54f66eb1ff10e5741becf486f68f0b141511fad9db622816004a4ae2e1a59cff048abe2d5697848eff127640663e9246f33cca
@@ -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
- require "rake/extensiontask"
10
+ if RUBY_ENGINE == "truffleruby"
11
+ task :compile do
12
+ # noop
13
+ end
11
14
 
12
- Rake::ExtensionTask.new("stackprof") do |ext|
13
- ext.ext_dir = "ext/stackprof"
14
- ext.lib_dir = "lib/stackprof"
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
- options = {}
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
- parser = OptionParser.new(ARGV) do |o|
8
- o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]"
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
- o.on('--text', 'Text summary per method (default)'){ options[:format] = :text }
11
- o.on('--json', 'JSON output (use with web viewers)'){ options[:format] = :json }
12
- o.on('--files', 'List of files'){ |f| options[:format] = :files }
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
- parser.parse!
39
- parser.abort(parser.help) if ARGV.empty?
22
+ o.on('--interval [MILLISECONDS]', Integer, 'Mode-relative sample rate') do |interval|
23
+ env['STACKPROF_INTERVAL'] = interval.to_s
24
+ end
40
25
 
41
- reports = []
42
- while ARGV.size > 0
43
- begin
44
- file = ARGV.pop
45
- reports << StackProf::Report.new(Marshal.load(IO.binread(file)))
46
- rescue TypeError => e
47
- STDERR.puts "** error parsing #{file}: #{e.inspect}"
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
- end
50
- report = reports.inject(:+)
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
- default_options = {
53
- :format => :text,
54
- :sort => false,
55
- :limit => 30
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
- if options[:format] == :graphviz
59
- default_options[:limit] = 120
60
- default_options[:node_fraction] = 0.005
61
- end
71
+ parser.parse!
72
+ parser.abort(parser.help) if ARGV.empty?
62
73
 
63
- options = default_options.merge(options)
64
- options.delete(:limit) if options[:limit] == 0
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
- case options[:format]
67
- when :text
68
- report.print_text(options[:sort], options[:limit], options[:select_files], options[:reject_files], options[:select_names], options[:reject_names])
69
- when :json
70
- report.print_json
71
- when :debug
72
- report.print_debug
73
- when :dump
74
- report.print_dump
75
- when :callgrind
76
- report.print_callgrind
77
- when :graphviz
78
- report.print_graphviz(options)
79
- when :stackcollapse
80
- report.print_stackcollapse
81
- when :timeline_flamegraph
82
- report.print_timeline_flamegraph
83
- when :alphabetical_flamegraph
84
- report.print_alphabetical_flamegraph
85
- when :d3_flamegraph
86
- report.print_d3_flamegraph
87
- when :method
88
- options[:walk] ? report.walk_method(options[:filter]) : report.print_method(options[:filter])
89
- when :file
90
- report.print_file(options[:filter])
91
- when :files
92
- report.print_files(options[:sort], options[:limit])
93
- else
94
- raise ArgumentError, "unknown format: #{options[:format]}"
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
@@ -1,4 +1,10 @@
1
1
  require 'mkmf'
2
+
3
+ if RUBY_ENGINE == 'truffleruby'
4
+ File.write('Makefile', dummy_makefile($srcdir).join(""))
5
+ return
6
+ end
7
+
2
8
  if have_func('rb_postponed_job_register_one') &&
3
9
  have_func('rb_profile_frames') &&
4
10
  have_func('rb_tracepoint_new') &&
@@ -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 timeval last_sample_at;
66
- int *raw_timestamp_deltas;
67
- size_t raw_timestamp_deltas_len;
68
- size_t raw_timestamp_deltas_capa;
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
- VALUE metadata_val = rb_hash_aref(opts, sym_metadata);
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
- gettimeofday(&_stackprof.last_sample_at, NULL);
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, rb_obj_id((VALUE)key), INT2FIX(weight));
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, rb_obj_id(frame), details);
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, rb_obj_id(_stackprof.raw_samples[n]));
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
- raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_timestamp_deltas_len);
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.raw_timestamp_deltas_len; n++) {
340
- rb_ary_push(raw_timestamp_deltas, INT2FIX(_stackprof.raw_timestamp_deltas[n]));
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.raw_timestamp_deltas);
344
- _stackprof.raw_timestamp_deltas = NULL;
345
- _stackprof.raw_timestamp_deltas_len = 0;
346
- _stackprof.raw_timestamp_deltas_capa = 0;
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, int timestamp_delta)
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.raw_timestamp_deltas) {
487
- _stackprof.raw_timestamp_deltas_capa = 100;
488
- _stackprof.raw_timestamp_deltas = malloc(sizeof(int) * _stackprof.raw_timestamp_deltas_capa);
489
- _stackprof.raw_timestamp_deltas_len = 0;
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.raw_timestamp_deltas_capa <= _stackprof.raw_timestamp_deltas_len + 1) {
494
- _stackprof.raw_timestamp_deltas_capa *= 2;
495
- _stackprof.raw_timestamp_deltas = realloc(_stackprof.raw_timestamp_deltas, sizeof(int) * _stackprof.raw_timestamp_deltas_capa);
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 time between samples) */
499
- _stackprof.raw_timestamp_deltas[_stackprof.raw_timestamp_deltas_len++] = timestamp_delta;
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
- gettimeofday(&_stackprof.last_sample_at, NULL);
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
- stackprof_record_sample()
604
+ stackprof_buffer_sample(void)
538
605
  {
539
- int timestamp_delta = 0;
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 timeval t;
543
- struct timeval diff;
544
- gettimeofday(&t, NULL);
545
- timersub(&t, &_stackprof.last_sample_at, &diff);
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
- stackprof_record_sample_for_stack(num, timestamp_delta);
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
- int delta_to_first_unrecorded_gc_sample = 0;
556
- int i;
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 timeval t;
559
- struct timeval diff;
560
- gettimeofday(&t, NULL);
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 = (1000 * diff.tv_sec + diff.tv_usec) - (_stackprof.unrecorded_gc_samples - 1) * NUM2LONG(_stackprof.interval);
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
- int timestamp_delta = i == 0 ? delta_to_first_unrecorded_gc_sample : NUM2LONG(_stackprof.interval);
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
- stackprof_gc_job_handler(void *data)
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
- stackprof_job_handler(void *data)
706
+ stackprof_job_sample_and_record(void *data)
614
707
  {
615
708
  if (!_stackprof.running) return;
616
709
 
617
- stackprof_record_sample();
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
- if (!ruby_native_thread_p()) return;
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, stackprof_gc_job_handler, (void*)0);
760
+ rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0);
640
761
  } else {
641
- #ifdef USE_POSTPONED_JOB
642
- rb_postponed_job_register_one(0, stackprof_job_handler, (void*)0);
643
- #else
644
- stackprof_job_handler(0);
645
- #endif
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
- stackprof_job_handler(0);
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
- stackprof_job_handler(0);
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.raw_timestamp_deltas = NULL;
768
- _stackprof.raw_timestamp_deltas_len = 0;
769
- _stackprof.raw_timestamp_deltas_capa = 0;
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)
@@ -1,10 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pp'
4
- require 'digest/md5'
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::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}")
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.weight
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
- require "stackprof/stackprof"
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'
17
+ VERSION = '0.2.24'
5
18
  end
6
19
 
7
20
  StackProf.autoload :Report, "stackprof/report.rb"
data/stackprof.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'stackprof'
3
- s.version = '0.2.17'
3
+ s.version = '0.2.24'
4
4
  s.homepage = 'http://github.com/tmm1/stackprof'
5
5
 
6
6
  s.authors = 'Aman Gupta'
@@ -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
@@ -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
- offset = RUBY_VERSION >= '3' ? 1 : 0
82
- frame = profile[:frames].values[offset]
83
- assert_includes frame[:name], "StackProfTest#math"
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.17
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: 2021-05-03 00:00:00.000000000 Z
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.17/CHANGELOG.md
98
- documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.17
99
- source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.17
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.2
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: []