datadog 2.43.0 → 2.45.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +136 -132
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -5
  5. data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +0 -2
  6. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +19 -20
  7. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +7 -7
  9. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +146 -109
  11. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  12. data/ext/datadog_profiling_native_extension/extconf.rb +6 -2
  13. data/ext/datadog_profiling_native_extension/heap_recorder.c +2 -2
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +122 -53
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +6 -2
  16. data/ext/datadog_profiling_native_extension/profiling.c +22 -23
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +3 -1
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
  19. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
  20. data/ext/datadog_profiling_native_extension/stack_recorder.c +10 -28
  21. data/ext/datadog_profiling_native_extension/stack_recorder.h +0 -1
  22. data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
  23. data/ext/datadog_profiling_native_extension/time_helpers.h +13 -14
  24. data/ext/libdatadog_api/di.c +8 -3
  25. data/ext/libdatadog_api/extconf.rb +1 -1
  26. data/ext/libdatadog_api/feature_flags.c +8 -0
  27. data/ext/libdatadog_api/init.c +3 -1
  28. data/ext/libdatadog_api/otel_thread_context.c +38 -17
  29. data/ext/libdatadog_api/trace_exporter.c +77 -11
  30. data/ext/libdatadog_extconf_helpers.rb +14 -9
  31. data/lib/datadog/ai_guard/component.rb +17 -8
  32. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  33. data/lib/datadog/ai_guard/configuration.rb +13 -0
  34. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  35. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  36. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  37. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  38. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  39. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  40. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  41. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  42. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  43. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  44. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  45. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  46. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  47. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  48. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  49. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  50. data/lib/datadog/ai_guard/ext.rb +1 -0
  51. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  52. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  53. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  54. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  55. data/lib/datadog/ai_guard/redaction.rb +80 -0
  56. data/lib/datadog/ai_guard.rb +58 -57
  57. data/lib/datadog/core/configuration/components.rb +9 -1
  58. data/lib/datadog/core/configuration/settings.rb +5 -2
  59. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  60. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  61. data/lib/datadog/core/remote/component.rb +9 -2
  62. data/lib/datadog/core/remote/worker.rb +8 -0
  63. data/lib/datadog/di/instrumenter.rb +1 -1
  64. data/lib/datadog/di/probe_manager.rb +10 -5
  65. data/lib/datadog/di/proc_responder.rb +1 -1
  66. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  67. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  68. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  69. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  70. data/lib/datadog/open_feature/component.rb +57 -1
  71. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  72. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  73. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  74. data/lib/datadog/open_feature/configuration.rb +118 -0
  75. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  76. data/lib/datadog/open_feature/remote.rb +5 -5
  77. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  78. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +7 -2
  79. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  80. data/lib/datadog/profiling/component.rb +2 -3
  81. data/lib/datadog/profiling/exporter.rb +7 -5
  82. data/lib/datadog/symbol_database/extractor.rb +84 -102
  83. data/lib/datadog/tracing/component.rb +3 -1
  84. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  85. data/lib/datadog/tracing/configuration/settings.rb +9 -2
  86. data/lib/datadog/tracing/context.rb +24 -2
  87. data/lib/datadog/tracing/context_provider.rb +9 -5
  88. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  89. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +6 -3
  90. data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -0
  91. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  92. data/lib/datadog/tracing/otel_thread_context.rb +76 -11
  93. data/lib/datadog/tracing/tracer.rb +15 -2
  94. data/lib/datadog/tracing/transport/native.rb +23 -22
  95. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  96. data/lib/datadog/tracing/transport/traces.rb +1 -1
  97. data/lib/datadog/version.rb +1 -1
  98. metadata +23 -7
  99. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -42,6 +42,7 @@
42
42
  //
43
43
  // When `thread_context_collector_on_gc_start` gets called, the current cpu and wall-time get recorded to the thread
44
44
  // context: `cpu_time_at_gc_start_ns` and `wall_time_at_gc_start_ns`.
45
+ // (Ruby won't switch threads during GC, so `gc_start` and `gc_finish` will happen on the same thread)
45
46
  //
46
47
  // While `cpu_time_at_gc_start_ns` is set, we don't expect the thread to be sampled: the VM is doing GC
47
48
  // on the thread holding the GVL so no other samples can/will be triggered until GC finishes.
@@ -268,7 +269,7 @@ struct per_thread_context {
268
269
  bool was_skipped_at_last_sample;
269
270
  // Set as true for CpuAndWallTimeWorker and IdleSamplingHelper threads.
270
271
  // When true, per-tick samples are skipped entirely; the thread is sampled only once per
271
- // reporting period during the on_serialize flush.
272
+ // reporting period during prepare_serialize.
272
273
  //
273
274
  // These threads are always in native code so their stacks aren't interesting;
274
275
  // the Profiling::Scheduler thread on the other hand does a lot of different
@@ -311,7 +312,6 @@ static void update_metrics_and_sample(
311
312
  thread_context_collector_state *state,
312
313
  VALUE thread_being_sampled,
313
314
  per_thread_context *thread_context,
314
- long current_cpu_time_ns,
315
315
  long current_monotonic_wall_time_ns,
316
316
  bool force_sample
317
317
  );
@@ -327,8 +327,8 @@ static void trigger_sample_for_thread(
327
327
  );
328
328
  static VALUE _native_thread_list(VALUE self);
329
329
  static void check_frozen_thread(VALUE thread);
330
- static per_thread_context *get_or_create_context_for(VALUE thread);
331
- static void initialize_context(VALUE thread, per_thread_context *thread_context);
330
+ static per_thread_context *get_or_create_context_for(VALUE thread, long current_monotonic_wall_time_ns);
331
+ static void initialize_context(VALUE thread, per_thread_context *thread_context, long current_monotonic_wall_time_ns);
332
332
  static VALUE _native_inspect(VALUE self, VALUE collector_instance);
333
333
  static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context);
334
334
  static VALUE stats_to_ruby_hash(thread_context_collector_state *state, VALUE hash);
@@ -373,7 +373,8 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread);
373
373
  static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE waiting_for_gvl_threshold_ns);
374
374
  static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
375
375
  #endif
376
- static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns);
376
+ static VALUE _native_apply_delta_to_time_at_previous_sample_ns(int argc, VALUE *argv, DDTRACE_UNUSED VALUE self);
377
+ static VALUE _native_apply_delta_to_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns);
377
378
  static void otel_without_ddtrace_trace_identifiers_for(
378
379
  thread_context_collector_state *state,
379
380
  VALUE thread,
@@ -386,6 +387,7 @@ static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE k
386
387
  static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
387
388
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self);
388
389
  static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread);
390
+ static VALUE _native_prepare_serialize(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
389
391
  static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
390
392
  static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
391
393
  static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample);
@@ -427,6 +429,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
427
429
  rb_define_singleton_method(testing_module, "_native_remove_per_thread_context_for", _native_remove_per_thread_context_for, 1);
428
430
  rb_define_singleton_method(testing_module, "_native_global_reset_per_thread_context", _native_global_reset_per_thread_context, 1);
429
431
  rb_define_singleton_method(testing_module, "_native_mark_thread_as_profiler_internal", _native_mark_thread_as_profiler_internal, 1);
432
+ rb_define_singleton_method(testing_module, "_native_prepare_serialize", _native_prepare_serialize, 1);
430
433
  rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
431
434
  rb_define_singleton_method(testing_module, "_native_on_gvl_released", _native_on_gvl_released, 1);
432
435
  #ifndef NO_GVL_INSTRUMENTATION
@@ -434,7 +437,8 @@ void collectors_thread_context_init(VALUE profiling_module) {
434
437
  rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 2);
435
438
  rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 2);
436
439
  #endif
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);
440
+ rb_define_singleton_method(testing_module, "_native_apply_delta_to_time_at_previous_sample_ns", _native_apply_delta_to_time_at_previous_sample_ns, -1);
441
+ rb_define_singleton_method(testing_module, "_native_apply_delta_to_gvl_waiting_at_for", _native_apply_delta_to_gvl_waiting_at_for, 2);
438
442
 
439
443
  at_active_span_id = rb_intern_const("@active_span");
440
444
  at_active_trace_id = rb_intern_const("@active_trace");
@@ -613,7 +617,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
613
617
  // Update this when modifying state struct
614
618
  sample_locations_initialize(&state->locations, max_frame_int, show_classes == Qtrue);
615
619
  state->recorder_instance = enforce_recorder_instance(recorder_instance);
616
- recorder_install_on_serialize(recorder_instance, self_instance);
617
620
  state->endpoint_collection_enabled = (endpoint_collection_enabled == Qtrue);
618
621
  state->native_filenames_enabled = (native_filenames_enabled == Qtrue);
619
622
  state->show_classes = (show_classes == Qtrue);
@@ -652,7 +655,7 @@ static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance
652
655
  if (allow_exception == Qfalse) debug_enter_unsafe_context();
653
656
 
654
657
  bool needs_otel_span_key =
655
- thread_context_collector_sample(collector_instance, monotonic_wall_time_now_ns(RAISE_ON_FAILURE));
658
+ thread_context_collector_sample(collector_instance, monotonic_wall_time_now_ns());
656
659
 
657
660
  if (allow_exception == Qfalse) debug_leave_unsafe_context();
658
661
 
@@ -698,7 +701,7 @@ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_
698
701
 
699
702
  // Record profiler sampling overhead as a placeholder stack
