From 1da16402ac91b27e254644c557a16e9127628f04 Mon Sep 17 00:00:00 2001 From: Alexandre Fonseca Date: Thu, 28 Dec 2023 15:15:37 +0000 Subject: [PATCH] [prof-heap] Heap size and sample rate configurations --- benchmarks/profiler_sample_loop_v2.rb | 12 +++- .../heap_recorder.c | 35 +++++++++- .../heap_recorder.h | 15 ++++ .../stack_recorder.c | 7 +- lib/datadog/core/configuration/settings.rb | 31 ++++++++ lib/datadog/profiling/component.rb | 39 +++++++++-- lib/datadog/profiling/stack_recorder.rb | 3 +- .../core/configuration/settings_spec.rb | 70 +++++++++++++++++++ spec/datadog/profiling/component_spec.rb | 28 +++++++- spec/datadog/profiling/spec_helper.rb | 6 +- spec/datadog/profiling/stack_recorder_spec.rb | 16 +++++ 11 files changed, 246 insertions(+), 16 deletions(-) diff --git a/benchmarks/profiler_sample_loop_v2.rb b/benchmarks/profiler_sample_loop_v2.rb index bfd66c7d596..13649759b8f 100644 --- a/benchmarks/profiler_sample_loop_v2.rb +++ b/benchmarks/profiler_sample_loop_v2.rb @@ -21,6 +21,7 @@ def create_profiler alloc_samples_enabled: false, heap_samples_enabled: false, heap_size_enabled: false, + heap_sample_every: 1, timeline_enabled: false, ) @collector = Datadog::Profiling::Collectors::ThreadContext.new( @@ -42,8 +43,11 @@ def thread_with_very_deep_stack(depth: 500) def run_benchmark Benchmark.ips do |x| - benchmark_time = VALIDATE_BENCHMARK_MODE ? {time: 0.01, warmup: 0} : {time: 10, warmup: 2} - x.config(**benchmark_time, suite: report_to_dogstatsd_if_enabled_via_environment_variable(benchmark_name: 'profiler_sample_loop_v2')) + benchmark_time = VALIDATE_BENCHMARK_MODE ? { time: 0.01, warmup: 0 } : { time: 10, warmup: 2 } + x.config( + **benchmark_time, + suite: report_to_dogstatsd_if_enabled_via_environment_variable(benchmark_name: 'profiler_sample_loop_v2') + ) x.report("stack collector #{ENV['CONFIG']}") do Datadog::Profiling::Collectors::ThreadContext::Testing._native_sample(@collector, PROFILER_OVERHEAD_STACK_THREAD) @@ -58,7 +62,9 @@ def run_benchmark def run_forever while true - 1000.times { Datadog::Profiling::Collectors::ThreadContext::Testing._native_sample(@collector, PROFILER_OVERHEAD_STACK_THREAD) } + 1000.times do + Datadog::Profiling::Collectors::ThreadContext::Testing._native_sample(@collector, PROFILER_OVERHEAD_STACK_THREAD) + end @recorder.serialize print '.' end diff --git a/ext/ddtrace_profiling_native_extension/heap_recorder.c b/ext/ddtrace_profiling_native_extension/heap_recorder.c index 8b7fdd4ef29..74a222527df 100644 --- a/ext/ddtrace_profiling_native_extension/heap_recorder.c +++ b/ext/ddtrace_profiling_native_extension/heap_recorder.c @@ -93,6 +93,7 @@ typedef struct { static object_record* object_record_new(long, heap_record*, live_object_data); static void object_record_free(object_record*); static VALUE object_record_inspect(object_record*); +static object_record SKIPPED_RECORD = {0}; struct heap_recorder { // Map[key: heap_record_key*, record: heap_record*] @@ -122,6 +123,10 @@ struct heap_recorder { // Reusable location array, implementing a flyweight pattern for things like iteration. ddog_prof_Location *reusable_locations; + + // Sampling controls + uint sample_rate; + uint num_recordings_skipped; }; static heap_record* get_or_create_heap_record(heap_recorder*, ddog_prof_Slice_Location); static void cleanup_heap_record_if_unused(heap_recorder*, heap_record*); @@ -151,6 +156,7 @@ heap_recorder* heap_recorder_new(void) { recorder->object_records_snapshot = NULL; recorder->reusable_locations = ruby_xcalloc(MAX_FRAMES_LIMIT, sizeof(ddog_prof_Location)); recorder->partial_object_record = NULL; + recorder->sample_rate = 1; // By default do no sampling return recorder; } @@ -184,6 +190,19 @@ void heap_recorder_free(heap_recorder *heap_recorder) { ruby_xfree(heap_recorder); } +void heap_recorder_set_sample_rate(heap_recorder *heap_recorder, int sample_rate) { + if (heap_recorder == NULL) { + return; + } + + if (sample_rate < 0) { + rb_raise(rb_eArgError, "Heap sample rate must be a positive integer value but was %d", sample_rate); + } + + heap_recorder->sample_rate = sample_rate; + heap_recorder->num_recordings_skipped = 0; +} + // WARN: Assumes this gets called before profiler is reinitialized on the fork void heap_recorder_after_fork(heap_recorder *heap_recorder) { if (heap_recorder == NULL) { @@ -214,6 +233,14 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj return; } + if (heap_recorder->num_recordings_skipped + 1 < heap_recorder->sample_rate) { + heap_recorder->partial_object_record = &SKIPPED_RECORD; + heap_recorder->num_recordings_skipped++; + return; + } + + heap_recorder->num_recordings_skipped = 0; + VALUE ruby_obj_id = rb_obj_id(new_obj); if (!FIXNUM_P(ruby_obj_id)) { rb_raise(rb_eRuntimeError, "Detected a bignum object id. These are not supported by heap profiling."); @@ -224,7 +251,7 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj } heap_recorder->partial_object_record = object_record_new(FIX2LONG(ruby_obj_id), NULL, (live_object_data) { - .weight = weight, + .weight = weight * heap_recorder->sample_rate, .class = alloc_class != NULL ? string_from_char_slice(*alloc_class) : NULL, .alloc_gen = rb_gc_count(), }); @@ -241,11 +268,15 @@ void end_heap_allocation_recording(struct heap_recorder *heap_recorder, ddog_pro // Recording ended without having been started? rb_raise(rb_eRuntimeError, "Ended a heap recording that was not started"); } - // From now on, mark active recording as invalid so we can short-circuit at any point and // not end up with a still active recording. partial_object_record still holds the object for this recording heap_recorder->partial_object_record = NULL; + if (partial_object_record == &SKIPPED_RECORD) { + // special marker when we decided to skip due to sampling + return; + } + heap_record *heap_record = get_or_create_heap_record(heap_recorder, locations); // And then commit the new allocation. diff --git a/ext/ddtrace_profiling_native_extension/heap_recorder.h b/ext/ddtrace_profiling_native_extension/heap_recorder.h index abff3bc6bbe..e35e1908a7b 100644 --- a/ext/ddtrace_profiling_native_extension/heap_recorder.h +++ b/ext/ddtrace_profiling_native_extension/heap_recorder.h @@ -53,6 +53,21 @@ heap_recorder* heap_recorder_new(void); // Free a previously initialized heap recorder. void heap_recorder_free(heap_recorder *heap_recorder); +// Set sample rate used by this heap recorder. +// +// Controls how many recordings will be ignored before committing a heap allocation and +// the weight of the committed heap allocation. +// +// A value of 1 will effectively track all objects that are passed through +// start/end_heap_allocation_recording pairs. A value of 10 will only track every 10th +// object passed through such calls and its effective weight for the purposes of heap +// profiling will be multiplied by 10. +// +// NOTE: Default is 1, i.e., track all heap allocation recordings. +// +// WARN: Non-positive values will lead to an exception being thrown. +void heap_recorder_set_sample_rate(heap_recorder *heap_recorder, int sample_rate); + // Do any cleanup needed after forking. // WARN: Assumes this gets called before profiler is reinitialized on the fork void heap_recorder_after_fork(heap_recorder *heap_recorder); diff --git a/ext/ddtrace_profiling_native_extension/stack_recorder.c b/ext/ddtrace_profiling_native_extension/stack_recorder.c index f1a44715d72..b9db0626647 100644 --- a/ext/ddtrace_profiling_native_extension/stack_recorder.c +++ b/ext/ddtrace_profiling_native_extension/stack_recorder.c @@ -218,6 +218,7 @@ static VALUE _native_initialize( VALUE alloc_samples_enabled, VALUE heap_samples_enabled, VALUE heap_sizes_enabled, + VALUE heap_sample_every, VALUE timeline_enabled ); static VALUE _native_serialize(VALUE self, VALUE recorder_instance); @@ -257,7 +258,7 @@ void stack_recorder_init(VALUE profiling_module) { // https://bugs.ruby-lang.org/issues/18007 for a discussion around this. rb_define_alloc_func(stack_recorder_class, _native_new); - rb_define_singleton_method(stack_recorder_class, "_native_initialize", _native_initialize, 6); + rb_define_singleton_method(stack_recorder_class, "_native_initialize", _native_initialize, 7); rb_define_singleton_method(stack_recorder_class, "_native_serialize", _native_serialize, 1); rb_define_singleton_method(stack_recorder_class, "_native_reset_after_fork", _native_reset_after_fork, 1); rb_define_singleton_method(testing_module, "_native_active_slot", _native_active_slot, 1); @@ -373,17 +374,21 @@ static VALUE _native_initialize( VALUE alloc_samples_enabled, VALUE heap_samples_enabled, VALUE heap_size_enabled, + VALUE heap_sample_every, VALUE timeline_enabled ) { ENFORCE_BOOLEAN(cpu_time_enabled); ENFORCE_BOOLEAN(alloc_samples_enabled); ENFORCE_BOOLEAN(heap_samples_enabled); ENFORCE_BOOLEAN(heap_size_enabled); + ENFORCE_TYPE(heap_sample_every, T_FIXNUM); ENFORCE_BOOLEAN(timeline_enabled); struct stack_recorder_state *state; TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state); + heap_recorder_set_sample_rate(state->heap_recorder, NUM2INT(heap_sample_every)); + uint8_t requested_values_count = ALL_VALUE_TYPES_COUNT - (cpu_time_enabled == Qtrue ? 0 : 1) - (alloc_samples_enabled == Qtrue? 0 : 1) - diff --git a/lib/datadog/core/configuration/settings.rb b/lib/datadog/core/configuration/settings.rb index d7b5691bc25..e25ebfe66f9 100644 --- a/lib/datadog/core/configuration/settings.rb +++ b/lib/datadog/core/configuration/settings.rb @@ -356,6 +356,21 @@ def initialize(*_) o.default false end + # Can be used to enable/disable the collection of heap size profiles. + # + # This feature is alpha and enabled by default when heap profiling is enabled. + # + # @warn To enable heap size profiling you are required to also enable allocation and heap profiling. + # @note Heap profiles are not yet GA in the Datadog UI, get in touch if you want to help us test it. + # + # @default `DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED` environment variable as a boolean, otherwise + # whatever the value of DD_PROFILING_EXPERIMENTAL_HEAP_ENABLED is. + option :experimental_heap_size_enabled do |o| + o.type :bool + o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED' + o.default true # This gets ANDed with experimental_heap_enabled in the profiler component. + end + # Can be used to configure the allocation sampling rate: a sample will be collected every x allocations. # # The lower the value, the more accuracy in allocation and heap tracking but the bigger the overhead. In @@ -368,6 +383,22 @@ def initialize(*_) o.default 50 end + # Can be used to configure the heap sampling rate: a heap sample will be collected for every x allocation + # samples. + # + # The lower the value, the more accuracy in heap tracking but the bigger the overhead. In particular, a + # value of 1 will track ALL allocations samples for heap profiles. + # + # The effective heap sampling rate in terms of allocations (not allocation samples) can be calculated via + # effective_heap_sample_rate = allocation_sample_rate * heap_sample_rate. + # + # @default `DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE` environment variable, otherwise `10`. + option :experimental_heap_sample_rate do |o| + o.type :int + o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE' + o.default 10 + end + # Can be used to disable checking which version of `libmysqlclient` is being used by the `mysql2` gem. # # This setting is only used when the `mysql2` gem is installed. diff --git a/lib/datadog/profiling/component.rb b/lib/datadog/profiling/component.rb index d72f887779d..43e64938909 100644 --- a/lib/datadog/profiling/component.rb +++ b/lib/datadog/profiling/component.rb @@ -7,7 +7,7 @@ module Component # Passing in a `nil` tracer is supported and will disable the following profiling features: # * Code Hotspots panel in the trace viewer, as well as scoping a profile down to a span # * Endpoint aggregation in the profiler UX, including normalization (resource per endpoint call) - def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) + def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) # rubocop:disable Metrics/MethodLength require_relative '../profiling/diagnostics/environment_logger' Profiling::Diagnostics::EnvironmentLogger.collect_and_log! @@ -43,7 +43,9 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) timeline_enabled = settings.profiling.advanced.experimental_timeline_enabled allocation_sample_every = get_allocation_sample_every(settings) allocation_profiling_enabled = enable_allocation_profiling?(settings, allocation_sample_every) - heap_profiling_enabled = enable_heap_profiling?(settings, allocation_profiling_enabled) + heap_sample_every = get_heap_sample_every(settings) + heap_profiling_enabled = enable_heap_profiling?(settings, allocation_profiling_enabled, heap_sample_every) + heap_size_profiling_enabled = enable_heap_size_profiling?(settings, heap_profiling_enabled) overhead_target_percentage = valid_overhead_target(settings.profiling.advanced.overhead_target_percentage) upload_period_seconds = [60, settings.profiling.advanced.upload_period_seconds].max @@ -51,6 +53,8 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) recorder = build_recorder( allocation_profiling_enabled: allocation_profiling_enabled, heap_profiling_enabled: heap_profiling_enabled, + heap_size_profiling_enabled: heap_size_profiling_enabled, + heap_sample_every: heap_sample_every, timeline_enabled: timeline_enabled, ) thread_context_collector = build_thread_context_collector(settings, recorder, optional_tracer, timeline_enabled) @@ -67,6 +71,7 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) no_signals_workaround_enabled: no_signals_workaround_enabled, timeline_enabled: timeline_enabled, allocation_sample_every: allocation_sample_every, + heap_sample_every: heap_sample_every, }.freeze exporter = build_profiler_exporter(settings, recorder, internal_metadata: internal_metadata) @@ -79,13 +84,16 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) private_class_method def self.build_recorder( allocation_profiling_enabled:, heap_profiling_enabled:, + heap_size_profiling_enabled:, + heap_sample_every:, timeline_enabled: ) Datadog::Profiling::StackRecorder.new( cpu_time_enabled: RUBY_PLATFORM.include?('linux'), # Only supported on Linux currently alloc_samples_enabled: allocation_profiling_enabled, heap_samples_enabled: heap_profiling_enabled, - heap_size_enabled: heap_profiling_enabled, + heap_size_enabled: heap_size_profiling_enabled, + heap_sample_every: heap_sample_every, timeline_enabled: timeline_enabled, ) end @@ -147,6 +155,14 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) allocation_sample_rate end + private_class_method def self.get_heap_sample_every(settings) + heap_sample_rate = settings.profiling.advanced.experimental_heap_sample_rate + + raise ArgumentError, "Heap sample rate must be a positive integer. Was #{heap_sample_rate}" if heap_sample_rate <= 0 + + heap_sample_rate + end + private_class_method def self.enable_allocation_profiling?(settings, allocation_sample_every) unless settings.profiling.advanced.experimental_allocation_enabled # Allocation profiling disabled, short-circuit out @@ -200,7 +216,7 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) true end - private_class_method def self.enable_heap_profiling?(settings, allocation_profiling_enabled) + private_class_method def self.enable_heap_profiling?(settings, allocation_profiling_enabled, heap_sample_rate) heap_profiling_enabled = settings.profiling.advanced.experimental_heap_enabled return false unless heap_profiling_enabled @@ -227,7 +243,20 @@ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) end Datadog.logger.warn( - 'Enabled experimental heap profiling. This is experimental, not recommended, and will increase overhead!' + "Enabled experimental heap profiling: heap_sample_rate=#{heap_sample_rate}. This is experimental, not " \ + 'recommended, and will increase overhead!' + ) + + true + end + + private_class_method def self.enable_heap_size_profiling?(settings, heap_profiling_enabled) + heap_size_profiling_enabled = settings.profiling.advanced.experimental_heap_size_enabled + + return false unless heap_profiling_enabled && heap_size_profiling_enabled + + Datadog.logger.warn( + 'Enabled experimental heap size profiling. This is experimental, not recommended, and will increase overhead!' ) true diff --git a/lib/datadog/profiling/stack_recorder.rb b/lib/datadog/profiling/stack_recorder.rb index faa917c46d0..64e89b90a34 100644 --- a/lib/datadog/profiling/stack_recorder.rb +++ b/lib/datadog/profiling/stack_recorder.rb @@ -6,7 +6,7 @@ module Profiling class StackRecorder def initialize( cpu_time_enabled:, alloc_samples_enabled:, heap_samples_enabled:, heap_size_enabled:, - timeline_enabled: + heap_sample_every:, timeline_enabled: ) # This mutex works in addition to the fancy C-level mutexes we have in the native side (see the docs there). # It prevents multiple Ruby threads calling serialize at the same time -- something like @@ -22,6 +22,7 @@ def initialize( alloc_samples_enabled, heap_samples_enabled, heap_size_enabled, + heap_sample_every, timeline_enabled, ) end diff --git a/spec/datadog/core/configuration/settings_spec.rb b/spec/datadog/core/configuration/settings_spec.rb index 40abc0fae95..d4b4c11e7ec 100644 --- a/spec/datadog/core/configuration/settings_spec.rb +++ b/spec/datadog/core/configuration/settings_spec.rb @@ -575,6 +575,41 @@ end end + describe '#experimental_heap_size_enabled' do + subject(:experimental_heap_size_enabled) { settings.profiling.advanced.experimental_heap_size_enabled } + + context 'when DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED' do + around do |example| + ClimateControl.modify('DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED' => environment) do + example.run + end + end + + context 'is not defined' do + let(:environment) { nil } + + it { is_expected.to be true } + end + + [true, false].each do |value| + context "is defined as #{value}" do + let(:environment) { value.to_s } + + it { is_expected.to be value } + end + end + end + end + + describe '#experimental_heap_size_enabled=' do + it 'updates the #experimental_heap_size_enabled setting' do + expect { settings.profiling.advanced.experimental_heap_size_enabled = false } + .to change { settings.profiling.advanced.experimental_heap_size_enabled } + .from(true) + .to(false) + end + end + describe '#experimental_allocation_sample_rate' do subject(:experimental_allocation_sample_rate) { settings.profiling.advanced.experimental_allocation_sample_rate } @@ -610,6 +645,41 @@ end end + describe '#experimental_heap_sample_rate' do + subject(:experimental_heap_sample_rate) { settings.profiling.advanced.experimental_heap_sample_rate } + + context 'when DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE' do + around do |example| + ClimateControl.modify('DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE' => environment) do + example.run + end + end + + context 'is not defined' do + let(:environment) { nil } + + it { is_expected.to be 10 } + end + + [100, 30.5].each do |value| + context "is defined as #{value}" do + let(:environment) { value.to_s } + + it { is_expected.to be value.to_i } + end + end + end + end + + describe '#experimental_heap_sample_rate=' do + it 'updates the #experimental_heap_sample_rate setting' do + expect { settings.profiling.advanced.experimental_heap_sample_rate = 100 } + .to change { settings.profiling.advanced.experimental_heap_sample_rate } + .from(10) + .to(100) + end + end + describe '#skip_mysql2_check' do subject(:skip_mysql2_check) { settings.profiling.advanced.skip_mysql2_check } diff --git a/spec/datadog/profiling/component_spec.rb b/spec/datadog/profiling/component_spec.rb index 2fe6c1eff44..245f8a7f34d 100644 --- a/spec/datadog/profiling/component_spec.rb +++ b/spec/datadog/profiling/component_spec.rb @@ -242,7 +242,7 @@ it 'initializes StackRecorder without heap sampling support and warns' do expect(Datadog::Profiling::StackRecorder).to receive(:new) - .with(hash_including(heap_samples_enabled: false)) + .with(hash_including(heap_samples_enabled: false, heap_size_enabled: false)) .and_call_original expect(Datadog.logger).to receive(:warn).with(/upgrade to Ruby >= 2.7/) @@ -268,12 +268,13 @@ it 'initializes StackRecorder with heap sampling support and warns' do expect(Datadog::Profiling::StackRecorder).to receive(:new) - .with(hash_including(heap_samples_enabled: true)) + .with(hash_including(heap_samples_enabled: true, heap_size_enabled: true)) .and_call_original expect(Datadog.logger).to receive(:warn).with(/Ractors.+stopping/) expect(Datadog.logger).to receive(:warn).with(/experimental allocation profiling/) expect(Datadog.logger).to receive(:warn).with(/experimental heap profiling/) + expect(Datadog.logger).to receive(:warn).with(/experimental heap size profiling/) build_profiler_component end @@ -288,11 +289,30 @@ expect(Datadog.logger).to receive(:warn).with(/experimental allocation profiling/) expect(Datadog.logger).to receive(:warn).with(/experimental heap profiling/) + expect(Datadog.logger).to receive(:warn).with(/experimental heap size profiling/) expect(Datadog.logger).to receive(:warn).with(/forced object recycling.+upgrade to Ruby >= 3.1/) build_profiler_component end end + + context 'but heap size profiling is disabled' do + before do + settings.profiling.advanced.experimental_heap_size_enabled = false + end + + it 'initializes StackRecorder without heap size profiling support' do + expect(Datadog::Profiling::StackRecorder).to receive(:new) + .with(hash_including(heap_samples_enabled: true, heap_size_enabled: false)) + .and_call_original + + expect(Datadog.logger).to receive(:warn).with(/experimental allocation profiling/) + expect(Datadog.logger).to receive(:warn).with(/experimental heap profiling/) + expect(Datadog.logger).not_to receive(:warn).with(/experimental heap size profiling/) + + build_profiler_component + end + end end end @@ -303,7 +323,7 @@ it 'initializes StackRecorder without heap sampling support' do expect(Datadog::Profiling::StackRecorder).to receive(:new) - .with(hash_including(heap_samples_enabled: false)) + .with(hash_including(heap_samples_enabled: false, heap_size_enabled: false)) .and_call_original build_profiler_component @@ -356,12 +376,14 @@ expect(described_class).to receive(:no_signals_workaround_enabled?).and_return(:no_signals_result) expect(settings.profiling.advanced).to receive(:experimental_timeline_enabled).and_return(:timeline_result) expect(settings.profiling.advanced).to receive(:experimental_allocation_sample_rate).and_return(123) + expect(settings.profiling.advanced).to receive(:experimental_heap_sample_rate).and_return(456) expect(Datadog::Profiling::Exporter).to receive(:new).with( hash_including( internal_metadata: { no_signals_workaround_enabled: :no_signals_result, timeline_enabled: :timeline_result, allocation_sample_every: 123, + heap_sample_every: 456, } ) ) diff --git a/spec/datadog/profiling/spec_helper.rb b/spec/datadog/profiling/spec_helper.rb index a663055406c..bbb21400d4d 100644 --- a/spec/datadog/profiling/spec_helper.rb +++ b/spec/datadog/profiling/spec_helper.rb @@ -81,12 +81,16 @@ def samples_for_thread(samples, thread) # We disable heap_sample collection by default in tests since it requires some extra mocking/ # setup for it to properly work. - def build_stack_recorder(heap_samples_enabled: false, heap_size_enabled: false, timeline_enabled: false) + def build_stack_recorder( + heap_samples_enabled: false, heap_size_enabled: false, heap_sample_every: 1, + timeline_enabled: false + ) Datadog::Profiling::StackRecorder.new( cpu_time_enabled: true, alloc_samples_enabled: true, heap_samples_enabled: heap_samples_enabled, heap_size_enabled: heap_size_enabled, + heap_sample_every: heap_sample_every, timeline_enabled: timeline_enabled, ) end diff --git a/spec/datadog/profiling/stack_recorder_spec.rb b/spec/datadog/profiling/stack_recorder_spec.rb index c4c314866dd..7025a0d062d 100644 --- a/spec/datadog/profiling/stack_recorder_spec.rb +++ b/spec/datadog/profiling/stack_recorder_spec.rb @@ -11,6 +11,7 @@ # Enabling this is tested in a particular context below. let(:heap_samples_enabled) { false } let(:heap_size_enabled) { false } + let(:heap_sample_every) { 1 } let(:timeline_enabled) { true } subject(:stack_recorder) do @@ -19,6 +20,7 @@ alloc_samples_enabled: alloc_samples_enabled, heap_samples_enabled: heap_samples_enabled, heap_size_enabled: heap_size_enabled, + heap_sample_every: heap_sample_every, timeline_enabled: timeline_enabled, ) end @@ -544,6 +546,20 @@ def sample_allocation(obj) expect(relevant_sample).not_to be nil expect(relevant_sample.values[:'heap-live-samples']).to eq test_num_allocated_object * sample_rate end + + context 'with custom heap sample rate configuration' do + let(:heap_sample_every) { 2 } + + it 'only keeps track of some allocations' do + # By only sampling every 2nd allocation we only track the odd objects which means our array + # should be the only heap sample captured (string is index 0, array is index 1, hash is 4) + expect(heap_samples.size).to eq(1) + + heap_sample = heap_samples.first + expect(heap_sample.labels[:'allocation class']).to eq('Array') + expect(heap_sample.values[:'heap-live-samples']).to eq(sample_rate * heap_sample_every) + end + end end end