From 4c92546baa10ae2299274be6af16b50476fab52f Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 9 Oct 2025 17:40:28 +0200 Subject: [PATCH] Add new config.profiles_sample_interval --- CHANGELOG.md | 2 ++ sentry-ruby/lib/sentry/configuration.rb | 13 ++++++++++ sentry-ruby/lib/sentry/profiler.rb | 5 ++-- sentry-ruby/lib/sentry/vernier/profiler.rb | 3 ++- sentry-ruby/spec/sentry/configuration_spec.rb | 6 +++++ sentry-ruby/spec/sentry/profiler_spec.rb | 24 +++++++++++++++++++ .../spec/sentry/vernier/profiler_spec.rb | 23 ++++++++++++++++-- 7 files changed, 70 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f03ff8e92..5e71ad682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ config.trace_ignore_status_codes = [404, (501..503)] end ``` +- Add `config.profiles_sample_interval` to control sampling frequency ([#2745](https://github.com/getsentry/sentry-ruby/pull/2745)) + - Both `stackprof` and `vernier` now get sampled at a default frequency of 101 Hz. ### Internal diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 587e71619..aae16487d 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -319,6 +319,15 @@ class Configuration # @return [Float, nil] attr_reader :profiles_sample_rate + # Interval in microseconds at which to take samples. + # The default is 1e6 / 101, or 101Hz. + # Note that the 101 is intentional to avoid lockstep sampling. + # + # @example + # config.profiles_sample_interval = 1e5 / 101 + # @return [Float] + attr_accessor :profiles_sample_interval + # Array of patches to apply. # Default is {DEFAULT_PATCHES} # @return [Array] @@ -373,6 +382,9 @@ class Configuration APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/ + # 101 Hz in microseconds + DEFAULT_PROFILES_SAMPLE_INTERVAL = 1e6 / 101 + class << self # Post initialization callbacks are called at the end of initialization process # allowing extending the configuration of sentry-ruby by multiple extensions @@ -492,6 +504,7 @@ def initialize self.enable_logs = false self.profiler_class = Sentry::Profiler + self.profiles_sample_interval = DEFAULT_PROFILES_SAMPLE_INTERVAL @transport = Transport::Configuration.new @cron = Cron::Configuration.new diff --git a/sentry-ruby/lib/sentry/profiler.rb b/sentry-ruby/lib/sentry/profiler.rb index 5c5dd28a9..ba5b5d525 100644 --- a/sentry-ruby/lib/sentry/profiler.rb +++ b/sentry-ruby/lib/sentry/profiler.rb @@ -10,8 +10,6 @@ class Profiler VERSION = "1" PLATFORM = "ruby" - # 101 Hz in microseconds - DEFAULT_INTERVAL = 1e6 / 101 MICRO_TO_NANO_SECONDS = 1e3 MIN_SAMPLES_REQUIRED = 2 @@ -24,6 +22,7 @@ def initialize(configuration) @profiling_enabled = defined?(StackProf) && configuration.profiling_enabled? @profiles_sample_rate = configuration.profiles_sample_rate + @profiles_sample_interval = configuration.profiles_sample_interval @project_root = configuration.project_root @app_dirs_pattern = configuration.app_dirs_pattern @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}") @@ -32,7 +31,7 @@ def initialize(configuration) def start return unless @sampled - @started = StackProf.start(interval: DEFAULT_INTERVAL, + @started = StackProf.start(interval: @profiles_sample_interval, mode: :wall, raw: true, aggregate: false) diff --git a/sentry-ruby/lib/sentry/vernier/profiler.rb b/sentry-ruby/lib/sentry/vernier/profiler.rb index c3678eb70..028f85979 100644 --- a/sentry-ruby/lib/sentry/vernier/profiler.rb +++ b/sentry-ruby/lib/sentry/vernier/profiler.rb @@ -20,6 +20,7 @@ def initialize(configuration) @profiling_enabled = defined?(Vernier) && configuration.profiling_enabled? @profiles_sample_rate = configuration.profiles_sample_rate + @profiles_sample_interval = configuration.profiles_sample_interval @project_root = configuration.project_root @app_dirs_pattern = configuration.app_dirs_pattern @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}") @@ -56,7 +57,7 @@ def start return unless @sampled return if @started - @started = ::Vernier.start_profile + @started = ::Vernier.start_profile(interval: @profiles_sample_interval) log("Started") diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index e6d94146c..17a7d9730 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -194,6 +194,12 @@ end end + describe "#profiles_sample_interval" do + it "defaults to 101 Hz" do + expect(subject.profiles_sample_interval).to eq(1e6 / 101) + end + end + describe "#transport" do it "returns an initialized Transport::Configuration object" do transport_config = subject.transport diff --git a/sentry-ruby/spec/sentry/profiler_spec.rb b/sentry-ruby/spec/sentry/profiler_spec.rb index e5ca44cbe..2de41ca13 100644 --- a/sentry-ruby/spec/sentry/profiler_spec.rb +++ b/sentry-ruby/spec/sentry/profiler_spec.rb @@ -74,6 +74,30 @@ expect(subject.started).to eq(false) end end + + context 'with custom profiles_sample_interval' do + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.profiles_sample_rate = 1.0 + config.profiles_sample_interval = 1e5 / 101 + end + end + + it 'starts StackProf with custom interval' do + subject.set_initial_sample_decision(true) + + expect(StackProf).to receive(:start).with( + interval: 1e5 / 101, + mode: :wall, + raw: true, + aggregate: false + ).and_call_original + + subject.start + expect(subject.started).to eq(true) + end + end end describe '#stop' do diff --git a/sentry-ruby/spec/sentry/vernier/profiler_spec.rb b/sentry-ruby/spec/sentry/vernier/profiler_spec.rb index eeb1222b4..d6c564a77 100644 --- a/sentry-ruby/spec/sentry/vernier/profiler_spec.rb +++ b/sentry-ruby/spec/sentry/vernier/profiler_spec.rb @@ -74,7 +74,7 @@ end it 'starts Vernier if sampled' do - expect(Vernier).to receive(:start_profile).and_return(true) + expect(Vernier).to receive(:start_profile).with(interval: 1e6 / 101).and_return(true) profiler.start @@ -82,7 +82,7 @@ end it 'does not start Vernier again if already started' do - expect(Vernier).to receive(:start_profile).and_return(true).once + expect(Vernier).to receive(:start_profile).with(interval: 1e6 / 101).and_return(true).once profiler.start profiler.start @@ -110,6 +110,25 @@ expect(profiler.started).to eq(false) end end + + context 'with custom profiles_sample_interval' do + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.profiles_sample_rate = 1.0 + config.profiles_sample_interval = 1e5 / 101 + end + end + + it 'starts Vernier with custom interval' do + expect(Vernier).to receive(:start_profile).with(interval: 1e5 / 101).and_return(true) + + profiler.set_initial_sample_decision(true) + profiler.start + + expect(profiler.started).to eq(true) + end + end end describe '#stop' do