datadog 2.41.0 → 2.42.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  9. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  10. data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
  11. data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
  12. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  20. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  21. data/ext/libdatadog_api/di.c +10 -0
  22. data/ext/libdatadog_api/extconf.rb +3 -0
  23. data/ext/libdatadog_api/init.c +2 -0
  24. data/ext/libdatadog_api/otel_thread_context.c +232 -0
  25. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  26. data/ext/libdatadog_extconf_helpers.rb +1 -1
  27. data/lib/datadog/appsec/assets/blocked.html +1 -108
  28. data/lib/datadog/core/configuration/components.rb +1 -0
  29. data/lib/datadog/core/crashtracking/component.rb +5 -1
  30. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  31. data/lib/datadog/data_streams/processor.rb +31 -0
  32. data/lib/datadog/di/instrumenter.rb +41 -1
  33. data/lib/datadog/di/logger.rb +2 -2
  34. data/lib/datadog/di/probe.rb +9 -1
  35. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  36. data/lib/datadog/di/remote.rb +3 -3
  37. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  38. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  39. data/lib/datadog/open_feature/ext.rb +19 -0
  40. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  41. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  42. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  43. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  44. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  45. data/lib/datadog/open_feature/provider.rb +11 -2
  46. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  47. data/lib/datadog/opentelemetry/trace.rb +3 -0
  48. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  49. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  50. data/lib/datadog/profiling/component.rb +8 -16
  51. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  52. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  53. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  54. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  55. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  56. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  57. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  58. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  59. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  60. data/lib/datadog/tracing/otel_thread_context.rb +30 -0
  61. data/lib/datadog/tracing/remote.rb +195 -27
  62. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  63. data/lib/datadog/tracing/trace_digest.rb +22 -4
  64. data/lib/datadog/tracing/trace_operation.rb +22 -10
  65. data/lib/datadog/tracing/tracer.rb +5 -5
  66. data/lib/datadog/version.rb +1 -1
  67. metadata +14 -8
  68. 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,19 @@ 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_recorder_heap_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
264
279
  static VALUE _native_benchmark_intern(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE string, VALUE times, VALUE use_all);
265
280
  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);
281
+ static VALUE _native_commit_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
267
282
 
268
283
  void stack_recorder_init(VALUE profiling_module) {
269
284
  VALUE stack_recorder_class = rb_define_class_under(profiling_module, "StackRecorder", rb_cObject);
@@ -288,7 +303,6 @@ void stack_recorder_init(VALUE profiling_module) {
288
303
  rb_define_singleton_method(testing_module, "_native_slot_one_mutex_locked?", _native_is_slot_one_mutex_locked, 1);
289
304
  rb_define_singleton_method(testing_module, "_native_slot_two_mutex_locked?", _native_is_slot_two_mutex_locked, 1);
290
305
  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
306
  rb_define_singleton_method(testing_module, "_native_start_fake_slow_heap_serialization",
293
307
  _native_start_fake_slow_heap_serialization, 1);
294
308
  rb_define_singleton_method(testing_module, "_native_end_fake_slow_heap_serialization",
@@ -296,11 +310,13 @@ void stack_recorder_init(VALUE profiling_module) {
296
310
  rb_define_singleton_method(testing_module, "_native_debug_heap_recorder",
297
311
  _native_debug_heap_recorder, 1);
298
312
  rb_define_singleton_method(testing_module, "_native_is_object_recorded?", _native_is_object_recorded, 2);
313
+ rb_define_singleton_method(testing_module, "_native_record_id_for", _native_record_id_for, 2);
299
314
  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);
315
+ rb_define_singleton_method(testing_module, "_native_heap_recorder_exhaust_record_ids", _native_heap_recorder_exhaust_record_ids, 1);
316
+ rb_define_singleton_method(testing_module, "_native_recorder_heap_update", _native_recorder_heap_update, 1);
301
317
  rb_define_singleton_method(testing_module, "_native_benchmark_intern", _native_benchmark_intern, 4);
302
318
  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);
319
+ rb_define_singleton_method(testing_module, "_native_commit_heap_recordings", _native_commit_heap_recordings, 1);
304
320
 
305
321
  ok_symbol = ID2SYM(rb_intern_const("ok"));
306
322
  error_symbol = ID2SYM(rb_intern_const("error"));
@@ -398,7 +414,7 @@ static void stack_recorder_typed_data_mark(void *state_ptr) {
398
414
  stack_recorder_state *state = (stack_recorder_state *) state_ptr;
399
415
 
400
416
  rb_gc_mark(state->thread_context_collector_instance);
401
- heap_recorder_mark_pending_recordings(state->heap_recorder);
417
+ heap_recorder_mark(state->heap_recorder);
402
418
  }
403
419
 
404
420
  static void stack_recorder_typed_data_free(void *state_ptr) {
@@ -605,7 +621,28 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
605
621
  stack_recorder_state *state;
606
622
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
607
623
 
608
- locked_profile_slot active_slot = sampler_lock_active_profile(state);
624
+ record_sample_arguments args = {
625
+ .state = state,
626
+ .active_slot = sampler_lock_active_profile(state),
627
+ .locations = locations,
628
+ .values = values,
629
+ .labels = labels,
630
+ };
631
+
632
+ // NOTE: The heap recorder is allowed to raise exceptions if something's wrong. We use `rb_ensure` to make sure we
633
+ // properly unlock the active slot mutex on our way out. Otherwise, this would later lead to deadlocks (since the
634
+ // active slot mutex is not expected to be locked forever).
635
+ rb_ensure(record_sample_locked, (VALUE) &args, record_sample_unlock_ensure, (VALUE) &args);
636
+
637
+ if (args.result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
638
+ raise_error(rb_eArgError, "Failed to record sample: %"PRIsVALUE, get_error_details_and_drop(&args.result.err));
639
+ }
640
+ }
641
+
642
+ static VALUE record_sample_locked(VALUE record_sample_arguments_as_value) {
643
+ record_sample_arguments *args = (record_sample_arguments *) record_sample_arguments_as_value;
644
+ stack_recorder_state *state = args->state;
645
+ sample_values values = args->values;
609
646
 
610
647
  // Note: We initialize this array to have ALL_VALUE_TYPES_COUNT but only tell libdatadog to use the first
611
648
  // state->enabled_values_count values. This simplifies handling disabled value types -- we still put them on the
@@ -621,50 +658,39 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
621
658
  metric_values[position_for[ALLOC_SAMPLES_UNSCALED_VALUE_ID]] = values.alloc_samples_unscaled;
622
659
  metric_values[position_for[TIMELINE_VALUE_ID]] = values.timeline_wall_time_ns;
623
660
 
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
- }
661
+ if (values.heap_sample != NULL) {
662
+ heap_recorder_record_allocation(
663
+ state->heap_recorder,
664
+ values.heap_sample->new_object,
665
+ values.alloc_samples,
666
+ values.heap_sample->alloc_class,
667
+ args->locations,
668
+ &values.heap_sample->out_needs_commit
669
+ );
638
670
  }
639
671
 
640
- ddog_prof_Profile_Result result = ddog_prof_Profile_add(
641
- &active_slot.data->profile,
672
+ args->result = ddog_prof_Profile_add(
673
+ &args->active_slot.data->profile,
642
674
  (ddog_prof_Sample) {
643
- .locations = locations,
675
+ .locations = args->locations,
644
676
  .values = (ddog_Slice_I64) {.ptr = metric_values, .len = state->enabled_values_count},
645
- .labels = labels.labels
677
+ .labels = args->labels.labels
646
678
  },
647
679
  /* To disable end_timestamp_ns to get aggregated profiles in pprof, replace the below with `0` */
648
- labels.end_timestamp_ns
680
+ args->labels.end_timestamp_ns
649
681
  );
650
682
 
651
- active_slot.data->stats.recorded_samples++;
683
+ args->active_slot.data->stats.recorded_samples++;
652
684
 
653
- sampler_unlock_active_profile(active_slot);
654
-
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
- }
685
+ return Qnil; // Unused
658
686
  }
659
687
 
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);
688
+ static VALUE record_sample_unlock_ensure(VALUE record_sample_arguments_as_value) {
689
+ record_sample_arguments *args = (record_sample_arguments *) record_sample_arguments_as_value;
690
+
691
+ sampler_unlock_active_profile(args->active_slot);
692
+
693
+ return Qnil; // Unused
668
694
  }
669
695
 
670
696
  void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_CharSlice endpoint) {
@@ -682,18 +708,18 @@ void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_
682
708
  }
683
709
  }
684
710
 
685
- void recorder_after_gc_step(VALUE recorder_instance) {
711
+ void recorder_heap_update_may_lose_gvl(VALUE recorder_instance) {
686
712
  stack_recorder_state *state;
687
713
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
688
714
 
689
715
  if (state->heap_clean_after_gc_enabled) heap_recorder_update_young_objects(state->heap_recorder);
690
716
  }
691
717
 
692
- void recorder_after_sample(VALUE recorder_instance) {
718
+ void recorder_commit_heap_recordings_may_lose_gvl(VALUE recorder_instance) {
693
719
  stack_recorder_state *state;
694
720
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
695
721
 
696
- heap_recorder_finalize_pending_recordings(state->heap_recorder);
722
+ heap_recorder_commit_recordings_may_lose_gvl(state->heap_recorder);
697
723
  }
698
724
 
699
725
  #define MAX_LEN_HEAP_ITERATION_ERROR_MSG 256
@@ -938,15 +964,6 @@ static VALUE _native_record_endpoint(DDTRACE_UNUSED VALUE _self, VALUE recorder_
938
964
  return Qtrue;
939
965
  }
940
966
 
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
967
  static void reset_profile_slot(profile_slot *slot, ddog_Timespec start_timestamp) {
951
968
  ddog_prof_Profile_Result reset_result = ddog_prof_Profile_reset(&slot->profile);
952
969
  if (reset_result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
@@ -1024,13 +1041,28 @@ static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns,
1024
1041
  return stats_as_hash;
1025
1042
  }
1026
1043
 
1027
- static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE obj_id) {
1028
- ENFORCE_TYPE(obj_id, T_FIXNUM);
1044
+ static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE record_id) {
1045
+ ENFORCE_TYPE(record_id, T_FIXNUM);
1046
+
1047
+ stack_recorder_state *state;
1048
+ TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1049
+
1050
+ return heap_recorder_testonly_is_object_recorded(state->heap_recorder, FIX2LONG(record_id));
1051
+ }
1052
+
1053
+ static VALUE _native_record_id_for(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE obj) {
1054
+ stack_recorder_state *state;
1055
+ TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1029
1056
 
1057
+ return heap_recorder_testonly_record_id_for(state->heap_recorder, obj);
1058
+ }
1059
+
1060
+ static VALUE _native_heap_recorder_exhaust_record_ids(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1030
1061
  stack_recorder_state *state;
1031
1062
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1032
1063
 
1033
- return heap_recorder_testonly_is_object_recorded(state->heap_recorder, obj_id);
1064
+ heap_recorder_testonly_exhaust_record_ids(state->heap_recorder);
1065
+ return Qtrue;
1034
1066
  }
1035
1067
 
1036
1068
  static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
@@ -1042,8 +1074,8 @@ static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self,
1042
1074
  return Qtrue;
1043
1075
  }
1044
1076
 
1045
- static VALUE _native_recorder_after_gc_step(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1046
- recorder_after_gc_step(recorder_instance);
1077
+ static VALUE _native_recorder_heap_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1078
+ recorder_heap_update_may_lose_gvl(recorder_instance);
1047
1079
  return Qtrue;
1048
1080
  }
1049
1081
 
@@ -1153,11 +1185,11 @@ static VALUE _native_test_managed_string_storage_produces_valid_profiles(DDTRACE
1153
1185
  return rb_ary_new_from_args(2, encoded_pprof_1, encoded_pprof_2);
1154
1186
  }
1155
1187
 
1156
- static VALUE _native_finalize_pending_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1188
+ static VALUE _native_commit_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
1157
1189
  stack_recorder_state *state;
1158
1190
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1159
1191
 
1160
- heap_recorder_finalize_pending_recordings(state->heap_recorder);
1192
+ heap_recorder_commit_recordings_may_lose_gvl(state->heap_recorder);
1161
1193
 
1162
1194
  return Qtrue;
1163
1195
  }
@@ -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);
@@ -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
  }