stackprof 0.2.19 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: