stackprof 0.2.19 → 0.2.25

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: 6662250e5e20ee3388bfce740a053c83e53fe9304b518ad22f0ce9ffc5d10077
4
- data.tar.gz: 7480e1896e253bd530573fc2f66b3d7690db97bee3f892c77729b75c2dc8c94e
3
+ metadata.gz: b299eb696cf0c2748e931532afab3f90bab1c94447ff1c844bce0eda878a93c6
4
+ data.tar.gz: bd2c389baa8253fc06beda425abc87762b9bca425c6bc5046655610fb9852e79
5
5
  SHA512:
6
- metadata.gz: 3f92e6401b16f83d9398b94e8b0506f597a785a19e1552245f39e1c77ab0c2d3d40186b8b05f91972b6d899995718c28e1724acff97248d497a3768deed601bb
7
- data.tar.gz: 0663a1cdac8730bcbc4dd68750fffa6885b72eca7c37915191b6ff44ff101349ba274648b6641531c1bfff34144179b76d527ca8426db91af2c0536629f6ee65
6
+ metadata.gz: 322e506fc77bd964f39e7b3f0c5584e57efdec8e1dcc432cdea924536e334234d916a1d5e1ff085e2e5210a49751897ca36a4b1bd007468a5b16184151129be3
7
+ data.tar.gz: 3234d4159c78119a9238200d1f4516280cbd1e0cfe3affca1c4fa4827b805ecf154e38857c917fafe7aed042d67525bb7e6c5fce85a039e30cde4672ae08f97e
@@ -8,7 +8,7 @@ jobs:
8
8
  strategy:
9
9
  fail-fast: false
10
10
  matrix:
11
- ruby: [ ruby-head, '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2' ]
11
+ ruby: [ ruby-head, '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2', truffleruby ]
12
12
  steps:
13
13
  - name: Checkout
14
14
  uses: actions/checkout@v2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 0.2.25
2
+
3
+ * Fix GC marking
4
+
1
5
  # 0.2.16
2
6
 
3
7
  * [flamegraph.pl] Update to latest version
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') &&
@@ -12,6 +12,7 @@
12
12
  #include <ruby/st.h>
13
13
  #include <ruby/io.h>
14
14
  #include <ruby/intern.h>
15
+ #include <ruby/vm.h>
15
16
  #include <signal.h>
16
17
  #include <sys/time.h>
17
18
  #include <time.h>
@@ -25,20 +26,15 @@
25
26
  #define FAKE_FRAME_MARK INT2FIX(1)
26
27
  #define FAKE_FRAME_SWEEP INT2FIX(2)
27
28
 
28
- /*
29
- * As of Ruby 3.0, it should be safe to read stack frames at any time
30
- * See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21
31
- */
32
- #if RUBY_API_VERSION_MAJOR < 3
33
- #define USE_POSTPONED_JOB
34
- #endif
35
-
36
29
  static const char *fake_frame_cstrs[] = {
37
30
  "(garbage collection)",
38
31
  "(marking)",
39
32
  "(sweeping)",
40
33
  };
41
34
 
35
+ static int stackprof_use_postponed_job = 1;
36
+ static int ruby_vm_running = 0;
37
+
42
38
  #define TOTAL_FAKE_FRAMES (sizeof(fake_frame_cstrs) / sizeof(char *))
43
39
 
44
40
  #ifdef _POSIX_MONOTONIC_CLOCK
@@ -131,6 +127,8 @@ static struct {
131
127
  sample_time_t buffer_time;
132
128
  VALUE frames_buffer[BUF_SIZE];
133
129
  int lines_buffer[BUF_SIZE];
130
+
131
+ pthread_t target_thread;
134
132
  } _stackprof;
135
133
 
136
134
  static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
@@ -152,6 +150,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
152
150
  VALUE opts = Qnil, mode = Qnil, interval = Qnil, metadata = rb_hash_new(), out = Qfalse;
153
151
  int ignore_gc = 0;
154
152
  int raw = 0, aggregate = 1;
153
+ VALUE metadata_val;
155
154
 
156
155
  if (_stackprof.running)
157
156
  return Qfalse;
@@ -166,7 +165,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
166
165
  ignore_gc = 1;
167
166
  }
168
167
 
