ddtrace 1.18.0 → 1.19.0

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