From e94699de7932c4cb428b338fdc48dc63c1dd87c1 Mon Sep 17 00:00:00 2001 From: Yusuke Sangenya Date: Mon, 20 Sep 2021 17:42:45 +0900 Subject: [PATCH 01/35] Show line number --- lib/stackprof/report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index 9b80f8ba..563856b4 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -191,7 +191,7 @@ def convert_to_d3_flame_graph_format(name, stacks, depth) end else frame = @data[:frames][val] - child_name = "#{ frame[:name] } : #{ frame[:file] }" + child_name = "#{ frame[:name] } : #{ frame[:file] } : #{ frame[:line] }" child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1) weight += child_data["value"] children << child_data From 02b44b3af62f094175bc695f9b4ea315b1abc578 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 3 May 2022 15:25:14 -0700 Subject: [PATCH 02/35] Switch hash algorithm from MD5 to SHA256 Even though this use of MD5 isn't for security purposes, MD5 may not be allowed at all on FIPS-compliant systems. Switch to SHA256 to comply with FIPS 140-2 standards. --- lib/stackprof/report.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index 9a499a83..419630bc 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'pp' -require 'digest/md5' +require 'digest/sha2' require 'json' module StackProf @@ -52,7 +52,7 @@ def frames(sort_by_total=false) def normalized_frames id2hash = {} @data[:frames].each do |frame, info| - id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}") + id2hash[frame.to_s] = info[:hash] = Digest::SHA256.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}") end @data[:frames].inject(Hash.new) do |hash, (frame, info)| info = hash[id2hash[frame.to_s]] = info.dup From 6911b6133a5cdc83bfcd0159d2e2a6a6df252e6a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 12 Jul 2022 14:21:00 +0200 Subject: [PATCH 03/35] Add Ruby 3.1 to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e14f8fb..c95361fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ ruby-head, '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2' ] + ruby: [ ruby-head, '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2' ] steps: - name: Checkout uses: actions/checkout@v2 From 179c5ac779b5401d6805b27c22e75df436acb978 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 12 Jul 2022 14:17:11 +0200 Subject: [PATCH 04/35] Use postponed jobs if YJIT is enabled. Fix: https://github.com/tmm1/stackprof/issues/179 YJIT doesn't support being interrupted by signal in random places, and probably will never support it. --- ext/stackprof/stackprof.c | 46 +++++++++++++++++++++++---------------- lib/stackprof.rb | 4 ++++ test/test_stackprof.rb | 4 ++++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index f667feb6..a814b8ef 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -25,20 +25,14 @@ #define FAKE_FRAME_MARK INT2FIX(1) #define FAKE_FRAME_SWEEP INT2FIX(2) -/* - * As of Ruby 3.0, it should be safe to read stack frames at any time - * See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21 - */ -#if RUBY_API_VERSION_MAJOR < 3 - #define USE_POSTPONED_JOB -#endif - static const char *fake_frame_cstrs[] = { "(garbage collection)", "(marking)", "(sweeping)", }; +static int stackprof_use_postponed_job = 1; + #define TOTAL_FAKE_FRAMES (sizeof(fake_frame_cstrs) / sizeof(char *)) #ifdef _POSIX_MONOTONIC_CLOCK @@ -701,7 +695,6 @@ stackprof_job_record_gc(void *data) stackprof_record_gc_samples(); } -#ifdef USE_POSTPONED_JOB static void stackprof_job_sample_and_record(void *data) { @@ -709,7 +702,6 @@ stackprof_job_sample_and_record(void *data) stackprof_sample_and_record(); } -#endif static void stackprof_job_record_buffer(void *data) @@ -740,15 +732,15 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext) _stackprof.unrecorded_gc_samples++; rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0); } else { -#ifdef USE_POSTPONED_JOB - rb_postponed_job_register_one(0, stackprof_job_sample_and_record, (void*)0); -#else - // Buffer a sample immediately, if an existing sample exists this will - // return immediately - stackprof_buffer_sample(); - // Enqueue a job to record the sample - rb_postponed_job_register_one(0, stackprof_job_record_buffer, (void*)0); -#endif + if (stackprof_use_postponed_job) { + rb_postponed_job_register_one(0, stackprof_job_sample_and_record, (void*)0); + } else { + // Buffer a sample immediately, if an existing sample exists this will + // return immediately + stackprof_buffer_sample(); + // Enqueue a job to record the sample + rb_postponed_job_register_one(0, stackprof_job_record_buffer, (void*)0); + } } pthread_mutex_unlock(&lock); } @@ -826,9 +818,24 @@ stackprof_atfork_child(void) stackprof_stop(rb_mStackProf); } +static VALUE +stackprof_use_postponed_job_l(VALUE self) +{ + stackprof_use_postponed_job = 1; + return Qnil; +} + void Init_stackprof(void) { + /* + * As of Ruby 3.0, it should be safe to read stack frames at any time, unless YJIT is enabled + * See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21 + */ + #if RUBY_API_VERSION_MAJOR < 3 + stackprof_use_postponed_job = 0; + #endif + size_t i; #define S(name) sym_##name = ID2SYM(rb_intern(#name)); S(object); @@ -890,6 +897,7 @@ Init_stackprof(void) rb_define_singleton_method(rb_mStackProf, "stop", stackprof_stop, 0); rb_define_singleton_method(rb_mStackProf, "results", stackprof_results, -1); rb_define_singleton_method(rb_mStackProf, "sample", stackprof_sample, 0); + rb_define_singleton_method(rb_mStackProf, "use_postponed_job!", stackprof_use_postponed_job_l, 0); pthread_atfork(stackprof_atfork_prepare, stackprof_atfork_parent, stackprof_atfork_child); } diff --git a/lib/stackprof.rb b/lib/stackprof.rb index db9c554f..0d6947a5 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -1,5 +1,9 @@ require "stackprof/stackprof" +if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? + StackProf.use_postponed_job! +end + module StackProf VERSION = '0.2.19' end diff --git a/test/test_stackprof.rb b/test/test_stackprof.rb index 2b86d1fb..82b12960 100644 --- a/test/test_stackprof.rb +++ b/test/test_stackprof.rb @@ -5,6 +5,10 @@ require 'pathname' class StackProfTest < MiniTest::Test + def setup + Object.new # warm some caches to avoid flakiness + end + def test_info profile = StackProf.run{} assert_equal 1.2, profile[:version] From 6bbabf14207e8bde5e8d39002f5d1a6f638b0342 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 26 Jul 2022 09:51:30 -0700 Subject: [PATCH 05/35] bumping version --- lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index 0d6947a5..cb797750 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -5,7 +5,7 @@ end module StackProf - VERSION = '0.2.19' + VERSION = '0.2.20' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index da82f354..6ae235ab 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.19' + s.version = '0.2.20' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From 36a47117876b010089e177a3f17ebbe2fc2c783a Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 13 Aug 2022 14:25:33 +0200 Subject: [PATCH 06/35] Fix mixed declarations and code warnings on Ruby <= 2.6 --- ext/stackprof/stackprof.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index a814b8ef..7f0df71b 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -146,6 +146,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self) VALUE opts = Qnil, mode = Qnil, interval = Qnil, metadata = rb_hash_new(), out = Qfalse; int ignore_gc = 0; int raw = 0, aggregate = 1; + VALUE metadata_val; if (_stackprof.running) return Qfalse; @@ -160,7 +161,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self) ignore_gc = 1; } - VALUE metadata_val = rb_hash_aref(opts, sym_metadata); + metadata_val = rb_hash_aref(opts, sym_metadata); if (RTEST(metadata_val)) { if (!RB_TYPE_P(metadata_val, T_HASH)) rb_raise(rb_eArgError, "metadata should be a hash"); @@ -597,14 +598,15 @@ stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t ti void stackprof_buffer_sample(void) { + uint64_t start_timestamp = 0; + int64_t timestamp_delta = 0; + int num; + if (_stackprof.buffer_count > 0) { // Another sample is already pending return; } - uint64_t start_timestamp = 0; - int64_t timestamp_delta = 0; - int num; if (_stackprof.raw) { struct timestamp_t t; capture_timestamp(&t); @@ -828,6 +830,7 @@ stackprof_use_postponed_job_l(VALUE self) void Init_stackprof(void) { + size_t i; /* * As of Ruby 3.0, it should be safe to read stack frames at any time, unless YJIT is enabled * See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21 @@ -836,7 +839,6 @@ Init_stackprof(void) stackprof_use_postponed_job = 0; #endif - size_t i; #define S(name) sym_##name = ID2SYM(rb_intern(#name)); S(object); S(custom); From 8245c33687c3c5b39319ea56532ed309f32110d3 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 13 Aug 2022 14:58:03 +0200 Subject: [PATCH 07/35] Support installing the gem on TruffleRuby * Fixes https://github.com/tmm1/stackprof/issues/159 --- .github/workflows/ci.yml | 2 +- Rakefile | 18 ++++++++++++++---- ext/stackprof/extconf.rb | 6 ++++++ lib/stackprof.rb | 6 +++++- lib/stackprof/truffleruby.rb | 37 ++++++++++++++++++++++++++++++++++++ test/test_stackprof.rb | 2 +- test/test_truffleruby.rb | 18 ++++++++++++++++++ 7 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 lib/stackprof/truffleruby.rb create mode 100644 test/test_truffleruby.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c95361fa..983b2082 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ ruby-head, '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2' ] + ruby: [ ruby-head, '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2', truffleruby ] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/Rakefile b/Rakefile index a5b72221..27877e55 100644 --- a/Rakefile +++ b/Rakefile @@ -7,11 +7,21 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList["test/**/test_*.rb"] end -require "rake/extensiontask" +if RUBY_ENGINE == "truffleruby" + task :compile do + # noop + end -Rake::ExtensionTask.new("stackprof") do |ext| - ext.ext_dir = "ext/stackprof" - ext.lib_dir = "lib/stackprof" + task :clean do + # noop + end +else + require "rake/extensiontask" + + Rake::ExtensionTask.new("stackprof") do |ext| + ext.ext_dir = "ext/stackprof" + ext.lib_dir = "lib/stackprof" + end end task default: %i(compile test) diff --git a/ext/stackprof/extconf.rb b/ext/stackprof/extconf.rb index 7d1a52c7..742ff463 100644 --- a/ext/stackprof/extconf.rb +++ b/ext/stackprof/extconf.rb @@ -1,4 +1,10 @@ require 'mkmf' + +if RUBY_ENGINE == 'truffleruby' + File.write('Makefile', dummy_makefile($srcdir).join("")) + return +end + if have_func('rb_postponed_job_register_one') && have_func('rb_profile_frames') && have_func('rb_tracepoint_new') && diff --git a/lib/stackprof.rb b/lib/stackprof.rb index cb797750..6e48ca4a 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -1,4 +1,8 @@ -require "stackprof/stackprof" +if RUBY_ENGINE == 'truffleruby' + require "stackprof/truffleruby" +else + require "stackprof/stackprof" +end if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? StackProf.use_postponed_job! diff --git a/lib/stackprof/truffleruby.rb b/lib/stackprof/truffleruby.rb new file mode 100644 index 00000000..ba8b8e4e --- /dev/null +++ b/lib/stackprof/truffleruby.rb @@ -0,0 +1,37 @@ +module StackProf + # Define the same methods as stackprof.c + class << self + def running? + false + end + + def run(*args) + unimplemented + end + + def start(*args) + unimplemented + end + + def stop + unimplemented + end + + def results(*args) + unimplemented + end + + def sample + unimplemented + end + + def use_postponed_job! + # noop + end + + private def unimplemented + raise "Use --cpusampler=flamegraph or --cpusampler instead of StackProf on TruffleRuby.\n" \ + "See https://www.graalvm.org/tools/profiling/ and `ruby --help:cpusampler` for more details." + end + end +end diff --git a/test/test_stackprof.rb b/test/test_stackprof.rb index 82b12960..2aedc0bc 100644 --- a/test/test_stackprof.rb +++ b/test/test_stackprof.rb @@ -308,4 +308,4 @@ def idle r.close w.close end -end +end unless RUBY_ENGINE == 'truffleruby' diff --git a/test/test_truffleruby.rb b/test/test_truffleruby.rb new file mode 100644 index 00000000..6b6b8a5e --- /dev/null +++ b/test/test_truffleruby.rb @@ -0,0 +1,18 @@ +$:.unshift File.expand_path('../../lib', __FILE__) +require 'stackprof' +require 'minitest/autorun' + +if RUBY_ENGINE == 'truffleruby' + class StackProfTruffleRubyTest < MiniTest::Test + def test_error + error = assert_raises RuntimeError do + StackProf.run(mode: :cpu) do + unreacheable + end + end + + assert_match(/TruffleRuby/, error.message) + assert_match(/--cpusampler/, error.message) + end + end +end From 9f78db526f86dac29c0704ecb83cd739276aac08 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 22 Aug 2022 13:26:19 +0200 Subject: [PATCH 08/35] Use postponed jobs on Ruby 2.x --- ext/stackprof/stackprof.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index a814b8ef..5d8dac0e 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -832,9 +832,7 @@ Init_stackprof(void) * As of Ruby 3.0, it should be safe to read stack frames at any time, unless YJIT is enabled * See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21 */ - #if RUBY_API_VERSION_MAJOR < 3 - stackprof_use_postponed_job = 0; - #endif + stackprof_use_postponed_job = RUBY_API_VERSION_MAJOR < 3; size_t i; #define S(name) sym_##name = ID2SYM(rb_intern(#name)); From 867946759fa48c8a3b2e706ce50838c74ac225d1 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 22 Aug 2022 10:13:02 -0700 Subject: [PATCH 09/35] bump version --- lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index 6e48ca4a..63902309 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -9,7 +9,7 @@ end module StackProf - VERSION = '0.2.20' + VERSION = '0.2.21' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index 6ae235ab..a69ed180 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.20' + s.version = '0.2.21' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From 408d04eb5ae12d553dab9eccbc843fbe8d082cc4 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 14 Sep 2022 13:26:07 +0200 Subject: [PATCH 10/35] `stackprof run` CLI Sometime when you want to profile a ruby process for its entire lifetime, it's a bit complicated to find the proper entrypoint and exit point to insert `StackProf.start` etc. This new CLI command allows to do this easily, e.g: ```bash $ stackprof run -- rubocop --cache false StackProf results dumped at: /var/folders/pg/ykz6j94s7dv_2z4l5x_m17l00000gn/T/stackprof20220914-26008-7iipjc.dump ``` Or for profiling a Rails application boot sequence: ```bash $ stackprof run -- bin/rails runner ':ok' ``` The most common `StackProf.start` arguments can be passed as command line arguments as well. --- bin/stackprof | 197 +++++++++++++++++++++++---------------- lib/stackprof/autorun.rb | 19 ++++ 2 files changed, 135 insertions(+), 81 deletions(-) create mode 100644 lib/stackprof/autorun.rb diff --git a/bin/stackprof b/bin/stackprof index c7e2be0c..eedbfc38 100755 --- a/bin/stackprof +++ b/bin/stackprof @@ -2,94 +2,129 @@ require 'optparse' require 'stackprof' -options = {} +if ARGV.first == "run" + ARGV.shift + env = {} + parser = OptionParser.new(ARGV) do |o| + o.banner = "Usage: stackprof run [--mode|--out|--interval] -- COMMAND" + o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]" -parser = OptionParser.new(ARGV) do |o| - o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]" + o.on('--mode', 'Mode of sampling: cpu, wall, object, default to wall') do |mode| + env["STACKPROF_MODE"] = mode + end - o.on('--text', 'Text summary per method (default)'){ options[:format] = :text } - o.on('--json', 'JSON output (use with web viewers)'){ options[:format] = :json } - o.on('--files', 'List of files'){ |f| options[:format] = :files } - o.on('--limit [num]', Integer, 'Limit --text, --files, or --graphviz output to N entries'){ |n| options[:limit] = n } - o.on('--sort-total', "Sort --text or --files output on total samples\n\n"){ options[:sort] = true } - o.on('--method [grep]', 'Zoom into specified method'){ |f| options[:format] = :method; options[:filter] = f } - o.on('--file [grep]', "Show annotated code for specified file"){ |f| options[:format] = :file; options[:filter] = f } - o.on('--walk', "Walk the stacktrace interactively\n\n"){ |f| options[:walk] = true } - o.on('--callgrind', 'Callgrind output (use with kcachegrind, stackprof-gprof2dot.py)'){ options[:format] = :callgrind } - o.on('--graphviz', "Graphviz output (use with dot)"){ options[:format] = :graphviz } - o.on('--node-fraction [frac]', OptionParser::DecimalNumeric, 'Drop nodes representing less than [frac] fraction of samples'){ |n| options[:node_fraction] = n } - o.on('--stackcollapse', 'stackcollapse.pl compatible output (use with stackprof-flamegraph.pl)'){ options[:format] = :stackcollapse } - o.on('--timeline-flamegraph', "timeline-flamegraph output (js)"){ options[:format] = :timeline_flamegraph } - o.on('--alphabetical-flamegraph', "alphabetical-flamegraph output (js)"){ options[:format] = :alphabetical_flamegraph } - o.on('--flamegraph', "alias to --timeline-flamegraph"){ options[:format] = :timeline_flamegraph } - o.on('--flamegraph-viewer [f.js]', String, "open html viewer for flamegraph output"){ |file| - puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}") - exit - } - o.on('--d3-flamegraph', "flamegraph output (html using d3-flame-graph)\n\n"){ options[:format] = :d3_flamegraph } - o.on('--select-files []', String, 'Show results of matching files'){ |path| (options[:select_files] ||= []) << File.expand_path(path) } - o.on('--reject-files []', String, 'Exclude results of matching files'){ |path| (options[:reject_files] ||= []) << File.expand_path(path) } - o.on('--select-names []', Regexp, 'Show results of matching method names'){ |regexp| (options[:select_names] ||= []) << regexp } - o.on('--reject-names []', Regexp, 'Exclude results of matching method names'){ |regexp| (options[:reject_names] ||= []) << regexp } - o.on('--dump', 'Print marshaled profile dump (combine multiple profiles)'){ options[:format] = :dump } - o.on('--debug', 'Pretty print raw profile data'){ options[:format] = :debug } -end + o.on('--out', 'The target file, which will be overwritten. Defaults to a random temporary file') do |out| + env['STACKPROF_OUT'] = out + end -parser.parse! -parser.abort(parser.help) if ARGV.empty? + o.on('--interval', 'Mode-relative sample rate') do |interval| + env['STACKPROF_INTERVAL'] = Integer(interval).to_s + end -reports = [] -while ARGV.size > 0 - begin - file = ARGV.pop - reports << StackProf::Report.from_file(file) - rescue TypeError => e - STDERR.puts "** error parsing #{file}: #{e.inspect}" + o.on('--raw', 'collects the extra data required by the --flamegraph and --stackcollapse report types') do + env['STACKPROF_RAW'] = '1' + end + + o.on('--ignore-gc', 'Ignore garbage collection frames') do + env['STACKPROF_IGNORE_GC'] = '1' + end end -end -report = reports.inject(:+) + parser.parse! + parser.abort(parser.help) if ARGV.empty? + stackprof_path = File.expand_path('../lib', __dir__) + env['RUBYOPT'] = "-I #{stackprof_path} -r stackprof/autorun #{ENV['RUBYOPT']}" + Kernel.exec(env, *ARGV) +else + options = {} -default_options = { - :format => :text, - :sort => false, - :limit => 30 -} + parser = OptionParser.new(ARGV) do |o| + o.banner = "Usage: stackprof run [--mode|--out|--interval] -- COMMAND" + o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]" -if options[:format] == :graphviz - default_options[:limit] = 120 - default_options[:node_fraction] = 0.005 -end + o.on('--text', 'Text summary per method (default)'){ options[:format] = :text } + o.on('--json', 'JSON output (use with web viewers)'){ options[:format] = :json } + o.on('--files', 'List of files'){ |f| options[:format] = :files } + o.on('--limit [num]', Integer, 'Limit --text, --files, or --graphviz output to N entries'){ |n| options[:limit] = n } + o.on('--sort-total', "Sort --text or --files output on total samples\n\n"){ options[:sort] = true } + o.on('--method [grep]', 'Zoom into specified method'){ |f| options[:format] = :method; options[:filter] = f } + o.on('--file [grep]', "Show annotated code for specified file"){ |f| options[:format] = :file; options[:filter] = f } + o.on('--walk', "Walk the stacktrace interactively\n\n"){ |f| options[:walk] = true } + o.on('--callgrind', 'Callgrind output (use with kcachegrind, stackprof-gprof2dot.py)'){ options[:format] = :callgrind } + o.on('--graphviz', "Graphviz output (use with dot)"){ options[:format] = :graphviz } + o.on('--node-fraction [frac]', OptionParser::DecimalNumeric, 'Drop nodes representing less than [frac] fraction of samples'){ |n| options[:node_fraction] = n } + o.on('--stackcollapse', 'stackcollapse.pl compatible output (use with stackprof-flamegraph.pl)'){ options[:format] = :stackcollapse } + o.on('--timeline-flamegraph', "timeline-flamegraph output (js)"){ options[:format] = :timeline_flamegraph } + o.on('--alphabetical-flamegraph', "alphabetical-flamegraph output (js)"){ options[:format] = :alphabetical_flamegraph } + o.on('--flamegraph', "alias to --timeline-flamegraph"){ options[:format] = :timeline_flamegraph } + o.on('--flamegraph-viewer [f.js]', String, "open html viewer for flamegraph output"){ |file| + puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}") + exit + } + o.on('--d3-flamegraph', "flamegraph output (html using d3-flame-graph)\n\n"){ options[:format] = :d3_flamegraph } + o.on('--select-files []', String, 'Show results of matching files'){ |path| (options[:select_files] ||= []) << File.expand_path(path) } + o.on('--reject-files []', String, 'Exclude results of matching files'){ |path| (options[:reject_files] ||= []) << File.expand_path(path) } + o.on('--select-names []', Regexp, 'Show results of matching method names'){ |regexp| (options[:select_names] ||= []) << regexp } + o.on('--reject-names []', Regexp, 'Exclude results of matching method names'){ |regexp| (options[:reject_names] ||= []) << regexp } + o.on('--dump', 'Print marshaled profile dump (combine multiple profiles)'){ options[:format] = :dump } + o.on('--debug', 'Pretty print raw profile data'){ options[:format] = :debug } + end -options = default_options.merge(options) -options.delete(:limit) if options[:limit] == 0 + parser.parse! + parser.abort(parser.help) if ARGV.empty? -case options[:format] -when :text - report.print_text(options[:sort], options[:limit], options[:select_files], options[:reject_files], options[:select_names], options[:reject_names]) -when :json - report.print_json -when :debug - report.print_debug -when :dump - report.print_dump -when :callgrind - report.print_callgrind -when :graphviz - report.print_graphviz(options) -when :stackcollapse - report.print_stackcollapse -when :timeline_flamegraph - report.print_timeline_flamegraph -when :alphabetical_flamegraph - report.print_alphabetical_flamegraph -when :d3_flamegraph - report.print_d3_flamegraph -when :method - options[:walk] ? report.walk_method(options[:filter]) : report.print_method(options[:filter]) -when :file - report.print_file(options[:filter]) -when :files - report.print_files(options[:sort], options[:limit]) -else - raise ArgumentError, "unknown format: #{options[:format]}" + reports = [] + while ARGV.size > 0 + begin + file = ARGV.pop + reports << StackProf::Report.from_file(file) + rescue TypeError => e + STDERR.puts "** error parsing #{file}: #{e.inspect}" + end + end + report = reports.inject(:+) + + default_options = { + :format => :text, + :sort => false, + :limit => 30 + } + + if options[:format] == :graphviz + default_options[:limit] = 120 + default_options[:node_fraction] = 0.005 + end + + options = default_options.merge(options) + options.delete(:limit) if options[:limit] == 0 + + case options[:format] + when :text + report.print_text(options[:sort], options[:limit], options[:select_files], options[:reject_files], options[:select_names], options[:reject_names]) + when :json + report.print_json + when :debug + report.print_debug + when :dump + report.print_dump + when :callgrind + report.print_callgrind + when :graphviz + report.print_graphviz(options) + when :stackcollapse + report.print_stackcollapse + when :timeline_flamegraph + report.print_timeline_flamegraph + when :alphabetical_flamegraph + report.print_alphabetical_flamegraph + when :d3_flamegraph + report.print_d3_flamegraph + when :method + options[:walk] ? report.walk_method(options[:filter]) : report.print_method(options[:filter]) + when :file + report.print_file(options[:filter]) + when :files + report.print_files(options[:sort], options[:limit]) + else + raise ArgumentError, "unknown format: #{options[:format]}" + end end diff --git a/lib/stackprof/autorun.rb b/lib/stackprof/autorun.rb new file mode 100644 index 00000000..23afe6fe --- /dev/null +++ b/lib/stackprof/autorun.rb @@ -0,0 +1,19 @@ +require "stackprof" + +options = {} +options[:mode] = ENV["STACKPROF_MODE"].to_sym if ENV.key?("STACKPROF_MODE") +options[:interval] = Integer(ENV["STACKPROF_INTERVAL"]) if ENV.key?("STACKPROF_INTERVAL") +options[:raw] = true if ENV["STACKPROF_RAW"] +options[:ignore_gc] = true if ENV["STACKPROF_IGNORE_GC"] + +at_exit do + StackProf.stop + output_path = ENV.fetch("STACKPROF_OUT") do + require "tempfile" + Tempfile.create(["stackprof", ".dump"]).path + end + StackProf.results(output_path) + $stderr.puts("StackProf results dumped at: #{output_path}") +end + +StackProf.start(**options) From 911583804348578dc0a009affc37c1464c1d7909 Mon Sep 17 00:00:00 2001 From: Nate Berkopec Date: Mon, 19 Sep 2022 12:44:12 +0900 Subject: [PATCH 11/35] Correct parsing of opts for bin/rubocop --- bin/stackprof | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/stackprof b/bin/stackprof index eedbfc38..6981e357 100755 --- a/bin/stackprof +++ b/bin/stackprof @@ -6,27 +6,27 @@ if ARGV.first == "run" ARGV.shift env = {} parser = OptionParser.new(ARGV) do |o| - o.banner = "Usage: stackprof run [--mode|--out|--interval] -- COMMAND" + o.banner = "Usage: stackprof run [--mode=MODE|--out=FILE|--interval=INTERVAL|--format=FORMAT] -- COMMAND" o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]" - o.on('--mode', 'Mode of sampling: cpu, wall, object, default to wall') do |mode| + o.on('--mode [MODE]', String, 'Mode of sampling: cpu, wall, object, default to wall') do |mode| env["STACKPROF_MODE"] = mode end - o.on('--out', 'The target file, which will be overwritten. Defaults to a random temporary file') do |out| + o.on('--out [FILENAME]', String, 'The target file, which will be overwritten. Defaults to a random temporary file') do |out| env['STACKPROF_OUT'] = out end - o.on('--interval', 'Mode-relative sample rate') do |interval| - env['STACKPROF_INTERVAL'] = Integer(interval).to_s + o.on('--interval [MILLISECONDS]', Integer, 'Mode-relative sample rate') do |interval| + env['STACKPROF_INTERVAL'] = interval.to_s end - o.on('--raw', 'collects the extra data required by the --flamegraph and --stackcollapse report types') do - env['STACKPROF_RAW'] = '1' + o.on('--raw', 'collects the extra data required by the --flamegraph and --stackcollapse report types') do |raw| + env['STACKPROF_RAW'] = raw.to_s end - o.on('--ignore-gc', 'Ignore garbage collection frames') do - env['STACKPROF_IGNORE_GC'] = '1' + o.on('--ignore-gc', 'Ignore garbage collection frames') do |gc| + env['STACKPROF_IGNORE_GC'] = gc.to_s end end parser.parse! From bbf654124a428028e87f8d1267a91440404453dc Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 13 Oct 2022 12:57:09 -0700 Subject: [PATCH 12/35] bump version --- lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index 63902309..adb9f2f5 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -9,7 +9,7 @@ end module StackProf - VERSION = '0.2.21' + VERSION = '0.2.22' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index a69ed180..4033f00e 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.21' + s.version = '0.2.22' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From 67ec8fe92c8c8578311be3d8f795a34003f5f79a Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 22 May 2022 22:11:36 -0700 Subject: [PATCH 13/35] Forward SIGALRM to original thread --- ext/stackprof/stackprof.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 28b84984..813ca76c 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -125,6 +125,8 @@ static struct { sample_time_t buffer_time; VALUE frames_buffer[BUF_SIZE]; int lines_buffer[BUF_SIZE]; + + pthread_t target_thread; } _stackprof; static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line; @@ -219,6 +221,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self) _stackprof.ignore_gc = ignore_gc; _stackprof.metadata = metadata; _stackprof.out = out; + _stackprof.target_thread = pthread_self(); if (raw) { capture_timestamp(&_stackprof.last_sample_at); @@ -721,7 +724,22 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext) _stackprof.overall_signals++; if (!_stackprof.running) return; - if (!ruby_native_thread_p()) return; + + if (_stackprof.mode == sym_wall) { + // In "wall" mode, the SIGALRM signal will arrive at an arbitrary thread. + // In order to provide more useful results, especially under threaded web + // servers, we want to forward this signal to the original thread + // StackProf was started from. + // According to POSIX.1-2008 TC1 pthread_kill and pthread_self should be + // async-signal-safe. + if (pthread_self() != _stackprof.target_thread) { + pthread_kill(_stackprof.target_thread, sig); + return; + } + } else { + if (!ruby_native_thread_p()) return; + } + if (pthread_mutex_trylock(&lock)) return; if (!_stackprof.ignore_gc && rb_during_gc()) { From 86b9cc6a4fdd244627e7472a41cc861fef133643 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 28 Nov 2022 18:41:22 -0600 Subject: [PATCH 14/35] bumping version Signed-off-by: Aaron Patterson --- lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index adb9f2f5..d595d207 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -9,7 +9,7 @@ end module StackProf - VERSION = '0.2.22' + VERSION = '0.2.23' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index 4033f00e..d415a11b 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.22' + s.version = '0.2.23' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From cfe2a6e819214a7b76289d0e4b466cc319a44bcc Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 20 Jan 2023 11:57:21 +0100 Subject: [PATCH 15/35] Use postponed jobs on Ruby 3.2.0 Otherwise it can cause a VM crash. Won't be a problem in 3.2.1. --- lib/stackprof.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index d595d207..4de14e22 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -6,6 +6,11 @@ if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? StackProf.use_postponed_job! +elsif RUBY_VERSION == "3.2.0" + # 3.2.0 crash is the signal is received at the wrong time. + # Fixed in https://github.com/ruby/ruby/pull/7116 + # The fix is backported in 3.2.1: https://bugs.ruby-lang.org/issues/19336 + StackProf.use_postponed_job! end module StackProf From f7ba37b5a6d3521c3f6a1a4889e56e1303b09211 Mon Sep 17 00:00:00 2001 From: fatkodima Date: Tue, 21 Feb 2023 15:16:03 +0200 Subject: [PATCH 16/35] Fix printing CLI banner --- bin/stackprof | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/bin/stackprof b/bin/stackprof index 6981e357..e9b8afd5 100755 --- a/bin/stackprof +++ b/bin/stackprof @@ -2,13 +2,15 @@ require 'optparse' require 'stackprof' +banner = <<-END +Usage: stackprof run [--mode=MODE|--out=FILE|--interval=INTERVAL|--format=FORMAT] -- COMMAND +Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz] +END + if ARGV.first == "run" ARGV.shift env = {} - parser = OptionParser.new(ARGV) do |o| - o.banner = "Usage: stackprof run [--mode=MODE|--out=FILE|--interval=INTERVAL|--format=FORMAT] -- COMMAND" - o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]" - + parser = OptionParser.new(banner) do |o| o.on('--mode [MODE]', String, 'Mode of sampling: cpu, wall, object, default to wall') do |mode| env["STACKPROF_MODE"] = mode end @@ -37,10 +39,7 @@ if ARGV.first == "run" else options = {} - parser = OptionParser.new(ARGV) do |o| - o.banner = "Usage: stackprof run [--mode|--out|--interval] -- COMMAND" - o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]" - + parser = OptionParser.new(banner) do |o| o.on('--text', 'Text summary per method (default)'){ options[:format] = :text } o.on('--json', 'JSON output (use with web viewers)'){ options[:format] = :json } o.on('--files', 'List of files'){ |f| options[:format] = :files } From 455d76aba61f5a6e6c9a944ec75d1f57a2df102c Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Thu, 16 Mar 2023 01:04:05 -0400 Subject: [PATCH 17/35] Check that VM is running in sigaction --- ext/stackprof/stackprof.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 813ca76c..8d8f4ae2 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ static const char *fake_frame_cstrs[] = { }; static int stackprof_use_postponed_job = 1; +static int ruby_vm_running = 0; #define TOTAL_FAKE_FRAMES (sizeof(fake_frame_cstrs) / sizeof(char *)) @@ -725,6 +727,11 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext) if (!_stackprof.running) return; + // There's a possibility that the signal handler is invoked *after* the Ruby + // VM has been shut down (e.g. after ruby_cleanup(0)). In this case, things + // that rely on global VM state (e.g. rb_during_gc) will segfault. + if (!ruby_vm_running) return; + if (_stackprof.mode == sym_wall) { // In "wall" mode, the SIGALRM signal will arrive at an arbitrary thread. // In order to provide more useful results, especially under threaded web @@ -845,6 +852,12 @@ stackprof_use_postponed_job_l(VALUE self) return Qnil; } +static void +stackprof_at_exit(ruby_vm_t* vm) +{ + ruby_vm_running = 0; +} + void Init_stackprof(void) { @@ -855,6 +868,9 @@ Init_stackprof(void) */ stackprof_use_postponed_job = RUBY_API_VERSION_MAJOR < 3; + ruby_vm_running = 1; + ruby_vm_at_exit(stackprof_at_exit); + #define S(name) sym_##name = ID2SYM(rb_intern(#name)); S(object); S(custom); From f27ee5b5b3e47cfc8c3832cd38d0f158d713938a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 20 Mar 2023 09:14:09 -0700 Subject: [PATCH 18/35] bump version --- lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index 4de14e22..b6c34d95 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -14,7 +14,7 @@ end module StackProf - VERSION = '0.2.23' + VERSION = '0.2.24' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index d415a11b..3ff99bfb 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.23' + s.version = '0.2.24' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From c1f12ca76773267669b8234f769a87a74ac41fd7 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 5 Apr 2023 11:38:58 -0400 Subject: [PATCH 19/35] Mark frames_buffer Objects in frames_buffer may not have been placed the frames ST table and may not be held on by Ruby meaning it could get garbage collected. --- ext/stackprof/stackprof.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 8d8f4ae2..508c22cd 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -811,6 +811,10 @@ stackprof_gc_mark(void *data) if (_stackprof.frames) st_foreach(_stackprof.frames, frame_mark_i, 0); + + for (int i = 0; i < _stackprof.buffer_count; i++) { + rb_gc_mark(_stackprof.frames_buffer[i]); + } } static void From e26374695343e6eab0d43644f4503391fd1966ed Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 6 Apr 2023 13:28:39 -0700 Subject: [PATCH 20/35] bump version --- CHANGELOG.md | 4 ++++ lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da3a055a..c6774d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.2.25 + +* Fix GC marking + # 0.2.16 * [flamegraph.pl] Update to latest version diff --git a/lib/stackprof.rb b/lib/stackprof.rb index b6c34d95..0d39dca8 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -14,7 +14,7 @@ end module StackProf - VERSION = '0.2.24' + VERSION = '0.2.25' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index 3ff99bfb..2964d4c2 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.24' + s.version = '0.2.25' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From 0cee272499999504989fd905e79d5a01d4be7f88 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 2 May 2023 23:15:39 -0700 Subject: [PATCH 21/35] Add Ruby 3.2 to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 983b2082..fcf12a14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ ruby-head, '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2', truffleruby ] + ruby: [ ruby-head, '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2', truffleruby ] steps: - name: Checkout uses: actions/checkout@v2 From 4f4e783cff16806825152d13b77c3c793f1946c5 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 9 May 2023 09:51:43 +0900 Subject: [PATCH 22/35] Restore pre-C99 compatibility. Fix: https://github.com/tmm1/stackprof/issues/210 Ref: https://github.com/tmm1/stackprof/pull/202 --- ext/stackprof/stackprof.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 508c22cd..54722e28 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -812,7 +812,8 @@ stackprof_gc_mark(void *data) if (_stackprof.frames) st_foreach(_stackprof.frames, frame_mark_i, 0); - for (int i = 0; i < _stackprof.buffer_count; i++) { + int i; + for (i = 0; i < _stackprof.buffer_count; i++) { rb_gc_mark(_stackprof.frames_buffer[i]); } } From bb92978cfc7d1f3ccddb62ae59c67c93c1d3de8b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jul 2023 12:01:57 -0700 Subject: [PATCH 23/35] Remove mocha / clean up assertions This commit removes mocha and cleans up some assertions. I want to reduce the dependencies and get CI green --- .github/workflows/ci.yml | 2 +- stackprof.gemspec | 1 - test/test_middleware.rb | 45 ++++++++++++++++++++++++++-------------- test/test_stackprof.rb | 11 ++++++++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcf12a14..03f2f1e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ ruby-head, '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2', truffleruby ] + ruby: [ ruby-head, '3.2', '3.1', '3.0', '2.7', truffleruby ] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/stackprof.gemspec b/stackprof.gemspec index 2964d4c2..4f6b34cf 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -29,6 +29,5 @@ Gem::Specification.new do |s| s.license = 'MIT' s.add_development_dependency 'rake-compiler', '~> 0.9' - s.add_development_dependency 'mocha', '~> 0.14' s.add_development_dependency 'minitest', '~> 5.0' end diff --git a/test/test_middleware.rb b/test/test_middleware.rb index afb335aa..b92e1ce1 100644 --- a/test/test_middleware.rb +++ b/test/test_middleware.rb @@ -2,7 +2,7 @@ require 'stackprof' require 'stackprof/middleware' require 'minitest/autorun' -require 'mocha/setup' +require 'tmpdir' class StackProf::MiddlewareTest < MiniTest::Test @@ -19,23 +19,36 @@ def test_path_custom end def test_save_default - StackProf::Middleware.new(Object.new) - - StackProf.stubs(:results).returns({ mode: 'foo' }) - FileUtils.expects(:mkdir_p).with('tmp/') - File.expects(:open).with(regexp_matches(/^tmp\/stackprof-foo/), 'wb') - - StackProf::Middleware.save + middleware = StackProf::Middleware.new(->(env) { 100.times { Object.new } }, + save_every: 1, + enabled: true) + Dir.mktmpdir do |dir| + Dir.chdir(dir) { middleware.call({}) } + dir = File.join(dir, "tmp") + assert File.directory? dir + profile = Dir.entries(dir).reject { |x| File.directory?(x) }.first + assert profile + assert_equal "stackprof", profile.split("-")[0] + assert_equal "cpu", profile.split("-")[1] + assert_equal Process.pid.to_s, profile.split("-")[2] + end end def test_save_custom - StackProf::Middleware.new(Object.new, { path: 'foo/' }) - - StackProf.stubs(:results).returns({ mode: 'foo' }) - FileUtils.expects(:mkdir_p).with('foo/') - File.expects(:open).with(regexp_matches(/^foo\/stackprof-foo/), 'wb') - - StackProf::Middleware.save + middleware = StackProf::Middleware.new(->(env) { 100.times { Object.new } }, + path: "foo/", + save_every: 1, + enabled: true) + Dir.mktmpdir do |dir| + Dir.chdir(dir) { middleware.call({}) } + dir = File.join(dir, "foo") + assert File.directory? dir + profile = Dir.entries(dir).reject { |x| File.directory?(x) }.first + assert profile + assert_equal "stackprof", profile.split("-")[0] + assert_equal "cpu", profile.split("-")[1] + assert_equal Process.pid.to_s, profile.split("-")[2] + end end def test_enabled_should_use_a_proc_if_passed @@ -70,4 +83,4 @@ def test_metadata StackProf::Middleware.new(Object.new, metadata: metadata) assert_equal metadata, StackProf::Middleware.metadata end -end +end unless RUBY_ENGINE == 'truffleruby' diff --git a/test/test_stackprof.rb b/test/test_stackprof.rb index 2aedc0bc..dc9948be 100644 --- a/test/test_stackprof.rb +++ b/test/test_stackprof.rb @@ -93,6 +93,7 @@ def test_cputime end def test_walltime + GC.disable profile = StackProf.run(mode: :wall) do idle end @@ -104,6 +105,8 @@ def test_walltime assert_equal "StackProfTest#idle", frame[:name] end assert_in_delta 200, frame[:samples], 25 + ensure + GC.enable end def test_custom @@ -241,8 +244,12 @@ def test_gc assert marking_frame assert sweeping_frame - assert_equal gc_frame[:total_samples], profile[:gc_samples] - assert_equal profile[:gc_samples], [gc_frame, marking_frame, sweeping_frame].map{|x| x[:samples] }.inject(:+) + # We can't guarantee a certain number of GCs to run, so just assert + # that it's within some kind of delta + assert_in_delta gc_frame[:total_samples], profile[:gc_samples], 2 + + # Lazy marking / sweeping can cause this math to not add up, so also use a delta + assert_in_delta profile[:gc_samples], [gc_frame, marking_frame, sweeping_frame].map{|x| x[:samples] }.inject(:+), 2 assert_operator profile[:gc_samples], :>, 0 assert_operator profile[:missed_samples], :<=, 25 From 03369b945018b1fc05732dcd90c8daea32b2938d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jul 2023 17:05:02 -0700 Subject: [PATCH 24/35] Add raw line numbers for raw mode This commit records line numbers in raw mode along with the frame information. Before this commit, stackprof would lose information about callee frames at a particular line. For example, you could not answer "given a frame and line, what function do we call in to at that line?" This commit encodes the line information along with the raw frame information so that we can answer that question. This is probably TMI, but when we ask the Ruby frame profiler, it returns a list of memory addresses (CMEs or possibly iseqs?). AArch64 systems guarantee the top 16 bits won't be used, x86 doesn't use them either (but possibly could in the future, but probably not). Armed with this information, we just put the line number in those top 16 bits and we don't need to allocate any extra memory when doing a profile. For backwards compatibility, this patch splits apart the information in the `.result` method so existing tools should just work. --- ext/stackprof/stackprof.c | 33 +++++++++++++++++++++++++++------ test/test_stackprof.rb | 3 +++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 54722e28..266109b2 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -102,7 +102,7 @@ static struct { VALUE metadata; int ignore_gc; - VALUE *raw_samples; + uint64_t *raw_samples; size_t raw_samples_len; size_t raw_samples_capa; size_t raw_sample_index; @@ -133,7 +133,7 @@ static struct { static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line; static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines; -static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_metadata, sym_frames, sym_ignore_gc, sym_out; +static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_raw_lines, sym_metadata, sym_frames, sym_ignore_gc, sym_out; static VALUE sym_aggregate, sym_raw_sample_timestamps, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping; static VALUE sym_gc_samples, objtracer; static VALUE gc_hook; @@ -374,14 +374,23 @@ stackprof_results(int argc, VALUE *argv, VALUE self) size_t len, n, o; VALUE raw_sample_timestamps, raw_timestamp_deltas; VALUE raw_samples = rb_ary_new_capa(_stackprof.raw_samples_len); + VALUE raw_lines = rb_ary_new_capa(_stackprof.raw_samples_len); for (n = 0; n < _stackprof.raw_samples_len; n++) { len = (size_t)_stackprof.raw_samples[n]; rb_ary_push(raw_samples, SIZET2NUM(len)); + rb_ary_push(raw_lines, SIZET2NUM(len)); + + for (o = 0, n++; o < len; n++, o++) { + // Line is in the upper 16 bits + rb_ary_push(raw_lines, INT2NUM(_stackprof.raw_samples[n] >> 48)); + + VALUE frame = _stackprof.raw_samples[n] & ~((uint64_t)0xFFFF << 48); + rb_ary_push(raw_samples, PTR2NUM(frame)); + } - for (o = 0, n++; o < len; n++, o++) - rb_ary_push(raw_samples, PTR2NUM(_stackprof.raw_samples[n])); rb_ary_push(raw_samples, SIZET2NUM((size_t)_stackprof.raw_samples[n])); + rb_ary_push(raw_lines, SIZET2NUM((size_t)_stackprof.raw_samples[n])); } free(_stackprof.raw_samples); @@ -391,6 +400,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self) _stackprof.raw_sample_index = 0; rb_hash_aset(results, sym_raw, raw_samples); + rb_hash_aset(results, sym_raw_lines, raw_lines); raw_sample_timestamps = rb_ary_new_capa(_stackprof.raw_sample_times_len); raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_sample_times_len); @@ -520,7 +530,12 @@ stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t ti * in the frames buffer that came from Ruby. */ for (i = num-1, n = 0; i >= 0; i--, n++) { VALUE frame = _stackprof.frames_buffer[i]; - if (_stackprof.raw_samples[_stackprof.raw_sample_index + 1 + n] != frame) + int line = _stackprof.lines_buffer[i]; + + // Encode the line in to the upper 16 bits. + uint64_t key = ((uint64_t)line << 48) | (uint64_t)frame; + + if (_stackprof.raw_samples[_stackprof.raw_sample_index + 1 + n] != key) break; } if (i == -1) { @@ -538,7 +553,12 @@ stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t ti _stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)num; for (i = num-1; i >= 0; i--) { VALUE frame = _stackprof.frames_buffer[i]; - _stackprof.raw_samples[_stackprof.raw_samples_len++] = frame; + int line = _stackprof.lines_buffer[i]; + + // Encode the line in to the upper 16 bits. + uint64_t key = ((uint64_t)line << 48) | (uint64_t)frame; + + _stackprof.raw_samples[_stackprof.raw_samples_len++] = key; } _stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)1; } @@ -894,6 +914,7 @@ Init_stackprof(void) S(mode); S(interval); S(raw); + S(raw_lines); S(raw_sample_timestamps); S(raw_timestamp_deltas); S(out); diff --git a/test/test_stackprof.rb b/test/test_stackprof.rb index dc9948be..d0de3309 100644 --- a/test/test_stackprof.rb +++ b/test/test_stackprof.rb @@ -145,10 +145,13 @@ def test_raw after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) raw = profile[:raw] + raw_lines = profile[:raw_lines] assert_equal 10, raw[-1] assert_equal raw[0] + 2, raw.size + assert_equal 10, raw_lines[-1] # seen 10 times offset = RUBY_VERSION >= '3' ? -3 : -2 + assert_equal 140, raw_lines[offset] # sample caller is on 140 assert_includes profile[:frames][raw[offset]][:name], 'StackProfTest#test_raw' assert_equal 10, profile[:raw_sample_timestamps].size From 329e57bced0a163bcf93288b2debae60d3514657 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 11 Jul 2023 16:49:57 -0700 Subject: [PATCH 25/35] Fix GC profiling timing We were recording GC profile timestamps inside the VM postponed job that flushes temporary information. The postponed job could be run much later than the sample was actually taken, so this commit records a timestamp in the signal handler when there is a GC event. It only records one time stamp, so if there are multiple GC events in a row, we have to assume that the total duration spent in GC starts from the first time stamp and extends until the timestamp of the next non-GC sample. In other words, we don't record a correct timestamp for each GC sample, only the first one. --- ext/stackprof/stackprof.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 266109b2..e7864b85 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -120,6 +120,8 @@ static struct { size_t unrecorded_gc_sweeping_samples; st_table *frames; + timestamp_t gc_start_timestamp; + VALUE fake_frame_names[TOTAL_FAKE_FRAMES]; VALUE empty_string; @@ -646,6 +648,7 @@ stackprof_buffer_sample(void) _stackprof.buffer_time.delta_usec = timestamp_delta; } +// Postponed job void stackprof_record_gc_samples(void) { @@ -653,8 +656,7 @@ stackprof_record_gc_samples(void) uint64_t start_timestamp = 0; size_t i; if (_stackprof.raw) { - struct timestamp_t t; - capture_timestamp(&t); + struct timestamp_t t = _stackprof.gc_start_timestamp; start_timestamp = timestamp_usec(&t); // We don't know when the GC samples were actually marked, so let's @@ -776,6 +778,10 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext) } else if (mode == sym_sweeping) { _stackprof.unrecorded_gc_sweeping_samples++; } + if(!_stackprof.unrecorded_gc_samples) { + // record start + capture_timestamp(&_stackprof.gc_start_timestamp); + } _stackprof.unrecorded_gc_samples++; rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0); } else { From c771fd4fd87a55cd7eb1ff60830518f024b8a2b9 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 8 Nov 2023 15:30:04 +0100 Subject: [PATCH 26/35] Don't use postponed jobs on Ruby 3.3+YJIT Ref: https://github.com/ruby/ruby/commit/a1dc1a3de9683daf5a543d6f618e17aabfcb8708 --- lib/stackprof.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index 0d39dca8..fbea42bb 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -5,7 +5,11 @@ end if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? - StackProf.use_postponed_job! + if RUBY_VERSION < "3.3" + # On 3.3 we don't need postponed jobs: + # https://github.com/ruby/ruby/commit/a1dc1a3de9683daf5a543d6f618e17aabfcb8708 + StackProf.use_postponed_job! + end elsif RUBY_VERSION == "3.2.0" # 3.2.0 crash is the signal is received at the wrong time. # Fixed in https://github.com/ruby/ruby/pull/7116 From c1e71bd10a4a7206fc8674669080cac5d404341a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 8 Nov 2023 15:33:36 +0100 Subject: [PATCH 27/35] Fix compatibility with newer minitest --- test/test_middleware.rb | 2 +- test/test_report.rb | 4 ++-- test/test_stackprof.rb | 2 +- test/test_truffleruby.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_middleware.rb b/test/test_middleware.rb index b92e1ce1..191c9385 100644 --- a/test/test_middleware.rb +++ b/test/test_middleware.rb @@ -4,7 +4,7 @@ require 'minitest/autorun' require 'tmpdir' -class StackProf::MiddlewareTest < MiniTest::Test +class StackProf::MiddlewareTest < Minitest::Test def test_path_default StackProf::Middleware.new(Object.new) diff --git a/test/test_report.rb b/test/test_report.rb index 3b0dd1b4..2dffb390 100644 --- a/test/test_report.rb +++ b/test/test_report.rb @@ -2,7 +2,7 @@ require 'stackprof' require 'minitest/autorun' -class ReportDumpTest < MiniTest::Test +class ReportDumpTest < Minitest::Test require 'stringio' def test_dump_to_stdout @@ -33,7 +33,7 @@ def assert_dump(expected, marshal_data) end end -class ReportReadTest < MiniTest::Test +class ReportReadTest < Minitest::Test require 'pathname' def test_from_file_read_json diff --git a/test/test_stackprof.rb b/test/test_stackprof.rb index d0de3309..b979e344 100644 --- a/test/test_stackprof.rb +++ b/test/test_stackprof.rb @@ -4,7 +4,7 @@ require 'tempfile' require 'pathname' -class StackProfTest < MiniTest::Test +class StackProfTest < Minitest::Test def setup Object.new # warm some caches to avoid flakiness end diff --git a/test/test_truffleruby.rb b/test/test_truffleruby.rb index 6b6b8a5e..37ae767c 100644 --- a/test/test_truffleruby.rb +++ b/test/test_truffleruby.rb @@ -3,7 +3,7 @@ require 'minitest/autorun' if RUBY_ENGINE == 'truffleruby' - class StackProfTruffleRubyTest < MiniTest::Test + class StackProfTruffleRubyTest < Minitest::Test def test_error error = assert_raises RuntimeError do StackProf.run(mode: :cpu) do From aaeef7163f38b720e18bf902a246b1439d000467 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 30 Nov 2023 12:58:00 +0100 Subject: [PATCH 28/35] Migrate to the TypedData API It's a single object so not really worth implementing write barriers or any other advanced features. The only goal here is to stop using a deprecated API. --- ext/stackprof/stackprof.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index e7864b85..4cd865c2 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -844,6 +844,12 @@ stackprof_gc_mark(void *data) } } +static size_t +stackprof_memsize(const void *data) +{ + return sizeof(_stackprof); +} + static void stackprof_atfork_prepare(void) { @@ -889,6 +895,15 @@ stackprof_at_exit(ruby_vm_t* vm) ruby_vm_running = 0; } +static const rb_data_type_t stackprof_type = { + "StackProf", + { + stackprof_gc_mark, + NULL, + stackprof_memsize, + } +}; + void Init_stackprof(void) { @@ -936,8 +951,8 @@ Init_stackprof(void) /* Need to run this to warm the symbol table before we call this during GC */ rb_gc_latest_gc_info(sym_state); - gc_hook = Data_Wrap_Struct(rb_cObject, stackprof_gc_mark, NULL, &_stackprof); rb_global_variable(&gc_hook); + gc_hook = TypedData_Wrap_Struct(rb_cObject, &stackprof_type, &_stackprof); _stackprof.raw_samples = NULL; _stackprof.raw_samples_len = 0; From ebdd3af48a2c4ddf35b0a73858ea72dcdb551188 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 15 Jan 2024 08:59:24 -0800 Subject: [PATCH 29/35] bumping version --- lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index fbea42bb..d46c93b3 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -18,7 +18,7 @@ end module StackProf - VERSION = '0.2.25' + VERSION = '0.2.26' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index 4f6b34cf..e853ca3b 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.25' + s.version = '0.2.26' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From 08b5127b032c3dbffd0e0ce8eac719008658318c Mon Sep 17 00:00:00 2001 From: Nathan Froyd Date: Mon, 5 Feb 2024 09:11:51 -0500 Subject: [PATCH 30/35] don't set `running` until all relevant state is initialized --- ext/stackprof/stackprof.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 4cd865c2..015a0bbd 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -217,7 +217,6 @@ stackprof_start(int argc, VALUE *argv, VALUE self) rb_raise(rb_eArgError, "unknown profiler mode"); } - _stackprof.running = 1; _stackprof.raw = raw; _stackprof.aggregate = aggregate; _stackprof.mode = mode; @@ -226,6 +225,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self) _stackprof.metadata = metadata; _stackprof.out = out; _stackprof.target_thread = pthread_self(); + _stackprof.running = 1; if (raw) { capture_timestamp(&_stackprof.last_sample_at); From 4e504d37188bb06dfa2ab07b253bedcf4d03ef40 Mon Sep 17 00:00:00 2001 From: Nathan Froyd Date: Mon, 5 Feb 2024 09:37:26 -0500 Subject: [PATCH 31/35] be more diligent about atomic operations --- ext/stackprof/stackprof.c | 50 +++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/ext/stackprof/stackprof.c b/ext/stackprof/stackprof.c index 015a0bbd..36033812 100644 --- a/ext/stackprof/stackprof.c +++ b/ext/stackprof/stackprof.c @@ -91,7 +91,19 @@ typedef struct { int64_t delta_usec; } sample_time_t; +/* We need to ensure that various memory operations are visible across + * threads. Ruby doesn't offer a portable way to do this sort of detection + * across all the Ruby versions we support, so we use something that casts a + * wide net (Clang, along with ICC, defines __GNUC__). */ +#if defined(__GNUC__) && defined(__ATOMIC_SEQ_CST) +#define STACKPROF_HAVE_ATOMICS 1 +#else +#define STACKPROF_HAVE_ATOMICS 0 +#endif + static struct { + /* Access this field with the `STACKPROF_RUNNING` macro, below, since we + * can't properly express that this field has an atomic type. */ int running; int raw; int aggregate; @@ -133,6 +145,12 @@ static struct { pthread_t target_thread; } _stackprof; +#if STACKPROF_HAVE_ATOMICS +#define STACKPROF_RUNNING() __atomic_load_n(&_stackprof.running, __ATOMIC_ACQUIRE) +#else +#define STACKPROF_RUNNING() _stackprof.running +#endif + static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line; static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines; static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_raw_lines, sym_metadata, sym_frames, sym_ignore_gc, sym_out; @@ -154,7 +172,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self) int raw = 0, aggregate = 1; VALUE metadata_val; - if (_stackprof.running) + if (STACKPROF_RUNNING()) return Qfalse; rb_scan_args(argc, argv, "0:", &opts); @@ -225,7 +243,13 @@ stackprof_start(int argc, VALUE *argv, VALUE self) _stackprof.metadata = metadata; _stackprof.out = out; _stackprof.target_thread = pthread_self(); + /* We need to ensure previous initialization stores are visible across + * threads. */ +#if STACKPROF_HAVE_ATOMICS + __atomic_store_n(&_stackprof.running, 1, __ATOMIC_SEQ_CST); +#else _stackprof.running = 1; +#endif if (raw) { capture_timestamp(&_stackprof.last_sample_at); @@ -240,9 +264,15 @@ stackprof_stop(VALUE self) struct sigaction sa; struct itimerval timer; +#if STACKPROF_HAVE_ATOMICS + int was_running = __atomic_exchange_n(&_stackprof.running, 0, __ATOMIC_SEQ_CST); + if (!was_running) + return Qfalse; +#else if (!_stackprof.running) return Qfalse; _stackprof.running = 0; +#endif if (_stackprof.mode == sym_object) { rb_tracepoint_disable(objtracer); @@ -351,7 +381,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self) { VALUE results, frames; - if (!_stackprof.frames || _stackprof.running) + if (!_stackprof.frames || STACKPROF_RUNNING()) return Qnil; results = rb_hash_new(); @@ -455,7 +485,7 @@ stackprof_run(int argc, VALUE *argv, VALUE self) static VALUE stackprof_running_p(VALUE self) { - return _stackprof.running ? Qtrue : Qfalse; + return STACKPROF_RUNNING() ? Qtrue : Qfalse; } static inline frame_data_t * @@ -719,7 +749,7 @@ stackprof_sample_and_record(void) static void stackprof_job_record_gc(void *data) { - if (!_stackprof.running) return; + if (!STACKPROF_RUNNING()) return; stackprof_record_gc_samples(); } @@ -727,7 +757,7 @@ stackprof_job_record_gc(void *data) static void stackprof_job_sample_and_record(void *data) { - if (!_stackprof.running) return; + if (!STACKPROF_RUNNING()) return; stackprof_sample_and_record(); } @@ -735,7 +765,7 @@ stackprof_job_sample_and_record(void *data) static void stackprof_job_record_buffer(void *data) { - if (!_stackprof.running) return; + if (!STACKPROF_RUNNING()) return; stackprof_record_buffer(); } @@ -747,7 +777,7 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext) _stackprof.overall_signals++; - if (!_stackprof.running) return; + if (!STACKPROF_RUNNING()) return; // There's a possibility that the signal handler is invoked *after* the Ruby // VM has been shut down (e.g. after ruby_cleanup(0)). In this case, things @@ -810,7 +840,7 @@ stackprof_newobj_handler(VALUE tpval, void *data) static VALUE stackprof_sample(VALUE self) { - if (!_stackprof.running) + if (!STACKPROF_RUNNING()) return Qfalse; _stackprof.overall_signals++; @@ -854,7 +884,7 @@ static void stackprof_atfork_prepare(void) { struct itimerval timer; - if (_stackprof.running) { + if (STACKPROF_RUNNING()) { if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) { memset(&timer, 0, sizeof(timer)); setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0); @@ -866,7 +896,7 @@ static void stackprof_atfork_parent(void) { struct itimerval timer; - if (_stackprof.running) { + if (STACKPROF_RUNNING()) { if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) { timer.it_interval.tv_sec = 0; timer.it_interval.tv_usec = NUM2LONG(_stackprof.interval); From 02b866a30be24b53e0957464bd24f63acde91a08 Mon Sep 17 00:00:00 2001 From: s4na Date: Fri, 14 Jun 2024 10:28:50 +0900 Subject: [PATCH 32/35] Add Ruby 3.3 to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03f2f1e8..88f42d90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ ruby-head, '3.2', '3.1', '3.0', '2.7', truffleruby ] + ruby: [ ruby-head, '3.3','3.2', '3.1', '3.0', '2.7', truffleruby ] steps: - name: Checkout uses: actions/checkout@v2 From a4d23d18fe27967da3de8246803fe42cb2f76009 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 15 Jan 2024 08:59:24 -0800 Subject: [PATCH 33/35] bumping version --- lib/stackprof.rb | 2 +- stackprof.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stackprof.rb b/lib/stackprof.rb index d46c93b3..98a892ad 100644 --- a/lib/stackprof.rb +++ b/lib/stackprof.rb @@ -18,7 +18,7 @@ end module StackProf - VERSION = '0.2.26' + VERSION = '0.2.27' end StackProf.autoload :Report, "stackprof/report.rb" diff --git a/stackprof.gemspec b/stackprof.gemspec index e853ca3b..0f70a5f0 100644 --- a/stackprof.gemspec +++ b/stackprof.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'stackprof' - s.version = '0.2.26' + s.version = '0.2.27' s.homepage = 'http://github.com/tmm1/stackprof' s.authors = 'Aman Gupta' From 93ab15fa673a3954bea6687553779576ecf38db3 Mon Sep 17 00:00:00 2001 From: Bojan Marjanovic Date: Fri, 21 Feb 2025 13:36:14 +0100 Subject: [PATCH 34/35] Add Ruby 3.4 to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88f42d90..86ab2746 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ ruby-head, '3.3','3.2', '3.1', '3.0', '2.7', truffleruby ] + ruby: [ ruby-head, '3.4', '3.3','3.2', '3.1', '3.0', truffleruby ] steps: - name: Checkout uses: actions/checkout@v2 From fae0152c6269a8fb7f60aabfd7bc6fb715499bd6 Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Wed, 1 Oct 2025 09:35:44 -0400 Subject: [PATCH 35/35] Read only first two bytes to check signature --- lib/stackprof/report.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/stackprof/report.rb b/lib/stackprof/report.rb index e282f668..26478802 100644 --- a/lib/stackprof/report.rb +++ b/lib/stackprof/report.rb @@ -10,10 +10,14 @@ class Report class << self def from_file(file) - if (content = IO.binread(file)).start_with?(MARSHAL_SIGNATURE) - new(Marshal.load(content)) - else - from_json(JSON.parse(content)) + File.open(file, 'rb') do |f| + signature_bytes = f.read(2) + f.rewind + if signature_bytes == MARSHAL_SIGNATURE + new(Marshal.load(f)) + else + from_json(JSON.parse(f.read)) + end end end