700
703
  static void record_sampling_overhead(thread_context_collector_state *state, per_thread_context *current_thread_context) {
701
- long wall_time_after_sampling = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
704
+ long wall_time_after_sampling = monotonic_wall_time_now_ns();
702
705
  long cpu_time_after_sampling = cpu_time_now_ns(current_thread_context);
703
706
 
704
707
  long overhead_cpu_time_ns = update_cpu_time_since_previous_sample(current_thread_context, cpu_time_after_sampling);
@@ -741,32 +744,38 @@ static void record_sampling_overhead(thread_context_collector_state *state, per_
741
744
  // Assumption 3: This function IS NOT called from a signal handler. This function is not async-signal-safe.
742
745
  // Assumption 4: This function IS NOT called in a reentrant way.
743
746
  // Assumption 5: This function is called from the main Ractor (if Ruby has support for Ractors).
747
+ // Assumption 6: When called while the profiler is active, `during_sample` MUST be set.
744
748
  //
745
749
  bool thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns) {
746
750
  thread_context_collector_state *state;
747
751
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
748
752
 
749
753
  VALUE current_thread = rb_thread_current();
750
- per_thread_context *current_thread_context = get_or_create_context_for(current_thread);
751
- long cpu_time_at_sample_start_for_current_thread = cpu_time_now_ns(current_thread_context);
754
+ per_thread_context *current_thread_context = get_or_create_context_for(current_thread, current_monotonic_wall_time_ns);
755
+
756
+ // Sample the current thread (which will use the current CPU-time) first.
757
+ // This ensures any CPU-time the profiler spends sampling (e.g. the rest of the function) is not wrongly blamed on this thread,
758
+ // and instead is separately accounted by `record_sampling_overhead()` below.
759
+ update_metrics_and_sample(
760
+ state,
761
+ current_thread,
762
+ current_thread_context,
763
+ current_monotonic_wall_time_ns,
764
+ false);
752
765
 
753
766
  VALUE threads = thread_list(state);
754
767
 
755
768
  const long thread_count = RARRAY_LEN(threads);
756
769
  for (long i = 0; i < thread_count; i++) {
757
- VALUE thread = RARRAY_AREF(threads, i);
758
- per_thread_context *thread_context = get_or_create_context_for(thread);
770
+ VALUE thread = rb_ary_entry(threads, i);
771
+ ENFORCE_THREAD(thread);
759
772
 
760
- // We account for cpu-time for the current thread in a different way: we use the cpu-time at sampling start,
761
- // to avoid blaming the time the profiler took on whatever is currently running on the thread,
762
- // and instead we report that time the profiler took as sampling overhead below.
763
- long current_cpu_time_ns = (thread == current_thread) ? cpu_time_at_sample_start_for_current_thread : cpu_time_now_ns(thread_context);
773
+ if (thread == current_thread) continue; // Already sampled above
764
774
 
765
775
  update_metrics_and_sample(
766
776
  state,
767
777
  thread,
768
- thread_context,
769
- current_cpu_time_ns,
778
+ get_or_create_context_for(thread, current_monotonic_wall_time_ns),
770
779
  current_monotonic_wall_time_ns,
771
780
  false);
772
781
  }
@@ -790,35 +799,36 @@ static void update_metrics_and_sample(
790
799
  thread_context_collector_state *state,
791
800
  VALUE thread_being_sampled,
792
801
  per_thread_context *thread_context,
793
- long current_cpu_time_ns,
794
802
  long current_monotonic_wall_time_ns,
795
803
  bool force_sample
796
804
  ) {
805
+ long current_cpu_time_ns = cpu_time_now_ns(thread_context);
797
806
  bool is_gvl_waiting_state =
798
807
  handle_gvl_waiting(state, thread_being_sampled, thread_context, current_cpu_time_ns);
799
808
 
800
809
  if (skip_sample(state, thread_context, is_gvl_waiting_state, force_sample)) return;
801
810
 
802
- // Don't assign/update cpu during "Waiting for GVL"
803
- long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_cpu_time_since_previous_sample(thread_context, current_cpu_time_ns);
804
-
805
- long wall_time_elapsed_ns = update_wall_time_since_previous_sample(thread_context, current_monotonic_wall_time_ns);
806
-
807
811
  // A thread enters "Waiting for GVL", well, as the name implies, without the GVL.
808
812
  //
809
- // As a consequence, it's possible that a thread enters "Waiting for GVL" in parallel with the current thread working
813
+ // As a consequence, it's possible for another thread to enter "Waiting for GVL" in parallel with the current thread working
810
814
  // on sampling, and thus for the `current_monotonic_wall_time_ns` (which is recorded at the start of sampling)
811
- // to be < the time at which we started Waiting for GVL.
815
+ // to be <= the time we observe for a "Waiting for GVL".
812
816
  //
813
- // All together, this means that when `handle_gvl_waiting` creates an extra sample (see comments on that function for
814
- // what the extra sample is), it's possible that there's no more wall-time to be assigned.
815
- // Thus, in this case, we don't want to produce a sample representing Waiting for GVL with a wall-time of 0, and
817
+ // When that happens, `handle_gvl_waiting` has already accounted for all wall-time up to the (later) start of the
818
+ // wait via the extra sample it pushes (see comments on that function), so there may be no remaining time
819
+ // to account for here.
820
+ // Thus, in this case, we don't want to produce a sample representing "Waiting for GVL" with a wall-time of 0 (nor negative!), and
816
821
  // thus we skip creating such a sample.
817
- if (is_gvl_waiting_state && wall_time_elapsed_ns == 0) return;
822
+ if (is_gvl_waiting_state && current_monotonic_wall_time_ns <= thread_context->wall_time_at_previous_sample_ns) return;
818
823
  // ...you may also wonder: is there any other situation where it makes sense to produce a sample with
819
824
  // wall_time_elapsed_ns == 0? I believe that yes, because the sample still includes a timestamp and a stack, but we
820
825
  // may revisit/change our minds on this in the future.
821
826
 
827
+ // Don't assign/update cpu during "Waiting for GVL"
828
+ long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_cpu_time_since_previous_sample(thread_context, current_cpu_time_ns);
829
+
830
+ long wall_time_elapsed_ns = update_wall_time_since_previous_sample(thread_context, current_monotonic_wall_time_ns);
831
+
822
832
  trigger_sample_for_thread(
823
833
  state,
824
834
  thread_being_sampled,
@@ -843,7 +853,7 @@ static bool skip_sample(thread_context_collector_state *state, per_thread_contex
843
853
  // Skip this per-tick sample entirely when the thread does not have the GVL and did not acquire
844
854
  // it since the previous sample: its Ruby-level stack has not changed. The skipped wall-time will
845
855
  // be picked up by either by an extra sample when the thread acquires the GVL, or by
846
- // the on-serialize flush in the stack recorder (using was_skipped_at_last_sample).
856
+ // prepare_serialize (using was_skipped_at_last_sample).
847
857
  // The check is gated by `!is_gvl_waiting_state` so the existing "Waiting for GVL" machinery
848
858
  // in handle_gvl_waiting (situation 1 extra sample, situation 2 regular sample) keeps running.
849
859
  // TODO: we could probably also skip while "Waiting for GVL"
@@ -888,16 +898,14 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
888
898
  }
889
899
 
890
900
  // 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
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;
901
+ thread_context->gc_tracking.wall_time_at_start_ns = monotonic_wall_time_now_ns();
897
902
  thread_context->gc_tracking.cpu_time_at_start_ns = cpu_time_now_ns(thread_context);
898
903
  }
899
904
 
900
905
  // This function gets called when Ruby has finished running the Garbage Collector on the current thread.
906
+ // (Note: The `GC_EXIT` event gets emitted by the VM while `during_gc` is still set to true -- the flag
907
+ // is only cleared after the event gets emitted -- see top of the file for more details)
908
+ //
901
909
  // It records the cpu/wall-time observed during GC, which will be used to later
902
910
  // create an event including the cpu/wall time spent from the start of garbage collector work until now.
903
911
  //
@@ -936,16 +944,13 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
936
944
 
937
945
  // Here we record the wall-time second and in on_gc_start we record it first to try to avoid having wall-time be slightly < cpu-time
938
946
  long cpu_time_at_finish_ns = cpu_time_now_ns(thread_context);
939
- long wall_time_at_finish_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
940
-
941
- // If our end timestamp is not OK, we bail out
942
- if (wall_time_at_finish_ns == 0) return false;
947
+ long wall_time_at_finish_ns = monotonic_wall_time_now_ns();
943
948
 
944
949
  long gc_cpu_time_elapsed_ns = cpu_time_at_finish_ns - cpu_time_at_start_ns;
945
950
  long gc_wall_time_elapsed_ns = wall_time_at_finish_ns - wall_time_at_start_ns;
946
951
 
947
- // Wall-time can go backwards if the system clock gets changed (and we observed spurious jumps back on macOS as well)
948
- // so let's ensure we don't get negative values for time deltas.
952
+ // @ivoanjo: We don't expect these clocks to ever go backwards (and in other code paths we enforce that with an exception)
953
+ // but during GC it's really really awkward to signal back failures so for now I left the clamping.
949
954
  gc_cpu_time_elapsed_ns = long_max_of(gc_cpu_time_elapsed_ns, 0);
950
955
  gc_wall_time_elapsed_ns = long_max_of(gc_wall_time_elapsed_ns, 0);
951
956
 
@@ -968,10 +973,10 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
968
973
  // Let the caller know if it should schedule a flush or not. Returning true every time would cause a lot of overhead
969
974
  // on the application (see GC tracking introduction at the top of the file), so instead we try to accumulate a few
970
975
  // samples first.
971
- bool over_flush_time_treshold =
976
+ bool over_flush_time_threshold =
972
977
  (wall_time_at_finish_ns - state->gc_tracking.wall_time_at_last_flushed_gc_event_ns) >= TIME_BETWEEN_GC_EVENTS_NS;
973
978
 
974
- if (over_flush_time_treshold) {
979
+ if (over_flush_time_threshold) {
975
980
  return true;
976
981
  } else {
977
982
  return gc_profiling_has_major_gc_finished();
@@ -986,6 +991,7 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
986
991
  // Assumption 2: This function is allowed to raise exceptions. Caller is responsible for handling them, if needed.
987
992
  // Assumption 3: Unlike `on_gc_start` and `on_gc_finish`, this method is allowed to allocate memory as needed.
988
993
  // Assumption 4: This function is called from the main Ractor (if Ruby has support for Ractors).
994
+ // Assumption 5: When called while the profiler is active, `during_sample` MUST be set.
989
995
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
990
996
  thread_context_collector_state *state;
991
997
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -1209,14 +1215,14 @@ static void check_frozen_thread(VALUE thread) {
1209
1215
 
1210
1216
  // See the docs on struct per_thread_context.
1211
1217
  // This allocates a Ruby object and therefore needs the GVL and is not safe to call from RUBY_INTERNAL_EVENT_* hooks.
1212
- static per_thread_context *get_or_create_context_for(VALUE thread) {
1218
+ static per_thread_context *get_or_create_context_for(VALUE thread, long current_monotonic_wall_time_ns) {
1213
1219
  per_thread_context *thread_context = get_per_thread_context(thread);
1214
1220
  if (thread_context != NULL) return thread_context;
1215
1221
 
1216
1222
  check_frozen_thread(thread);
1217
1223
 
1218
1224
  thread_context = calloc(1, sizeof(per_thread_context)); // See "note on calloc vs ruby_xcalloc use" in heap_recorder.c
1219
- initialize_context(thread, thread_context);
1225
+ initialize_context(thread, thread_context, current_monotonic_wall_time_ns);
1220
1226
 
1221
1227
  VALUE wrapper = TypedData_Wrap_Struct(rb_cObject, &per_thread_context_typed_data, thread_context);
1222
1228
  rb_ivar_set(thread, dd_per_thread_context_id, wrapper);
@@ -1230,7 +1236,8 @@ static void on_thread_begin_event(VALUE tracepoint_data, DDTRACE_UNUSED void *un
1230
1236
 
1231
1237
  VALUE thread = rb_tracearg_self(rb_tracearg_from_tracepoint(tracepoint_data));
1232
1238
  ENFORCE_THREAD(thread);
1233
- get_or_create_context_for(thread);
1239
+
1240
+ get_or_create_context_for(thread, monotonic_wall_time_now_ns());
1234
1241
  }
1235
1242
 
1236
1243
  #define LOGGING_GEM_PATH "/lib/logging/diagnostic_context.rb"
@@ -1254,7 +1261,7 @@ static bool is_logging_gem_monkey_patch(VALUE invoke_file_location) {
1254
1261
  return strncmp(invoke_file + invoke_file_len - logging_gem_path_len, LOGGING_GEM_PATH, logging_gem_path_len) == 0;
1255
1262
  }
1256
1263
 
1257
- static void initialize_context(VALUE thread, per_thread_context *thread_context) {
1264
+ static void initialize_context(VALUE thread, per_thread_context *thread_context, long current_monotonic_wall_time_ns) {
1258
1265
  // We always create per_thread_context's with latest_max_frames; that value is kept in sync with the
1259
1266
  // active profiler's max_frames by the global reset that runs when profiling starts
1260
1267
  // so we expect to always see here the latest correct value to be used.
@@ -1291,7 +1298,7 @@ static void initialize_context(VALUE thread, per_thread_context *thread_context)
1291
1298
 
1292
1299
  thread_context->thread_cpu_time_id = thread_cpu_time_id_for(thread);
1293
1300
 
1294
- thread_context->wall_time_at_previous_sample_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1301
+ thread_context->wall_time_at_previous_sample_ns = current_monotonic_wall_time_ns;
1295
1302
  thread_context->cpu_time_at_previous_sample_ns = cpu_time_now_ns(thread_context);
1296
1303
 
1297
1304
  // These will only be used during a GC operation
@@ -1321,22 +1328,26 @@ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance)
1321
1328
 
1322
1329
  latest_max_frames = state->locations.len;
1323
1330
 
1331
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns();
1332
+
1324
1333
  VALUE threads = thread_list(state);
1325
1334
  const long thread_count = RARRAY_LEN(threads);
1326
1335
  for (long i = 0; i < thread_count; i++) {
1327
1336
  VALUE thread = rb_ary_entry(threads, i);
1337
+ ENFORCE_THREAD(thread);
1338
+
1328
1339
  per_thread_context *thread_context = get_per_thread_context(thread);
1329
1340
  if (thread_context != NULL) {
1330
1341
  bool is_profiler_internal_thread = thread_context->is_profiler_internal_thread;
1331
1342
 
1332
1343
  sampling_buffer_free(&thread_context->sampling_buffer);
1333
1344
  memset(thread_context, 0, sizeof(per_thread_context));
1334
- initialize_context(thread, thread_context);
1345
+ initialize_context(thread, thread_context, current_monotonic_wall_time_ns);
1335
1346
 
1336
1347
  thread_context->is_profiler_internal_thread = is_profiler_internal_thread;
1337
1348
  } else {
1338
1349
  // If thread didn't have a context, let's trigger its creation
1339
- get_or_create_context_for(thread);
1350
+ get_or_create_context_for(thread, current_monotonic_wall_time_ns);
1340
1351
  }
1341
1352
  }
1342
1353
  }
@@ -1439,7 +1450,9 @@ static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collec
1439
1450
  VALUE threads = thread_list(state);
1440
1451
  const long thread_count = RARRAY_LEN(threads);
1441
1452
  for (long i = 0; i < thread_count; i++) {
1442
- VALUE thread = RARRAY_AREF(threads, i);
1453
+ VALUE thread = rb_ary_entry(threads, i);
1454
+ ENFORCE_THREAD(thread);
1455
+
1443
1456
  per_thread_context *thread_context = get_per_thread_context(thread);
1444
1457
  if (thread_context != NULL) {
1445
1458
  rb_hash_aset(result, thread, per_thread_context_to_ruby_hash(thread_context));
@@ -1459,7 +1472,7 @@ static long update_time_since_previous_sample(long *time_at_previous_sample_ns,
1459
1472
  thread_context->thread_id,
1460
1473
  thread_context->gc_tracking.cpu_time_at_start_ns,
1461
1474
  thread_context->gc_tracking.wall_time_at_start_ns,
1462
- monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
1475
+ monotonic_wall_time_now_ns()
1463
1476
  );
1464
1477
  }
1465
1478
 
@@ -1483,10 +1496,7 @@ static long update_cpu_time_since_previous_sample(per_thread_context *thread_con
1483
1496
  thread_context
1484
1497
  );
1485
1498
 
1486
- // We don't expect cpu-time to go backwards, so let's flag this as a bug
1487
- if (elapsed_time_ns < 0) {
1488
- raise_error(rb_eRuntimeError, "BUG: Unexpected CPU time going backwards between samples");
1489
- }
1499
+ if (elapsed_time_ns < 0) raise_error(rb_eRuntimeError, "BUG: Unexpected CPU time going backwards between samples");
1490
1500
 
1491
1501
  return elapsed_time_ns;
1492
1502
  }
@@ -1498,11 +1508,9 @@ static long update_wall_time_since_previous_sample(per_thread_context *thread_co
1498
1508
  thread_context
1499
1509
  );
1500
1510
 
1501
- // Wall-time can actually go backwards (e.g. when the system clock gets set) so we can't assume time going backwards
1502
- // was a bug.
1503
- // @ivoanjo: I've also observed time going backwards spuriously on macOS, see discussion on
1504
- // https://github.com/DataDog/dd-trace-rb/pull/2336.
1505
- return long_max_of(elapsed_time_ns, 0);
1511
+ if (elapsed_time_ns < 0) raise_error(rb_eRuntimeError, "BUG: Unexpected wall time going backwards between samples");
1512
+
1513
+ return elapsed_time_ns;
1506
1514
  }
1507
1515
 
1508
1516
  // Safety: This function is assumed never to raise exceptions by callers
@@ -1675,14 +1683,7 @@ static VALUE thread_list(thread_context_collector_state *state) {
1675
1683
  return result;
1676
1684
  }
1677
1685
 
1678
- // Inside a signal handler, we don't want to do the whole work of recording a sample, but we only record the stack of
1679
- // the current thread.
1680
- //
1681
- // Assumptions for this function are same as for `thread_context_collector_sample` except that this function is
1682
- // expected to be called from a signal handler and to be async-signal-safe.
1683
- //
1684
- // Also, no allocation (Ruby or malloc) can happen.
1685
- bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1686
+ static inline bool thread_context_collector_prepare_sample_internal(void) {
1686
1687
  VALUE current_thread = rb_thread_current();
1687
1688
  per_thread_context *thread_context = get_per_thread_context(current_thread);
1688
1689
  if (thread_context == NULL) return false;
@@ -1690,13 +1691,26 @@ bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1690
1691
  return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1691
1692
  }
1692
1693
 
1694
+ // Called from a signal handler on the main Ractor with the GVL held, outside GC and with `during_sample` unset.
1695
+ // Must remain async-signal-safe: no allocation (Ruby or malloc), exceptions, or releasing the GVL.
1696
+ bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1697
+ return thread_context_collector_prepare_sample_internal();
1698
+ }
1699
+
1700
+ // Called at RUBY_INTERNAL_EVENT_GC_EXIT on the main Ractor with the GVL held; `during_sample` MUST be set to prevent reentrancy.
1701
+ // Ruby is still doing GC: no allocation (Ruby or malloc), exceptions, or releasing the GVL.
1702
+ void thread_context_collector_prepare_sample_on_gc_finish(void) {
1703
+ thread_context_collector_prepare_sample_internal();
1704
+ }
1705
+
1693
1706
  // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1694
1707
  // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1695
1708
  //
1696
1709
  // Returns true if `thread_context_collector_commit_heap_recordings_may_lose_gvl` needs to be called (to do work
1697
1710
  // that can't be done from inside the tracepoint, such as allocate new objects), and false if it doesn't
1698
1711
  //
1699
- // The callers must ensure thread_context is non-NULL.
1712
+ // Assumption 1: The callers must ensure thread_context is non-NULL.
1713
+ // Assumption 2: When called while the profiler is active, `during_sample` MUST be set.
1700
1714
  bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object) {
1701
1715
  thread_context_collector_state *state;
1702
1716
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -1942,6 +1956,7 @@ static void ddtrace_otel_trace_identifiers_for(
1942
1956
  *numeric_span_id = resolved_numeric_span_id;
1943
1957
  }
1944
1958
 
1959
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
1945
1960
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples) {
1946
1961
  thread_context_collector_state *state;
1947
1962
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -2141,70 +2156,79 @@ static void mark_thread_as_profiler_internal(per_thread_context *ctx) {
2141
2156
  }
2142
2157
 
2143
2158
  void thread_context_collector_profiler_internal_thread_started(void) {
2144
- per_thread_context *ctx = get_or_create_context_for(rb_thread_current());
2159
+ per_thread_context *ctx = get_or_create_context_for(rb_thread_current(), monotonic_wall_time_now_ns());
2145
2160
  mark_thread_as_profiler_internal(ctx);
2146
2161
  }
2147
2162
 
2148
2163
  static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread) {
2149
- per_thread_context *ctx = get_or_create_context_for(thread);
2164
+ per_thread_context *ctx = get_or_create_context_for(thread, monotonic_wall_time_now_ns());
2150
2165
  mark_thread_as_profiler_internal(ctx);
2151
2166
  return Qnil;
2152
2167
  }
2153
2168
 
2154
2169
  // Called via rb_ensure when a profiler-internal thread (worker or idle helper) is about to exit.
2155
- // Records a final sample so the thread's accumulated cpu/wall time since the last on_serialize
2156
- // flush is not lost. on_serialize (below) also flushes profiler-internal threads during periodic
2170
+ // Records a final sample so the thread's accumulated cpu/wall time since the last prepare_serialize
2171
+ // is not lost. prepare_serialize (below) also flushes profiler-internal threads during periodic
2157
2172
  // serialization, but it can't help at shutdown: by the time the final serialize runs, these
2158
2173
  // threads are already dead and absent from thread_list.
2159
- void thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2174
+ //
2175
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2176
+ VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2160
2177
  thread_context_collector_state *state;
2161
2178
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2162
2179
 
2163
2180
  VALUE current_thread = rb_thread_current();
2164
- per_thread_context *thread_context = get_or_create_context_for(current_thread);
2181
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns();
2182
+ per_thread_context *thread_context = get_or_create_context_for(current_thread, current_monotonic_wall_time_ns);
2165
2183
  if (!thread_context->is_profiler_internal_thread) {
2166
2184
  rb_raise(rb_eRuntimeError, "current thread %"PRIsVALUE" is not profiler-internal thread", current_thread);
2167
2185
  }
2168
2186
 
2169
- long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2170
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2171
-
2172
2187
  update_metrics_and_sample(
2173
2188
  state,
2174
2189
  current_thread,
2175
2190
  thread_context,
2176
- current_cpu_time_ns,
2177
2191
  current_monotonic_wall_time_ns,
2178
2192
  true);
2193
+
2194
+ return Qnil;
2179
2195
  }
2180
2196
 
2181
2197
  // Flushes threads whose last per-tick sample was skipped (either by the SUSPENDED-skip
2182
2198
  // optimization, or by is_profiler_internal_thread) so their accumulated time is recorded.
2183
- // Called by the stack recorder at the start of _native_serialize (regular periodic flush).
2184
- void thread_context_collector_on_serialize(VALUE self_instance) {
2199
+ // Called before serialization so that threads suspended across the whole profile period still get sampled.
2200
+ //
2201
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2202
+ VALUE thread_context_prepare_serialize(VALUE self_instance) {
2185
2203
  thread_context_collector_state *state;
2186
2204
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2187
2205
 
2188
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2206
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns();
2189
2207
  VALUE threads = thread_list(state);
2190
2208
  const long thread_count = RARRAY_LEN(threads);
2191
2209
 
2192
2210
  for (long i = 0; i < thread_count; i++) {
2193
- VALUE thread = RARRAY_AREF(threads, i);
2211
+ VALUE thread = rb_ary_entry(threads, i);
2212
+ ENFORCE_THREAD(thread);
2213
+
2194
2214
  per_thread_context *thread_context = get_per_thread_context(thread);
2195
2215
 
2196
2216
  if (thread_context != NULL && (thread_context->was_skipped_at_last_sample || thread_context->is_profiler_internal_thread)) {
2197
- long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2198
- // We need to force_sample=true otherwise this sample would be skipped too
2199
2217
  update_metrics_and_sample(
2200
2218
  state,
2201
2219
  thread,
2202
2220
  thread_context,
2203
- current_cpu_time_ns,
2204
2221
  current_monotonic_wall_time_ns,
2205
- true);
2222
+ true // We need to force_sample=true otherwise this sample would be skipped too
2223
+ );
2206
2224
  }
2207
2225
  }
2226
+
2227
+ return state->recorder_instance;
2228
+ }
2229
+
2230
+ static VALUE _native_prepare_serialize(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
2231
+ return thread_context_prepare_serialize(collector_instance);
2208
2232
  }
2209
2233
 
2210
2234
  void thread_context_collector_on_gvl_released(per_thread_context *thread_context) {
@@ -2212,10 +2236,7 @@ void thread_context_collector_on_gvl_released(per_thread_context *thread_context
2212
2236
  }
2213
2237
 
2214
2238
  void thread_context_collector_on_gvl_waiting(per_thread_context *thread_context) {
2215
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
2216
- if (current_monotonic_wall_time_ns <= 0) return;
2217
-
2218
- thread_context->gvl_waiting_at = current_monotonic_wall_time_ns;
2239
+ thread_context->gvl_waiting_at = monotonic_wall_time_now_ns();
2219
2240
  }
2220
2241
 
2221
2242
  static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
@@ -2246,6 +2267,8 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2246
2267
 
2247
2268
  #ifndef NO_GVL_INSTRUMENTATION
2248
2269
  // We must only use async-signal-safe functions here, see notes in the caller
2270
+ //
2271
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2249
2272
  __attribute__((warn_unused_result))
2250
2273
  on_gvl_running_result thread_context_collector_on_gvl_running(VALUE thread, per_thread_context *thread_context, uint32_t waiting_for_gvl_threshold_ns) {
2251
2274
  // Bump the event counter and clears the state bit to "running"
@@ -2262,7 +2285,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2262
2285
  return (on_gvl_running_result) {.action = ON_GVL_RUNNING_SAMPLE, .waiting_for_gvl_duration_ns = 0};
2263
2286
  }
2264
2287
 
2265
- long waiting_for_gvl_duration_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - gvl_waiting_at;
2288
+ long waiting_for_gvl_duration_ns = monotonic_wall_time_now_ns() - gvl_waiting_at;
2266
2289
 
2267
2290
  bool should_sample = waiting_for_gvl_duration_ns >= waiting_for_gvl_threshold_ns;
2268
2291
 
@@ -2322,7 +2345,8 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2322
2345
  //
2323
2346
  // ---
2324
2347
  //
2325
- // Always called with the GVL, either from a postponed_job or from tests.
2348
+ // Assumption 1: Always called with the GVL, either from a postponed_job or from tests.
2349
+ // Assumption 2: When called while the profiler is active, `during_sample` MUST be set.
2326
2350
  //
2327
2351
  // NOTE: In normal use, current_thread is expected to be == rb_thread_current(); the `current_thread` parameter only
2328
2352
  // exists to enable testing.
@@ -2330,7 +2354,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2330
2354
  thread_context_collector_state *state;
2331
2355
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2332
2356
 
2333
- per_thread_context *thread_context = get_or_create_context_for(current_thread);
2357
+ per_thread_context *thread_context = get_or_create_context_for(current_thread, current_monotonic_wall_time_ns);
2334
2358
 
2335
2359
  long gvl_waiting_at = thread_context->gvl_waiting_at;
2336
2360
 
@@ -2342,18 +2366,12 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2342
2366
  return Qfalse;
2343
2367
  }
2344
2368
 
2345
- // We don't actually account for cpu-time during Waiting for GVL. BUT, we may choose to push an
2346
- // extra sample to represent the period prior to Waiting for GVL. To support that, we retrieve the current
2347
- // cpu-time of the thread and let `update_metrics_and_sample` decide what to do with it.
2348
- long cpu_time_for_thread = cpu_time_now_ns(thread_context);
2349
-
2350
2369
  // TODO: Should we update the dynamic sampling rate overhead tracking with this sample as well?
2351
2370
 
2352
2371
  update_metrics_and_sample(
2353
2372
  state,
2354
2373
  current_thread,
2355
2374
  thread_context,
2356
- cpu_time_for_thread,
2357
2375
  current_monotonic_wall_time_ns,
2358
2376
  false);
2359
2377
 
@@ -2488,7 +2506,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2488
2506
  VALUE result = thread_context_collector_sample_after_gvl_running(
2489
2507
  collector_instance,
2490
2508
  thread,
2491
- monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
2509
+ monotonic_wall_time_now_ns()
2492
2510
  );
2493
2511
 
2494
2512
  debug_leave_unsafe_context();
@@ -2506,13 +2524,33 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2506
2524
 
2507
2525
  #endif // NO_GVL_INSTRUMENTATION
2508
2526
 
2509
- static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns) {
2527
+ static VALUE _native_apply_delta_to_time_at_previous_sample_ns(int argc, VALUE *argv, DDTRACE_UNUSED VALUE self) {
2528
+ VALUE thread;
2529
+ VALUE options;
2530
+ rb_scan_args(argc, argv, "1:", &thread, &options);
2531
+ if (options == Qnil) options = rb_hash_new();
2532
+
2533
+ VALUE cpu_time_delta_ns = rb_hash_lookup2(options, ID2SYM(rb_intern("cpu_time")), INT2FIX(0));
2534
+ VALUE wall_time_delta_ns = rb_hash_lookup2(options, ID2SYM(rb_intern("wall_time")), INT2FIX(0));
2535
+
2536
+ ENFORCE_THREAD(thread);
2537
+
2538
+ per_thread_context *thread_context = get_per_thread_context(thread);
2539
+ if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2540
+
2541
+ thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(cpu_time_delta_ns);
2542
+ thread_context->wall_time_at_previous_sample_ns += NUM2LONG(wall_time_delta_ns);
2543
+
2544
+ return Qtrue;
2545
+ }
2546
+
2547
+ static VALUE _native_apply_delta_to_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns) {
2510
2548
  ENFORCE_THREAD(thread);
2511
2549
 
2512
2550
  per_thread_context *thread_context = get_per_thread_context(thread);
2513
2551
  if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2514
2552
 
2515
- thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
2553
+ thread_context->gvl_waiting_at += NUM2LONG(delta_ns);
2516
2554
 
2517
2555
  return Qtrue;
2518
2556
  }
@@ -2555,8 +2593,7 @@ static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE c
2555
2593
  thread_context_collector_state *state;
2556
2594
  TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2557
2595
 
2558
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2559
- long system_epoch_time_ns = monotonic_to_system_epoch_ns(&state->time_converter_state, current_monotonic_wall_time_ns);
2596
+ long system_epoch_time_ns = monotonic_to_system_epoch_ns(&state->time_converter_state, monotonic_wall_time_now_ns());
2560
2597
 
2561
2598
  return LONG2NUM(system_epoch_time_ns);
2562
2599
  }