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
@@ -74,6 +74,11 @@
74
74
  #define THREAD_ID_LIMIT_CHARS 44 // Why 44? "#{2**64} (#{2**64})".size + 1 for \0
75
75
  #define THREAD_INVOKE_LOCATION_LIMIT_CHARS 512
76
76
  #define MISSING_TRACER_CONTEXT_KEY 0
77
+
78
+ // Used as markers in the code
79
+ #define OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED INT2FIX(1)
80
+ #define OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED INT2FIX(2)
81
+
77
82
  #define TIME_BETWEEN_GC_EVENTS_NS MILLIS_AS_NS(10)
78
83
  #define GVL_SUSPENDED ((uint64_t)1)
79
84
  #define GVL_RUNNING ((uint64_t)0)
@@ -106,7 +111,7 @@ static ID otel_fiber_context_storage_id; // id of :@opentelemetry_context in Rub
106
111
 
107
112
  // This is mutable and gets set last-writer-wins style by
108
113
  // `thread_context_collector_reset_all_per_thread_contexts`, which is called whenever
109
- // profiling is starting or restating.
114
+ // profiling is starting or restarting.
110
115
  //
111
116
  // Note: We must be careful to not change this value while the profiler is still running,
112
117
  // otherwise new threads can get the new value and cause profiling to stop with an
@@ -148,8 +153,9 @@ typedef struct {
148
153
  // Used to identify the main thread, to give it a fallback name
149
154
  VALUE main_thread;
150
155
  // Used when extracting trace identifiers from otel spans. Lazily initialized.
151
- // Qtrue serves as a marker we've not yet extracted it; when we try to extract it, we set it to an object if
152
- // successful and Qnil if not.
156
+ // * `OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED`: we've not needed it yet
157
+ // * `OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED`: a sample needed it, and it's waiting to be extracted
158
+ // * anything else: the extracted value, or Qnil if we tried to extract it and failed
153
159
  VALUE otel_current_span_key;
154
160
  // Used to enable native filenames in stack traces
155
161
  bool native_filenames_enabled;
@@ -159,8 +165,6 @@ typedef struct {
159
165
  st_table *native_filenames_cache;
160
166
  // Used to attribute overhead during sampling to this component
161
167
  VALUE overhead_filename;
162
- // Minimum duration of a "Waiting for GVL" period to trigger a sample
163
- uint32_t waiting_for_gvl_threshold_ns;
164
168
 
165
169
  struct stats {
166
170
  // Track how many regular samples we've taken. Does not include garbage collection samples.
@@ -169,6 +173,8 @@ typedef struct {
169
173
  unsigned int gc_samples;
170
174
  // See thread_context_collector_on_gc_start for details
171
175
  unsigned int gc_samples_missed_due_to_missing_context;
176
+ // See thread_context_collector_sample_after_gc for details
177
+ unsigned int gc_samples_skipped_nothing_to_flush;
172
178
  // How many per-thread samples were skipped because the thread has been continuously suspended
173
179
  // (no GVL) since its previous sample, so its Ruby stack cannot have changed.
174
180
  unsigned int inactive_thread_samples_skipped;
@@ -300,7 +306,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
300
306
  static VALUE _native_sample(VALUE self, VALUE collector_instance, VALUE allow_exception);
301
307
  static VALUE _native_on_gc_start(VALUE self, VALUE collector_instance);
302
308
  static VALUE _native_on_gc_finish(VALUE self, VALUE collector_instance);
303
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE allow_exception);
309
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
304
310
  static void update_metrics_and_sample(
305
311
  thread_context_collector_state *state,
306
312
  VALUE thread_being_sampled,
@@ -317,8 +323,7 @@ static void trigger_sample_for_thread(
317
323
  long current_monotonic_wall_time_ns,
318
324
  ddog_CharSlice *ruby_vm_type,
319
325
  ddog_CharSlice *class_name,
320
- bool is_gvl_waiting_state,
321
- bool is_safe_to_allocate_objects
326
+ bool is_gvl_waiting_state
322
327
  );
323
328
  static VALUE _native_thread_list(VALUE self);
324
329
  static void check_frozen_thread(VALUE thread);
@@ -338,8 +343,7 @@ static VALUE _native_gc_tracking(VALUE self, VALUE collector_instance);
338
343
  static void trace_identifiers_for(
339
344
  thread_context_collector_state *state,
340
345
  VALUE thread,
341
- trace_identifiers *trace_identifiers_result,
342
- bool is_safe_to_allocate_objects
346
+ trace_identifiers *trace_identifiers_result
343
347
  );
344
348
  static bool should_collect_resource(VALUE root_span);
345
349
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
@@ -353,8 +357,7 @@ static void ddtrace_otel_trace_identifiers_for(
353
357
  VALUE *root_span,
354
358
  VALUE *numeric_span_id,
355
359
  VALUE active_span,
356
- VALUE otel_values,
357
- bool is_safe_to_allocate_objects
360
+ VALUE otel_values
358
361
  );
359
362
  static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE skipped_samples);
360
363
  static bool handle_gvl_waiting(
@@ -367,17 +370,17 @@ static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
367
370
  static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread);
368
371
  #ifndef NO_GVL_INSTRUMENTATION
369
372
  static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread);
370
- static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
371
- static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception);
373
+ static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE waiting_for_gvl_threshold_ns);
374
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
372
375
  #endif
373
376
  static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns);
374
377
  static void otel_without_ddtrace_trace_identifiers_for(
375
378
  thread_context_collector_state *state,
376
379
  VALUE thread,
377
- trace_identifiers *trace_identifiers_result,
378
- bool is_safe_to_allocate_objects
380
+ trace_identifiers *trace_identifiers_result
379
381
  );
380
382
  static otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key);
383
+ static bool is_otel_current_span_key_needed(thread_context_collector_state *state);
381
384
  static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
382
385
  static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key);
383
386
  static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
@@ -412,7 +415,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
412
415
  rb_define_singleton_method(testing_module, "_native_sample_allocation", _native_sample_allocation, 3);
413
416
  rb_define_singleton_method(testing_module, "_native_on_gc_start", _native_on_gc_start, 1);
414
417
  rb_define_singleton_method(testing_module, "_native_on_gc_finish", _native_on_gc_finish, 1);
415
- rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 2);
418
+ rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 1);
416
419
  rb_define_singleton_method(testing_module, "_native_thread_list", _native_thread_list, 0);
417
420
  rb_define_singleton_method(testing_module, "_native_per_thread_context", _native_per_thread_context, 1);
418
421
  rb_define_singleton_method(testing_module, "_native_stats", _native_stats, 1);
@@ -429,7 +432,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
429
432
  #ifndef NO_GVL_INSTRUMENTATION
430
433
  rb_define_singleton_method(testing_module, "_native_gvl_waiting_at_for", _native_gvl_waiting_at_for, 1);
431
434
  rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 2);
432
- rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 3);
435
+ rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 2);
433
436
  #endif
434
437
  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, 2);
435
438
 
@@ -565,7 +568,7 @@ static VALUE _native_new(VALUE klass) {
565
568
  state->time_converter_state = (monotonic_to_system_epoch_state) MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER;
566
569
  VALUE main_thread = rb_thread_main();
567
570
  state->main_thread = main_thread;
568
- state->otel_current_span_key = Qtrue;
571
+ state->otel_current_span_key = OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED;
569
572
  state->gc_tracking.wall_time_at_previous_gc_ns = INVALID_TIME;
570
573
  state->gc_tracking.wall_time_at_last_flushed_gc_event_ns = 0;
571
574
 
@@ -591,7 +594,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
591
594
  VALUE max_frames = rb_hash_fetch(options, ID2SYM(rb_intern("max_frames")));
592
595
  VALUE tracer_context_key = rb_hash_fetch(options, ID2SYM(rb_intern("tracer_context_key")));
593
596
  VALUE endpoint_collection_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("endpoint_collection_enabled")));
594
- VALUE waiting_for_gvl_threshold_ns = rb_hash_fetch(options, ID2SYM(rb_intern("waiting_for_gvl_threshold_ns")));
595
597
  VALUE otel_context_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("otel_context_enabled")));
596
598
  VALUE native_filenames_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("native_filenames_enabled")));
597
599
  VALUE show_classes = rb_hash_fetch(options, ID2SYM(rb_intern("show_classes")));
@@ -599,7 +601,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
599
601
 
600
602
  ENFORCE_TYPE(max_frames, T_FIXNUM);
601
603
  ENFORCE_BOOLEAN(endpoint_collection_enabled);
602
- ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
603
604
  ENFORCE_BOOLEAN(native_filenames_enabled);
604
605
  ENFORCE_BOOLEAN(show_classes);
605
606
  ENFORCE_TYPE(overhead_filename, T_STRING);
@@ -627,8 +628,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
627
628
  raise_error(rb_eArgError, "Unexpected value for otel_context_enabled: %+" PRIsVALUE, otel_context_enabled);
628
629
  }
629
630
 
630
- state->waiting_for_gvl_threshold_ns = NUM2UINT(waiting_for_gvl_threshold_ns);
631
-
632
631
  if (RTEST(tracer_context_key)) {
633
632
  ENFORCE_TYPE(tracer_context_key, T_SYMBOL);
634
633
  // Note about rb_to_id and dynamic symbols: calling `rb_to_id` prevents symbols from ever being garbage collected.
@@ -652,10 +651,14 @@ static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance
652
651
 
653
652
  if (allow_exception == Qfalse) debug_enter_unsafe_context();
654
653
 
655
- thread_context_collector_sample(collector_instance, monotonic_wall_time_now_ns(RAISE_ON_FAILURE));
654
+ bool needs_otel_span_key =
655
+ thread_context_collector_sample(collector_instance, monotonic_wall_time_now_ns(RAISE_ON_FAILURE));
656
656
 
657
657
  if (allow_exception == Qfalse) debug_leave_unsafe_context();
658
658
 
659
+ // Same as the CpuAndWallTimeWorker does: this can't happen during a sample (and thus not inside the unsafe context)
660
+ if (needs_otel_span_key) thread_context_collector_resolve_otel_span_key_may_lose_gvl(collector_instance);
661
+
659
662
  return Qtrue;
660
663
  }
661
664
 
@@ -683,14 +686,12 @@ static VALUE _native_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE collector_ins
683
686
  return Qtrue;
684
687
  }
685
688
 
686
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE allow_exception) {
687
- ENFORCE_BOOLEAN(allow_exception);
688
-
689
- if (allow_exception == Qfalse) debug_enter_unsafe_context();
689
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
690
+ debug_enter_unsafe_context();
690
691
 
691
692
  thread_context_collector_sample_after_gc(collector_instance);
692
693
 
693
- if (allow_exception == Qfalse) debug_leave_unsafe_context();
694
+ debug_leave_unsafe_context();
694
695
 
695
696
  return Qtrue;
696
697
  }
@@ -741,7 +742,7 @@ static void record_sampling_overhead(thread_context_collector_state *state, per_
741
742
  // Assumption 4: This function IS NOT called in a reentrant way.
742
743
  // Assumption 5: This function is called from the main Ractor (if Ruby has support for Ractors).
743
744
  //
744
- void thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns) {
745
+ bool thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns) {
745
746
  thread_context_collector_state *state;
746
747
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
747
748
 
@@ -781,6 +782,8 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
781
782
  if (!current_thread_context->is_profiler_internal_thread) {
782
783
  record_sampling_overhead(state, current_thread_context);
783
784
  }
785
+
786
+ return is_otel_current_span_key_needed(state);
784
787
  }
785
788
 
786
789
  static void update_metrics_and_sample(
@@ -824,8 +827,7 @@ static void update_metrics_and_sample(
824
827
  current_monotonic_wall_time_ns,
825
828
  NULL,
826
829
  NULL,
827
- is_gvl_waiting_state,
828
- /* is_safe_to_allocate_objects: */ true // We called from a context that's safe to run any regular code, including allocations
830
+ is_gvl_waiting_state
829
831
  );
830
832
  }
831
833
 
@@ -886,7 +888,12 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
886
888
  }
887
889
 
888
890
  // Here we record the wall-time first and in on_gc_finish we record it second to try to avoid having wall-time be slightly < cpu-time
889
- thread_context->gc_tracking.wall_time_at_start_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
891
+ long wall_time_at_start_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
892
+
893
+ // If our start timestamp is not OK, we skip tracking this GC as well
894
+ if (wall_time_at_start_ns == 0) return;
895
+
896
+ thread_context->gc_tracking.wall_time_at_start_ns = wall_time_at_start_ns;
890
897
  thread_context->gc_tracking.cpu_time_at_start_ns = cpu_time_now_ns(thread_context);
891
898
  }
892
899
 
@@ -915,7 +922,8 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
915
922
  long cpu_time_at_start_ns = thread_context->gc_tracking.cpu_time_at_start_ns;
916
923
  long wall_time_at_start_ns = thread_context->gc_tracking.wall_time_at_start_ns;
917
924
 
918
- if (cpu_time_at_start_ns == INVALID_TIME && wall_time_at_start_ns == INVALID_TIME) {
925
+ // Both fields are always set and cleared together, so checking one is enough
926
+ if (cpu_time_at_start_ns == INVALID_TIME) {
919
927
  // If this happened, it means that on_gc_start was either never called for the thread OR it was called but no thread
920
928
  // context existed at the time. The former can be the result of a bug, but since we can't distinguish them, we just
921
929
  // do nothing.
@@ -983,7 +991,14 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
983
991
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
984
992
 
985
993
  if (state->gc_tracking.wall_time_at_previous_gc_ns == INVALID_TIME) {
986
- raise_error(rb_eRuntimeError, "BUG: Unexpected call to sample_after_gc without valid GC information available");
994
+ // Rarely, we might be called with nothing to do, as an earlier call already flushed the needed info.
995
+ // This is because Ruby clears the "pending" flag for an entire batch of postponed jobs BEFORE running any of them
996
+ // (see `rb_postponed_job_flush`). Thus, if another GC happens while an earlier job in the postponed batch is
997
+ // running (and other parts of the profiler such as the heap profiler can trigger this)
998
+ // then `on_gc_finish` records that data and asks for a second flush, even though Ruby has not yet run the
999
+ // first one. Because of this, Ruby can call us once more with nothing left to do.
1000
+ state->stats.gc_samples_skipped_nothing_to_flush++;
1001
+ return Qnil;
987
1002
  }
988
1003
 
989
1004
  int max_labels_needed_for_gc = 7; // Magic number gets validated inside gc_profiling_set_metadata
@@ -1017,13 +1032,23 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
1017
1032
 
1018
1033
  state->stats.gc_samples++;
1019
1034
 
1020
- // Let recorder do any cleanup/updates it requires after a GC step.
1021
- recorder_after_gc_step(state->recorder_instance);
1022
-
1023
1035
  // Return a VALUE to make it easier to call this function from Ruby APIs that expect a return value (such as rb_rescue2)
1024
1036
  return Qnil;
1025
1037
  }
1026
1038
 
1039
+ // Let the recorder do any cleanup/updates it requires after a GC step.
1040
+ //
1041
+ // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
1042
+ // Assumption 2: This function is allowed to raise exceptions. Caller is responsible for handling them, if needed.
1043
+ VALUE thread_context_collector_heap_update_may_lose_gvl(VALUE self_instance) {
1044
+ thread_context_collector_state *state;
1045
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1046
+
1047
+ recorder_heap_update_may_lose_gvl(state->recorder_instance);
1048
+
1049
+ return Qnil; // for rb_rescue2
1050
+ }
1051
+
1027
1052
  static void trigger_sample_for_thread(
1028
1053
  thread_context_collector_state *state,
1029
1054
  VALUE thread_being_sampled,
@@ -1033,10 +1058,7 @@ static void trigger_sample_for_thread(
1033
1058
  // These two labels are only used for allocation profiling; @ivoanjo: may want to refactor this at some point?
1034
1059
  ddog_CharSlice *ruby_vm_type,
1035
1060
  ddog_CharSlice *class_name,
1036
- bool is_gvl_waiting_state,
1037
- // If the Ruby VM is at a state that can allocate objects safely, or not. Added for allocation profiling: we're not
1038
- // allowed to allocate objects (or raise exceptions) when inside the NEWOBJ tracepoint.
1039
- bool is_safe_to_allocate_objects
1061
+ bool is_gvl_waiting_state
1040
1062
  ) {
1041
1063
  int max_label_count =
1042
1064
  1 + // thread id
@@ -1074,11 +1096,11 @@ static void trigger_sample_for_thread(
1074
1096
  }
1075
1097
 
1076
1098
  trace_identifiers trace_identifiers_result = {.valid = false, .trace_endpoint = Qnil};
1077
- trace_identifiers_for(state, thread_being_sampled, &trace_identifiers_result, is_safe_to_allocate_objects);
1099
+ trace_identifiers_for(state, thread_being_sampled, &trace_identifiers_result);
1078
1100
 
1079
1101
  if (!trace_identifiers_result.valid && state->otel_context_enabled != OTEL_CONTEXT_ENABLED_FALSE) {
1080
1102
  // If we couldn't get something with ddtrace, let's see if we can get some trace identifiers from opentelemetry directly
1081
- otel_without_ddtrace_trace_identifiers_for(state, thread_being_sampled, &trace_identifiers_result, is_safe_to_allocate_objects);
1103
+ otel_without_ddtrace_trace_identifiers_for(state, thread_being_sampled, &trace_identifiers_result);
1082
1104
  }
1083
1105
 
1084
1106
  if (trace_identifiers_result.valid) {
@@ -1345,7 +1367,6 @@ static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instanc
1345
1367
  rb_str_concat(result, rb_sprintf(" main_thread=%"PRIsVALUE, state->main_thread));
1346
1368
  rb_str_concat(result, rb_sprintf(" gc_tracking=%"PRIsVALUE, gc_tracking_as_ruby_hash(state)));
1347
1369
  rb_str_concat(result, rb_sprintf(" otel_current_span_key=%"PRIsVALUE, state->otel_current_span_key));
1348
- rb_str_concat(result, rb_sprintf(" waiting_for_gvl_threshold_ns=%u", state->waiting_for_gvl_threshold_ns));
1349
1370
 
1350
1371
  return result;
1351
1372
  }
@@ -1385,6 +1406,7 @@ static VALUE stats_to_ruby_hash(thread_context_collector_state *state, VALUE has
1385
1406
  ID2SYM(rb_intern("sample_count")), /* => */ UINT2NUM(state->stats.sample_count),
1386
1407
  ID2SYM(rb_intern("gc_samples")), /* => */ UINT2NUM(state->stats.gc_samples),
1387
1408
  ID2SYM(rb_intern("gc_samples_missed_due_to_missing_context")), /* => */ UINT2NUM(state->stats.gc_samples_missed_due_to_missing_context),
1409
+ ID2SYM(rb_intern("gc_samples_skipped_nothing_to_flush")), /* => */ UINT2NUM(state->stats.gc_samples_skipped_nothing_to_flush),
1388
1410
  ID2SYM(rb_intern("inactive_thread_samples_skipped")), /* => */ UINT2NUM(state->stats.inactive_thread_samples_skipped),
1389
1411
  ID2SYM(rb_intern("profiler_thread_samples_skipped")), /* => */ UINT2NUM(state->stats.profiler_thread_samples_skipped),
1390
1412
  };
@@ -1427,9 +1449,6 @@ static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collec
1427
1449
  }
1428
1450
 
1429
1451
  static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, per_thread_context *thread_context) {
1430
- // If we didn't have a time for the previous sample, we use the current one
1431
- if (*time_at_previous_sample_ns == INVALID_TIME) *time_at_previous_sample_ns = current_time_ns;
1432
-
1433
1452
  // We don't expect to be sampling a thread (and thus updating these counters) while Ruby is doing GC (between
1434
1453
  // `thread_context_collector_on_gc_start` and `thread_context_collector_on_gc_finish`)
1435
1454
  if (thread_context->gc_tracking.cpu_time_at_start_ns != INVALID_TIME) {
@@ -1451,6 +1470,13 @@ static long update_time_since_previous_sample(long *time_at_previous_sample_ns,
1451
1470
  }
1452
1471
 
1453
1472
  static long update_cpu_time_since_previous_sample(per_thread_context *thread_context, long current_cpu_time_ns) {
1473
+ // A previous `cpu_time_now_ns` may have failed to read the clock and thus invalidated our baseline; in that case we
1474
+ // use the current time, so this sample gets no cpu-time and the next one gets an accurate delta.
1475
+ // Note wall-time never needs this: it is always seeded with a real value in `initialize_context`.
1476
+ if (thread_context->cpu_time_at_previous_sample_ns == INVALID_TIME) {
1477
+ thread_context->cpu_time_at_previous_sample_ns = current_cpu_time_ns;
1478
+ }
1479
+
1454
1480
  long elapsed_time_ns = update_time_since_previous_sample(
1455
1481
  &thread_context->cpu_time_at_previous_sample_ns,
1456
1482
  current_cpu_time_ns,
@@ -1515,14 +1541,14 @@ VALUE enforce_thread_context_collector_instance(VALUE object) {
1515
1541
  return object;
1516
1542
  }
1517
1543
 
1518
- // Finalize any pending heap allocation recordings.
1519
- // On Ruby 4+, heap allocations are recorded in two phases: during on_newobj_event we capture
1520
- // the object reference, then later we safely call rb_obj_id() to get the object ID.
1521
- void thread_context_collector_after_allocation(VALUE self_instance) {
1544
+ // Commit any pending heap allocation recordings.
1545
+ // Recording an allocation only puts it on a pending list, because taking the weak reference the heap profiler needs
1546
+ // can't be done from inside the NEWOBJ tracepoint -- see `heap_recorder_commit_recordings_may_lose_gvl`.
1547
+ void thread_context_collector_commit_heap_recordings_may_lose_gvl(VALUE self_instance) {
1522
1548
  thread_context_collector_state *state;
1523
1549
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1524
1550
 
1525
- recorder_after_sample(state->recorder_instance);
1551
+ recorder_commit_heap_recordings_may_lose_gvl(state->recorder_instance);
1526
1552
  }
1527
1553
 
1528
1554
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
@@ -1547,8 +1573,7 @@ static VALUE _native_gc_tracking(DDTRACE_UNUSED VALUE _self, VALUE collector_ins
1547
1573
  static void trace_identifiers_for(
1548
1574
  thread_context_collector_state *state,
1549
1575
  VALUE thread,
1550
- trace_identifiers *trace_identifiers_result,
1551
- bool is_safe_to_allocate_objects
1576
+ trace_identifiers *trace_identifiers_result
1552
1577
  ) {
1553
1578
  if (state->otel_context_enabled == OTEL_CONTEXT_ENABLED_ONLY) return;
1554
1579
  if (state->tracer_context_key == MISSING_TRACER_CONTEXT_KEY) return;
@@ -1569,7 +1594,7 @@ static void trace_identifiers_for(
1569
1594
  VALUE numeric_span_id = Qnil;
1570
1595
 
1571
1596
  if (otel_values != Qnil) {
1572
- ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values, is_safe_to_allocate_objects);
1597
+ ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values);
1573
1598
  }
1574
1599
 
1575
1600
  if (root_span == Qnil || (active_span == Qnil && numeric_span_id == Qnil)) return;
@@ -1665,11 +1690,11 @@ bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1665
1690
  return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1666
1691
  }
1667
1692
 
1668
- // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should never allocate in the
1669
- // Ruby heap.
1693
+ // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1694
+ // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1670
1695
  //
1671
- // Returns true if the after_allocation needs to be called (to do work that can't be done from inside the
1672
- // tracepoint, such as allocate new objects), and false if it doesn't
1696
+ // Returns true if `thread_context_collector_commit_heap_recordings_may_lose_gvl` needs to be called (to do work
1697
+ // that can't be done from inside the tracepoint, such as allocate new objects), and false if it doesn't
1673
1698
  //
1674
1699
  // The callers must ensure thread_context is non-NULL.
1675
1700
  bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object) {
@@ -1714,13 +1739,10 @@ bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_
1714
1739
  // Thus, we need to make sure there's actually a class before getting its name.
1715
1740
 
1716
1741
  if (klass != 0) {
1717
- const char *name = rb_class2name(klass);
1718
- size_t name_length = name != NULL ? strlen(name) : 0;
1719
-
1720
- if (name_length > 0) {
1721
- class_name = (ddog_CharSlice) {.ptr = name, .len = name_length};
1742
+ VALUE name = ddtrace_alloc_free_rb_mod_name(klass);
1743
+ if (name != Qnil) {
1744
+ class_name = char_slice_from_ruby_string(name);
1722
1745
  } else {
1723
- // @ivoanjo: I'm not sure this can ever happen, but just-in-case
1724
1746
  class_name = ruby_value_type_to_class_name(type);
1725
1747
  }
1726
1748
  } else {
@@ -1740,23 +1762,22 @@ bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_
1740
1762
  class_name = ruby_vm_type; // For other weird internal things we just use the VM type
1741
1763
  }
1742
1764
 
1743
- bool needs_after_allocation = track_object(state->recorder_instance, new_object, sample_weight, class_name);
1744
-
1745
1765
  VALUE current_thread = rb_thread_current();
1746
1766
 
1767
+ heap_sample_values heap_sample = {.new_object = new_object, .alloc_class = class_name};
1768
+
1747
1769
  trigger_sample_for_thread(
1748
1770
  state,
1749
1771
  current_thread,
1750
1772
  thread_context,
1751
- (sample_values) {.alloc_samples = sample_weight, .alloc_samples_unscaled = 1, .heap_sample = true},
1773
+ (sample_values) {.alloc_samples = sample_weight, .alloc_samples_unscaled = 1, .heap_sample = &heap_sample},
1752
1774
  INVALID_TIME, // For now we're not collecting timestamps for allocation events, as per profiling team internal discussions
1753
1775
  &ruby_vm_type,
1754
1776
  &class_name,
1755
- /* is_gvl_waiting_state: */ false,
1756
- /* is_safe_to_allocate_objects: */ false // Not safe to allocate further inside the NEWOBJ tracepoint
1777
+ /* is_gvl_waiting_state: */ false
1757
1778
  );
1758
1779
 
1759
- return needs_after_allocation;
1780
+ return heap_sample.out_needs_commit;
1760
1781
  }
1761
1782
 
1762
1783
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
@@ -1769,13 +1790,13 @@ static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collecto
1769
1790
 
1770
1791
  debug_enter_unsafe_context();
1771
1792
 
1772
- bool needs_after_allocation = thread_context_collector_sample_allocation(collector_instance, thread_context, NUM2UINT(sample_weight), new_object);
1793
+ bool needs_commit = thread_context_collector_sample_allocation(collector_instance, thread_context, NUM2UINT(sample_weight), new_object);
1773
1794
 
1774
1795
  debug_leave_unsafe_context();
1775
1796
 
1776
1797
  // We could instead choose to automatically trigger the after allocation here; yet, it seems kinda nice to keep it manual for
1777
1798
  // the tests so we can pull on each lever separately and observe "the sausage being made" in steps
1778
- return needs_after_allocation ? Qtrue : Qfalse;
1799
+ return needs_commit ? Qtrue : Qfalse;
1779
1800
  }
1780
1801
 
1781
1802
  static VALUE new_empty_thread_inner(DDTRACE_UNUSED void *arg) {
@@ -1825,26 +1846,57 @@ static VALUE read_otel_current_span_key_const(DDTRACE_UNUSED VALUE _unused) {
1825
1846
  return rb_const_get(trace_module, rb_intern("CURRENT_SPAN_KEY"));
1826
1847
  }
1827
1848
 
1828
- static VALUE get_otel_current_span_key(thread_context_collector_state *state, bool is_safe_to_allocate_objects) {
1829
- if (state->otel_current_span_key == Qtrue) { // Qtrue means we haven't tried to extract it yet
1830
- if (!is_safe_to_allocate_objects) {
1831
- // Calling read_otel_current_span_key_const below can trigger exceptions and arbitrary Ruby code running (e.g.
1832
- // `const_missing`, etc). Not safe to call in this situation, so we just skip otel info for this sample.
1833
- return Qnil;
1834
- }
1835
-
1836
- // If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
1837
- // gets refactored, we should not fall on our face)
1838
- VALUE span_key = rb_protect(read_otel_current_span_key_const, Qnil, NULL);
1839
- rb_set_errinfo(Qnil); // **Clear any pending exception after ignoring it**
1849
+ static VALUE otel_current_span_key_not_found(DDTRACE_UNUSED VALUE _unused, DDTRACE_UNUSED VALUE _exception) {
1850
+ return Qnil;
1851
+ }
1840
1852
 
1841
- // 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
1842
- state->otel_current_span_key = span_key;
1853
+ static VALUE get_otel_current_span_key(thread_context_collector_state *state) {
1854
+ if (state->otel_current_span_key == OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED) {
1855
+ // Extracting this needs calling into Ruby code, which we don't want to do in the middle of a sample.
1856
+ // So instead we signal that we want this, and let it be resolved separately.
1857
+ state->otel_current_span_key = OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED;
1858
+ return Qnil;
1843
1859
  }
1844
1860
 
1861
+ // We asked for it, but it hasn't been extracted yet
1862
+ if (is_otel_current_span_key_needed(state)) return Qnil;
1863
+
1845
1864
  return state->otel_current_span_key;
1846
1865
  }
1847
1866
 
1867
+ static bool is_otel_current_span_key_needed(thread_context_collector_state *state) {
1868
+ return state->otel_current_span_key == OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED;
1869
+ }
1870
+
1871
+ // Extracts `otel_current_span_key`, if a sample asked for it (see `get_otel_current_span_key`).
1872
+ // This is deliberately not done during sampling because we can lose the GVL.
1873
+ //
1874
+ // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
1875
+ void thread_context_collector_resolve_otel_span_key_may_lose_gvl(VALUE self_instance) {
1876
+ thread_context_collector_state *state;
1877
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1878
+
1879
+ if (!is_otel_current_span_key_needed(state)) return;
1880
+
1881
+ // If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
1882
+ // gets refactored, we should not fall on our face).
1883
+ //
1884
+ // Note that we use `rb_rescue2` and not `rb_protect` so that we only swallow exceptions: anything else that can
1885
+ // unwind through here (e.g. a `Thread#kill`) is not ours to swallow. `rb_rescue2` also takes care of restoring
1886
+ // any previously-pending exception for us.
1887
+ VALUE span_key = rb_rescue2(
1888
+ read_otel_current_span_key_const,
1889
+ Qnil,
1890
+ otel_current_span_key_not_found,
1891
+ Qnil,
1892
+ rb_eException, // rb_eException is the base class of all Ruby exceptions
1893
+ 0 // Required by API to be the last argument
1894
+ );
1895
+
1896
+ // 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
1897
+ state->otel_current_span_key = span_key;
1898
+ }
1899
+
1848
1900
  // This method gets used when ddtrace is being used indirectly via the opentelemetry APIs. Information gets stored slightly
1849
1901
  // differently, and this codepath handles it.
1850
1902
  static void ddtrace_otel_trace_identifiers_for(
@@ -1853,8 +1905,7 @@ static void ddtrace_otel_trace_identifiers_for(
1853
1905
  VALUE *root_span,
1854
1906
  VALUE *numeric_span_id,
1855
1907
  VALUE active_span,
1856
- VALUE otel_values,
1857
- bool is_safe_to_allocate_objects
1908
+ VALUE otel_values
1858
1909
  ) {
1859
1910
  VALUE resolved_numeric_span_id =
1860
1911
  active_span == Qnil ?
@@ -1865,7 +1916,7 @@ static void ddtrace_otel_trace_identifiers_for(
1865
1916
 
1866
1917
  if (resolved_numeric_span_id == Qnil) return;
1867
1918
 
1868
- VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
1919
+ VALUE otel_current_span_key = get_otel_current_span_key(state);
1869
1920
  if (otel_current_span_key == Qnil) return;
1870
1921
  VALUE current_trace = *active_trace;
1871
1922
 
@@ -1985,15 +2036,14 @@ static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self
1985
2036
  static void otel_without_ddtrace_trace_identifiers_for(
1986
2037
  thread_context_collector_state *state,
1987
2038
  VALUE thread,
1988
- trace_identifiers *trace_identifiers_result,
1989
- bool is_safe_to_allocate_objects
2039
+ trace_identifiers *trace_identifiers_result
1990
2040
  ) {
1991
2041
  VALUE context_storage = otel_context_storage_for(state, thread);
1992
2042
 
1993
2043
  // If it exists, context_storage is expected to be an Array[OpenTelemetry::Context]
1994
2044
  if (context_storage == Qnil || !RB_TYPE_P(context_storage, T_ARRAY)) return;
1995
2045
 
1996
- VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
2046
+ VALUE otel_current_span_key = get_otel_current_span_key(state);
1997
2047
  if (otel_current_span_key == Qnil) return;
1998
2048
 
1999
2049
  long active_context_index = RARRAY_LEN(context_storage) - 1;
@@ -2195,12 +2245,9 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2195
2245
  }
2196
2246
 
2197
2247
  #ifndef NO_GVL_INSTRUMENTATION
2198
- // This function runs on the passed thread and has the GVL because it gets called just after the Ruby thread acquired the GVL
2248
+ // We must only use async-signal-safe functions here, see notes in the caller
2199
2249
  __attribute__((warn_unused_result))
2200
- on_gvl_running_result thread_context_collector_on_gvl_running(VALUE self_instance, VALUE thread, per_thread_context *thread_context) {
2201
- thread_context_collector_state *state;
2202
- TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2203
-
2250
+ on_gvl_running_result thread_context_collector_on_gvl_running(VALUE thread, per_thread_context *thread_context, uint32_t waiting_for_gvl_threshold_ns) {
2204
2251
  // Bump the event counter and clears the state bit to "running"
2205
2252
  uint64_t counter_portion = thread_context->gvl_state_change_count >> 1;
2206
2253
  thread_context->gvl_state_change_count = ((counter_portion + 1) << 1) | GVL_RUNNING;
@@ -2217,7 +2264,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2217
2264
 
2218
2265
  long waiting_for_gvl_duration_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - gvl_waiting_at;
2219
2266
 
2220
- bool should_sample = waiting_for_gvl_duration_ns >= state->waiting_for_gvl_threshold_ns;
2267
+ bool should_sample = waiting_for_gvl_duration_ns >= waiting_for_gvl_threshold_ns;
2221
2268
 
2222
2269
  if (should_sample) {
2223
2270
  // We flip the gvl_waiting_at to negative to mark that the thread is now running and no longer waiting
@@ -2392,8 +2439,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2392
2439
  gvl_waiting_started_wall_time_ns,
2393
2440
  NULL,
2394
2441
  NULL,
2395
- /* is_gvl_waiting_state: */ false, // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
2396
- /* is_safe_to_allocate_objects: */ true // This is similar to a regular cpu/wall sample, so it's also safe
2442
+ /* is_gvl_waiting_state: */ false // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
2397
2443
  );
2398
2444
  }
2399
2445
 
@@ -2413,15 +2459,18 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2413
2459
  return result;
2414
2460
  }
2415
2461
 
2416
- static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread) {
2462
+ static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE waiting_for_gvl_threshold_ns) {
2417
2463
  ENFORCE_THREAD(thread);
2464
+ ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
2418
2465
 
2419
2466
  debug_enter_unsafe_context();
2420
2467
 
2421
2468
  per_thread_context *thread_context = get_per_thread_context(thread);
2422
2469
  VALUE result;
2423
2470
  if (thread_context) {
2424
- result = thread_context_collector_on_gvl_running(collector_instance, thread, thread_context).action == ON_GVL_RUNNING_SAMPLE ? Qtrue : Qfalse;
2471
+ on_gvl_running_result gvl_running_result =
2472
+ thread_context_collector_on_gvl_running(thread, thread_context, NUM2UINT(waiting_for_gvl_threshold_ns));
2473
+ result = gvl_running_result.action == ON_GVL_RUNNING_SAMPLE ? Qtrue : Qfalse;
2425
2474
  } else {
2426
2475
  result = Qfalse;
2427
2476
  }
@@ -2431,11 +2480,10 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2431
2480
  return result;
2432
2481
  }
2433
2482
 
2434
- static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception) {
2483
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread) {
2435
2484
  ENFORCE_THREAD(thread);
2436
- ENFORCE_BOOLEAN(allow_exception);
2437
2485
 
2438
- if (allow_exception == Qfalse) debug_enter_unsafe_context();
2486
+ debug_enter_unsafe_context();
2439
2487
 
2440
2488
  VALUE result = thread_context_collector_sample_after_gvl_running(
2441
2489
  collector_instance,
@@ -2443,7 +2491,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2443
2491
  monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
2444
2492
  );
2445
2493
 
2446
- if (allow_exception == Qfalse) debug_leave_unsafe_context();
2494
+ debug_leave_unsafe_context();
2447
2495
 
2448
2496
  return result;
2449
2497
  }