datadog 2.41.0 → 2.43.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  5. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +120 -61
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +156 -108
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  9. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  10. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +351 -255
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +50 -31
  13. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +518 -442
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  16. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +106 -61
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  21. data/ext/datadog_profiling_native_extension/time_helpers.h +2 -1
  22. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  23. data/ext/libdatadog_api/di.c +10 -0
  24. data/ext/libdatadog_api/extconf.rb +3 -0
  25. data/ext/libdatadog_api/init.c +2 -0
  26. data/ext/libdatadog_api/otel_thread_context.c +247 -0
  27. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  28. data/ext/libdatadog_api/trace_exporter.c +642 -22
  29. data/ext/libdatadog_extconf_helpers.rb +1 -1
  30. data/lib/datadog/appsec/assets/blocked.html +1 -108
  31. data/lib/datadog/core/configuration/components.rb +1 -0
  32. data/lib/datadog/core/crashtracking/component.rb +5 -1
  33. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  34. data/lib/datadog/data_streams/processor.rb +31 -0
  35. data/lib/datadog/di/instrumenter.rb +41 -1
  36. data/lib/datadog/di/logger.rb +2 -2
  37. data/lib/datadog/di/probe.rb +9 -1
  38. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  39. data/lib/datadog/di/remote.rb +3 -3
  40. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  41. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  42. data/lib/datadog/open_feature/ext.rb +19 -0
  43. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  44. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  45. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  46. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  47. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  48. data/lib/datadog/open_feature/provider.rb +11 -2
  49. data/lib/datadog/open_feature/transport.rb +5 -0
  50. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  51. data/lib/datadog/opentelemetry/trace.rb +3 -0
  52. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  53. data/lib/datadog/profiling/collectors/info.rb +2 -6
  54. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  55. data/lib/datadog/profiling/component.rb +8 -16
  56. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  57. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  58. data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
  59. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  60. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  61. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  62. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  63. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  64. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  65. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  66. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  67. data/lib/datadog/tracing/otel_thread_context.rb +34 -0
  68. data/lib/datadog/tracing/remote.rb +195 -27
  69. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  70. data/lib/datadog/tracing/trace_digest.rb +22 -4
  71. data/lib/datadog/tracing/trace_operation.rb +22 -10
  72. data/lib/datadog/tracing/tracer.rb +5 -5
  73. data/lib/datadog/tracing/transport/native.rb +17 -25
  74. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  75. data/lib/datadog/tracing/transport/traces.rb +2 -22
  76. data/lib/datadog/version.rb +1 -1
  77. metadata +16 -8
  78. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -2,9 +2,9 @@
2
2
 
3
3
  #include <stdbool.h>
4
4
 
5
- // The private_vm_api_access.c includes the RUBY_MJIT_HEADER which replaces and conflicts with any other Ruby headers;
6
- // so we use PRIVATE_VM_API_ACCESS_SKIP_RUBY_INCLUDES to be able to include private_vm_api_access.h on that file
7
- // without also dragging the incompatible includes
5
+ // The private_vm_api_access.c includes private VM headers (via datadog-ruby_core_source) which replace and conflict
6
+ // with any other Ruby headers; so we use PRIVATE_VM_API_ACCESS_SKIP_RUBY_INCLUDES to be able to include
7
+ // private_vm_api_access.h on that file without also dragging the incompatible includes
8
8
  #ifndef PRIVATE_VM_API_ACCESS_SKIP_RUBY_INCLUDES
9
9
  #include <ruby/thread_native.h>
10
10
  #include <ruby/vm.h>
@@ -86,8 +86,20 @@ VALUE current_fiber_for(VALUE thread);
86
86
 
87
87
  void self_test_current_fiber_for(void);
88
88
 
89
+ // Returns the module name without allocating, or nil if the module is anonymous (i.e., Module#name is nil).
90
+ // Never returns an empty String (returns nil instead if it the name would be empty).
91
+ // Also consider ddtrace_permanent_mod_name().
92
+ //
93
+ // Use this instead of rb_class2name() or rb_mod_name() which can trigger allocations
94
+ VALUE ddtrace_alloc_free_rb_mod_name(VALUE mod);
95
+ // Same as `ddtrace_alloc_free_rb_mod_name` but additionally returns nil (filters out)
96
+ // if the module does not have a permanent name (e.g. `#<Module:0x0123>::Foo`).
97
+ VALUE ddtrace_permanent_mod_name(VALUE mod);
98
+
89
99
  ssize_t ddtrace_location_label(const rb_callable_method_entry_t *cme, const rb_iseq_t *iseq, char *buf, size_t buf_size);
