stackprof 0.2.17 → 0.2.24

Sign up to get free protection for your applications and to get access to all the features.
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: []