datadog 2.3.0 → 2.5.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 (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -2
  3. data/ext/datadog_profiling_loader/datadog_profiling_loader.c +9 -1
  4. data/ext/datadog_profiling_loader/extconf.rb +10 -22
  5. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
  6. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +198 -41
  7. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +4 -2
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +89 -46
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +645 -107
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +15 -1
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +0 -27
  12. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -4
  13. data/ext/datadog_profiling_native_extension/extconf.rb +42 -25
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
  15. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
  16. data/ext/datadog_profiling_native_extension/heap_recorder.c +194 -34
  17. data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
  18. data/ext/datadog_profiling_native_extension/http_transport.c +38 -6
  19. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
  20. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +53 -2
  21. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -0
  22. data/ext/datadog_profiling_native_extension/profiling.c +1 -1
  23. data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
  24. data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
  25. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -0
  26. data/ext/libdatadog_api/crashtracker.c +20 -18
  27. data/ext/libdatadog_api/datadog_ruby_common.c +0 -27
  28. data/ext/libdatadog_api/datadog_ruby_common.h +0 -4
  29. data/ext/libdatadog_extconf_helpers.rb +1 -1
  30. data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
  31. data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
  32. data/lib/datadog/appsec/component.rb +29 -8
  33. data/lib/datadog/appsec/configuration/settings.rb +10 -2
  34. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
  35. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
  36. data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
  37. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +0 -14
  38. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +67 -31
  39. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +14 -15
  40. data/lib/datadog/appsec/contrib/graphql/integration.rb +14 -1
  41. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
  42. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +2 -5
  43. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
  44. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
  45. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
  46. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
  47. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
  48. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
  49. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
  50. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
  51. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
  52. data/lib/datadog/appsec/event.rb +25 -1
  53. data/lib/datadog/appsec/ext.rb +4 -0
  54. data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
  55. data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
  56. data/lib/datadog/appsec/processor/context.rb +109 -0
  57. data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
  58. data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
  59. data/lib/datadog/appsec/processor.rb +42 -107
  60. data/lib/datadog/appsec/rate_limiter.rb +25 -40
  61. data/lib/datadog/appsec/remote.rb +7 -3
  62. data/lib/datadog/appsec/scope.rb +1 -4
  63. data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
  64. data/lib/datadog/appsec/utils.rb +2 -0
  65. data/lib/datadog/appsec.rb +3 -2
  66. data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
  67. data/lib/datadog/core/configuration/components.rb +4 -3
  68. data/lib/datadog/core/configuration/settings.rb +96 -5
  69. data/lib/datadog/core/configuration.rb +1 -3
  70. data/lib/datadog/core/crashtracking/component.rb +9 -6
  71. data/lib/datadog/core/environment/execution.rb +5 -5
  72. data/lib/datadog/core/environment/yjit.rb +5 -0
  73. data/lib/datadog/core/metrics/client.rb +7 -0
  74. data/lib/datadog/core/rate_limiter.rb +183 -0
  75. data/lib/datadog/core/remote/client/capabilities.rb +4 -3
  76. data/lib/datadog/core/remote/component.rb +4 -2
  77. data/lib/datadog/core/remote/negotiation.rb +4 -4
  78. data/lib/datadog/core/remote/tie.rb +2 -0
  79. data/lib/datadog/core/remote/transport/http.rb +5 -0
  80. data/lib/datadog/core/remote/worker.rb +1 -1
  81. data/lib/datadog/core/runtime/ext.rb +1 -0
  82. data/lib/datadog/core/runtime/metrics.rb +5 -1
  83. data/lib/datadog/core/semaphore.rb +35 -0
  84. data/lib/datadog/core/telemetry/component.rb +2 -0
  85. data/lib/datadog/core/telemetry/event.rb +12 -7
  86. data/lib/datadog/core/telemetry/logger.rb +51 -0
  87. data/lib/datadog/core/telemetry/logging.rb +50 -14
  88. data/lib/datadog/core/telemetry/request.rb +13 -1
  89. data/lib/datadog/core/transport/ext.rb +1 -0
  90. data/lib/datadog/core/utils/time.rb +12 -0
  91. data/lib/datadog/core/workers/async.rb +1 -1
  92. data/lib/datadog/di/code_tracker.rb +166 -0
  93. data/lib/datadog/di/configuration/settings.rb +163 -0
  94. data/lib/datadog/di/configuration.rb +11 -0
  95. data/lib/datadog/di/error.rb +31 -0
  96. data/lib/datadog/di/extensions.rb +16 -0
  97. data/lib/datadog/di/instrumenter.rb +301 -0
  98. data/lib/datadog/di/probe.rb +162 -0
  99. data/lib/datadog/di/probe_builder.rb +47 -0
  100. data/lib/datadog/di/probe_notification_builder.rb +207 -0
  101. data/lib/datadog/di/probe_notifier_worker.rb +244 -0
  102. data/lib/datadog/di/redactor.rb +188 -0
  103. data/lib/datadog/di/serializer.rb +215 -0
  104. data/lib/datadog/di/transport.rb +67 -0
  105. data/lib/datadog/di/utils.rb +39 -0
  106. data/lib/datadog/di.rb +57 -0
  107. data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
  108. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +12 -10
  109. data/lib/datadog/profiling/collectors/info.rb +12 -3
  110. data/lib/datadog/profiling/collectors/thread_context.rb +32 -8
  111. data/lib/datadog/profiling/component.rb +21 -4
  112. data/lib/datadog/profiling/http_transport.rb +6 -1
  113. data/lib/datadog/profiling/scheduler.rb +2 -0
  114. data/lib/datadog/profiling/stack_recorder.rb +40 -9
  115. data/lib/datadog/single_step_instrument.rb +12 -0
  116. data/lib/datadog/tracing/component.rb +13 -0
  117. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
  118. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
  119. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
  120. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
  121. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
  122. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
  123. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
  124. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +3 -1
  125. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
  126. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
  127. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  128. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
  129. data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
  130. data/lib/datadog/tracing/contrib/faraday/middleware.rb +12 -0
  131. data/lib/datadog/tracing/contrib/grape/endpoint.rb +24 -2
  132. data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
  133. data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
  134. data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
  135. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +13 -9
  136. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +6 -3
  137. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
  138. data/lib/datadog/tracing/contrib/http/instrumentation.rb +22 -15
  139. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +10 -5
  140. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
  141. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +9 -0
  142. data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
  143. data/lib/datadog/tracing/contrib/lograge/patcher.rb +1 -2
  144. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  145. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
  146. data/lib/datadog/tracing/contrib/patcher.rb +2 -1
  147. data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
  148. data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
  149. data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
  150. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
  151. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
  152. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
  153. data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
  154. data/lib/datadog/tracing/distributed/propagation.rb +7 -0
  155. data/lib/datadog/tracing/metadata/ext.rb +2 -0
  156. data/lib/datadog/tracing/remote.rb +5 -2
  157. data/lib/datadog/tracing/sampling/matcher.rb +6 -1
  158. data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
  159. data/lib/datadog/tracing/sampling/rule.rb +2 -0
  160. data/lib/datadog/tracing/sampling/rule_sampler.rb +15 -9
  161. data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
  162. data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
  163. data/lib/datadog/tracing/trace_operation.rb +26 -2
  164. data/lib/datadog/tracing/tracer.rb +29 -22
  165. data/lib/datadog/tracing/transport/http/client.rb +1 -0
  166. data/lib/datadog/tracing/transport/http.rb +4 -0
  167. data/lib/datadog/tracing/transport/io/client.rb +1 -0
  168. data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
  169. data/lib/datadog/tracing/workers.rb +2 -2
  170. data/lib/datadog/tracing/writer.rb +26 -28
  171. data/lib/datadog/version.rb +1 -1
  172. metadata +40 -15
  173. data/lib/datadog/tracing/sampling/rate_limiter.rb +0 -185
@@ -76,6 +76,11 @@
76
76
  #define MISSING_TRACER_CONTEXT_KEY 0
77
77
  #define TIME_BETWEEN_GC_EVENTS_NS MILLIS_AS_NS(10)
78
78
 
79
+ // This is used as a placeholder to mark threads that are allowed to be profiled (enabled)
80
+ // (e.g. to avoid trying to gvl profile threads that are not from the main Ractor)
81
+ // and for which there's no data yet
82
+ #define GVL_WAITING_ENABLED_EMPTY RUBY_FIXNUM_MAX
83
+
79
84
  static ID at_active_span_id; // id of :@active_span in Ruby
80
85
  static ID at_active_trace_id; // id of :@active_trace in Ruby
81
86
  static ID at_id_id; // id of :@id in Ruby
@@ -86,6 +91,26 @@ static ID at_otel_values_id; // id of :@otel_values in Ruby
86
91
  static ID at_parent_span_id_id; // id of :@parent_span_id in Ruby
87
92
  static ID at_datadog_trace_id; // id of :@datadog_trace in Ruby
88
93
 
94
+ // Used to support reading trace identifiers from the opentelemetry Ruby library when the ddtrace gem tracing
95
+ // integration is NOT in use.
96
+ static ID at_span_id_id; // id of :@span_id in Ruby
97
+ static ID at_trace_id_id; // id of :@trace_id in Ruby
98
+ static ID at_entries_id; // id of :@entries in Ruby
99
+ static ID at_context_id; // id of :@context in Ruby
100
+ static ID at_kind_id; // id of :@kind in Ruby
101
+ static ID at_name_id; // id of :@name in Ruby
102
+ static ID server_id; // id of :server in Ruby
103
+ static ID otel_context_storage_id; // id of :__opentelemetry_context_storage__ in Ruby
104
+
105
+ // This is used by `thread_context_collector_on_gvl_running`. Because when that method gets called we're not sure if
106
+ // it's safe to access the state of the thread context collector, we store this setting as a global value. This does
107
+ // mean this setting is shared among all thread context collectors, and thus it's "last writer wins".
108
+ // In production this should not be a problem: there should only be one profiler, which is the last one created,
109
+ // and that'll be the one that last wrote this setting.
110
+ static uint32_t global_waiting_for_gvl_threshold_ns = MILLIS_AS_NS(10);
111
+
112
+ typedef enum { OTEL_CONTEXT_ENABLED_FALSE, OTEL_CONTEXT_ENABLED_ONLY, OTEL_CONTEXT_ENABLED_BOTH } otel_context_enabled;
113
+
89
114
  // Contains state for a single ThreadContext instance
90
115
  struct thread_context_collector_state {
91
116
  // Note: Places in this file that usually need to be changed when this struct is changed are tagged with
@@ -112,13 +137,15 @@ struct thread_context_collector_state {
112
137
  bool endpoint_collection_enabled;
113
138
  // Used to omit timestamps / timeline events from collected data
114
139
  bool timeline_enabled;
115
- // Used to omit class information from collected allocation data
116
- bool allocation_type_enabled;
140
+ // Used to control context collection
141
+ otel_context_enabled otel_context_enabled;
117
142
  // Used when calling monotonic_to_system_epoch_ns
118
143
  monotonic_to_system_epoch_state time_converter_state;
119
144
  // Used to identify the main thread, to give it a fallback name
120
145
  VALUE main_thread;
121
146
  // Used when extracting trace identifiers from otel spans. Lazily initialized.
147
+ // Qtrue serves as a marker we've not yet extracted it; when we try to extract it, we set it to an object if
148
+ // successful and Qnil if not.
122
149
  VALUE otel_current_span_key;
123
150
 
124
151
  struct stats {
@@ -164,26 +191,23 @@ struct trace_identifiers {
164
191
  VALUE trace_endpoint;
165
192
  };
166
193
 
194
+ struct otel_span {
195
+ VALUE span;
196
+ VALUE span_id;
197
+ VALUE trace_id;
198
+ };
199
+
167
200
  static void thread_context_collector_typed_data_mark(void *state_ptr);
168
201
  static void thread_context_collector_typed_data_free(void *state_ptr);
169
202
  static int hash_map_per_thread_context_mark(st_data_t key_thread, st_data_t _value, st_data_t _argument);
170
203
  static int hash_map_per_thread_context_free_values(st_data_t _thread, st_data_t value_per_thread_context, st_data_t _argument);
171
204
  static VALUE _native_new(VALUE klass);
172
- static VALUE _native_initialize(
173
- VALUE self,
174
- VALUE collector_instance,
175
- VALUE recorder_instance,
176
- VALUE max_frames,
177
- VALUE tracer_context_key,
178
- VALUE endpoint_collection_enabled,
179
- VALUE timeline_enabled,
180
- VALUE allocation_type_enabled
181
- );
205
+ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
182
206
  static VALUE _native_sample(VALUE self, VALUE collector_instance, VALUE profiler_overhead_stack_thread);
183
207
  static VALUE _native_on_gc_start(VALUE self, VALUE collector_instance);
184
208
  static VALUE _native_on_gc_finish(VALUE self, VALUE collector_instance);
185
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
186
- void update_metrics_and_sample(
209
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE reset_monotonic_to_system_state);
210
+ static void update_metrics_and_sample(
187
211
  struct thread_context_collector_state *state,
188
212
  VALUE thread_being_sampled,
189
213
  VALUE stack_from_thread,
@@ -201,7 +225,8 @@ static void trigger_sample_for_thread(
201
225
  sample_values values,
202
226
  long current_monotonic_wall_time_ns,
203
227
  ddog_CharSlice *ruby_vm_type,
204
- ddog_CharSlice *class_name
228
+ ddog_CharSlice *class_name,
229
+ bool is_gvl_waiting_state
205
230
  );
206
231
  static VALUE _native_thread_list(VALUE self);
207
232
  static struct per_thread_context *get_or_create_context_for(VALUE thread, struct thread_context_collector_state *state);
@@ -237,6 +262,26 @@ static void ddtrace_otel_trace_identifiers_for(
237
262
  VALUE otel_values
238
263
  );
239
264
  static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE skipped_samples);
265
+ static bool handle_gvl_waiting(
266
+ struct thread_context_collector_state *state,
267
+ VALUE thread_being_sampled,
268
+ VALUE stack_from_thread,
269
+ struct per_thread_context *thread_context,
270
+ sampling_buffer* sampling_buffer,
271
+ long current_cpu_time_ns
272
+ );
273
+ static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
274
+ static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread);
275
+ static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread);
276
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
277
+ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns);
278
+ static void otel_without_ddtrace_trace_identifiers_for(
279
+ struct thread_context_collector_state *state,
280
+ VALUE thread,
281
+ struct trace_identifiers *trace_identifiers_result
282
+ );
283
+ static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key);
284
+ static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
240
285
 
241
286
  void collectors_thread_context_init(VALUE profiling_module) {
242
287
  VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
@@ -254,20 +299,27 @@ void collectors_thread_context_init(VALUE profiling_module) {
254
299
  // https://bugs.ruby-lang.org/issues/18007 for a discussion around this.
255
300
  rb_define_alloc_func(collectors_thread_context_class, _native_new);
256
301
 
257
- rb_define_singleton_method(collectors_thread_context_class, "_native_initialize", _native_initialize, 7);
302
+ rb_define_singleton_method(collectors_thread_context_class, "_native_initialize", _native_initialize, -1);
258
303
  rb_define_singleton_method(collectors_thread_context_class, "_native_inspect", _native_inspect, 1);
259
304
  rb_define_singleton_method(collectors_thread_context_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
260
305
  rb_define_singleton_method(testing_module, "_native_sample", _native_sample, 2);
261
306
  rb_define_singleton_method(testing_module, "_native_sample_allocation", _native_sample_allocation, 3);
262
307
  rb_define_singleton_method(testing_module, "_native_on_gc_start", _native_on_gc_start, 1);
263
308
  rb_define_singleton_method(testing_module, "_native_on_gc_finish", _native_on_gc_finish, 1);
264
- rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 1);
309
+ rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 2);
265
310
  rb_define_singleton_method(testing_module, "_native_thread_list", _native_thread_list, 0);
266
311
  rb_define_singleton_method(testing_module, "_native_per_thread_context", _native_per_thread_context, 1);
267
312
  rb_define_singleton_method(testing_module, "_native_stats", _native_stats, 1);
268
313
  rb_define_singleton_method(testing_module, "_native_gc_tracking", _native_gc_tracking, 1);
269
314
  rb_define_singleton_method(testing_module, "_native_new_empty_thread", _native_new_empty_thread, 0);
270
315
  rb_define_singleton_method(testing_module, "_native_sample_skipped_allocation_samples", _native_sample_skipped_allocation_samples, 2);
316
+ #ifndef NO_GVL_INSTRUMENTATION
317
+ rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
318
+ rb_define_singleton_method(testing_module, "_native_gvl_waiting_at_for", _native_gvl_waiting_at_for, 1);
319
+ rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 1);
320
+ rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 2);
321
+ rb_define_singleton_method(testing_module, "_native_apply_delta_to_cpu_time_at_previous_sample_ns", _native_apply_delta_to_cpu_time_at_previous_sample_ns, 3);
322
+ #endif
271
323
 
272
324
  at_active_span_id = rb_intern_const("@active_span");
273
325
  at_active_trace_id = rb_intern_const("@active_trace");
@@ -278,6 +330,19 @@ void collectors_thread_context_init(VALUE profiling_module) {
278
330
  at_otel_values_id = rb_intern_const("@otel_values");
279
331
  at_parent_span_id_id = rb_intern_const("@parent_span_id");
280
332
  at_datadog_trace_id = rb_intern_const("@datadog_trace");
333
+ at_span_id_id = rb_intern_const("@span_id");
334
+ at_trace_id_id = rb_intern_const("@trace_id");
335
+ at_entries_id = rb_intern_const("@entries");
336
+ at_context_id = rb_intern_const("@context");
337
+ at_kind_id = rb_intern_const("@kind");
338
+ at_name_id = rb_intern_const("@name");
339
+ server_id = rb_intern_const("server");
340
+ otel_context_storage_id = rb_intern_const("__opentelemetry_context_storage__");
341
+
342
+ #ifndef NO_GVL_INSTRUMENTATION
343
+ // This will raise if Ruby already ran out of thread-local keys
344
+ gvl_profiling_init();
345
+ #endif
281
346
 
282
347
  gc_profiling_init();
283
348
  }
@@ -357,11 +422,11 @@ static VALUE _native_new(VALUE klass) {
357
422
  state->thread_list_buffer = thread_list_buffer;
358
423
  state->endpoint_collection_enabled = true;
359
424
  state->timeline_enabled = true;
360
- state->allocation_type_enabled = true;
425
+ state->otel_context_enabled = OTEL_CONTEXT_ENABLED_FALSE;
361
426
  state->time_converter_state = (monotonic_to_system_epoch_state) MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER;
362
427
  VALUE main_thread = rb_thread_main();
363
428
  state->main_thread = main_thread;
364
- state->otel_current_span_key = Qnil;
429
+ state->otel_current_span_key = Qtrue;
365
430
  state->gc_tracking.wall_time_at_previous_gc_ns = INVALID_TIME;
366
431
  state->gc_tracking.wall_time_at_last_flushed_gc_event_ns = 0;
367
432
 
@@ -377,22 +442,27 @@ static VALUE _native_new(VALUE klass) {
377
442
  return instance;
378
443
  }
379
444
 
380
- static VALUE _native_initialize(
381
- DDTRACE_UNUSED VALUE _self,
382
- VALUE collector_instance,
383
- VALUE recorder_instance,
384
- VALUE max_frames,
385
- VALUE tracer_context_key,
386
- VALUE endpoint_collection_enabled,
387
- VALUE timeline_enabled,
388
- VALUE allocation_type_enabled
389
- ) {
445
+ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
446
+ VALUE options;
447
+ rb_scan_args(argc, argv, "0:", &options);
448
+ if (options == Qnil) options = rb_hash_new();
449
+
450
+ VALUE self_instance = rb_hash_fetch(options, ID2SYM(rb_intern("self_instance")));
451
+ VALUE recorder_instance = rb_hash_fetch(options, ID2SYM(rb_intern("recorder")));
452
+ VALUE max_frames = rb_hash_fetch(options, ID2SYM(rb_intern("max_frames")));
453
+ VALUE tracer_context_key = rb_hash_fetch(options, ID2SYM(rb_intern("tracer_context_key")));
454
+ VALUE endpoint_collection_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("endpoint_collection_enabled")));
455
+ VALUE timeline_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("timeline_enabled")));
456
+ VALUE waiting_for_gvl_threshold_ns = rb_hash_fetch(options, ID2SYM(rb_intern("waiting_for_gvl_threshold_ns")));
457
+ VALUE otel_context_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("otel_context_enabled")));
458
+
459
+ ENFORCE_TYPE(max_frames, T_FIXNUM);
390
460
  ENFORCE_BOOLEAN(endpoint_collection_enabled);
391
461
  ENFORCE_BOOLEAN(timeline_enabled);
392
- ENFORCE_BOOLEAN(allocation_type_enabled);
462
+ ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
393
463
 
394
464
  struct thread_context_collector_state *state;
395
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
465
+ TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
396
466
 
397
467
  // Update this when modifying state struct
398
468
  state->max_frames = sampling_buffer_check_max_frames(NUM2INT(max_frames));
@@ -401,7 +471,17 @@ static VALUE _native_initialize(
401
471
  state->recorder_instance = enforce_recorder_instance(recorder_instance);
402
472
  state->endpoint_collection_enabled = (endpoint_collection_enabled == Qtrue);
403
473
  state->timeline_enabled = (timeline_enabled == Qtrue);
404
- state->allocation_type_enabled = (allocation_type_enabled == Qtrue);
474
+ if (otel_context_enabled == Qfalse || otel_context_enabled == Qnil) {
475
+ state->otel_context_enabled = OTEL_CONTEXT_ENABLED_FALSE;
476
+ } else if (otel_context_enabled == ID2SYM(rb_intern("only"))) {
477
+ state->otel_context_enabled = OTEL_CONTEXT_ENABLED_ONLY;
478
+ } else if (otel_context_enabled == ID2SYM(rb_intern("both"))) {
479
+ state->otel_context_enabled = OTEL_CONTEXT_ENABLED_BOTH;
480
+ } else {
481
+ rb_raise(rb_eArgError, "Unexpected value for otel_context_enabled: %+" PRIsVALUE, otel_context_enabled);
482
+ }
483
+
484
+ global_waiting_for_gvl_threshold_ns = NUM2UINT(waiting_for_gvl_threshold_ns);
405
485
 
406
486
  if (RTEST(tracer_context_key)) {
407
487
  ENFORCE_TYPE(tracer_context_key, T_SYMBOL);
@@ -433,13 +513,22 @@ static VALUE _native_on_gc_start(DDTRACE_UNUSED VALUE self, VALUE collector_inst
433
513
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
434
514
  // It SHOULD NOT be used for other purposes.
435
515
  static VALUE _native_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
436
- thread_context_collector_on_gc_finish(collector_instance);
516
+ (void) !thread_context_collector_on_gc_finish(collector_instance);
437
517
  return Qtrue;
438
518
  }
439
519
 
440
520
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
441
521
  // It SHOULD NOT be used for other purposes.
442
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
522
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE reset_monotonic_to_system_state) {
523
+ ENFORCE_BOOLEAN(reset_monotonic_to_system_state);
524
+
525
+ struct thread_context_collector_state *state;
526
+ TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
527
+
528
+ if (reset_monotonic_to_system_state == Qtrue) {
529
+ state->time_converter_state = (monotonic_to_system_epoch_state) MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER;
530
+ }
531
+
443
532
  thread_context_collector_sample_after_gc(collector_instance);
444
533
  return Qtrue;
445
534
  }
@@ -502,7 +591,7 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
502
591
  );
503
592
  }
504
593
 
505
- void update_metrics_and_sample(
594
+ static void update_metrics_and_sample(
506
595
  struct thread_context_collector_state *state,
507
596
  VALUE thread_being_sampled,
508
597
  VALUE stack_from_thread, // This can be different when attributing profiler overhead using a different stack
@@ -511,12 +600,17 @@ void update_metrics_and_sample(
511
600
  long current_cpu_time_ns,
512
601
  long current_monotonic_wall_time_ns
513
602
  ) {
514
- long cpu_time_elapsed_ns = update_time_since_previous_sample(
603
+ bool is_gvl_waiting_state =
604
+ handle_gvl_waiting(state, thread_being_sampled, stack_from_thread, thread_context, sampling_buffer, current_cpu_time_ns);
605
+
606
+ // Don't assign/update cpu during "Waiting for GVL"
607
+ long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_time_since_previous_sample(
515
608
  &thread_context->cpu_time_at_previous_sample_ns,
516
609
  current_cpu_time_ns,
517
610
  thread_context->gc_tracking.cpu_time_at_start_ns,
518
611
  IS_NOT_WALL_TIME
519
612
  );
613
+
520
614
  long wall_time_elapsed_ns = update_time_since_previous_sample(
521
615
  &thread_context->wall_time_at_previous_sample_ns,
522
616
  current_monotonic_wall_time_ns,
@@ -528,6 +622,21 @@ void update_metrics_and_sample(
528
622
  IS_WALL_TIME
529
623
  );
530
624
 
625
+ // A thread enters "Waiting for GVL", well, as the name implies, without the GVL.
626
+ //
627
+ // As a consequence, it's possible that a thread enters "Waiting for GVL" in parallel with the current thread working
628
+ // on sampling, and thus for the `current_monotonic_wall_time_ns` (which is recorded at the start of sampling)
629
+ // to be < the time at which we started Waiting for GVL.
630
+ //
631
+ // All together, this means that when `handle_gvl_waiting` creates an extra sample (see comments on that function for
632
+ // what the extra sample is), it's possible that there's no more wall-time to be assigned.
633
+ // Thus, in this case, we don't want to produce a sample representing Waiting for GVL with a wall-time of 0, and
634
+ // thus we skip creating such a sample.
635
+ if (is_gvl_waiting_state && wall_time_elapsed_ns == 0) return;
636
+ // ...you may also wonder: is there any other situation where it makes sense to produce a sample with
637
+ // wall_time_elapsed_ns == 0? I believe that yes, because the sample still includes a timestamp and a stack, but we
638
+ // may revisit/change our minds on this in the future.
639
+
531
640
  trigger_sample_for_thread(
532
641
  state,
533
642
  thread_being_sampled,
@@ -537,7 +646,8 @@ void update_metrics_and_sample(
537
646
  (sample_values) {.cpu_time_ns = cpu_time_elapsed_ns, .cpu_or_wall_samples = 1, .wall_time_ns = wall_time_elapsed_ns},
538
647
  current_monotonic_wall_time_ns,
539
648
  NULL,
540
- NULL
649
+ NULL,
650
+ is_gvl_waiting_state
541
651
  );
542
652
  }
543
653
 
@@ -583,6 +693,7 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
583
693
  //
584
694
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
585
695
  // Assumption 2: This function is called from the main Ractor (if Ruby has support for Ractors).
696
+ __attribute__((warn_unused_result))
586
697
  bool thread_context_collector_on_gc_finish(VALUE self_instance) {
587
698
  struct thread_context_collector_state *state;
588
699
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
@@ -704,6 +815,9 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
704
815
 
705
816
  state->stats.gc_samples++;
706
817
 
818
+ // Let recorder do any cleanup/updates it requires after a GC step.
819
+ recorder_after_gc_step(state->recorder_instance);
820
+
707
821
  // Return a VALUE to make it easier to call this function from Ruby APIs that expect a return value (such as rb_rescue2)
708
822
  return Qnil;
709
823
  }
@@ -718,7 +832,8 @@ static void trigger_sample_for_thread(
718
832
  long current_monotonic_wall_time_ns,
719
833
  // These two labels are only used for allocation profiling; @ivoanjo: may want to refactor this at some point?
720
834
  ddog_CharSlice *ruby_vm_type,
721
- ddog_CharSlice *class_name
835
+ ddog_CharSlice *class_name,
836
+ bool is_gvl_waiting_state
722
837
  ) {
723
838
  int max_label_count =
724
839
  1 + // thread id
@@ -759,6 +874,11 @@ static void trigger_sample_for_thread(
759
874
  struct trace_identifiers trace_identifiers_result = {.valid = false, .trace_endpoint = Qnil};
760
875
  trace_identifiers_for(state, thread, &trace_identifiers_result);
761
876
 
877
+ if (!trace_identifiers_result.valid && state->otel_context_enabled != OTEL_CONTEXT_ENABLED_FALSE) {
878
+ // If we couldn't get something with ddtrace, let's see if we can get some trace identifiers from opentelemetry directly
879
+ otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result);
880
+ }
881
+
762
882
  if (trace_identifiers_result.valid) {
763
883
  labels[label_pos++] = (ddog_prof_Label) {.key = DDOG_CHARSLICE_C("local root span id"), .num = trace_identifiers_result.local_root_span_id};
764
884
  labels[label_pos++] = (ddog_prof_Label) {.key = DDOG_CHARSLICE_C("span id"), .num = trace_identifiers_result.span_id};
@@ -837,7 +957,12 @@ static void trigger_sample_for_thread(
837
957
  sampling_buffer,
838
958
  state->recorder_instance,
839
959
  values,
840
- (sample_labels) {.labels = slice_labels, .state_label = state_label, .end_timestamp_ns = end_timestamp_ns}
960
+ (sample_labels) {
961
+ .labels = slice_labels,
962
+ .state_label = state_label,
963
+ .end_timestamp_ns = end_timestamp_ns,
964
+ .is_gvl_waiting_state = is_gvl_waiting_state,
965
+ }
841
966
  );
842
967
  }
843
968
 
@@ -887,9 +1012,9 @@ static struct per_thread_context *get_context_for(VALUE thread, struct thread_co
887
1012
  // to either run Ruby code during sampling (not great), or otherwise use some of the VM private APIs to detect this.
888
1013
  //
889
1014
  static bool is_logging_gem_monkey_patch(VALUE invoke_file_location) {
890
- int logging_gem_path_len = strlen(LOGGING_GEM_PATH);
1015
+ unsigned long logging_gem_path_len = strlen(LOGGING_GEM_PATH);
891
1016
  char *invoke_file = StringValueCStr(invoke_file_location);
892
- int invoke_file_len = strlen(invoke_file);
1017
+ unsigned long invoke_file_len = strlen(invoke_file);
893
1018
 
894
1019
  if (invoke_file_len < logging_gem_path_len) return false;
895
1020
 
@@ -937,6 +1062,20 @@ static void initialize_context(VALUE thread, struct per_thread_context *thread_c
937
1062
  // These will only be used during a GC operation
938
1063
  thread_context->gc_tracking.cpu_time_at_start_ns = INVALID_TIME;
939
1064
  thread_context->gc_tracking.wall_time_at_start_ns = INVALID_TIME;
1065
+
1066
+ #ifndef NO_GVL_INSTRUMENTATION
1067
+ // We use this special location to store data that can be accessed without any
1068
+ // kind of synchronization (e.g. by threads without the GVL).
1069
+ //
1070
+ // We set this marker here for two purposes:
1071
+ // * To make sure there's no stale data from a previous execution of the profiler.
1072
+ // * To mark threads that are actually being profiled
1073
+ //
1074
+ // (Setting this is potentially a race, but what we want is to avoid _stale_ data, so
1075
+ // if this gets set concurrently with context initialization, then such a value will belong
1076
+ // to the current profiler instance, so that's OK)
1077
+ gvl_profiling_state_thread_object_set(thread, GVL_WAITING_ENABLED_EMPTY);
1078
+ #endif
940
1079
  }
941
1080
 
942
1081
  static void free_context(struct per_thread_context* thread_context) {
@@ -960,7 +1099,7 @@ static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instanc
960
1099
  rb_str_concat(result, rb_sprintf(" stats=%"PRIsVALUE, stats_as_ruby_hash(state)));
961
1100
  rb_str_concat(result, rb_sprintf(" endpoint_collection_enabled=%"PRIsVALUE, state->endpoint_collection_enabled ? Qtrue : Qfalse));
962
1101
  rb_str_concat(result, rb_sprintf(" timeline_enabled=%"PRIsVALUE, state->timeline_enabled ? Qtrue : Qfalse));
963
- rb_str_concat(result, rb_sprintf(" allocation_type_enabled=%"PRIsVALUE, state->allocation_type_enabled ? Qtrue : Qfalse));
1102
+ rb_str_concat(result, rb_sprintf(" otel_context_enabled=%d", state->otel_context_enabled));
964
1103
  rb_str_concat(result, rb_sprintf(
965
1104
  " time_converter_state={.system_epoch_ns_reference=%ld, .delta_to_epoch_ns=%ld}",
966
1105
  state->time_converter_state.system_epoch_ns_reference,
@@ -969,6 +1108,7 @@ static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instanc
969
1108
  rb_str_concat(result, rb_sprintf(" main_thread=%"PRIsVALUE, state->main_thread));
970
1109
  rb_str_concat(result, rb_sprintf(" gc_tracking=%"PRIsVALUE, gc_tracking_as_ruby_hash(state)));
971
1110
  rb_str_concat(result, rb_sprintf(" otel_current_span_key=%"PRIsVALUE, state->otel_current_span_key));
1111
+ rb_str_concat(result, rb_sprintf(" global_waiting_for_gvl_threshold_ns=%u", global_waiting_for_gvl_threshold_ns));
972
1112
 
973
1113
  return result;
974
1114
  }
@@ -996,6 +1136,10 @@ static int per_thread_context_as_ruby_hash(st_data_t key_thread, st_data_t value
996
1136
 
997
1137
  ID2SYM(rb_intern("gc_tracking.cpu_time_at_start_ns")), /* => */ LONG2NUM(thread_context->gc_tracking.cpu_time_at_start_ns),
998
1138
  ID2SYM(rb_intern("gc_tracking.wall_time_at_start_ns")), /* => */ LONG2NUM(thread_context->gc_tracking.wall_time_at_start_ns),
1139
+
1140
+ #ifndef NO_GVL_INSTRUMENTATION
1141
+ ID2SYM(rb_intern("gvl_waiting_at")), /* => */ LONG2NUM(gvl_profiling_state_thread_object_get(thread)),
1142
+ #endif
999
1143
  };
1000
1144
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(context_as_hash, arguments[i], arguments[i+1]);
1001
1145
 
@@ -1146,6 +1290,7 @@ static VALUE _native_gc_tracking(DDTRACE_UNUSED VALUE _self, VALUE collector_ins
1146
1290
 
1147
1291
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
1148
1292
  static void trace_identifiers_for(struct thread_context_collector_state *state, VALUE thread, struct trace_identifiers *trace_identifiers_result) {
1293
+ if (state->otel_context_enabled == OTEL_CONTEXT_ENABLED_ONLY) return;
1149
1294
  if (state->tracer_context_key == MISSING_TRACER_CONTEXT_KEY) return;
1150
1295
 
1151
1296
  VALUE current_context = rb_thread_local_aref(thread, state->tracer_context_key);
@@ -1200,7 +1345,7 @@ static bool should_collect_resource(VALUE root_span) {
1200
1345
  if (root_span_type == Qnil) return false;
1201
1346
  ENFORCE_TYPE(root_span_type, T_STRING);
1202
1347
 
1203
- int root_span_type_length = RSTRING_LEN(root_span_type);
1348
+ long root_span_type_length = RSTRING_LEN(root_span_type);
1204
1349
  const char *root_span_type_value = StringValuePtr(root_span_type);
1205
1350
 
1206
1351
  bool is_web_request =
@@ -1223,6 +1368,9 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector
1223
1368
  struct thread_context_collector_state *state;
1224
1369
  TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1225
1370
 
1371
+ // Release all context memory before clearing the existing context
1372
+ st_foreach(state->hash_map_per_thread_context, hash_map_per_thread_context_free_values, 0 /* unused */);
1373
+
1226
1374
  st_clear(state->hash_map_per_thread_context);
1227
1375
 
1228
1376
  state->stats = (struct stats) {}; // Resets all stats back to zero
@@ -1255,62 +1403,61 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1255
1403
  ddog_CharSlice *optional_class_name = NULL;
1256
1404
  char imemo_type[100];
1257
1405
 
1258
- if (state->allocation_type_enabled) {
1259
- optional_class_name = &class_name;
1260
-
1261
- if (
1262
- type == RUBY_T_OBJECT ||
1263
- type == RUBY_T_CLASS ||
1264
- type == RUBY_T_MODULE ||
1265
- type == RUBY_T_FLOAT ||
1266
- type == RUBY_T_STRING ||
1267
- type == RUBY_T_REGEXP ||
1268
- type == RUBY_T_ARRAY ||
1269
- type == RUBY_T_HASH ||
1270
- type == RUBY_T_STRUCT ||
1271
- type == RUBY_T_BIGNUM ||
1272
- type == RUBY_T_FILE ||
1273
- type == RUBY_T_DATA ||
1274
- type == RUBY_T_MATCH ||
1275
- type == RUBY_T_COMPLEX ||
1276
- type == RUBY_T_RATIONAL ||
1277
- type == RUBY_T_NIL ||
1278
- type == RUBY_T_TRUE ||
1279
- type == RUBY_T_FALSE ||
1280
- type == RUBY_T_SYMBOL ||
1281
- type == RUBY_T_FIXNUM
1282
- ) {
1283
- VALUE klass = rb_class_of(new_object);
1284
-
1285
- // Ruby sometimes plays a bit fast and loose with some of its internal objects, e.g.
1286
- // `rb_str_tmp_frozen_acquire` allocates a string with no class (klass=0).
1287
- // Thus, we need to make sure there's actually a class before getting its name.
1288
-
1289
- if (klass != 0) {
1290
- const char *name = rb_class2name(klass);
1291
- size_t name_length = name != NULL ? strlen(name) : 0;
1292
-
1293
- if (name_length > 0) {
1294
- class_name = (ddog_CharSlice) {.ptr = name, .len = name_length};
1295
- } else {
1296
- // @ivoanjo: I'm not sure this can ever happen, but just-in-case
1297
- class_name = ruby_value_type_to_class_name(type);
1298
- }
1406
+ optional_class_name = &class_name;
1407
+
1408
+ if (
1409
+ type == RUBY_T_OBJECT ||
1410
+ type == RUBY_T_CLASS ||
1411
+ type == RUBY_T_MODULE ||
1412
+ type == RUBY_T_FLOAT ||
1413
+ type == RUBY_T_STRING ||
1414
+ type == RUBY_T_REGEXP ||
1415
+ type == RUBY_T_ARRAY ||
1416
+ type == RUBY_T_HASH ||
1417
+ type == RUBY_T_STRUCT ||
1418
+ type == RUBY_T_BIGNUM ||
1419
+ type == RUBY_T_FILE ||
1420
+ type == RUBY_T_DATA ||
1421
+ type == RUBY_T_MATCH ||
1422
+ type == RUBY_T_COMPLEX ||
1423
+ type == RUBY_T_RATIONAL ||
1424
+ type == RUBY_T_NIL ||
1425
+ type == RUBY_T_TRUE ||
1426
+ type == RUBY_T_FALSE ||
1427
+ type == RUBY_T_SYMBOL ||
1428
+ type == RUBY_T_FIXNUM
1429
+ ) {
1430
+ VALUE klass = rb_class_of(new_object);
1431
+
1432
+ // Ruby sometimes plays a bit fast and loose with some of its internal objects, e.g.
1433
+ // `rb_str_tmp_frozen_acquire` allocates a string with no class (klass=0).
1434
+ // Thus, we need to make sure there's actually a class before getting its name.
1435
+
1436
+ if (klass != 0) {
1437
+ const char *name = rb_class2name(klass);
1438
+ size_t name_length = name != NULL ? strlen(name) : 0;
1439
+
1440
+ if (name_length > 0) {
1441
+ class_name = (ddog_CharSlice) {.ptr = name, .len = name_length};
1299
1442
  } else {
1300
- // Fallback for objects with no class
1443
+ // @ivoanjo: I'm not sure this can ever happen, but just-in-case
1301
1444
  class_name = ruby_value_type_to_class_name(type);
1302
1445
  }
1303
- } else if (type == RUBY_T_IMEMO) {
1304
- const char *imemo_string = imemo_kind(new_object);
1305
- if (imemo_string != NULL) {
1306
- snprintf(imemo_type, 100, "(VM Internal, T_IMEMO, %s)", imemo_string);
1307
- class_name = (ddog_CharSlice) {.ptr = imemo_type, .len = strlen(imemo_type)};
1308
- } else { // Ruby < 3
1309
- class_name = DDOG_CHARSLICE_C("(VM Internal, T_IMEMO)");
1310
- }
1311
1446
  } else {
1312
- class_name = ruby_vm_type; // For other weird internal things we just use the VM type
1447
+ // Fallback for objects with no class. Objects with no class are a way for the Ruby VM to mark them
1448
+ // as internal objects; see rb_objspace_internal_object_p for details.
1449
+ class_name = ruby_value_type_to_class_name(type);
1450
+ }
1451
+ } else if (type == RUBY_T_IMEMO) {
1452
+ const char *imemo_string = imemo_kind(new_object);
1453
+ if (imemo_string != NULL) {
1454
+ snprintf(imemo_type, 100, "(VM Internal, T_IMEMO, %s)", imemo_string);
1455
+ class_name = (ddog_CharSlice) {.ptr = imemo_type, .len = strlen(imemo_type)};
1456
+ } else { // Ruby < 3
1457
+ class_name = DDOG_CHARSLICE_C("(VM Internal, T_IMEMO)");
1313
1458
  }
1459
+ } else {
1460
+ class_name = ruby_vm_type; // For other weird internal things we just use the VM type
1314
1461
  }
1315
1462
 
1316
1463
  track_object(state->recorder_instance, new_object, sample_weight, optional_class_name);
@@ -1326,7 +1473,8 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1326
1473
  (sample_values) {.alloc_samples = sample_weight, .alloc_samples_unscaled = 1, .heap_sample = true},
1327
1474
  INVALID_TIME, // For now we're not collecting timestamps for allocation events, as per profiling team internal discussions
1328
1475
  &ruby_vm_type,
1329
- optional_class_name
1476
+ optional_class_name,
1477
+ false
1330
1478
  );
1331
1479
  }
1332
1480
 
@@ -1372,25 +1520,29 @@ static ddog_CharSlice ruby_value_type_to_class_name(enum ruby_value_type type) {
1372
1520
  }
1373
1521
  }
1374
1522
 
1523
+ // Used to access OpenTelemetry::Trace.const_get(:CURRENT_SPAN_KEY). Will raise exceptions if it fails.
1524
+ static VALUE read_otel_current_span_key_const(DDTRACE_UNUSED VALUE _unused) {
1525
+ VALUE opentelemetry_module = rb_const_get(rb_cObject, rb_intern("OpenTelemetry"));
1526
+ ENFORCE_TYPE(opentelemetry_module, T_MODULE);
1527
+ VALUE trace_module = rb_const_get(opentelemetry_module, rb_intern("Trace"));
1528
+ ENFORCE_TYPE(trace_module, T_MODULE);
1529
+ return rb_const_get(trace_module, rb_intern("CURRENT_SPAN_KEY"));
1530
+ }
1531
+
1375
1532
  static VALUE get_otel_current_span_key(struct thread_context_collector_state *state) {
1376
- if (state->otel_current_span_key == Qnil) {
1377
- VALUE datadog_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
1378
- VALUE opentelemetry_module = rb_const_get(datadog_module, rb_intern("OpenTelemetry"));
1379
- VALUE api_module = rb_const_get(opentelemetry_module, rb_intern("API"));
1380
- VALUE context_module = rb_const_get(api_module, rb_intern_const("Context"));
1381
- VALUE current_span_key = rb_const_get(context_module, rb_intern_const("CURRENT_SPAN_KEY"));
1382
-
1383
- if (current_span_key == Qnil) {
1384
- rb_raise(rb_eRuntimeError, "Unexpected: Missing Datadog::OpenTelemetry::API::Context::CURRENT_SPAN_KEY");
1385
- }
1533
+ if (state->otel_current_span_key == Qtrue) { // Qtrue means we haven't tried to extract it yet
1534
+ // If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
1535
+ // gets refactored, we should not fall on our face)
1536
+ VALUE span_key = rb_protect(read_otel_current_span_key_const, Qnil, NULL);
1386
1537
 
1387
- state->otel_current_span_key = current_span_key;
1538
+ // Note that this gets set to Qnil if we failed to extract the correct value, and thus we won't try to extract it again
1539
+ state->otel_current_span_key = span_key;
1388
1540
  }
1389
1541
 
1390
1542
  return state->otel_current_span_key;
1391
1543
  }
1392
1544
 
1393
- // This method gets used when ddtrace is being used indirectly via the otel APIs. Information gets stored slightly
1545
+ // This method gets used when ddtrace is being used indirectly via the opentelemetry APIs. Information gets stored slightly
1394
1546
  // differently, and this codepath handles it.
1395
1547
  static void ddtrace_otel_trace_identifiers_for(
1396
1548
  struct thread_context_collector_state *state,
@@ -1410,6 +1562,7 @@ static void ddtrace_otel_trace_identifiers_for(
1410
1562
  if (resolved_numeric_span_id == Qnil) return;
1411
1563
 
1412
1564
  VALUE otel_current_span_key = get_otel_current_span_key(state);
1565
+ if (otel_current_span_key == Qnil) return;
1413
1566
  VALUE current_trace = *active_trace;
1414
1567
 
1415
1568
  // ddtrace uses a different structure when spans are created from otel, where each otel span will have a unique ddtrace
@@ -1462,3 +1615,388 @@ static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self
1462
1615
  thread_context_collector_sample_skipped_allocation_samples(collector_instance, NUM2UINT(skipped_samples));
1463
1616
  return Qtrue;
1464
1617
  }
1618
+
1619
+ // This method differs from trace_identifiers_for/ddtrace_otel_trace_identifiers_for to support the situation where
1620
+ // the opentelemetry ruby library is being used for tracing AND the ddtrace tracing bits are not involved at all.
1621
+ //
1622
+ // Thus, in this case, we're directly reading from the opentelemetry stuff, which is different to how ddtrace tracing
1623
+ // does it.
1624
+ //
1625
+ // This is somewhat brittle: we're coupling on internal details of the opentelemetry gem to get what we need. In the
1626
+ // future maybe the otel ruby folks would be open to having a nice public way of getting this data that suits the
1627
+ // usecase of profilers.
1628
+ // Until then, the strategy below is to be extremely defensive, and if anything is out of place, we immediately return
1629
+ // and give up on getting trace data from opentelemetry. (Thus, worst case would be -- you upgrade opentelemetry and
1630
+ // profiling features relying on reading this data stop working, but you'll still get profiles and the app will be
1631
+ // otherwise undisturbed).
1632
+ //
1633
+ // Specifically, the way this works is:
1634
+ // 1. The latest entry in the opentelemetry context storage represents the current span (if any). We take the span id
1635
+ // and trace id from this span.
1636
+ // 2. To find the local root span id, we walk the context storage backwards from the current span, and find the earliest
1637
+ // entry in the context storage that has the same trace id as the current span; we use the found span as the local
1638
+ // root span id.
1639
+ // This matches the semantics of how ddtrace tracing creates a TraceOperation and assigns a local root span to it.
1640
+ static void otel_without_ddtrace_trace_identifiers_for(
1641
+ struct thread_context_collector_state *state,
1642
+ VALUE thread,
1643
+ struct trace_identifiers *trace_identifiers_result
1644
+ ) {
1645
+ VALUE context_storage = rb_thread_local_aref(thread, otel_context_storage_id /* __opentelemetry_context_storage__ */);
1646
+
1647
+ // If it exists, context_storage is expected to be an Array[OpenTelemetry::Context]
1648
+ if (context_storage == Qnil || !RB_TYPE_P(context_storage, T_ARRAY)) return;
1649
+
1650
+ VALUE otel_current_span_key = get_otel_current_span_key(state);
1651
+ if (otel_current_span_key == Qnil) return;
1652
+
1653
+ int active_context_index = RARRAY_LEN(context_storage) - 1;
1654
+ if (active_context_index < 0) return;
1655
+
1656
+ struct otel_span active_span = otel_span_from(rb_ary_entry(context_storage, active_context_index), otel_current_span_key);
1657
+ if (active_span.span == Qnil) return;
1658
+
1659
+ struct otel_span local_root_span = active_span;
1660
+
1661
+ // Now find the oldest span starting from the active span that still has the same trace id as the active span
1662
+ for (int i = active_context_index - 1; i >= 0; i--) {
1663
+ struct otel_span checking_span = otel_span_from(rb_ary_entry(context_storage, i), otel_current_span_key);
1664
+ if (checking_span.span == Qnil) return;
1665
+
1666
+ if (rb_str_equal(active_span.trace_id, checking_span.trace_id) == Qfalse) break;
1667
+
1668
+ local_root_span = checking_span;
1669
+ }
1670
+
1671
+ // Convert the span ids into uint64_t to match what the Datadog tracer does
1672
+ trace_identifiers_result->span_id = otel_span_id_to_uint(active_span.span_id);
1673
+ trace_identifiers_result->local_root_span_id = otel_span_id_to_uint(local_root_span.span_id);
1674
+
1675
+ if (trace_identifiers_result->span_id == 0 || trace_identifiers_result->local_root_span_id == 0) return;
1676
+
1677
+ trace_identifiers_result->valid = true;
1678
+
1679
+ if (!state->endpoint_collection_enabled) return;
1680
+
1681
+ VALUE root_span_type = rb_ivar_get(local_root_span.span, at_kind_id /* @kind */);
1682
+ // We filter out spans that don't have `kind: :server`
1683
+ if (root_span_type == Qnil || !RB_TYPE_P(root_span_type, T_SYMBOL) || SYM2ID(root_span_type) != server_id) return;
1684
+
1685
+ VALUE trace_resource = rb_ivar_get(local_root_span.span, at_name_id /* @name */);
1686
+ if (!RB_TYPE_P(trace_resource, T_STRING)) return;
1687
+
1688
+ trace_identifiers_result->trace_endpoint = trace_resource;
1689
+ }
1690
+
1691
+ static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key) {
1692
+ struct otel_span failed = {.span = Qnil, .span_id = Qnil, .trace_id = Qnil};
1693
+
1694
+ if (otel_context == Qnil) return failed;
1695
+
1696
+ VALUE context_entries = rb_ivar_get(otel_context, at_entries_id /* @entries */);
1697
+ if (context_entries == Qnil || !RB_TYPE_P(context_entries, T_HASH)) return failed;
1698
+
1699
+ // If it exists, context_entries is expected to be a Hash[OpenTelemetry::Context::Key, OpenTelemetry::Trace::Span]
1700
+ VALUE span = rb_hash_lookup(context_entries, otel_current_span_key);
1701
+ if (span == Qnil) return failed;
1702
+
1703
+ // If it exists, span_context is expected to be a OpenTelemetry::Trace::SpanContext (don't confuse it with OpenTelemetry::Context)
1704
+ VALUE span_context = rb_ivar_get(span, at_context_id /* @context */);
1705
+ if (span_context == Qnil) return failed;
1706
+
1707
+ VALUE span_id = rb_ivar_get(span_context, at_span_id_id /* @span_id */);
1708
+ VALUE trace_id = rb_ivar_get(span_context, at_trace_id_id /* @trace_id */);
1709
+ if (span_id == Qnil || trace_id == Qnil || !RB_TYPE_P(span_id, T_STRING) || !RB_TYPE_P(trace_id, T_STRING)) return failed;
1710
+
1711
+ return (struct otel_span) {.span = span, .span_id = span_id, .trace_id = trace_id};
1712
+ }
1713
+
1714
+ // Otel span ids are represented as a big-endian 8-byte string
1715
+ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1716
+ if (!RB_TYPE_P(otel_span_id, T_STRING) || RSTRING_LEN(otel_span_id) != 8) { return 0; }
1717
+
1718
+ unsigned char *span_bytes = (unsigned char*) StringValuePtr(otel_span_id);
1719
+
1720
+ return \
1721
+ ((uint64_t)span_bytes[0] << 56) |
1722
+ ((uint64_t)span_bytes[1] << 48) |
1723
+ ((uint64_t)span_bytes[2] << 40) |
1724
+ ((uint64_t)span_bytes[3] << 32) |
1725
+ ((uint64_t)span_bytes[4] << 24) |
1726
+ ((uint64_t)span_bytes[5] << 16) |
1727
+ ((uint64_t)span_bytes[6] << 8) |
1728
+ ((uint64_t)span_bytes[7]);
1729
+ }
1730
+
1731
+ #ifndef NO_GVL_INSTRUMENTATION
1732
+ // This function can get called from outside the GVL and even on non-main Ractors
1733
+ void thread_context_collector_on_gvl_waiting(gvl_profiling_thread thread) {
1734
+ // Because this function gets called from a thread that is NOT holding the GVL, we avoid touching the
1735
+ // per-thread context directly.
1736
+ //
1737
+ // Instead, we ask Ruby to hold the data we need in Ruby's own special per-thread context area
1738
+ // that's thread-safe and built for this kind of use
1739
+ //
1740
+ // Also, this function can get called on the non-main Ractor. We deal with this by checking if the value in the context
1741
+ // is non-zero, since only `initialize_context` ever sets the value from 0 to non-zero for threads it sees.
1742
+ intptr_t thread_being_profiled = gvl_profiling_state_get(thread);
1743
+ if (!thread_being_profiled) return;
1744
+
1745
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
1746
+ if (current_monotonic_wall_time_ns <= 0 || current_monotonic_wall_time_ns > GVL_WAITING_ENABLED_EMPTY) return;
1747
+
1748
+ gvl_profiling_state_set(thread, current_monotonic_wall_time_ns);
1749
+ }
1750
+
1751
+ // This function can get called from outside the GVL and even on non-main Ractors
1752
+ __attribute__((warn_unused_result))
1753
+ on_gvl_running_result thread_context_collector_on_gvl_running_with_threshold(gvl_profiling_thread thread, uint32_t waiting_for_gvl_threshold_ns) {
1754
+ intptr_t gvl_waiting_at = gvl_profiling_state_get(thread);
1755
+
1756
+ // Thread was not being profiled / not waiting on gvl
1757
+ if (gvl_waiting_at == 0 || gvl_waiting_at == GVL_WAITING_ENABLED_EMPTY) return ON_GVL_RUNNING_UNKNOWN;
1758
+
1759
+ // @ivoanjo: I'm not sure if this can happen -- It means we should've sampled already but haven't gotten the chance yet?
1760
+ if (gvl_waiting_at < 0) return ON_GVL_RUNNING_SAMPLE;
1761
+
1762
+ long waiting_for_gvl_duration_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - gvl_waiting_at;
1763
+
1764
+ bool should_sample = waiting_for_gvl_duration_ns >= waiting_for_gvl_threshold_ns;
1765
+
1766
+ if (should_sample) {
1767
+ // We flip the gvl_waiting_at to negative to mark that the thread is now running and no longer waiting
1768
+ intptr_t gvl_waiting_at_is_now_running = -gvl_waiting_at;
1769
+
1770
+ gvl_profiling_state_set(thread, gvl_waiting_at_is_now_running);
1771
+ } else {
1772
+ // We decided not to sample. Let's mark the thread back to the initial "enabled but empty" state
1773
+ gvl_profiling_state_set(thread, GVL_WAITING_ENABLED_EMPTY);
1774
+ }
1775
+
1776
+ return should_sample ? ON_GVL_RUNNING_SAMPLE : ON_GVL_RUNNING_DONT_SAMPLE;
1777
+ }
1778
+
1779
+ __attribute__((warn_unused_result))
1780
+ on_gvl_running_result thread_context_collector_on_gvl_running(gvl_profiling_thread thread) {
1781
+ return thread_context_collector_on_gvl_running_with_threshold(thread, global_waiting_for_gvl_threshold_ns);
1782
+ }
1783
+
1784
+ // Why does this method need to exist?
1785
+ //
1786
+ // You may be surprised to see that if we never call this function (from cpu_and_wall_time_worker), Waiting for GVL
1787
+ // samples will still show up.
1788
+ // This is because regular cpu/wall-time samples also use `update_metrics_and_sample` which will do the right thing
1789
+ // and push "Waiting for GVL" samples as needed.
1790
+ //
1791
+ // The reason this method needs to exist and be called very shortly after thread_context_collector_on_gvl_running
1792
+ // returning true is to ensure accuracy of both the timing and stack for the Waiting for GVL sample.
1793
+ //
1794
+ // Timing:
1795
+ // Because we currently only record the timestamp when the Waiting for GVL started and not when the Waiting for GVL ended,
1796
+ // we rely on pushing a sample as soon as possible when the Waiting for GVL ends so that the timestamp of the sample
1797
+ // actually matches when we stopped waiting.
1798
+ //
1799
+ // Stack:
1800
+ // If the thread starts working without the end of the Waiting for GVL sample, then by the time the thread is sampled
1801
+ // via the regular cpu/wall-time samples mechanism, the stack can be be inaccurate (e.g. does not correctly pinpoint
1802
+ // where the waiting happened).
1803
+ //
1804
+ // Arguably, the last sample after Waiting for GVL ended (when gvl_waiting_at < 0) should always come from this method
1805
+ // and not a regular cpu/wall-time sample BUT since all of these things are happening in parallel/concurrently I suspect
1806
+ // it's possible for a regular sample to kick in just before this one.
1807
+ //
1808
+ // ---
1809
+ //
1810
+ // NOTE: In normal use, current_thread is expected to be == rb_thread_current(); the `current_thread` parameter only
1811
+ // exists to enable testing.
1812
+ VALUE thread_context_collector_sample_after_gvl_running(VALUE self_instance, VALUE current_thread, long current_monotonic_wall_time_ns) {
1813
+ struct thread_context_collector_state *state;
1814
+ TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1815
+
1816
+ if (!state->timeline_enabled) rb_raise(rb_eRuntimeError, "GVL profiling requires timeline to be enabled");
1817
+
1818
+ intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(current_thread);
1819
+
1820
+ if (gvl_waiting_at >= 0) {
1821
+ // @ivoanjo: I'm not sure if this can ever happen. This means that we're not on the same thread
1822
+ // that ran `thread_context_collector_on_gvl_running` and made the decision to sample OR a regular sample was
1823
+ // triggered ahead of us.
1824
+ // We do nothing in this case.
1825
+ return Qfalse;
1826
+ }
1827
+
1828
+ struct per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1829
+
1830
+ // We don't actually account for cpu-time during Waiting for GVL. BUT, we may chose to push an
1831
+ // extra sample to represent the period prior to Waiting for GVL. To support that, we retrieve the current
1832
+ // cpu-time of the thread and let `update_metrics_and_sample` decide what to do with it.
1833
+ long cpu_time_for_thread = cpu_time_now_ns(thread_context);
1834
+
1835
+ // TODO: Should we update the dynamic sampling rate overhead tracking with this sample as well?
1836
+
1837
+ update_metrics_and_sample(
1838
+ state,
1839
+ /* thread_being_sampled: */ current_thread,
1840
+ /* stack_from_thread: */ current_thread,
1841
+ thread_context,
1842
+ thread_context->sampling_buffer,
1843
+ cpu_time_for_thread,
1844
+ current_monotonic_wall_time_ns
1845
+ );
1846
+
1847
+ return Qtrue;
1848
+ }
1849
+
1850
+ // This method is intended to be called from update_metrics_and_sample. It exists to handle extra sampling steps we
1851
+ // need to take when sampling cpu/wall-time for a thread that's in the "Waiting for GVL" state.
1852
+ __attribute__((warn_unused_result))
1853
+ static bool handle_gvl_waiting(
1854
+ struct thread_context_collector_state *state,
1855
+ VALUE thread_being_sampled,
1856
+ VALUE stack_from_thread,
1857
+ struct per_thread_context *thread_context,
1858
+ sampling_buffer* sampling_buffer,
1859
+ long current_cpu_time_ns
1860
+ ) {
1861
+ intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(thread_being_sampled);
1862
+
1863
+ bool is_gvl_waiting_state = gvl_waiting_at != 0 && gvl_waiting_at != GVL_WAITING_ENABLED_EMPTY;
1864
+
1865
+ if (!is_gvl_waiting_state) return false;
1866
+
1867
+ // We can be in one of 2 situations here:
1868
+ //
1869
+ // 1. The current sample is the first one after we entered the "Waiting for GVL" state
1870
+ // (wall_time_at_previous_sample_ns < abs(gvl_waiting_at))
1871
+ //
1872
+ // time ─────►
1873
+ // ...──────────────┬───────────────────...
1874
+ // Other state │ Waiting for GVL
1875
+ // ...──────────────┴───────────────────...
1876
+ // ▲ ▲
1877
+ // └─ Previous sample └─ Regular sample (caller)
1878
+ //
1879
+ // In this case, we'll want to push two samples: a) one for the current time (handled by the caller), b) an extra sample
1880
+ // to represent the remaining cpu/wall time before the "Waiting for GVL" started:
1881
+ //
1882
+ // time ─────►
1883
+ // ...──────────────┬───────────────────...
1884
+ // Other state │ Waiting for GVL
1885
+ // ...──────────────┴───────────────────...
1886
+ // ▲ ▲ ▲
1887
+ // └─ Prev... └─ Extra sample └─ Regular sample (caller)
1888
+ //
1889
+ // 2. The current sample is the n-th one after we entered the "Waiting for GVL" state
1890
+ // (wall_time_at_previous_sample_ns > abs(gvl_waiting_at))
1891
+ //
1892
+ // time ─────►
1893
+ // ...──────────────┬───────────────────────────────────────────────...
1894
+ // Other state │ Waiting for GVL
1895
+ // ...──────────────┴───────────────────────────────────────────────...
1896
+ // ▲ ▲ ▲
1897
+ // └─ Previous sample └─ Previous sample └─ Regular sample (caller)
1898
+ //
1899
+ // In this case, we just report back to the caller that the thread is in the "Waiting for GVL" state.
1900
+ //
1901
+ // ---
1902
+ //
1903
+ // Overall, gvl_waiting_at will be > 0 if still in the "Waiting for GVL" state and < 0 if we actually reached the end of
1904
+ // the wait.
1905
+ //
1906
+ // It doesn't really matter if the thread is still waiting or just reached the end of the wait: each sample represents
1907
+ // a snapshot at time ending now, so if the state finished, it just means the next sample will be a regular one.
1908
+
1909
+ if (gvl_waiting_at < 0) {
1910
+ // Negative means the waiting for GVL just ended, so we clear the state, so next samples no longer represent waiting
1911
+ gvl_profiling_state_thread_object_set(thread_being_sampled, GVL_WAITING_ENABLED_EMPTY);
1912
+ }
1913
+
1914
+ long gvl_waiting_started_wall_time_ns = labs(gvl_waiting_at);
1915
+
1916
+ if (thread_context->wall_time_at_previous_sample_ns < gvl_waiting_started_wall_time_ns) { // situation 1 above
1917
+ long cpu_time_elapsed_ns = update_time_since_previous_sample(
1918
+ &thread_context->cpu_time_at_previous_sample_ns,
1919
+ current_cpu_time_ns,
1920
+ thread_context->gc_tracking.cpu_time_at_start_ns,
1921
+ IS_NOT_WALL_TIME
1922
+ );
1923
+
1924
+ long duration_until_start_of_gvl_waiting_ns = update_time_since_previous_sample(
1925
+ &thread_context->wall_time_at_previous_sample_ns,
1926
+ gvl_waiting_started_wall_time_ns,
1927
+ INVALID_TIME,
1928
+ IS_WALL_TIME
1929
+ );
1930
+
1931
+ // Push extra sample
1932
+ trigger_sample_for_thread(
1933
+ state,
1934
+ thread_being_sampled,
1935
+ stack_from_thread,
1936
+ thread_context,
1937
+ sampling_buffer,
1938
+ (sample_values) {.cpu_time_ns = cpu_time_elapsed_ns, .cpu_or_wall_samples = 1, .wall_time_ns = duration_until_start_of_gvl_waiting_ns},
1939
+ gvl_waiting_started_wall_time_ns,
1940
+ NULL,
1941
+ NULL,
1942
+ false // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
1943
+ );
1944
+ }
1945
+
1946
+ return true;
1947
+ }
1948
+
1949
+ static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
1950
+ ENFORCE_THREAD(thread);
1951
+
1952
+ thread_context_collector_on_gvl_waiting(thread_from_thread_object(thread));
1953
+ return Qnil;
1954
+ }
1955
+
1956
+ static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
1957
+ ENFORCE_THREAD(thread);
1958
+
1959
+ intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(thread);
1960
+ return LONG2NUM(gvl_waiting_at);
1961
+ }
1962
+
1963
+ static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread) {
1964
+ ENFORCE_THREAD(thread);
1965
+
1966
+ return thread_context_collector_on_gvl_running(thread_from_thread_object(thread)) == ON_GVL_RUNNING_SAMPLE ? Qtrue : Qfalse;
1967
+ }
1968
+
1969
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread) {
1970
+ ENFORCE_THREAD(thread);
1971
+
1972
+ return thread_context_collector_sample_after_gvl_running(
1973
+ collector_instance,
1974
+ thread,
1975
+ monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
1976
+ );
1977
+ }
1978
+
1979
+ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns) {
1980
+ ENFORCE_THREAD(thread);
1981
+
1982
+ struct thread_context_collector_state *state;
1983
+ TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1984
+
1985
+ struct per_thread_context *thread_context = get_context_for(thread, state);
1986
+ if (thread_context == NULL) rb_raise(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
1987
+
1988
+ thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
1989
+
1990
+ return Qtrue;
1991
+ }
1992
+
1993
+ #else
1994
+ static bool handle_gvl_waiting(
1995
+ DDTRACE_UNUSED struct thread_context_collector_state *state,
1996
+ DDTRACE_UNUSED VALUE thread_being_sampled,
1997
+ DDTRACE_UNUSED VALUE stack_from_thread,
1998
+ DDTRACE_UNUSED struct per_thread_context *thread_context,
1999
+ DDTRACE_UNUSED sampling_buffer* sampling_buffer,
2000
+ DDTRACE_UNUSED long current_cpu_time_ns
2001
+ ) { return false; }
2002
+ #endif // NO_GVL_INSTRUMENTATION