90
100
  VALUE ddtrace_location_base_label(const rb_callable_method_entry_t *cme, const rb_iseq_t *iseq);
91
101
  void* ddtrace_cme_cfunc_func(const rb_callable_method_entry_t *cme);
92
102
  const char *ddtrace_cme_original_method_name(const rb_callable_method_entry_t *cme);
93
103
 
104
+ // Returns true for internal objects (like T_IMEMO/T_ICLASS/etc) and "hidden" objects (rb_class_of(obj) == 0)
105
+ bool ddtrace_is_internal_object_p(VALUE obj);
@@ -21,6 +21,7 @@ void collectors_dynamic_sampling_rate_init(VALUE profiling_module);
21
21
  void collectors_idle_sampling_helper_init(VALUE profiling_module);
22
22
  void collectors_stack_init(VALUE profiling_module);
23
23
  void collectors_thread_context_init(VALUE profiling_module);
24
+ void collectors_heap_recorder_init(void);
24
25
  void encoded_profile_init(VALUE profiling_module);
25
26
  void http_transport_init(VALUE profiling_module);
26
27
  void stack_recorder_init(VALUE profiling_module);
@@ -73,6 +74,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
73
74
  collectors_idle_sampling_helper_init(profiling_module);
74
75
  collectors_stack_init(profiling_module);
75
76
  collectors_thread_context_init(profiling_module);
77
+ collectors_heap_recorder_init();
76
78
  encoded_profile_init(profiling_module);
77
79
  http_transport_init(profiling_module);
78
80
  stack_recorder_init(profiling_module);
@@ -7,16 +7,10 @@
7
7
 
8
8
  // The following global variables are initialized at startup to save expensive lookups later.
9
9
  // They are not expected to be mutated outside of init.
10
- static VALUE module_object_space = Qnil;
11
- static ID _id2ref_id = Qnil;
12
10
  static ID inspect_id = Qnil;
13
11
  static ID to_s_id = Qnil;
14
12
 
15
13
  void ruby_helpers_init(void) {
16
- rb_global_variable(&module_object_space);
17
-
18
- module_object_space = rb_const_get(rb_cObject, rb_intern("ObjectSpace"));
19
- _id2ref_id = rb_intern("_id2ref");
20
14
  inspect_id = rb_intern("inspect");
21
15
  to_s_id = rb_intern("to_s");
22
16
  }
@@ -113,40 +107,6 @@ void private_raise_enforce_syserr(
113
107
  }
114
108
  }
115
109
 
116
- static VALUE _id2ref(VALUE obj_id) {
117
- // Call ::ObjectSpace._id2ref natively. It will raise if the id is no longer valid
118
- return rb_funcall(module_object_space, _id2ref_id, 1, obj_id);
119
- }
120
-
121
- static VALUE _id2ref_failure(DDTRACE_UNUSED VALUE _unused1, DDTRACE_UNUSED VALUE _unused2) {
122
- return Qfalse;
123
- }
124
-
125
- // See notes on header for important details
126
- bool ruby_ref_from_id(VALUE obj_id, VALUE *value) {
127
- // Call ::ObjectSpace._id2ref natively. It will raise if the id is no longer valid
128
- // so we need to call it via rb_rescue2
129
- // TODO: Benchmark rb_rescue2 vs rb_protect here
130
- VALUE result = rb_rescue2(
131
- _id2ref,
132
- obj_id,
133
- _id2ref_failure,
134
- Qnil,
135
- rb_eRangeError, // rb_eRangeError is the error used to flag invalid ids
136
- 0 // Required by API to be the last argument
137
- );
138
-
139
- if (result == Qfalse) {
140
- return false;
141
- }
142
-
143
- if (value != NULL) {
144
- (*value) = result;
145
- }
146
-
147
- return true;
148
- }
149
-
150
110
  // Not part of public headers but is externed from Ruby
151
111
  size_t rb_obj_memsize_of(VALUE obj);
152
112
 
@@ -162,16 +122,9 @@ size_t rb_obj_memsize_of(VALUE obj);
162
122
  size_t ruby_obj_memsize_of(VALUE obj) {
163
123
  switch (rb_type(obj)) {
164
124
  case T_OBJECT:
165
- // On Ruby 4.0, computing the size of a class/module/iclass seems to not be safe: `rb_obj_memsize_of` walks the
166
- // per-namespace class extensions (`rb_class_classext_foreach` -> `classext_memsize` -> `rb_id_table_memsize`)
167
- // and can crash the VM when called on objects tracked by heap profiling.
168
- // See https://github.com/DataDog/dd-trace-rb/issues/5936. Class objects contribute negligibly to heap size,
169
- // so we skip them (fall through to the `default` branch, returning 0) rather than risk a SIGSEGV.
170
- #ifndef NO_SAFE_CLASS_MEMSIZE
171
125
  case T_MODULE:
172
126
  case T_CLASS:
173
127
  case T_ICLASS:
174
- #endif
175
128
  case T_STRING:
176
129
  case T_ARRAY:
177
130
  case T_HASH:
@@ -198,36 +151,6 @@ size_t ruby_obj_memsize_of(VALUE obj) {
198
151
  }
199
152
  }
200
153
 
201
- // Inspired by rb_class_of but without actually returning classes or potentially doing assertions
202
- static bool ruby_is_obj_with_class(VALUE obj) {
203
- if (!RB_SPECIAL_CONST_P(obj)) {
204
- return true;
205
- }
206
- if (obj == RUBY_Qfalse) {
207
- return true;
208
- }
209
- else if (obj == RUBY_Qnil) {
210
- return true;
211
- }
212
- else if (obj == RUBY_Qtrue) {
213
- return true;
214
- }
215
- else if (RB_FIXNUM_P(obj)) {
216
- return true;
217
- }
218
- else if (RB_STATIC_SYM_P(obj)) {
219
- return true;
220
- }
221
- else if (RB_FLONUM_P(obj)) {
222
- return true;
223
- }
224
-
225
- return false;
226
- }
227
-
228
- // This function is not present in the VM headers, but is a public symbol that can be invoked.
229
- int rb_objspace_internal_object_p(VALUE obj);
230
-
231
154
  #ifdef NO_RB_OBJ_INFO
232
155
  const char* safe_object_info(DDTRACE_UNUSED VALUE obj) { return "(No rb_obj_info for current Ruby)"; }
233
156
  #else
@@ -239,8 +162,7 @@ int rb_objspace_internal_object_p(VALUE obj);
239
162
  #endif
240
163
 
241
164
  VALUE ruby_safe_inspect(VALUE obj) {
242
- if (!ruby_is_obj_with_class(obj)) return rb_str_new_cstr("(Not an object)");
243
- if (rb_objspace_internal_object_p(obj)) return rb_sprintf("(VM Internal, %s)", safe_object_info(obj));
165
+ if (ddtrace_is_internal_object_p(obj)) return rb_sprintf("(VM Internal, %s)", safe_object_info(obj));
244
166
  // @ivoanjo: I saw crashes on Ruby 3.1.4 when trying to #inspect matchdata objects. I'm not entirely sure why this
245
167
  // is needed, but since we only use this method for debug purposes I put in this alternative and decided not to
246
168
  // dig deeper.
@@ -79,13 +79,6 @@ NORETURN(void private_raise_enforce_syserr(
79
79
  const char *function_name
80
80
  ));
81
81
 
82
- // Native wrapper to get an object ref from an id. Returns true on success and
83
- // writes the ref to the value pointer parameter if !NULL. False if id doesn't
84
- // reference a valid object (in which case value is not changed).
85
- //
86
- // Note: GVL can be released and other threads may get to run before this method returns
87
- bool ruby_ref_from_id(VALUE obj_id, VALUE *value);
88
-
89
82
  // Native wrapper to get the approximate/estimated current size of the passed
90
83
  // object.
91
84
  size_t ruby_obj_memsize_of(VALUE obj);
@@ -231,6 +231,18 @@ typedef struct {
231
231
  bool serialize_ran;
232
232
  } call_serialize_without_gvl_arguments;
233
233
 
234
+ typedef struct {
235
+ // Set by caller
236
+ stack_recorder_state *state;
237
+ locked_profile_slot active_slot;
238
+ ddog_prof_Slice_Location locations;
239
+ sample_values values;
240
+ sample_labels labels;
241
+
242
+ // Set by callee
243
+ ddog_prof_Profile_Result result;
244
+ } record_sample_arguments;
245
+
234
246
  static VALUE _native_new(VALUE klass);
235
247
  static void initialize_slot_concurrency_control(stack_recorder_state *state);
236
248
  static void stack_recorder_typed_data_mark(void *data);
@@ -240,6 +252,8 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
240
252
  static VALUE _native_serialize(VALUE self, VALUE recorder_instance);
241
253
  static VALUE ruby_time_from(ddog_Timespec ddprof_time);
242
254
  static void *call_serialize_without_gvl(void *call_args);
255
+ static VALUE record_sample_locked(VALUE record_sample_arguments_as_value);
256
+ static VALUE record_sample_unlock_ensure(VALUE record_sample_arguments_as_value);
243
257
  static locked_profile_slot sampler_lock_active_profile(stack_recorder_state *state);
244
258
  static void sampler_unlock_active_profile(locked_profile_slot active_slot);
245
259
  static profile_slot* serializer_flip_active_and_inactive_slots(stack_recorder_state *state);
@@ -252,18 +266,20 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE recorder_
252
266
  static void serializer_set_start_timestamp_for_next_profile(stack_recorder_state *state, ddog_Timespec start_time);
253
267
  static VALUE _native_record_endpoint(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE local_root_span_id, VALUE endpoint);
254
268
  static void reset_profile_slot(profile_slot *slot, ddog_Timespec start_timestamp);
255
- static VALUE _native_track_object(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE new_obj, VALUE weight, VALUE alloc_class);
256
269
  static VALUE _native_start_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
257
270
  static VALUE _native_end_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
258
271
  static VALUE _native_debug_heap_recorder(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
259
272
  static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance);
260
273
  static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns, long heap_iteration_prep_time_ns, long heap_profile_build_time_ns);
261
- static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE object_id);
274
+ static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE record_id);
275
+ static VALUE _native_record_id_for(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE obj);
262
276
  static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
263
- static VALUE _native_recorder_after_gc_step(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
277
+ static VALUE _native_heap_recorder_exhaust_record_ids(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
278
+ static VALUE _native_heap_recorder_set_object_alloc_gen(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE record_id, VALUE alloc_gen);
279
+ static VALUE _native_recorder_heap_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
264
280
  static VALUE _native_benchmark_intern(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE string, VALUE times, VALUE use_all);
265
281
  static VALUE _native_test_managed_string_storage_produces_valid_profiles(DDTRACE_UNUSED VALUE _self);
266
- static VALUE _native_finalize_pending_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
282
+ static VALUE _native_commit_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
267
283
 
268
284
  void stack_recorder_init(VALUE profiling_module) {
269
285
  VALUE stack_recorder_class = rb_define_class_under(profiling_module, "StackRecorder", rb_cObject);
@@ -288,7 +304,6 @@ void stack_recorder_init(VALUE profiling_module) {
288
304
  rb_define_singleton_method(testing_module, "_native_slot_one_mutex_locked?", _native_is_slot_one_mutex_locked, 1);
289
305
  rb_define_singleton_method(testing_module, "_native_slot_two_mutex_locked?", _native_is_slot_two_mutex_locked, 1);
290
306
  rb_define_singleton_method(testing_module, "_native_record_endpoint", _native_record_endpoint, 3);
291
- rb_define_singleton_method(testing_module, "_native_track_object", _native_track_object, 4);
292
307
  rb_define_singleton_method(testing_module, "_native_start_fake_slow_heap_serialization",
293
308
  _native_start_fake_slow_heap_serialization, 1);
294
309
  rb_define_singleton_method(testing_module, "_native_end_fake_slow_heap_serialization",
@@ -296,11 +311,14 @@ void stack_recorder_init(VALUE profiling_module) {
296
311
  rb_define_singleton_method(testing_module, "_native_debug_heap_recorder",
297
312
  _native_debug_heap_recorder, 1);
298
313
  rb_define_singleton_method(testing_module, "_native_is_object_recorded?", _native_is_object_recorded, 2);
314
+ rb_define_singleton_method(testing_module, "_native_record_id_for", _native_record_id_for, 2);
299
315
  rb_define_singleton_method(testing_module, "_native_heap_recorder_reset_last_update", _native_heap_recorder_reset_last_update, 1);
300
- rb_define_singleton_method(testing_module, "_native_recorder_after_gc_step", _native_recorder_after_gc_step, 1);
316
+ rb_define_singleton_method(testing_module, "_native_heap_recorder_exhaust_record_ids", _native_heap_recorder_exhaust_record_ids, 1);
317
+ rb_define_singleton_method(testing_module, "_native_heap_recorder_set_object_alloc_gen", _native_heap_recorder_set_object_alloc_gen, 3);
318
+ rb_define_singleton_method(testing_module, "_native_recorder_heap_update", _native_recorder_heap_update, 1);
301
319
  rb_define_singleton_method(testing_module, "_native_benchmark_intern", _native_benchmark_intern, 4);
302
320
  rb_define_singleton_method(testing_module, "_native_test_managed_string_storage_produces_valid_profiles", _native_test_managed_string_storage_produces_valid_profiles, 0);
303
- rb_define_singleton_method(testing_module, "_native_finalize_pending_heap_recordings", _native_finalize_pending_heap_recordings, 1);
321
+ rb_define_singleton_method(testing_module, "_native_commit_heap_recordings", _native_commit_heap_recordings, 1);
304
322
 
305
323
  ok_symbol = ID2SYM(rb_intern_const("ok"));
306
324
  error_symbol = ID2SYM(rb_intern_const("error"));
@@ -398,7 +416,7 @@ static void stack_recorder_typed_data_mark(void *state_ptr) {
398
416
  stack_recorder_state *state = (stack_recorder_state *) state_ptr;
399
417
 
400
418
  rb_gc_mark(state->thread_context_collector_instance);
401
- heap_recorder_mark_pending_recordings(state->heap_recorder);
419
+ heap_recorder_mark(state->heap_recorder);
402
420
  }
403
421
 
404
422
  static void stack_recorder_typed_data_free(void *state_ptr) {
@@ -605,7 +623,28 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
605
623
  stack_recorder_state *state;
606
624
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
607
625
 
608
- locked_profile_slot active_slot = sampler_lock_active_profile(state);
626
+ record_sample_arguments args = {
627
+ .state = state,
628
+ .active_slot = sampler_lock_active_profile(state),
629
+ .locations = locations,
630
+ .values = values,
631
+ .labels = labels,
632
+ };
633
+
634
+ // NOTE: The heap recorder is allowed to raise exceptions if something's wrong. We use `rb_ensure` to make sure we
635
+ // properly unlock the active slot mutex on our way out. Otherwise, this would later lead to deadlocks (since the
636
+ // active slot mutex is not expected to be locked forever).
637
+ rb_ensure(record_sample_locked, (VALUE) &args, record_sample_unlock_ensure, (VALUE) &args);
638
+
639
+ if (args.result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
640
+ raise_error(rb_eArgError, "Failed to record sample: %"PRIsVALUE, get_error_details_and_drop(&args.result.err));
641
+ }
642
+ }
643
+
644
+ static VALUE record_sample_locked(VALUE record_sample_arguments_as_value) {
645
+ record_sample_arguments *args = (record_sample_arguments *) record_sample_arguments_as_value;
646
+ stack_recorder_state *state = args->state;
647
+ sample_values values = args->values;
609
648
 
610
649
  // Note: We initialize this array to have ALL_VALUE_TYPES_COUNT but only tell libdatadog to use the first
611
650
  // state->enabled_values_count values. This simplifies handling disabled value types -- we still put them on the
@@ -621,50 +660,39 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
621
660
  metric_values[position_for[ALLOC_SAMPLES_UNSCALED_VALUE_ID]] = values.alloc_samples_unscaled;
622
661
  metric_values[position_for[TIMELINE_VALUE_ID]] = values.timeline_wall_time_ns;
623
662
 
624
- if (values.heap_sample) {
625
- // If we got an allocation sample end the heap allocation recording to commit the heap sample.
626
- // FIXME: Heap sampling currently has to be done in 2 parts because the construction of locations is happening
627
- // very late in the allocation-sampling path (which is shared with the cpu sampling path). This can
628
- // be fixed with some refactoring but for now this leads to a less impactful change.
629
- //
630
- // NOTE: The heap recorder is allowed to raise exceptions if something's wrong. But we also need to handle it
631
- // on this side to make sure we properly unlock the active slot mutex on our way out. Otherwise, this would
632
- // later lead to deadlocks (since the active slot mutex is not expected to be locked forever).
633
- int exception_state = end_heap_allocation_recording_with_rb_protect(state->heap_recorder, locations);
634
- if (exception_state) {
635
- sampler_unlock_active_profile(active_slot);
636
- rb_jump_tag(exception_state);
637
- }
663
+ if (values.heap_sample != NULL) {
664
+ heap_recorder_record_allocation(
665
+ state->heap_recorder,
666
+ values.heap_sample->new_object,
667
+ values.alloc_samples,
668
+ values.heap_sample->alloc_class,
669
+ args->locations,
670
+ &values.heap_sample->out_needs_commit
671
+ );
638
672
  }
639
673
 
640
- ddog_prof_Profile_Result result = ddog_prof_Profile_add(
641
- &active_slot.data->profile,
674
+ args->result = ddog_prof_Profile_add(
675
+ &args->active_slot.data->profile,
642
676
  (ddog_prof_Sample) {
643
- .locations = locations,
677
+ .locations = args->locations,
644
678
  .values = (ddog_Slice_I64) {.ptr = metric_values, .len = state->enabled_values_count},
645
- .labels = labels.labels
679
+ .labels = args->labels.labels
646
680
  },
647
681
  /* To disable end_timestamp_ns to get aggregated profiles in pprof, replace the below with `0` */
648
- labels.end_timestamp_ns
682
+ args->labels.end_timestamp_ns
649
683
  );
650
684
 
651
- active_slot.data->stats.recorded_samples++;
652
-
653
- sampler_unlock_active_profile(active_slot);
685
+ args->active_slot.data->stats.recorded_samples++;
654
686
 
655
- if (result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
656
- raise_error(rb_eArgError, "Failed to record sample: %"PRIsVALUE, get_error_details_and_drop(&result.err));
657
- }
687
+ return Qnil; // Unused
658
688
  }
659
689
 
660
- // Returns needs_after_allocation: true whenever an after_sample callback is required
661
- bool track_object(VALUE recorder_instance, VALUE new_object, unsigned int sample_weight, ddog_CharSlice alloc_class) {
662
- stack_recorder_state *state;
663
- TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
664
- // FIXME: Heap sampling currently has to be done in 2 parts because the construction of locations is happening
665
- // very late in the allocation-sampling path (which is shared with the cpu sampling path). This can
666
- // be fixed with some refactoring but for now this leads to a less impactful change.
667
- return start_heap_allocation_recording(state->heap_recorder, new_object, sample_weight, alloc_class);
690
+ static VALUE record_sample_unlock_ensure(VALUE record_sample_arguments_as_value) {
691
+ record_sample_arguments *args = (record_sample_arguments *) record_sample_arguments_as_value;
692
+
693
+ sampler_unlock_active_profile(args->active_slot);
694
+
695
+ return Qnil; // Unused
668
696
  }
669
697
 
670
698
  void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_CharSlice endpoint) {
@@ -682,18 +710,18 @@ void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_
682
710
  }
683
711
  }
684
712
 
685
- void recorder_after_gc_step(VALUE recorder_instance) {
713
+ void recorder_heap_update_may_lose_gvl(VALUE recorder_instance) {
686
714
  stack_recorder_state *state;
687
715
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
688
716
 
689
717
  if (state->heap_clean_after_gc_enabled) heap_recorder_update_young_objects(state->heap_recorder);
690
718
  }
691
719
 
692
- void recorder_after_sample(VALUE recorder_instance) {
720
+ void recorder_commit_heap_recordings_may_lose_gvl(VALUE recorder_instance) {
693
721
  stack_recorder_state *state;
694
722
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
695
723
 
696
- heap_recorder_finalize_pending_recordings(state->heap_recorder);
724
+ heap_recorder_commit_recordings_may_lose_gvl(state->heap_recorder);
697
725
  }
698
726
 
699
727
  #define MAX_LEN_HEAP_ITERATION_ERROR_MSG 256
@@ -938,15 +966,6 @@ static VALUE _native_record_endpoint(DDTRACE_UNUSED VALUE _self, VALUE recorder_
938
966
  return Qtrue;
939
967
  }
940
968
 
941
- static VALUE _native_track_object(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE new_obj, VALUE weight, VALUE alloc_class) {
942
- ENFORCE_TYPE(weight, T_FIXNUM);
943
- bool needs_after_allocation = track_object(recorder_instance, new_obj, NUM2UINT(weight), char_slice_from_ruby_string(alloc_class));
944
-
945
- // We could instead choose to automatically trigger the after allocation here; yet, it seems kinda nice to keep it manual for
946
- // the tests so we can pull on each lever separately and observe "the sausage being made" in steps
947
- return needs_after_allocation ? Qtrue : Qfalse;
948
- }
949
-
950
969
  static void reset_profile_slot(profile_slot *slot, ddog_Timespec start_timestamp) {
951
970
  ddog_prof_Profile_Result reset_result = ddog_prof_Profile_reset(&slot->profile);
952
971
  if (reset_result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
@@ -1024,13 +1043,39 @@ static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns,
1024
1043
  return stats_as_hash;
1025
1044
  }
1026
1045
 
1027
- static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE obj_id) {
1028
- ENFORCE_TYPE(obj_id, T_FIXNUM);
1046
+ static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE record_id) {
1047
+ ENFORCE_TYPE(record_id, T_FIXNUM);
1048
+
1049
+ stack_recorder_state *state;
1050
+ TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1051
+
1052
+ return heap_recorder_testonly_is_object_recorded(state->heap_recorder, FIX2LONG(record_id));
1053
+ }
1029
1054
 
1055
+ static VALUE _native_record_id_for(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE obj) {
1030
1056
  stack_recorder_state *state;
1031
1057
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1032
1058
 
1033
- return heap_recorder_testonly_is_object_recorded(state->heap_recorder, obj_id);
1059
+ return heap_recorder_testonly_record_id_for(state->heap_recorder, obj);
1060
+ }
1061
+
1062
+ static VALUE _native_heap_recorder_exhaust_record_ids(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1063
+ stack_recorder_state *state;
1064
+ TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1065
+
1066
+ heap_recorder_testonly_exhaust_record_ids(state->heap_recorder);
1067
+ return Qtrue;
1068
+ }
1069
+
1070
+ static VALUE _native_heap_recorder_set_object_alloc_gen(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE record_id, VALUE alloc_gen) {
1071
+ ENFORCE_TYPE(record_id, T_FIXNUM);
1072
+ ENFORCE_TYPE(alloc_gen, T_FIXNUM);
1073
+
1074
+ stack_recorder_state *state;
1075
+ TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1076
+
1077
+ heap_recorder_testonly_set_object_alloc_gen(state->heap_recorder, FIX2LONG(record_id), NUM2SIZET(alloc_gen));
1078
+ return Qtrue;
1034
1079
  }
1035
1080
 
1036
1081
  static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
@@ -1042,8 +1087,8 @@ static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self,
1042
1087
  return Qtrue;
1043
1088
  }
1044
1089
 
1045
- static VALUE _native_recorder_after_gc_step(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1046
- recorder_after_gc_step(recorder_instance);
1090
+ static VALUE _native_recorder_heap_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1091
+ recorder_heap_update_may_lose_gvl(recorder_instance);
1047
1092
  return Qtrue;
1048
1093
  }
1049
1094
 
@@ -1153,11 +1198,11 @@ static VALUE _native_test_managed_string_storage_produces_valid_profiles(DDTRACE
1153
1198
  return rb_ary_new_from_args(2, encoded_pprof_1, encoded_pprof_2);
1154
1199
  }
1155
1200
 
1156
- static VALUE _native_finalize_pending_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1201
+ static VALUE _native_commit_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1157
1202
  stack_recorder_state *state;
1158
1203
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1159
1204
 
1160
- heap_recorder_finalize_pending_recordings(state->heap_recorder);
1205
+ heap_recorder_commit_recordings_may_lose_gvl(state->heap_recorder);
1161
1206
 
1162
1207
  return Qtrue;
1163
1208
  }
@@ -3,14 +3,23 @@
3
3
  #include <datadog/profiling.h>
4
4
  #include <ruby.h>
5
5
 
6
+ typedef struct {
7
+ // In: The object being allocated, and its class. (Its weight is the `alloc_samples` in `sample_values` below.)
8
+ VALUE new_object;
9
+ ddog_CharSlice alloc_class;
10
+ // Out: Set by `record_sample` to whether a `recorder_commit_heap_recordings_may_lose_gvl` callback is needed
11
+ bool out_needs_commit;
12
+ } heap_sample_values;
13
+
6
14
  typedef struct {
7
15
  int64_t cpu_time_ns;
8
16
  int64_t wall_time_ns;
9
17
  uint32_t cpu_or_wall_samples;
10
18
  uint32_t alloc_samples;
11
19
  uint32_t alloc_samples_unscaled;
12
- bool heap_sample;
13
20
  int64_t timeline_wall_time_ns;
21
+ // When not NULL, this sample also gets recorded for heap profiling
22
+ heap_sample_values *heap_sample;
14
23
  } sample_values;
15
24
 
16
25
  typedef struct {
@@ -26,8 +35,7 @@ typedef struct {
26
35
 
27
36
  void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations, sample_values values, sample_labels labels);
28
37
  void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_CharSlice endpoint);
29
- __attribute__((warn_unused_result)) bool track_object(VALUE recorder_instance, VALUE new_object, unsigned int sample_weight, ddog_CharSlice alloc_class);
30
- void recorder_after_sample(VALUE recorder_instance);
31
- void recorder_after_gc_step(VALUE recorder_instance);
38
+ void recorder_commit_heap_recordings_may_lose_gvl(VALUE recorder_instance);
39
+ void recorder_heap_update_may_lose_gvl(VALUE recorder_instance);
32
40
  void recorder_install_on_serialize(VALUE recorder_instance, VALUE thread_context_collector_instance);
33
41
  VALUE enforce_recorder_instance(VALUE object);
@@ -16,11 +16,12 @@ typedef enum { RAISE_ON_FAILURE, DO_NOT_RAISE_ON_FAILURE } raise_on_failure_sett
16
16
  #define INVALID_TIME -1
17
17
 
18
18
  typedef struct {
19
+ // INVALID_TIME until the first conversion, and the only field used to decide if the state needs to be (re)computed
19
20
  long system_epoch_ns_reference;
20
21
  long delta_to_epoch_ns;
21
22
  } monotonic_to_system_epoch_state;
22
23
 
23
- #define MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER {.system_epoch_ns_reference = INVALID_TIME, .delta_to_epoch_ns = INVALID_TIME}
24
+ #define MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER {.system_epoch_ns_reference = INVALID_TIME}
24
25
 
25
26
  static inline long retrieve_clock_as_ns(clockid_t clock_id, raise_on_failure_setting raise_on_failure) {
26
27
  struct timespec clock_value;
@@ -97,13 +97,3 @@ static inline VALUE get_error_details_and_drop(ddog_Error *error) {
97
97
  // Returns the amount of characters written to string (which are necessarily
98
98
  // bounded by capacity - 1 since the string will be null-terminated).
99
99
  size_t read_ddogerr_string_and_drop(ddog_Error *error, char *string, size_t capacity);
100
-
101
- #define IMEMO_MASK 0x0f
102
-
103
- // Returns the imemo type of an imemo object.
104
- // This mask is the same between Ruby 2.5 and 3.3-preview3. Furthermore, the intention of this method is to be used
105
- // to call `rb_imemo_name` which correctly handles invalid numbers so even if the mask changes in the future, at most
106
- // we'll get incorrect results (and never a VM crash)
107
- static inline int ddtrace_imemo_type(VALUE imemo) {
108
- return (RBASIC(imemo)->flags >> FL_USHIFT) & IMEMO_MASK;
109
- }
@@ -14,6 +14,10 @@ void rb_objspace_each_objects(
14
14
  int (*callback)(void *start, void *end, size_t stride, void *data),
15
15
  void *data);
16
16
 
17
+ // The mask is unchanged between Ruby 2.5 and 4.1-dev, but the `enum imemo_type` values do get
18
+ // renumbered from time to time (e.g. Ruby 4.0 moved imemo_callinfo from 11 to 10), and 4.1-dev already uses all
19
+ // 16 values the mask allows. Thus IMEMO_TYPE_ISEQ must be re-checked when adding support for a new Ruby.
20
+ #define IMEMO_MASK 0x0f
17
21
  #define IMEMO_TYPE_ISEQ 7
18
22
 
19
23
  // The ID value of the string "mesg" which is used in Ruby source as
@@ -27,6 +31,12 @@ static ID id_mesg;
27
31
  // method probes on Thread#[] / Thread#[]= cannot intercept guard reads/writes.
28
32
  static ID id_datadog_di_in_probe;
29
33
 
34
+ // Returns the imemo type of an imemo object, that is `imemo_type()` from CRuby's internal/imemo.h.
35
+ // The caller must have already checked the object is a T_IMEMO, otherwise the result is meaningless.
36
+ static inline int ddtrace_imemo_type(VALUE imemo) {
37
+ return (RBASIC(imemo)->flags >> FL_USHIFT) & IMEMO_MASK;
38
+ }
39
+
30
40
  // Returns whether the argument is an IMEMO of type ISEQ.
31
41
  static bool ddtrace_imemo_iseq_p(VALUE v) {
32
42
  return rb_objspace_internal_object_p(v) && RB_TYPE_P(v, T_IMEMO) && ddtrace_imemo_type(v) == IMEMO_TYPE_ISEQ;
@@ -98,6 +98,9 @@ EXTENSION_NAME = "libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}".fr
98
98
 
99
99
  have_func("rb_iseq_type")
100
100
 
101
+ $defs << "-DHAVE_RUBY_THREAD_STORAGE_API" if RUBY_VERSION >= "3.3"
102
+ $defs << "-DHAVE_RUBY_THREAD_HAS_GVL_P" if have_func("ruby_thread_has_gvl_p")
103
+
101
104
  create_makefile(EXTENSION_NAME)
102
105
 
103
106
  # rubocop:enable Style/GlobalVars
@@ -5,6 +5,7 @@
5
5
  #include "crashtracker.h"
6
6
  #include "feature_flags.h"
7
7
  #include "library_config.h"
8
+ #include "otel_thread_context.h"
8
9
  #include "process_discovery.h"
9
10
  #include "trace_exporter.h"
10
11
 
@@ -28,4 +29,5 @@ void DDTRACE_EXPORT Init_libdatadog_api(void) {
28
29
 
29
30
  VALUE tracing_module = rb_define_module_under(datadog_module, "Tracing");
30
31
  trace_exporter_init(tracing_module);
32
+ otel_thread_context_init(tracing_module);
31
33
  }