ddtrace 1.18.0 → 1.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +67 -52
  4. data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.c +22 -14
  5. data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.h +4 -0
  6. data/ext/ddtrace_profiling_native_extension/collectors_gc_profiling_helper.c +156 -0
  7. data/ext/ddtrace_profiling_native_extension/collectors_gc_profiling_helper.h +5 -0
  8. data/ext/ddtrace_profiling_native_extension/collectors_stack.c +43 -102
  9. data/ext/ddtrace_profiling_native_extension/collectors_stack.h +10 -3
  10. data/ext/ddtrace_profiling_native_extension/collectors_thread_context.c +159 -124
  11. data/ext/ddtrace_profiling_native_extension/collectors_thread_context.h +2 -1
  12. data/ext/ddtrace_profiling_native_extension/extconf.rb +16 -0
  13. data/ext/ddtrace_profiling_native_extension/heap_recorder.c +970 -0
  14. data/ext/ddtrace_profiling_native_extension/heap_recorder.h +155 -0
  15. data/ext/ddtrace_profiling_native_extension/helpers.h +2 -0
  16. data/ext/ddtrace_profiling_native_extension/libdatadog_helpers.c +20 -0
  17. data/ext/ddtrace_profiling_native_extension/libdatadog_helpers.h +11 -0
  18. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +5 -0
  19. data/ext/ddtrace_profiling_native_extension/profiling.c +1 -0
  20. data/ext/ddtrace_profiling_native_extension/ruby_helpers.c +147 -0
  21. data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +28 -0
  22. data/ext/ddtrace_profiling_native_extension/stack_recorder.c +329 -10
  23. data/ext/ddtrace_profiling_native_extension/stack_recorder.h +3 -0
  24. data/lib/datadog/core/configuration/settings.rb +139 -22
  25. data/lib/datadog/core/telemetry/collector.rb +10 -0
  26. data/lib/datadog/core/telemetry/event.rb +2 -1
  27. data/lib/datadog/core/telemetry/ext.rb +3 -0
  28. data/lib/datadog/core/telemetry/v1/app_event.rb +8 -1
  29. data/lib/datadog/core/telemetry/v1/install_signature.rb +38 -0
  30. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +6 -11
  31. data/lib/datadog/profiling/component.rb +197 -13
  32. data/lib/datadog/profiling/scheduler.rb +4 -6
  33. data/lib/datadog/profiling/stack_recorder.rb +13 -2
  34. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +4 -0
  35. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -1
  36. data/lib/datadog/tracing/contrib/rails/auto_instrument_railtie.rb +0 -2
  37. data/lib/ddtrace/version.rb +1 -1
  38. metadata +12 -7
@@ -314,32 +314,85 @@ module Datadog
314
314
 
315
315
  # Can be used to enable/disable the Datadog::Profiling.allocation_count feature.
316
316
  #
317
- # This feature is safe and enabled by default on Ruby 2.x, but has a few caveats on Ruby 3.x.
317
+ # This feature is now controlled via {:experimental_allocation_enabled}
318
+ option :allocation_counting_enabled do |o|
319
+ o.after_set do
320
+ Datadog.logger.warn(
321
+ 'The profiling.advanced.allocation_counting_enabled setting has been deprecated for removal and no ' \
322
+ 'longer does anything. Please remove it from your Datadog.configure block. ' \
323
+ 'Allocation counting is now controlled by the `experimental_allocation_enabled` setting instead.'
324
+ )
325
+ end
326
+ end
327
+
328
+ # Can be used to enable/disable collection of allocation profiles.
329
+ #
330
+ # This feature is alpha and disabled by default
318
331
  #
319
- # Caveat 1 (severe):
320
- # On Ruby versions 3.0 (all), 3.1.0 to 3.1.3, and 3.2.0 to 3.2.2 this is disabled by default because it
321
- # can trigger a VM bug that causes a segmentation fault during garbage collection of Ractors
322
- # (https://bugs.ruby-lang.org/issues/18464). We don't recommend using this feature on such Rubies.
323
- # This bug is fixed on Ruby versions 3.1.4, 3.2.3 and 3.3.0.
332
+ # @warn This feature is not supported/safe in all Rubies. Details in {Datadog::Profiling::Component} but
333
+ # in summary, this should be supported on Ruby 2.x, 3.1.4+, 3.2.3+ and 3.3.0+. Enabling it on
334
+ # unsupported Rubies may result in unexpected behaviour, including crashes.
324
335
  #
325
- # Caveat 2 (annoyance):
326
- # On all known versions of Ruby 3.x, due to https://bugs.ruby-lang.org/issues/19112, when a ractor gets
327
- # garbage collected, Ruby will disable all active tracepoints, which this feature internally relies on.
328
- # Thus this feature is only usable if you're not using Ractors.
336
+ # @default `DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED` environment variable as a boolean, otherwise `false`
337
+ option :experimental_allocation_enabled do |o|
338
+ o.type :bool
339
+ o.env 'DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED'
340
+ o.default false
341
+ end
342
+
343
+ # Can be used to enable/disable the collection of heap profiles.
329
344
  #
330
- # Caveat 3 (severe):
331
- # Ruby 3.2.0 to 3.2.2 have a bug in the newobj tracepoint (https://bugs.ruby-lang.org/issues/19482,
332
- # https://github.com/ruby/ruby/pull/7464) so that's an extra reason why it's not safe on those Rubies.
333
- # This bug is fixed on Ruby versions 3.2.3 and 3.3.0.
345
+ # This feature is alpha and disabled by default
334
346
  #
335
- # @default `true` on Ruby 2.x and 3.1.4+, 3.2.3+ and 3.3.0+; `false` for Ruby 3.0 and unpatched Rubies.
336
- option :allocation_counting_enabled do |o|
337
- o.default do
338
- RUBY_VERSION.start_with?('2.') ||
339
- (RUBY_VERSION.start_with?('3.1.') && RUBY_VERSION >= '3.1.4') ||
340
- (RUBY_VERSION.start_with?('3.2.') && RUBY_VERSION >= '3.2.3') ||
341
- RUBY_VERSION >= '3.3.'
342
- end
347
+ # @warn To enable heap profiling you are required to also enable allocation profiling.
348
+ #
349
+ # @default `DD_PROFILING_EXPERIMENTAL_HEAP_ENABLED` environment variable as a boolean, otherwise `false`
350
+ option :experimental_heap_enabled do |o|
351
+ o.type :bool
352
+ o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_ENABLED'
353
+ o.default false
354
+ end
355
+
356
+ # Can be used to enable/disable the collection of heap size profiles.
357
+ #
358
+ # This feature is alpha and enabled by default when heap profiling is enabled.
359
+ #
360
+ # @warn To enable heap size profiling you are required to also enable allocation and heap profiling.
361
+ #
362
+ # @default `DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED` environment variable as a boolean, otherwise
363
+ # whatever the value of DD_PROFILING_EXPERIMENTAL_HEAP_ENABLED is.
364
+ option :experimental_heap_size_enabled do |o|
365
+ o.type :bool
366
+ o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED'
367
+ o.default true # This gets ANDed with experimental_heap_enabled in the profiler component.
368
+ end
369
+
370
+ # Can be used to configure the allocation sampling rate: a sample will be collected every x allocations.
371
+ #
372
+ # The lower the value, the more accuracy in allocation and heap tracking but the bigger the overhead. In
373
+ # particular, a value of 1 will sample ALL allocations.
374
+ #
375
+ # @default `DD_PROFILING_EXPERIMENTAL_ALLOCATION_SAMPLE_RATE` environment variable, otherwise `50`.
376
+ option :experimental_allocation_sample_rate do |o|
377
+ o.type :int
378
+ o.env 'DD_PROFILING_EXPERIMENTAL_ALLOCATION_SAMPLE_RATE'
379
+ o.default 50
380
+ end
381
+
382
+ # Can be used to configure the heap sampling rate: a heap sample will be collected for every x allocation
383
+ # samples.
384
+ #
385
+ # The lower the value, the more accuracy in heap tracking but the bigger the overhead. In particular, a
386
+ # value of 1 will track ALL allocations samples for heap profiles.
387
+ #
388
+ # The effective heap sampling rate in terms of allocations (not allocation samples) can be calculated via
389
+ # effective_heap_sample_rate = allocation_sample_rate * heap_sample_rate.
390
+ #
391
+ # @default `DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE` environment variable, otherwise `10`.
392
+ option :experimental_heap_sample_rate do |o|
393
+ o.type :int
394
+ o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE'
395
+ o.default 10
343
396
  end
344
397
 
345
398
  # Can be used to disable checking which version of `libmysqlclient` is being used by the `mysql2` gem.
@@ -391,6 +444,34 @@ module Datadog
391
444
  end
392
445
  end
393
446
  end
447
+
448
+ # Configures how much wall-time overhead the profiler targets. The profiler will dynamically adjust the
449
+ # interval between samples it takes so as to try and maintain the property that it spends no longer than
450
+ # this amount of wall-clock time profiling. For example, with the default value of 2%, the profiler will
451
+ # try and cause no more than 1.2 seconds per minute of overhead. Decreasing this value will reduce the
452
+ # accuracy of the data collected. Increasing will impact the application.
453
+ #
454
+ # We do not recommend tweaking this value.
455
+ #
456
+ # This value should be a percentage i.e. a number between 0 and 100, not 0 and 1.
457
+ #
458
+ # @default `DD_PROFILING_OVERHEAD_TARGET_PERCENTAGE` as a float, otherwise 2.0
459
+ option :overhead_target_percentage do |o|
460
+ o.type :float
461
+ o.env 'DD_PROFILING_OVERHEAD_TARGET_PERCENTAGE'
462
+ o.default 2.0
463
+ end
464
+
465
+ # Controls how often the profiler reports data, in seconds. Cannot be lower than 60 seconds.
466
+ #
467
+ # We do not recommend tweaking this value.
468
+ #
469
+ # @default `DD_PROFILING_UPLOAD_PERIOD` environment variable, otherwise 60
470
+ option :upload_period_seconds do |o|
471
+ o.type :int
472
+ o.env 'DD_PROFILING_UPLOAD_PERIOD'
473
+ o.default 60
474
+ end
394
475
  end
395
476
 
396
477
  # @public_api
@@ -592,6 +673,42 @@ module Datadog
592
673
  o.env Core::Telemetry::Ext::ENV_HEARTBEAT_INTERVAL
593
674
  o.default 60.0
594
675
  end
676
+
677
+ # The install id of the application.
678
+ #
679
+ # This method is used internally, by library injection.
680
+ #
681
+ # @default `DD_INSTRUMENTATION_INSTALL_ID` environment variable, otherwise `nil`.
682
+ # @return [String,nil]
683
+ # @!visibility private
684
+ option :install_id do |o|
685
+ o.type :string, nilable: true
686
+ o.env Core::Telemetry::Ext::ENV_INSTALL_ID
687
+ end
688
+
689
+ # The install type of the application.
690
+ #
691
+ # This method is used internally, by library injection.
692
+ #
693
+ # @default `DD_INSTRUMENTATION_INSTALL_TYPE` environment variable, otherwise `nil`.
694
+ # @return [String,nil]
695
+ # @!visibility private
696
+ option :install_type do |o|
697
+ o.type :string, nilable: true
698
+ o.env Core::Telemetry::Ext::ENV_INSTALL_TYPE
699
+ end
700
+
701
+ # The install time of the application.
702
+ #
703
+ # This method is used internally, by library injection.
704
+ #
705
+ # @default `DD_INSTRUMENTATION_INSTALL_TIME` environment variable, otherwise `nil`.
706
+ # @return [String,nil]
707
+ # @!visibility private
708
+ option :install_time do |o|
709
+ o.type :string, nilable: true
710
+ o.env Core::Telemetry::Ext::ENV_INSTALL_TIME
711
+ end
595
712
  end
596
713
 
597
714
  # Remote configuration
@@ -9,6 +9,7 @@ require_relative '../utils/hash'
9
9
  require_relative 'v1/application'
10
10
  require_relative 'v1/dependency'
11
11
  require_relative 'v1/host'
12
+ require_relative 'v1/install_signature'
12
13
  require_relative 'v1/integration'
13
14
  require_relative 'v1/product'
14
15
  require_relative '../transport/ext'
@@ -81,6 +82,15 @@ module Datadog
81
82
  )
82
83
  end
83
84
 
85
+ # Forms a telemetry app-started install_signature object
86
+ def install_signature
87
+ Telemetry::V1::InstallSignature.new(
88
+ install_id: Datadog.configuration.dig('telemetry', 'install_id'),
89
+ install_type: Datadog.configuration.dig('telemetry', 'install_type'),
90
+ install_time: Datadog.configuration.dig('telemetry', 'install_time'),
91
+ )
92
+ end
93
+
84
94
  # Forms a telemetry app-started integrations object
85
95
  def integrations
86
96
  Datadog.registry.map do |integration|
@@ -60,7 +60,8 @@ module Datadog
60
60
  dependencies: dependencies,
61
61
  integrations: integrations,
62
62
  configuration: configurations,
63
- additional_payload: additional_payload
63
+ additional_payload: additional_payload,
64
+ install_signature: install_signature
64
65
  )
65
66
  end
66
67
 
@@ -6,6 +6,9 @@ module Datadog
6
6
  module Ext
7
7
  ENV_ENABLED = 'DD_INSTRUMENTATION_TELEMETRY_ENABLED'
8
8
  ENV_HEARTBEAT_INTERVAL = 'DD_TELEMETRY_HEARTBEAT_INTERVAL'
9
+ ENV_INSTALL_ID = 'DD_INSTRUMENTATION_INSTALL_ID'
10
+ ENV_INSTALL_TYPE = 'DD_INSTRUMENTATION_INSTALL_TYPE'
11
+ ENV_INSTALL_TIME = 'DD_INSTRUMENTATION_INSTALL_TIME'
9
12
  end
10
13
  end
11
14
  end
@@ -10,18 +10,24 @@ module Datadog
10
10
  :additional_payload,
11
11
  :configuration,
12
12
  :dependencies,
13
+ :install_signature,
13
14
  :integrations
14
15
 
15
16
  # @param additional_payload [Array<Telemetry::V1::Configuration>] List of Additional payload to track (any key
16
17
  # value not mentioned and doesn't fit under a metric)
17
18
  # @param configuration [Array<Telemetry::V1::Configuration>] List of Tracer related configuration data
18
19
  # @param dependencies [Array<Telemetry::V1::Dependency>] List of all loaded modules requested by the app
20
+ # @param install_signature [Telemetry::V1::InstallSignature] Install signature data
19
21
  # @param integrations [Array<Telemetry::V1::Integration>] List of integrations that are available within the app
20
22
  # and applicable to be traced
21
- def initialize(additional_payload: nil, configuration: nil, dependencies: nil, integrations: nil)
23
+ def initialize(
24
+ additional_payload: nil, configuration: nil, dependencies: nil, install_signature: nil,
25
+ integrations: nil
26
+ )
22
27
  @additional_payload = additional_payload
23
28
  @configuration = configuration
24
29
  @dependencies = dependencies
30
+ @install_signature = install_signature
25
31
  @integrations = integrations
26
32
  end
27
33
 
@@ -30,6 +36,7 @@ module Datadog
30
36
  hash[:additional_payload] = map_hash(@additional_payload) if @additional_payload
31
37
  hash[:configuration] = map_hash(@configuration) if @configuration
32
38
  hash[:dependencies] = map_array(@dependencies) if @dependencies
39
+ hash[:install_signature] = @install_signature.to_h if @install_signature
33
40
  hash[:integrations] = map_array(@integrations) if @integrations
34
41
  end
35
42
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Core
5
+ module Telemetry
6
+ module V1
7
+ # Describes attributes for install signature
8
+ class InstallSignature
9
+ using Core::Utils::Hash::Refinement
10
+
11
+ attr_reader \
12
+ :install_id,
13
+ :install_type,
14
+ :install_time
15
+
16
+ # @param id [String,nil] Install ID
17
+ # @param type [String,nil] Install type
18
+ # @param type [String,nil] Install time
19
+ def initialize(install_id:, install_type:, install_time:)
20
+ @install_id = install_id
21
+ @install_type = install_type
22
+ @install_time = install_time
23
+ end
24
+
25
+ def to_h
26
+ hash = {
27
+ install_id: @install_id,
28
+ install_type: @install_type,
29
+ install_time: @install_time
30
+ }
31
+ hash.compact!
32
+ hash
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -15,14 +15,15 @@ module Datadog
15
15
 
16
16
  def initialize(
17
17
  gc_profiling_enabled:,
18
- allocation_counting_enabled:,
19
18
  no_signals_workaround_enabled:,
20
19
  thread_context_collector:,
21
- idle_sampling_helper: IdleSamplingHelper.new,
20
+ dynamic_sampling_rate_overhead_target_percentage:,
21
+ allocation_sample_every:,
22
+ allocation_profiling_enabled:,
22
23
  # **NOTE**: This should only be used for testing; disabling the dynamic sampling rate will increase the
23
24
  # profiler overhead!
24
25
  dynamic_sampling_rate_enabled: true,
25
- allocation_sample_every: 0 # Currently only for testing; Setting this to > 0 can add a lot of overhead!
26
+ idle_sampling_helper: IdleSamplingHelper.new
26
27
  )
27
28
  unless dynamic_sampling_rate_enabled
28
29
  Datadog.logger.warn(
@@ -30,22 +31,16 @@ module Datadog
30
31
  )
31
32
  end
32
33
 