169
- VALUE metadata_val = rb_hash_aref(opts, sym_metadata);
168
+ metadata_val = rb_hash_aref(opts, sym_metadata);
170
169
  if (RTEST(metadata_val)) {
171
170
  if (!RB_TYPE_P(metadata_val, T_HASH))
172
171
  rb_raise(rb_eArgError, "metadata should be a hash");
@@ -224,6 +223,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
224
223
  _stackprof.ignore_gc = ignore_gc;
225
224
  _stackprof.metadata = metadata;
226
225
  _stackprof.out = out;
226
+ _stackprof.target_thread = pthread_self();
227
227
 
228
228
  if (raw) {
229
229
  capture_timestamp(&_stackprof.last_sample_at);
@@ -603,14 +603,15 @@ stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t ti
603
603
  void
604
604
  stackprof_buffer_sample(void)
605
605
  {
606
+ uint64_t start_timestamp = 0;
607
+ int64_t timestamp_delta = 0;
608
+ int num;
609
+
606
610
  if (_stackprof.buffer_count > 0) {
607
611
  // Another sample is already pending
608
612
  return;
609
613
  }
610
614
 
611
- uint64_t start_timestamp = 0;
612
- int64_t timestamp_delta = 0;
613
- int num;
614
615
  if (_stackprof.raw) {
615
616
  struct timestamp_t t;
616
617
  capture_timestamp(&t);
@@ -701,7 +702,6 @@ stackprof_job_record_gc(void *data)
701
702
  stackprof_record_gc_samples();
702
703
  }
703
704
 
704
- #ifdef USE_POSTPONED_JOB
705
705
  static void
706
706
  stackprof_job_sample_and_record(void *data)
707
707
  {
@@ -709,7 +709,6 @@ stackprof_job_sample_and_record(void *data)
709
709
 
710
710
  stackprof_sample_and_record();
711
711
  }
712
- #endif
713
712
 
714
713
  static void
715
714
  stackprof_job_record_buffer(void *data)
@@ -727,7 +726,27 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
727
726
  _stackprof.overall_signals++;
728
727
 
729
728
  if (!_stackprof.running) return;
730
- 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
+
731
750
  if (pthread_mutex_trylock(&lock)) return;
732
751
 
733
752
  if (!_stackprof.ignore_gc && rb_during_gc()) {
@@ -740,15 +759,15 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
740
759
  _stackprof.unrecorded_gc_samples++;
741
760
  rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0);
742
761
  } else {
743
- #ifdef USE_POSTPONED_JOB
744
- rb_postponed_job_register_one(0, stackprof_job_sample_and_record, (void*)0);
745
- #else
746
- // Buffer a sample immediately, if an existing sample exists this will
747
- // return immediately
748
- stackprof_buffer_sample();
749
- // Enqueue a job to record the sample
750
- rb_postponed_job_register_one(0, stackprof_job_record_buffer, (void*)0);
751
- #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
+ }
752
771
  }
753
772
  pthread_mutex_unlock(&lock);
754
773
  }
@@ -792,6 +811,10 @@ stackprof_gc_mark(void *data)
792
811
 
793
812
  if (_stackprof.frames)
794
813
  st_foreach(_stackprof.frames, frame_mark_i, 0);
814
+
815
+ for (int i = 0; i < _stackprof.buffer_count; i++) {
816
+ rb_gc_mark(_stackprof.frames_buffer[i]);
817
+ }
795
818
  }
796
819
 
797
820
  static void
@@ -826,10 +849,32 @@ stackprof_atfork_child(void)
826
849
  stackprof_stop(rb_mStackProf);
827
850
  }
828
851
 
852
+ static VALUE
853
+ stackprof_use_postponed_job_l(VALUE self)
854
+ {
855
+ stackprof_use_postponed_job = 1;
856
+ return Qnil;
857
+ }
858
+
859
+ static void
860
+ stackprof_at_exit(ruby_vm_t* vm)
861
+ {
862
+ ruby_vm_running = 0;
863
+ }
864
+
829
865
  void
830
866
  Init_stackprof(void)
831
867
  {
832
868
  size_t i;
869
+ /*
870
+ * As of Ruby 3.0, it should be safe to read stack frames at any time, unless YJIT is enabled
871
+ * See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21
872
+ */
873
+ stackprof_use_postponed_job = RUBY_API_VERSION_MAJOR < 3;
874
+
875
+ ruby_vm_running = 1;
876
+ ruby_vm_at_exit(stackprof_at_exit);
877
+
833
878
  #define S(name) sym_##name = ID2SYM(rb_intern(#name));
834
879
  S(object);
835
880
  S(custom);
@@ -890,6 +935,7 @@ Init_stackprof(void)
890
935
  rb_define_singleton_method(rb_mStackProf, "stop", stackprof_stop, 0);
891
936
  rb_define_singleton_method(rb_mStackProf, "results", stackprof_results, -1);
892
937
  rb_define_singleton_method(rb_mStackProf, "sample", stackprof_sample, 0);
938
+ rb_define_singleton_method(rb_mStackProf, "use_postponed_job!", stackprof_use_postponed_job_l, 0);
893
939
 
894
940
  pthread_atfork(stackprof_atfork_prepare, stackprof_atfork_parent, stackprof_atfork_child);
895
941
  }
@@ -0,0 +1,19 @@
1
+ require "stackprof"
2
+
3
+ options = {}
4
+ options[:mode] = ENV["STACKPROF_MODE"].to_sym if ENV.key?("STACKPROF_MODE")
5
+ options[:interval] = Integer(ENV["STACKPROF_INTERVAL"]) if ENV.key?("STACKPROF_INTERVAL")
6
+ options[:raw] = true if ENV["STACKPROF_RAW"]
7
+ options[:ignore_gc] = true if ENV["STACKPROF_IGNORE_GC"]
8
+
9
+ at_exit do
10
+ StackProf.stop
11
+ output_path = ENV.fetch("STACKPROF_OUT") do
12
+ require "tempfile"
13
+ Tempfile.create(["stackprof", ".dump"]).path
14
+ end
15
+ StackProf.results(output_path)
16
+ $stderr.puts("StackProf results dumped at: #{output_path}")
17
+ end
18
+
19
+ StackProf.start(**options)
@@ -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
@@ -191,7 +225,7 @@ module StackProf
191
225
  end
192
226
  else
193
227
  frame = @data[:frames][val]
194
- child_name = "#{ frame[:name] } : #{ frame[:file] }"
228
+ child_name = "#{ frame[:name] } : #{ frame[:file] } : #{ frame[:line] }"
195
229
  child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
196
230
  weight += child_data["value"]
197
231
  children << child_data
@@ -0,0 +1,37 @@
1
+ module StackProf
2
+ # Define the same methods as stackprof.c
3
+ class << self
4
+ def running?
5
+ false
6
+ end
7
+
8
+ def run(*args)
9
+ unimplemented
10
+ end
11
+
12
+ def start(*args)
13
+ unimplemented
14
+ end
15
+
16
+ def stop
17
+ unimplemented
18
+ end
19
+
20
+ def results(*args)
21
+ unimplemented
22
+ end
23
+
24
+ def sample
25
+ unimplemented
26
+ end
27
+
28
+ def use_postponed_job!
29
+ # noop
30
+ end
31
+
32
+ private def unimplemented
33
+ raise "Use --cpusampler=flamegraph or --cpusampler instead of StackProf on TruffleRuby.\n" \
34
+ "See https://www.graalvm.org/tools/profiling/ and `ruby --help:cpusampler` for more details."
35
+ end
36
+ end
37
+ end
data/lib/stackprof.rb CHANGED
@@ -1,7 +1,20 @@
1
- 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.19'
17
+ VERSION = '0.2.25'
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.19'
3
+ s.version = '0.2.25'
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]
@@ -304,4 +308,4 @@ class StackProfTest < MiniTest::Test
304
308
  r.close
305
309
  w.close
306
310
  end
307
- end
311
+ end unless RUBY_ENGINE == 'truffleruby'
@@ -0,0 +1,18 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'stackprof'
3
+ require 'minitest/autorun'
4
+
5
+ if RUBY_ENGINE == 'truffleruby'
6
+ class StackProfTruffleRubyTest < MiniTest::Test
7
+ def test_error
8
+ error = assert_raises RuntimeError do
9
+ StackProf.run(mode: :cpu) do
10
+ unreacheable
11
+ end
12
+ end
13
+
14
+ assert_match(/TruffleRuby/, error.message)
15
+ assert_match(/--cpusampler/, error.message)
16
+ end
17
+ end
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stackprof
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.19
4
+ version: 0.2.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aman Gupta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-24 00:00:00.000000000 Z
11
+ date: 2023-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -76,15 +76,20 @@ files:
76
76
  - ext/stackprof/extconf.rb
77
77
  - ext/stackprof/stackprof.c
78
78
  - lib/stackprof.rb
79
+ - lib/stackprof/autorun.rb
79
80
  - lib/stackprof/flamegraph/flamegraph.js
80
81
  - lib/stackprof/flamegraph/viewer.html
81
82
  - lib/stackprof/middleware.rb
82
83
  - lib/stackprof/report.rb
84
+ - lib/stackprof/truffleruby.rb
83
85
  - sample.rb
84
86
  - stackprof.gemspec
87
+ - test/fixtures/profile.dump
88
+ - test/fixtures/profile.json
85
89
  - test/test_middleware.rb
86
90
  - test/test_report.rb
87
91
  - test/test_stackprof.rb
92
+ - test/test_truffleruby.rb
88
93
  - vendor/FlameGraph/README
89
94
  - vendor/FlameGraph/flamegraph.pl
90
95
  - vendor/gprof2dot/gprof2dot.py
@@ -94,9 +99,9 @@ licenses:
94
99
  - MIT
95
100
  metadata:
96
101
  bug_tracker_uri: https://github.com/tmm1/stackprof/issues
97
- changelog_uri: https://github.com/tmm1/stackprof/blob/v0.2.19/CHANGELOG.md
98
- documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.19
99
- source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.19
102
+ changelog_uri: https://github.com/tmm1/stackprof/blob/v0.2.25/CHANGELOG.md
103
+ documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.25
104
+ source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.25
100
105
  post_install_message:
101
106
  rdoc_options: []
102
107
  require_paths: