ddtrace 1.7.0 → 1.8.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/README.md +2 -2
  4. data/ext/ddtrace_profiling_loader/extconf.rb +4 -1
  5. data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +1 -1
  6. data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +3 -2
  7. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.c +15 -41
  8. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.h +1 -1
  9. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +284 -74
  10. data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.c +142 -0
  11. data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.h +14 -0
  12. data/ext/ddtrace_profiling_native_extension/collectors_idle_sampling_helper.c +241 -0
  13. data/ext/ddtrace_profiling_native_extension/collectors_idle_sampling_helper.h +3 -0
  14. data/ext/ddtrace_profiling_native_extension/extconf.rb +21 -7
  15. data/ext/ddtrace_profiling_native_extension/helpers.h +5 -0
  16. data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +8 -0
  17. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +108 -24
  18. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +9 -0
  19. data/ext/ddtrace_profiling_native_extension/profiling.c +205 -0
  20. data/ext/ddtrace_profiling_native_extension/ruby_helpers.c +86 -0
  21. data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +28 -6
  22. data/ext/ddtrace_profiling_native_extension/setup_signal_handler.c +23 -4
  23. data/ext/ddtrace_profiling_native_extension/setup_signal_handler.h +4 -0
  24. data/ext/ddtrace_profiling_native_extension/stack_recorder.c +15 -18
  25. data/ext/ddtrace_profiling_native_extension/time_helpers.c +17 -0
  26. data/ext/ddtrace_profiling_native_extension/time_helpers.h +10 -0
  27. data/lib/datadog/core/configuration/components.rb +27 -6
  28. data/lib/datadog/core/configuration/ext.rb +18 -0
  29. data/lib/datadog/core/configuration/settings.rb +14 -341
  30. data/lib/datadog/core/diagnostics/health.rb +4 -22
  31. data/lib/datadog/core/environment/variable_helpers.rb +58 -10
  32. data/lib/datadog/core/utils.rb +0 -21
  33. data/lib/datadog/core.rb +21 -1
  34. data/lib/datadog/opentracer/distributed_headers.rb +2 -2
  35. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +16 -5
  36. data/lib/datadog/profiling/collectors/dynamic_sampling_rate.rb +14 -0
  37. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +68 -0
  38. data/lib/datadog/profiling/stack_recorder.rb +14 -0
  39. data/lib/datadog/profiling.rb +2 -0
  40. data/lib/datadog/tracing/configuration/ext.rb +33 -3
  41. data/lib/datadog/tracing/configuration/settings.rb +433 -0
  42. data/lib/datadog/tracing/contrib/aws/configuration/settings.rb +4 -1
  43. data/lib/datadog/tracing/contrib/aws/ext.rb +1 -0
  44. data/lib/datadog/tracing/contrib/dalli/configuration/settings.rb +4 -1
  45. data/lib/datadog/tracing/contrib/dalli/ext.rb +1 -0
  46. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +5 -1
  47. data/lib/datadog/tracing/contrib/elasticsearch/ext.rb +1 -0
  48. data/lib/datadog/tracing/contrib/ethon/configuration/settings.rb +6 -1
  49. data/lib/datadog/tracing/contrib/ethon/ext.rb +1 -0
  50. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +5 -1
  51. data/lib/datadog/tracing/contrib/excon/ext.rb +1 -0
  52. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +5 -1
  53. data/lib/datadog/tracing/contrib/faraday/ext.rb +1 -0
  54. data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +6 -1
  55. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +9 -4
  56. data/lib/datadog/tracing/contrib/grpc/ext.rb +1 -0
  57. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +6 -1
  58. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +9 -4
  59. data/lib/datadog/tracing/contrib/http/ext.rb +1 -0
  60. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +6 -1
  61. data/lib/datadog/tracing/contrib/httpclient/ext.rb +1 -0
  62. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +6 -1
  63. data/lib/datadog/tracing/contrib/httprb/ext.rb +1 -0
  64. data/lib/datadog/tracing/contrib/mongodb/configuration/settings.rb +5 -1
  65. data/lib/datadog/tracing/contrib/mongodb/ext.rb +1 -0
  66. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +4 -1
  67. data/lib/datadog/tracing/contrib/mysql2/ext.rb +1 -0
  68. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -2
  69. data/lib/datadog/tracing/contrib/patcher.rb +3 -2
  70. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +4 -1
  71. data/lib/datadog/tracing/contrib/pg/ext.rb +1 -0
  72. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -2
  73. data/lib/datadog/tracing/contrib/presto/configuration/settings.rb +4 -1
  74. data/lib/datadog/tracing/contrib/presto/ext.rb +1 -0
  75. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +1 -0
  76. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +10 -12
  77. data/lib/datadog/tracing/contrib/redis/configuration/settings.rb +4 -1
  78. data/lib/datadog/tracing/contrib/redis/ext.rb +1 -0
  79. data/lib/datadog/tracing/contrib/redis/instrumentation.rb +30 -23
  80. data/lib/datadog/tracing/contrib/redis/integration.rb +34 -2
  81. data/lib/datadog/tracing/contrib/redis/patcher.rb +18 -14
  82. data/lib/datadog/tracing/contrib/redis/quantize.rb +12 -9
  83. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -6
  84. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +72 -0
  85. data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +6 -1
  86. data/lib/datadog/tracing/contrib/rest_client/ext.rb +1 -0
  87. data/lib/datadog/{core → tracing}/diagnostics/ext.rb +1 -6
  88. data/lib/datadog/tracing/diagnostics/health.rb +40 -0
  89. data/lib/datadog/tracing/distributed/{b3.rb → b3_multi.rb} +2 -2
  90. data/lib/datadog/tracing/distributed/helpers.rb +2 -1
  91. data/lib/datadog/tracing/distributed/none.rb +19 -0
  92. data/lib/datadog/tracing/distributed/trace_context.rb +369 -0
  93. data/lib/datadog/tracing/metadata/ext.rb +1 -1
  94. data/lib/datadog/tracing/sampling/priority_sampler.rb +11 -0
  95. data/lib/datadog/tracing/sampling/rate_sampler.rb +3 -3
  96. data/lib/datadog/tracing/span.rb +3 -19
  97. data/lib/datadog/tracing/span_operation.rb +5 -4
  98. data/lib/datadog/tracing/trace_digest.rb +75 -2
  99. data/lib/datadog/tracing/trace_operation.rb +5 -4
  100. data/lib/datadog/tracing/utils.rb +50 -0
  101. data/lib/ddtrace/version.rb +1 -1
  102. metadata +20 -5
@@ -0,0 +1,142 @@
1
+ #include <ruby.h>
2
+
3
+ #include "collectors_dynamic_sampling_rate.h"
4
+ #include "helpers.h"
5
+ #include "ruby_helpers.h"
6
+ #include "time_helpers.h"
7
+
8
+ // Used to pace the rate of profiling samples based on the last observed time for a sample.
9
+ //
10
+ // This file implements the native bits of the Datadog::Profiling::Collectors::DynamicSamplingRate module, and is
11
+ // only exposed to Ruby for testing (it's always and only invoked by other C code in production).
12
+
13
+ // ---
14
+ // ## Dynamic Sampling Rate
15
+ //
16
+ // Our profilers get deployed in quite unpredictable situations in terms of system resources. While they can provide key
17
+ // information to help customers solve their performance problems, the profilers must always be careful not to make
18
+ // performance problems worse. This is where the idea of a dynamic sampling rate comes in.
19
+ //
20
+ // Instead of sampling at a fixed sample rate, the actual sampling rate should be decided by also observing the impact
21
+ // that running the profiler is having. This protects against issues such as the profiler being deployed in very busy
22
+ //machines or containers with unrealistic CPU restrictions.
23
+ //
24
+ // ### Implementation
25
+ //
26
+ // The APIs exposed by this file are used by the `CpuAndWallTimeWorker`.
27
+ //
28
+ // The main idea of the implementation below is the following: whenever the profiler takes a sample, the time we spent
29
+ // sampling and the current wall-time are recorded by calling `dynamic_sampling_rate_after_sample()`.
30
+ //
31
+ // Inside `dynamic_sampling_rate_after_sample()`, both values are combined to decide a future wall-time before which
32
+ // we should not sample. That is, we may decide that the next sample should happen no less than 200ms from now.
33
+ //
34
+ // Before taking a sample, the profiler checks using `dynamic_sampling_rate_should_sample()`, if it's time or not to
35
+ // sample. If it's not, it will skip sampling.
36
+ //
37
+ // Finally, as an additional optimization, there's a `dynamic_sampling_rate_get_sleep()` which, given the current
38
+ // wall-time, will return the time remaining (*there's an exception, check below) until the next sample.
39
+ //
40
+ // ---
41
+
42
+ // This is the wall-time overhead we're targeting. E.g. we target to spend no more than 2%, or 1.2 seconds per minute,
43
+ // taking profiling samples.
44
+ #define WALL_TIME_OVERHEAD_TARGET_PERCENTAGE 2.0 // %
45
+ // See `dynamic_sampling_rate_get_sleep()` for details
46
+ #define MAX_SLEEP_TIME_NS MILLIS_AS_NS(100)
47
+ // See `dynamic_sampling_rate_after_sample()` for details
48
+ #define MAX_TIME_UNTIL_NEXT_SAMPLE_NS SECONDS_AS_NS(10)
49
+
50
+ void dynamic_sampling_rate_init(dynamic_sampling_rate_state *state) {
51
+ atomic_init(&state->next_sample_after_monotonic_wall_time_ns, 0);
52
+ }
53
+
54
+ void dynamic_sampling_rate_reset(dynamic_sampling_rate_state *state) {
55
+ atomic_store(&state->next_sample_after_monotonic_wall_time_ns, 0);
56
+ }
57
+
58
+ uint64_t dynamic_sampling_rate_get_sleep(dynamic_sampling_rate_state *state, long current_monotonic_wall_time_ns) {
59
+ long next_sample_after_ns = atomic_load(&state->next_sample_after_monotonic_wall_time_ns);
60
+ long delta_ns = next_sample_after_ns - current_monotonic_wall_time_ns;
61
+
62
+ if (delta_ns > 0 && next_sample_after_ns > 0) {
63
+ // We don't want to sleep for too long as the profiler may be trying to stop.
64
+ //
65
+ // Instead, here we sleep for at most this time. Worst case, the profiler will still try to sample before
66
+ // `next_sample_after_monotonic_wall_time_ns`, BUT `dynamic_sampling_rate_should_sample()` will still be false
67
+ // so we still get the intended behavior.
68
+ return uint64_min_of(delta_ns, MAX_SLEEP_TIME_NS);
69
+ } else {
70
+ return 0;
71
+ }
72
+ }
73
+
74
+ bool dynamic_sampling_rate_should_sample(dynamic_sampling_rate_state *state, long wall_time_ns_before_sample) {
75
+ return wall_time_ns_before_sample >= atomic_load(&state->next_sample_after_monotonic_wall_time_ns);
76
+ }
77
+
78
+ void dynamic_sampling_rate_after_sample(dynamic_sampling_rate_state *state, long wall_time_ns_after_sample, uint64_t sampling_time_ns) {
79
+ double overhead_target = (double) WALL_TIME_OVERHEAD_TARGET_PERCENTAGE;
80
+
81
+ // The idea here is that we're targeting a maximum % of wall-time spent sampling.
82
+ // So for instance, if sampling_time_ns is 2% of the time we spend working, how much is the 98% we should spend
83
+ // sleeping? As an example, if the last sample took 1ms and the target overhead is 2%, we should sleep for 49ms.
84
+ uint64_t time_to_sleep_ns = sampling_time_ns * ((100.0 - overhead_target)/overhead_target);
85
+
86
+ // In case a sample took an unexpected long time (e.g. maybe a VM was paused, or a laptop was suspended), we clamp the
87
+ // value so it doesn't get too crazy
88
+ time_to_sleep_ns = uint64_min_of(time_to_sleep_ns, MAX_TIME_UNTIL_NEXT_SAMPLE_NS);
89
+
90
+ atomic_store(&state->next_sample_after_monotonic_wall_time_ns, wall_time_ns_after_sample + time_to_sleep_ns);
91
+ }
92
+
93
+ // ---
94
+ // Below here is boilerplate to expose the above code to Ruby so that we can test it with RSpec as usual.
95
+
96
+ VALUE _native_get_sleep(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE current_monotonic_wall_time_ns);
97
+ VALUE _native_should_sample(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE wall_time_ns_before_sample);
98
+ VALUE _native_after_sample(DDTRACE_UNUSED VALUE self, VALUE wall_time_ns_after_sample, VALUE sampling_time_ns);
99
+
100
+ void collectors_dynamic_sampling_rate_init(VALUE profiling_module) {
101
+ VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
102
+ VALUE dynamic_sampling_rate_module = rb_define_module_under(collectors_module, "DynamicSamplingRate");
103
+ VALUE testing_module = rb_define_module_under(dynamic_sampling_rate_module, "Testing");
104
+
105
+ rb_define_singleton_method(testing_module, "_native_get_sleep", _native_get_sleep, 2);
106
+ rb_define_singleton_method(testing_module, "_native_should_sample", _native_should_sample, 2);
107
+ rb_define_singleton_method(testing_module, "_native_after_sample", _native_after_sample, 2);
108
+ }
109
+
110
+ VALUE _native_get_sleep(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE current_monotonic_wall_time_ns) {
111
+ ENFORCE_TYPE(simulated_next_sample_after_monotonic_wall_time_ns, T_FIXNUM);
112
+ ENFORCE_TYPE(current_monotonic_wall_time_ns, T_FIXNUM);
113
+
114
+ dynamic_sampling_rate_state state;
115
+ dynamic_sampling_rate_init(&state);
116
+ atomic_store(&state.next_sample_after_monotonic_wall_time_ns, NUM2LONG(simulated_next_sample_after_monotonic_wall_time_ns));
117
+
118
+ return ULL2NUM(dynamic_sampling_rate_get_sleep(&state, NUM2LONG(current_monotonic_wall_time_ns)));
119
+ }
120
+
121
+ VALUE _native_should_sample(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE wall_time_ns_before_sample) {
122
+ ENFORCE_TYPE(simulated_next_sample_after_monotonic_wall_time_ns, T_FIXNUM);
123
+ ENFORCE_TYPE(wall_time_ns_before_sample, T_FIXNUM);
124
+
125
+ dynamic_sampling_rate_state state;
126
+ dynamic_sampling_rate_init(&state);
127
+ atomic_store(&state.next_sample_after_monotonic_wall_time_ns, NUM2LONG(simulated_next_sample_after_monotonic_wall_time_ns));
128
+
129
+ return dynamic_sampling_rate_should_sample(&state, NUM2LONG(wall_time_ns_before_sample)) ? Qtrue : Qfalse;
130
+ }
131
+
132
+ VALUE _native_after_sample(DDTRACE_UNUSED VALUE self, VALUE wall_time_ns_after_sample, VALUE sampling_time_ns) {
133
+ ENFORCE_TYPE(wall_time_ns_after_sample, T_FIXNUM);
134
+ ENFORCE_TYPE(sampling_time_ns, T_FIXNUM);
135
+
136
+ dynamic_sampling_rate_state state;
137
+ dynamic_sampling_rate_init(&state);
138
+
139
+ dynamic_sampling_rate_after_sample(&state, NUM2LONG(wall_time_ns_after_sample), NUM2ULL(sampling_time_ns));
140
+
141
+ return ULL2NUM(atomic_load(&state.next_sample_after_monotonic_wall_time_ns));
142
+ }
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+
3
+ #include <stdatomic.h>
4
+ #include <stdbool.h>
5
+
6
+ typedef struct {
7
+ atomic_long next_sample_after_monotonic_wall_time_ns;
8
+ } dynamic_sampling_rate_state;
9
+
10
+ void dynamic_sampling_rate_init(dynamic_sampling_rate_state *state);
11
+ void dynamic_sampling_rate_reset(dynamic_sampling_rate_state *state);
12
+ uint64_t dynamic_sampling_rate_get_sleep(dynamic_sampling_rate_state *state, long current_monotonic_wall_time_ns);
13
+ bool dynamic_sampling_rate_should_sample(dynamic_sampling_rate_state *state, long wall_time_ns_before_sample);
14
+ void dynamic_sampling_rate_after_sample(dynamic_sampling_rate_state *state, long wall_time_ns_after_sample, uint64_t sampling_time_ns);
@@ -0,0 +1,241 @@
1
+ #include <ruby.h>
2
+ #include <ruby/thread.h>
3
+ #include <pthread.h>
4
+ #include <stdbool.h>
5
+
6
+ #include "helpers.h"
7
+ #include "ruby_helpers.h"
8
+ #include "collectors_idle_sampling_helper.h"
9
+
10
+ // Used by the Collectors::CpuAndWallTimeWorker to gather samples when the Ruby process is idle.
11
+ //
12
+ // Specifically, the IdleSamplingHelper is expected to be triggered by the CpuAndWallTimeWorker whenever it needs to
13
+ // trigger a sample, but the VM is otherwise idle. See implementation of CpuAndWallTimeWorker for details.
14
+ //
15
+ // The IdleSamplingHelper keeps a background thread that waits for functions to run on a single-element "queue".
16
+ // Other threads communicate with it by asking it to ACTION_RUN a `requested_action` or ACTION_STOP to terminate.
17
+ //
18
+ // The state is protected by the `wakeup_mutex`, and the background thread is woken up after changes using the
19
+ // `wakeup` condition variable.
20
+
21
+ typedef enum { ACTION_WAIT, ACTION_RUN, ACTION_STOP } action;
22
+
23
+ // Contains state for a single CpuAndWallTimeWorker instance
24
+ struct idle_sampling_loop_state {
25
+ pthread_mutex_t wakeup_mutex;
26
+ pthread_cond_t wakeup;
27
+ action requested_action;
28
+ void (*run_action_function)(void);
29
+ };
30
+
31
+ static VALUE _native_new(VALUE klass);
32
+ static void reset_state(struct idle_sampling_loop_state *state);
33
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
34
+ static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
35
+ static void *run_idle_sampling_loop(void *state_ptr);
36
+ static void interrupt_idle_sampling_loop(void *state_ptr);
37
+ static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance);
38
+ static VALUE _native_idle_sampling_helper_request_action(DDTRACE_UNUSED VALUE self, VALUE self_instance);
39
+ static void *request_testing_action(void *self_instance_ptr);
40
+ static void grab_gvl_and_run_testing_action(void);
41
+ static void *run_testing_action(DDTRACE_UNUSED void *unused);
42
+
43
+ void collectors_idle_sampling_helper_init(VALUE profiling_module) {
44
+ VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
45
+ VALUE collectors_idle_sampling_helper_class = rb_define_class_under(collectors_module, "IdleSamplingHelper", rb_cObject);
46
+ // Hosts methods used for testing the native code using RSpec
47
+ VALUE testing_module = rb_define_module_under(collectors_idle_sampling_helper_class, "Testing");
48
+
49
+ // Instances of the IdleSamplingHelper class are "TypedData" objects.
50
+ // "TypedData" objects are special objects in the Ruby VM that can wrap C structs.
51
+ // In this case, it wraps the idle_sampling_loop_state.
52
+ //
53
+ // Because Ruby doesn't know how to initialize native-level structs, we MUST override the allocation function for objects
54
+ // of this class so that we can manage this part. Not overriding or disabling the allocation function is a common
55
+ // gotcha for "TypedData" objects that can very easily lead to VM crashes, see for instance
56
+ // https://bugs.ruby-lang.org/issues/18007 for a discussion around this.
57
+ rb_define_alloc_func(collectors_idle_sampling_helper_class, _native_new);
58
+
59
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_idle_sampling_loop", _native_idle_sampling_loop, 1);
60
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_stop", _native_stop, 1);
61
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_reset", _native_reset, 1);
62
+ rb_define_singleton_method(testing_module, "_native_idle_sampling_helper_request_action", _native_idle_sampling_helper_request_action, 1);
63
+ }
64
+
65
+ // This structure is used to define a Ruby object that stores a pointer to a struct idle_sampling_loop_state
66
+ // See also https://github.com/ruby/ruby/blob/master/doc/extension.rdoc for how this works
67
+ static const rb_data_type_t idle_sampling_helper_typed_data = {
68
+ .wrap_struct_name = "Datadog::Profiling::Collectors::IdleSamplingHelper",
69
+ .function = {
70
+ .dmark = NULL, // We don't store references to Ruby objects so we don't need to mark any of them
71
+ .dfree = RUBY_DEFAULT_FREE,
72
+ .dsize = NULL, // We don't track memory usage (although it'd be cool if we did!)
73
+ //.dcompact = NULL, // Not needed -- we don't store references to Ruby objects
74
+ },
75
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY
76
+ };
77
+
78
+ static VALUE _native_new(VALUE klass) {
79
+ struct idle_sampling_loop_state *state = ruby_xcalloc(1, sizeof(struct idle_sampling_loop_state));
80
+
81
+ reset_state(state);
82
+
83
+ return TypedData_Wrap_Struct(klass, &idle_sampling_helper_typed_data, state);
84
+ }
85
+
86
+ static void reset_state(struct idle_sampling_loop_state *state) {
87
+ state->wakeup_mutex = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER;
88
+ state->wakeup = (pthread_cond_t) PTHREAD_COND_INITIALIZER;
89
+ state->requested_action = ACTION_WAIT;
90
+ state->run_action_function = NULL;
91
+ }
92
+
93
+ // The same instance of the IdleSamplingHelper can be reused multiple times, and this resets it back to
94
+ // a pristine state before recreating the worker thread (this includes resetting the mutex in case it was left
95
+ // locked halfway through the VM forking)
96
+ static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
97
+ struct idle_sampling_loop_state *state;
98
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
99
+
100
+ reset_state(state);
101
+
102
+ return Qtrue;
103
+ }
104
+
105
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
106
+ struct idle_sampling_loop_state *state;
107
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
108
+
109
+ // Release GVL and run the loop waiting for requests
110
+ rb_thread_call_without_gvl(run_idle_sampling_loop, state, interrupt_idle_sampling_loop, state);
111
+
112
+ return Qtrue;
113
+ }
114
+
115
+ static void *run_idle_sampling_loop(void *state_ptr) {
116
+ struct idle_sampling_loop_state *state = (struct idle_sampling_loop_state *) state_ptr;
117
+ int error = 0;
118
+
119
+ while (true) {
120
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_lock(&state->wakeup_mutex));
121
+
122
+ action next_action;
123
+ void (*run_action_function)(void);
124
+
125
+ // Await for an action
126
+ while ((next_action = state->requested_action) == ACTION_WAIT) {
127
+ error = pthread_cond_wait(&state->wakeup, &state->wakeup_mutex);
128
+ if (error) {
129
+ // If something went wrong, try to leave the mutex unlocked at least
130
+ pthread_mutex_unlock(&state->wakeup_mutex);
131
+ ENFORCE_SUCCESS_NO_GVL(error);
132
+ }
133
+ }
134
+
135
+ // There's an action to be taken!
136
+
137
+ // Record function, if any
138
+ run_action_function = state->run_action_function;
139
+
140
+ // Reset buffer for next request
141
+ state->requested_action = ACTION_WAIT;
142
+
143
+ // Unlock the mutex immediately so other threads can continue to request actions without blocking
144
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
145
+
146
+ // Process pending action
147
+ if (next_action == ACTION_RUN) {
148
+ if (run_action_function == NULL) {
149
+ grab_gvl_and_raise(rb_eRuntimeError, "Unexpected NULL run_action_function in run_idle_sampling_loop");
150
+ }
151
+
152
+ run_action_function();
153
+ } else { // ACTION_STOP
154
+ return NULL;
155
+ }
156
+ }
157
+ }
158
+
159
+ static void interrupt_idle_sampling_loop(void *state_ptr) {
160
+ struct idle_sampling_loop_state *state = (struct idle_sampling_loop_state *) state_ptr;
161
+ int error = 0;
162
+
163
+ // Note about the error handling in this situation: Something bad happening at this stage is really really awkward to
164
+ // handle because we get called by the VM in a situation where we can't really raise exceptions, and the VM really really
165
+ // just wants us to stop what we're doing and return control of the thread to it.
166
+ //
167
+ // So if we return immediately on error, we may leave the VM hanging because we didn't actually interrupt the thread.
168
+ // We're also not at a great location to flag errors.
169
+ // That's why: a) I chose to log to stderr, as a last-ditch effort; b) even if something goes wrong we still try to
170
+ // ask the thread to stop, instead of exiting early.
171
+
172
+ error = pthread_mutex_lock(&state->wakeup_mutex);
173
+ if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_mutex_lock in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
174
+
175
+ state->requested_action = ACTION_STOP;
176
+
177
+ error = pthread_mutex_unlock(&state->wakeup_mutex);
178
+ if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_mutex_unlock in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
179
+
180
+ error = pthread_cond_broadcast(&state->wakeup);
181
+ if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_cond_broadcast in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
182
+ }
183
+
184
+ static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
185
+ struct idle_sampling_loop_state *state;
186
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
187
+
188
+ ENFORCE_SUCCESS_GVL(pthread_mutex_lock(&state->wakeup_mutex));
189
+ state->requested_action = ACTION_STOP;
190
+ ENFORCE_SUCCESS_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
191
+
192
+ // Wake up worker thread, if needed; It's OK to call broadcast after releasing the mutex
193
+ ENFORCE_SUCCESS_GVL(pthread_cond_broadcast(&state->wakeup));
194
+
195
+ return Qtrue;
196
+ }
197
+
198
+ // Assumption: Function gets called without the global VM lock
199
+ void idle_sampling_helper_request_action(VALUE self_instance, void (*run_action_function)(void)) {
200
+ struct idle_sampling_loop_state *state;
201
+ if (!rb_typeddata_is_kind_of(self_instance, &idle_sampling_helper_typed_data)) {
202
+ grab_gvl_and_raise(rb_eTypeError, "Wrong argument for idle_sampling_helper_request_action");
203
+ }
204
+ // This should never fail the the above check passes
205
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
206
+
207
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_lock(&state->wakeup_mutex));
208
+ if (state->requested_action == ACTION_WAIT) {
209
+ state->requested_action = ACTION_RUN;
210
+ state->run_action_function = run_action_function;
211
+ }
212
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
213
+
214
+ // Wake up worker thread, if needed; It's OK to call broadcast after releasing the mutex
215
+ ENFORCE_SUCCESS_NO_GVL(pthread_cond_broadcast(&state->wakeup));
216
+ }
217
+
218
+ // Because the idle_sampling_helper_request_action is built to be called without the global VM lock, here we release it
219
+ // to be able to call that API.
220
+ static VALUE _native_idle_sampling_helper_request_action(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
221
+ rb_thread_call_without_gvl(request_testing_action, (void *) self_instance, NULL, NULL);
222
+ return Qtrue;
223
+ }
224
+
225
+ static void *request_testing_action(void *self_instance_ptr) {
226
+ VALUE self_instance = (VALUE) self_instance_ptr;
227
+ idle_sampling_helper_request_action(self_instance, grab_gvl_and_run_testing_action);
228
+ return NULL;
229
+ }
230
+
231
+ // This gets called by the worker thread, which is not holding the global VM lock. To be able to actually run the action,
232
+ // we need to acquire it
233
+ static void grab_gvl_and_run_testing_action(void) {
234
+ rb_thread_call_with_gvl(run_testing_action, NULL);
235
+ }
236
+
237
+ static void *run_testing_action(DDTRACE_UNUSED void *unused) {
238
+ VALUE idle_sampling_helper_testing_action = rb_gv_get("$idle_sampling_helper_testing_action");
239
+ rb_funcall(idle_sampling_helper_testing_action, rb_intern("call"), 0);
240
+ return NULL;
241
+ }
@@ -0,0 +1,3 @@
1
+ #pragma once
2
+
3
+ void idle_sampling_helper_request_action(VALUE self_instance, void (*run_action_function)(void));
@@ -67,6 +67,10 @@ $stderr.puts(
67
67
  # that may fail on an environment not properly setup for building Ruby extensions.
68
68
  require 'mkmf'
69
69
 
70
+ Logging.message(" [ddtrace] Using compiler:\n")
71
+ xsystem("#{CONFIG['CC']} -v")
72
+ Logging.message(" [ddtrace] End of compiler information\n")
73
+
70
74
  # mkmf on modern Rubies actually has an append_cflags that does something similar
71
75
  # (see https://github.com/ruby/ruby/pull/5760), but as usual we need a bit more boilerplate to deal with legacy Rubies
72
76
  def add_compiler_flag(flag)
@@ -87,7 +91,7 @@ add_compiler_flag '-Werror' if ENV['DDTRACE_CI'] == 'true'
87
91
  # (https://github.com/msgpack/msgpack-ruby/blob/18ce08f6d612fe973843c366ac9a0b74c4e50599/ext/msgpack/extconf.rb#L8)
88
92
  add_compiler_flag '-std=gnu99'
89
93
 
90
- # Gets really noisy when we include the MJIT header, let's omit it
94
+ # Gets really noisy when we include the MJIT header, let's omit it (TODO: Use #pragma GCC diagnostic instead?)
91
95
  add_compiler_flag '-Wno-unused-function'
92
96
 
93
97
  # Allow defining variables at any point in a function
@@ -108,6 +112,9 @@ add_compiler_flag '-Wunused-parameter'
108
112
  # For more details see https://gcc.gnu.org/wiki/Visibility
109
113
  add_compiler_flag '-fvisibility=hidden'
110
114
 
115
+ # Avoid legacy C definitions
116
+ add_compiler_flag '-Wold-style-definition'
117
+
111
118
  # Enable all other compiler warnings
112
119
  add_compiler_flag '-Wall'
113
120
  add_compiler_flag '-Wextra'
@@ -126,6 +133,9 @@ end
126
133
  # On older Rubies, there was no struct rb_native_thread. See private_vm_api_acccess.c for details.
127
134
  $defs << '-DNO_RB_NATIVE_THREAD' if RUBY_VERSION < '3.2'
128
135
 
136
+ # On older Rubies, there was no struct rb_thread_sched (it was struct rb_global_vm_lock_struct)
137
+ $defs << '-DNO_RB_THREAD_SCHED' if RUBY_VERSION < '3.2'
138
+
129
139
  # On older Rubies, there was no tid member in the internal thread structure
130
140
  $defs << '-DNO_THREAD_TID' if RUBY_VERSION < '3.1'
131
141
 
@@ -135,9 +145,15 @@ $defs << '-DUSE_BACKPORTED_RB_PROFILE_FRAME_METHOD_NAME' if RUBY_VERSION < '3'
135
145
  # On older Rubies, there are no Ractors
136
146
  $defs << '-DNO_RACTORS' if RUBY_VERSION < '3'
137
147
 
148
+ # On older Rubies, rb_global_vm_lock_struct did not include the owner field
149
+ $defs << '-DNO_GVL_OWNER' if RUBY_VERSION < '2.6'
150
+
138
151
  # On older Rubies, we need to use rb_thread_t instead of rb_execution_context_t
139
152
  $defs << '-DUSE_THREAD_INSTEAD_OF_EXECUTION_CONTEXT' if RUBY_VERSION < '2.5'
140
153
 
154
+ # On older Rubies, extensions can't use GET_VM()
155
+ $defs << '-DNO_GET_VM' if RUBY_VERSION < '2.5'
156
+
141
157
  # On older Rubies...
142
158
  if RUBY_VERSION < '2.4'
143
159
  # ...we need to use RUBY_VM_NORMAL_ISEQ_P instead of VM_FRAME_RUBYFRAME_P
@@ -154,8 +170,6 @@ if RUBY_VERSION < '2.3'
154
170
  $defs << '-DUSE_LEGACY_RB_PROFILE_FRAMES'
155
171
  # ... you couldn't name threads
156
172
  $defs << '-DNO_THREAD_NAMES'
157
- # ...the ruby_thread_has_gvl_p function was not exposed to users outside of the VM
158
- $defs << '-DNO_THREAD_HAS_GVL'
159
173
  end
160
174
 
161
175
  # If we got here, libdatadog is available and loaded
@@ -173,6 +187,10 @@ unless pkg_config('datadog_profiling_with_rpath')
173
187
  )
174
188
  end
175
189
 
190
+ unless have_type('atomic_int', ['stdatomic.h'])
191
+ skip_building_extension!(Datadog::Profiling::NativeExtensionHelpers::Supported::COMPILER_ATOMIC_MISSING)
192
+ end
193
+
176
194
  # See comments on the helper method being used for why we need to additionally set this.
177
195
  # The extremely excessive escaping around ORIGIN below seems to be correct and was determined after a lot of
178
196
  # experimentation. We need to get these special characters across a lot of tools untouched...
@@ -181,10 +199,6 @@ $LDFLAGS += \
181
199
  "#{Datadog::Profiling::NativeExtensionHelpers.libdatadog_folder_relative_to_native_lib_folder}"
182
200
  Logging.message(" [ddtrace] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
183
201
 
184
- Logging.message(" [ddtrace] Using compiler:\n")
185
- xsystem("#{CONFIG['CC']} --version")
186
- Logging.message(" [ddtrace] End of compiler information\n")
187
-
188
202
  # Tag the native extension library with the Ruby version and Ruby platform.
189
203
  # This makes it easier for development (avoids "oops I forgot to rebuild when I switched my Ruby") and ensures that
190
204
  # the wrong library is never loaded.
@@ -10,3 +10,8 @@
10
10
  #else
11
11
  #define DDTRACE_UNUSED
12
12
  #endif
13
+
14
+ // @ivoanjo: After trying to read through https://stackoverflow.com/questions/3437404/min-and-max-in-c I decided I
15
+ // don't like C and I just implemented this as a function.
16
+ inline static uint64_t uint64_max_of(uint64_t a, uint64_t b) { return a > b ? a : b; }
17
+ inline static uint64_t uint64_min_of(uint64_t a, uint64_t b) { return a > b ? b : a; }
@@ -179,6 +179,14 @@ module Datadog
179
179
  suggested: CONTACT_SUPPORT,
180
180
  )
181
181
 
182
+ # Validation for this check is done in extconf.rb because it relies on mkmf
183
+ COMPILER_ATOMIC_MISSING = explain_issue(
184
+ 'your C compiler is missing support for the <stdatomic.h> header.',
185
+ 'This issue can usually be fixed by upgrading to a later version of your',
186
+ 'operating system image or compiler.',
187
+ suggested: CONTACT_SUPPORT,
188
+ )
189
+
182
190
  private_class_method def self.disabled_via_env?
183
191
  disabled_via_env = explain_issue(
184
192
  'the `DD_PROFILING_NO_EXTENSION` environment variable is/was set to',