33
- if allocation_counting_enabled && allocation_sample_every > 0
34
- Datadog.logger.warn(
35
- "Enabled experimental allocation profiling: allocation_sample_every=#{allocation_sample_every}. This is " \
36
- 'experimental, not recommended, and will increase overhead!'
37
- )
38
- end
39
-
40
34
  self.class._native_initialize(
41
35
  self,
42
36
  thread_context_collector,
43
37
  gc_profiling_enabled,
44
38
  idle_sampling_helper,
45
- allocation_counting_enabled,
46
39
  no_signals_workaround_enabled,
47
40
  dynamic_sampling_rate_enabled,
41
+ dynamic_sampling_rate_overhead_target_percentage,
48
42
  allocation_sample_every,
43
+ allocation_profiling_enabled,
49
44
  )
50
45
  @worker_thread = nil
51
46
  @failure_exception = nil
@@ -7,7 +7,7 @@ module Datadog
7
7
  # Passing in a `nil` tracer is supported and will disable the following profiling features:
8
8
  # * Code Hotspots panel in the trace viewer, as well as scoping a profile down to a span
9
9
  # * Endpoint aggregation in the profiler UX, including normalization (resource per endpoint call)
10
- def self.build_profiler_component(settings:, agent_settings:, optional_tracer:)
10
+ def self.build_profiler_component(settings:, agent_settings:, optional_tracer:) # rubocop:disable Metrics/MethodLength
11
11
  require_relative '../profiling/diagnostics/environment_logger'
12
12
 
13
13
  Profiling::Diagnostics::EnvironmentLogger.collect_and_log!
@@ -41,38 +41,57 @@ module Datadog
41
41
 
42
42
  no_signals_workaround_enabled = no_signals_workaround_enabled?(settings)
43
43
  timeline_enabled = settings.profiling.advanced.experimental_timeline_enabled
44
+ allocation_sample_every = get_allocation_sample_every(settings)
45
+ allocation_profiling_enabled = enable_allocation_profiling?(settings, allocation_sample_every)
46
+ heap_sample_every = get_heap_sample_every(settings)
47
+ heap_profiling_enabled = enable_heap_profiling?(settings, allocation_profiling_enabled, heap_sample_every)
48
+ heap_size_profiling_enabled = enable_heap_size_profiling?(settings, heap_profiling_enabled)
49
+
50
+ overhead_target_percentage = valid_overhead_target(settings.profiling.advanced.overhead_target_percentage)
51
+ upload_period_seconds = [60, settings.profiling.advanced.upload_period_seconds].max
44
52
 
45
53
  recorder = Datadog::Profiling::StackRecorder.new(
46
54
  cpu_time_enabled: RUBY_PLATFORM.include?('linux'), # Only supported on Linux currently
47
- alloc_samples_enabled: false, # Always disabled for now -- work in progress
48
- )
49
- thread_context_collector = Datadog::Profiling::Collectors::ThreadContext.new(
50
- recorder: recorder,
51
- max_frames: settings.profiling.advanced.max_frames,
52
- tracer: optional_tracer,
53
- endpoint_collection_enabled: settings.profiling.advanced.endpoint.collection.enabled,
55
+ alloc_samples_enabled: allocation_profiling_enabled,
56
+ heap_samples_enabled: heap_profiling_enabled,
57
+ heap_size_enabled: heap_size_profiling_enabled,
58
+ heap_sample_every: heap_sample_every,
54
59
  timeline_enabled: timeline_enabled,
55
60
  )
61
+ thread_context_collector = build_thread_context_collector(settings, recorder, optional_tracer, timeline_enabled)
56
62
  worker = Datadog::Profiling::Collectors::CpuAndWallTimeWorker.new(
57
63
  gc_profiling_enabled: enable_gc_profiling?(settings),
58
- allocation_counting_enabled: settings.profiling.advanced.allocation_counting_enabled,
59
64
  no_signals_workaround_enabled: no_signals_workaround_enabled,
60
65
  thread_context_collector: thread_context_collector,
61
- allocation_sample_every: 0,
66
+ dynamic_sampling_rate_overhead_target_percentage: overhead_target_percentage,
67
+ allocation_sample_every: allocation_sample_every,
68
+ allocation_profiling_enabled: allocation_profiling_enabled,
62
69
  )
63
70
 
64
71
  internal_metadata = {
65
72
  no_signals_workaround_enabled: no_signals_workaround_enabled,
66
73
  timeline_enabled: timeline_enabled,
74
+ allocation_sample_every: allocation_sample_every,
75
+ heap_sample_every: heap_sample_every,
67
76
  }.freeze
68
77
 
69
78
  exporter = build_profiler_exporter(settings, recorder, internal_metadata: internal_metadata)
70
79
  transport = build_profiler_transport(settings, agent_settings)
71
- scheduler = Profiling::Scheduler.new(exporter: exporter, transport: transport)
80
+ scheduler = Profiling::Scheduler.new(exporter: exporter, transport: transport, interval: upload_period_seconds)
72
81
 
73
82
  Profiling::Profiler.new(worker: worker, scheduler: scheduler)
74
83
  end
75
84
 
85
+ private_class_method def self.build_thread_context_collector(settings, recorder, optional_tracer, timeline_enabled)
86
+ Datadog::Profiling::Collectors::ThreadContext.new(
87
+ recorder: recorder,
88
+ max_frames: settings.profiling.advanced.max_frames,
89
+ tracer: optional_tracer,
90
+ endpoint_collection_enabled: settings.profiling.advanced.endpoint.collection.enabled,
91
+ timeline_enabled: timeline_enabled,
92
+ )
93
+ end
94
+
76
95
  private_class_method def self.build_profiler_exporter(settings, recorder, internal_metadata:)
77
96
  code_provenance_collector =
78
97
  (Profiling::Collectors::CodeProvenance.new if settings.profiling.advanced.code_provenance_enabled)
@@ -110,6 +129,126 @@ module Datadog
110
129
  end
111
130
  end
112
131
 
132
+ private_class_method def self.get_allocation_sample_every(settings)
133
+ allocation_sample_rate = settings.profiling.advanced.experimental_allocation_sample_rate
134
+
135
+ if allocation_sample_rate <= 0
136
+ raise ArgumentError, "Allocation sample rate must be a positive integer. Was #{allocation_sample_rate}"
137
+ end
138
+
139
+ allocation_sample_rate
140
+ end
141
+
142
+ private_class_method def self.get_heap_sample_every(settings)
143
+ heap_sample_rate = settings.profiling.advanced.experimental_heap_sample_rate
144
+
145
+ raise ArgumentError, "Heap sample rate must be a positive integer. Was #{heap_sample_rate}" if heap_sample_rate <= 0
146
+
147
+ heap_sample_rate
148
+ end
149
+
150
+ private_class_method def self.enable_allocation_profiling?(settings, allocation_sample_every)
151
+ unless settings.profiling.advanced.experimental_allocation_enabled
152
+ # Allocation profiling disabled, short-circuit out
153
+ return false
154
+ end
155
+
156
+ # Allocation sampling is safe and supported on Ruby 2.x, but has a few caveats on Ruby 3.x.
157
+
158
+ # SEVERE - All configurations
159
+ # Ruby 3.2.0 to 3.2.2 have a bug in the newobj tracepoint (https://bugs.ruby-lang.org/issues/19482,
160
+ # https://github.com/ruby/ruby/pull/7464) that makes this crash in any configuration. This bug is
161
+ # fixed on Ruby versions 3.2.3 and 3.3.0.
162
+ if RUBY_VERSION.start_with?('3.2.') && RUBY_VERSION < '3.2.3'
163
+ Datadog.logger.warn(
164
+ 'Allocation profiling is not supported in Ruby versions 3.2.0, 3.2.1 and 3.2.2 and will be forcibly '\
165
+ 'disabled. This is due to a VM bug that can lead to crashes (https://bugs.ruby-lang.org/issues/19482). '\
166
+ 'Other Ruby versions do not suffer from this issue.'
167
+ )
168
+ return false
169
+ end
170
+
171
+ # SEVERE - Only with Ractors
172
+ # On Ruby versions 3.0 (all), 3.1.0 to 3.1.3, and 3.2.0 to 3.2.2 allocation profiling can trigger a VM bug
173
+ # that causes a segmentation fault during garbage collection of Ractors
174
+ # (https://bugs.ruby-lang.org/issues/18464). We don't recommend using this feature on such Rubies.
175
+ # This bug is fixed on Ruby versions 3.1.4, 3.2.3 and 3.3.0.
176
+ if RUBY_VERSION.start_with?('3.0.') ||
177
+ (RUBY_VERSION.start_with?('3.1.') && RUBY_VERSION < '3.1.4') ||
178
+ (RUBY_VERSION.start_with?('3.2.') && RUBY_VERSION < '3.2.3')
179
+ Datadog.logger.warn(
180
+ "Current Ruby version (#{RUBY_VERSION}) has a VM bug where enabling allocation profiling while using "\
181
+ 'Ractors may cause unexpected issues, including crashes (https://bugs.ruby-lang.org/issues/18464). '\
182
+ 'This does not happen if Ractors are not used.'
183
+ )
184
+ # ANNOYANCE - Only with Ractors
185
+ # On all known versions of Ruby 3.x, due to https://bugs.ruby-lang.org/issues/19112, when a ractor gets
186
+ # garbage collected, Ruby will disable all active tracepoints, which this feature internally relies on.
187
+ elsif RUBY_VERSION.start_with?('3.')
188
+ Datadog.logger.warn(
189
+ 'In all known versions of Ruby 3.x, using Ractors may result in allocation profiling unexpectedly ' \
190
+ 'stopping (https://bugs.ruby-lang.org/issues/19112). Note that this stop has no impact in your ' \
191
+ 'application stability or performance. This does not happen if Ractors are not used.'
192
+ )
193
+ end
194
+
195
+ Datadog.logger.warn(
196
+ "Enabled experimental allocation profiling: allocation_sample_rate=#{allocation_sample_every}. This is " \
197
+ 'experimental, not recommended, and will increase overhead!'
198
+ )
199
+
200
+ true
201
+ end
202
+
203
+ private_class_method def self.enable_heap_profiling?(settings, allocation_profiling_enabled, heap_sample_rate)
204
+ heap_profiling_enabled = settings.profiling.advanced.experimental_heap_enabled
205
+
206
+ return false unless heap_profiling_enabled
207
+
208
+ if RUBY_VERSION.start_with?('2.') && RUBY_VERSION < '2.7'
209
+ Datadog.logger.warn(
210
+ 'Heap profiling currently relies on features introduced in Ruby 2.7 and will be forcibly disabled. '\
211
+ 'Please upgrade to Ruby >= 2.7 in order to use this feature.'
212
+ )
213
+ return false
214
+ end
215
+
216
+ if RUBY_VERSION < '3.1'
217
+ Datadog.logger.debug(
218
+ "Current Ruby version (#{RUBY_VERSION}) supports forced object recycling which has a bug that the " \
219
+ 'heap profiler is forced to work around to remain accurate. This workaround requires force-setting '\
220
+ "the SEEN_OBJ_ID flag on objects that should have it but don't. Full details can be found in " \
221
+ 'https://github.com/DataDog/dd-trace-rb/pull/3360. This workaround should be safe but can be ' \
222
+ 'bypassed by disabling the heap profiler or upgrading to Ruby >= 3.1 where forced object recycling ' \
223
+ 'was completely removed (https://bugs.ruby-lang.org/issues/18290).'
224
+ )
225
+ end
226
+
227
+ unless allocation_profiling_enabled
228
+ raise ArgumentError,
229
+ 'Heap profiling requires allocation profiling to be enabled'
230
+ end
231
+
232
+ Datadog.logger.warn(
233
+ "Enabled experimental heap profiling: heap_sample_rate=#{heap_sample_rate}. This is experimental, not " \
234
+ 'recommended, and will increase overhead!'
235
+ )
236
+
237
+ true
238
+ end
239
+
240
+ private_class_method def self.enable_heap_size_profiling?(settings, heap_profiling_enabled)
241
+ heap_size_profiling_enabled = settings.profiling.advanced.experimental_heap_size_enabled
242
+
243
+ return false unless heap_profiling_enabled && heap_size_profiling_enabled
244
+
245
+ Datadog.logger.warn(
246
+ 'Enabled experimental heap size profiling. This is experimental, not recommended, and will increase overhead!'
247
+ )
248
+
249
+ true
250
+ end
251
+
113
252
  private_class_method def self.no_signals_workaround_enabled?(settings) # rubocop:disable Metrics/MethodLength
114
253
  setting_value = settings.profiling.advanced.no_signals_workaround_enabled
115
254
  legacy_ruby_that_should_use_workaround = RUBY_VERSION.start_with?('2.3.', '2.4.', '2.5.')
@@ -217,9 +356,12 @@ module Datadog
217
356
 
218
357
  return true unless mysql2_client_class && mysql2_client_class.respond_to?(:info)
219
358
 
220
- libmysqlclient_version = Gem::Version.new(mysql2_client_class.info[:version])
359
+ info = mysql2_client_class.info
360
+ libmysqlclient_version = Gem::Version.new(info[:version])
221
361
 
222
- compatible = libmysqlclient_version >= Gem::Version.new('8.0.0')
362
+ compatible =
363
+ libmysqlclient_version >= Gem::Version.new('8.0.0') ||
364
+ looks_like_mariadb?(info, libmysqlclient_version)
223
365
 
224
366
  Datadog.logger.debug(
225
367
  "The `mysql2` gem is using #{compatible ? 'a compatible' : 'an incompatible'} version of " \
@@ -245,6 +387,48 @@ module Datadog
245
387
  true
246
388
  end
247
389
  end
390
+
391
+ private_class_method def self.valid_overhead_target(overhead_target_percentage)
392
+ if overhead_target_percentage > 0 && overhead_target_percentage <= 20
393
+ overhead_target_percentage
394
+ else
395
+ Datadog.logger.error(
396
+ 'Ignoring invalid value for profiling overhead_target_percentage setting: ' \
397
+ "#{overhead_target_percentage.inspect}. Falling back to default value."
398
+ )
399
+
400
+ 2.0
401
+ end
402
+ end
403
+
404
+ # To add just a bit more complexity to our detection code, in https://github.com/DataDog/dd-trace-rb/issues/3334
405
+ # a user reported that our code was incorrectly flagging the mariadb variant of libmysqlclient as being
406
+ # incompatible. In fact we have no reports of the mariadb variant needing the "no signals" workaround,
407
+ # so we flag it as compatible when it's in use.
408
+ #
409
+ # A problem is that there doesn't seem to be an obvious way to query the mysql2 gem on which kind of
410
+ # libmysqlclient it's using, so we detect it by looking at the version.
411
+ #
412
+ # The info method for mysql2 with mariadb looks something like this:
413
+ # `{:id=>30308, :version=>"3.3.8", :header_version=>"11.2.2"}`
414
+ #
415
+ # * The version seems to come from https://github.com/mariadb-corporation/mariadb-connector-c and the latest
416
+ # one is 3.x.
417
+ # * The header_version is what people usually see as the "mariadb version"
418
+ #
419
+ # As a comparison, for libmysql the info looks like:
420
+ # * `{:id=>80035, :version=>"8.0.35", :header_version=>"8.0.35"}`
421
+ #
422
+ # Thus our detection is version 4 or older, because libmysqlclient 4 is almost 20 years old so it's most probably
423
+ # not that one + header_version being 10 or newer, since according to https://endoflife.date/mariadb that's a
424
+ # sane range for modern mariadb releases.
425
+ private_class_method def self.looks_like_mariadb?(info, libmysqlclient_version)
426
+ header_version = Gem::Version.new(info[:header_version]) if info[:header_version]
427
+
428
+ !!(header_version &&
429
+ libmysqlclient_version < Gem::Version.new('5.0.0') &&
430
+ header_version >= Gem::Version.new('10.0.0'))
431
+ end
248
432
  end
249
433
  end
250
434
  end
@@ -5,12 +5,11 @@ require_relative '../core/workers/polling'
5
5
 
6
6
  module Datadog
7
7
  module Profiling
8
- # Periodically (every DEFAULT_INTERVAL_SECONDS) takes a profile from the `Exporter` and reports it using the
8
+ # Periodically (every interval, 60 seconds by default) takes a profile from the `Exporter` and reports it using the
9
9
  # configured transport. Runs on its own background thread.
10
10
  class Scheduler < Core::Worker
11
11
  include Core::Workers::Polling
12
12
 
13
- DEFAULT_INTERVAL_SECONDS = 60
14
13
  MINIMUM_INTERVAL_SECONDS = 0
15
14
 
16
15
  # We sleep for at most this duration seconds before reporting data to avoid multi-process applications all
@@ -28,8 +27,7 @@ module Datadog
28
27
  def initialize(
29
28
  exporter:,
30
29
  transport:,
31
- fork_policy: Core::Workers::Async::Thread::FORK_POLICY_RESTART, # Restart in forks by default
32
- interval: DEFAULT_INTERVAL_SECONDS,
30
+ interval:, fork_policy: Core::Workers::Async::Thread::FORK_POLICY_RESTART, # Restart in forks by default, # seconds
33
31
  enabled: true
34
32
  )
35
33
  @exporter = exporter
@@ -115,8 +113,8 @@ module Datadog
115
113
  #
116
114
  # During PR review (https://github.com/DataDog/dd-trace-rb/pull/1807) we discussed the possible alternative of
117
115
  # just sleeping before starting the scheduler loop. We ended up not going with that option to avoid the first
118
- # profile containing up to DEFAULT_INTERVAL_SECONDS + DEFAULT_FLUSH_JITTER_MAXIMUM_SECONDS instead of the
119
- # usual DEFAULT_INTERVAL_SECONDS size.
116
+ # profile containing up to interval + DEFAULT_FLUSH_JITTER_MAXIMUM_SECONDS instead of the
117
+ # usual interval seconds.
120
118
  if run_loop?
121
119
  jitter_seconds = rand * DEFAULT_FLUSH_JITTER_MAXIMUM_SECONDS # floating point number between (0.0...maximum)
122
120
  sleep(jitter_seconds)