datadog 2.44.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +114 -141
  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 +13 -13
  7. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +1 -0
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +106 -103
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -3
  11. data/ext/datadog_profiling_native_extension/extconf.rb +2 -0
  12. data/ext/datadog_profiling_native_extension/heap_recorder.c +2 -2
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +98 -54
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +5 -2
  15. data/ext/datadog_profiling_native_extension/profiling.c +21 -23
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +3 -1
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
  18. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +6 -24
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +0 -1
  21. data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
  22. data/ext/datadog_profiling_native_extension/time_helpers.h +10 -11
  23. data/ext/libdatadog_api/trace_exporter.c +77 -11
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +5 -0
  26. data/lib/datadog/profiling/component.rb +2 -3
  27. data/lib/datadog/profiling/exporter.rb +7 -5
  28. data/lib/datadog/tracing/component.rb +1 -1
  29. data/lib/datadog/tracing/configuration/settings.rb +3 -2
  30. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +6 -3
  31. data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -0
  32. data/lib/datadog/tracing/transport/native.rb +22 -21
  33. data/lib/datadog/version.rb +1 -1
  34. metadata +5 -5
@@ -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.
@@ -207,7 +208,6 @@ typedef struct {
207
208
  // however this only matters for a very short period when a thread starts.
208
209
  struct per_thread_context {
209
210
  sampling_buffer sampling_buffer;
210
- bool prepare_sample_on_gc_finish;
211
211
  char thread_id[THREAD_ID_LIMIT_CHARS];
212
212
  ddog_CharSlice thread_id_char_slice;
213
213
  char thread_invoke_location[THREAD_INVOKE_LOCATION_LIMIT_CHARS];
@@ -269,7 +269,7 @@ struct per_thread_context {
269
269
  bool was_skipped_at_last_sample;
270
270
  // Set as true for CpuAndWallTimeWorker and IdleSamplingHelper threads.
271
271
  // When true, per-tick samples are skipped entirely; the thread is sampled only once per
272
- // reporting period during the on_serialize flush.
272
+ // reporting period during prepare_serialize.
273
273
  //
274
274
  // These threads are always in native code so their stacks aren't interesting;
275
275
  // the Profiling::Scheduler thread on the other hand does a lot of different
@@ -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,
@@ -385,8 +386,8 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
385
386
  static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key);
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
- static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self);
389
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);
390
391
  static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
391
392
  static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
392
393
  static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample);
@@ -425,10 +426,10 @@ void collectors_thread_context_init(VALUE profiling_module) {
425
426
  rb_define_singleton_method(testing_module, "_native_sample_skipped_allocation_samples", _native_sample_skipped_allocation_samples, 2);
426
427
  rb_define_singleton_method(testing_module, "_native_system_epoch_time_now_ns", _native_system_epoch_time_now_ns, 1);
427
428
  rb_define_singleton_method(testing_module, "_native_prepare_sample_inside_signal_handler", _native_prepare_sample_inside_signal_handler, 0);
428
- rb_define_singleton_method(testing_module, "_native_request_prepare_on_gc_finish", _native_request_prepare_on_gc_finish, 0);
429
429
  rb_define_singleton_method(testing_module, "_native_remove_per_thread_context_for", _native_remove_per_thread_context_for, 1);
430
430
  rb_define_singleton_method(testing_module, "_native_global_reset_per_thread_context", _native_global_reset_per_thread_context, 1);
431
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);
432
433
  rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
433
434
  rb_define_singleton_method(testing_module, "_native_on_gvl_released", _native_on_gvl_released, 1);
434
435
  #ifndef NO_GVL_INSTRUMENTATION
@@ -436,7 +437,8 @@ void collectors_thread_context_init(VALUE profiling_module) {
436
437
  rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 2);
437
438
  rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 2);
438
439
  #endif
439
- 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);
440
442
 
441
443
  at_active_span_id = rb_intern_const("@active_span");
442
444
  at_active_trace_id = rb_intern_const("@active_trace");
@@ -615,7 +617,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
615
617
  // Update this when modifying state struct
616
618
  sample_locations_initialize(&state->locations, max_frame_int, show_classes == Qtrue);
617
619
  state->recorder_instance = enforce_recorder_instance(recorder_instance);
618
- recorder_install_on_serialize(recorder_instance, self_instance);
619
620
  state->endpoint_collection_enabled = (endpoint_collection_enabled == Qtrue);
620
621
  state->native_filenames_enabled = (native_filenames_enabled == Qtrue);
621
622
  state->show_classes = (show_classes == Qtrue);
@@ -654,7 +655,7 @@ static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance
654
655
  if (allow_exception == Qfalse) debug_enter_unsafe_context();
655
656
 
656
657
  bool needs_otel_span_key =
657
- 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());
658
659
 
659
660
  if (allow_exception == Qfalse) debug_leave_unsafe_context();
660
661
 
@@ -681,7 +682,7 @@ static VALUE _native_on_gc_start(DDTRACE_UNUSED VALUE self, VALUE collector_inst
681
682
  static VALUE _native_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
682
683
  debug_enter_unsafe_context();
683
684
 
684
- (void) !thread_context_collector_on_gc_finish(collector_instance, true);
685
+ (void) !thread_context_collector_on_gc_finish(collector_instance);
685
686
 
686
687
  debug_leave_unsafe_context();
687
688
 
@@ -700,7 +701,7 @@ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_
700
701
 
701
702
  // Record profiler sampling overhead as a placeholder stack
702
703
  static void record_sampling_overhead(thread_context_collector_state *state, per_thread_context *current_thread_context) {
703
- long wall_time_after_sampling = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
704
+ long wall_time_after_sampling = monotonic_wall_time_now_ns();
704
705
  long cpu_time_after_sampling = cpu_time_now_ns(current_thread_context);
705
706
 
706
707
  long overhead_cpu_time_ns = update_cpu_time_since_previous_sample(current_thread_context, cpu_time_after_sampling);
@@ -750,7 +751,7 @@ bool thread_context_collector_sample(VALUE self_instance, long current_monotonic
750
751
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
751
752
 
752
753
  VALUE current_thread = rb_thread_current();
753
- per_thread_context *current_thread_context = get_or_create_context_for(current_thread);
754
+ per_thread_context *current_thread_context = get_or_create_context_for(current_thread, current_monotonic_wall_time_ns);
754
755
 
755
756
  // Sample the current thread (which will use the current CPU-time) first.
756
757
  // This ensures any CPU-time the profiler spends sampling (e.g. the rest of the function) is not wrongly blamed on this thread,
@@ -774,7 +775,7 @@ bool thread_context_collector_sample(VALUE self_instance, long current_monotonic
774
775
  update_metrics_and_sample(
775
776
  state,
776
777
  thread,
777
- get_or_create_context_for(thread),
778
+ get_or_create_context_for(thread, current_monotonic_wall_time_ns),
778
779
  current_monotonic_wall_time_ns,
779
780
  false);
780
781
  }
@@ -807,26 +808,27 @@ static void update_metrics_and_sample(
807
808
 
808
809
  if (skip_sample(state, thread_context, is_gvl_waiting_state, force_sample)) return;
809
810
 
810
- // Don't assign/update cpu during "Waiting for GVL"
811
- long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_cpu_time_since_previous_sample(thread_context, current_cpu_time_ns);
812
-
813
- long wall_time_elapsed_ns = update_wall_time_since_previous_sample(thread_context, current_monotonic_wall_time_ns);
814
-
815
811
  // A thread enters "Waiting for GVL", well, as the name implies, without the GVL.
816
812
  //
817
- // 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
818
814
  // on sampling, and thus for the `current_monotonic_wall_time_ns` (which is recorded at the start of sampling)
819
- // to be < the time at which we started Waiting for GVL.
815
+ // to be <= the time we observe for a "Waiting for GVL".
820
816
  //
821
- // All together, this means that when `handle_gvl_waiting` creates an extra sample (see comments on that function for
822
- // what the extra sample is), it's possible that there's no more wall-time to be assigned.
823
- // 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
824
821
  // thus we skip creating such a sample.
825
- 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;
826
823
  // ...you may also wonder: is there any other situation where it makes sense to produce a sample with
827
824
  // wall_time_elapsed_ns == 0? I believe that yes, because the sample still includes a timestamp and a stack, but we
828
825
  // may revisit/change our minds on this in the future.
829
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
+
830
832
  trigger_sample_for_thread(
831
833
  state,
832
834
  thread_being_sampled,
@@ -851,7 +853,7 @@ static bool skip_sample(thread_context_collector_state *state, per_thread_contex
851
853
  // Skip this per-tick sample entirely when the thread does not have the GVL and did not acquire
852
854
  // it since the previous sample: its Ruby-level stack has not changed. The skipped wall-time will
853
855
  // be picked up by either by an extra sample when the thread acquires the GVL, or by
854
- // the on-serialize flush in the stack recorder (using was_skipped_at_last_sample).
856
+ // prepare_serialize (using was_skipped_at_last_sample).
855
857
  // The check is gated by `!is_gvl_waiting_state` so the existing "Waiting for GVL" machinery
856
858
  // in handle_gvl_waiting (situation 1 extra sample, situation 2 regular sample) keeps running.
857
859
  // TODO: we could probably also skip while "Waiting for GVL"
@@ -896,16 +898,14 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
896
898
  }
897
899
 
898
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
899
- long wall_time_at_start_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
900
-
901
- // If our start timestamp is not OK, we skip tracking this GC as well
902
- if (wall_time_at_start_ns == 0) return;
903
-
904
- 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();
905
902
  thread_context->gc_tracking.cpu_time_at_start_ns = cpu_time_now_ns(thread_context);
906
903
  }
907
904
 
908
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
+ //
909
909
  // It records the cpu/wall-time observed during GC, which will be used to later
910
910
  // create an event including the cpu/wall time spent from the start of garbage collector work until now.
911
911
  //
@@ -915,9 +915,8 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
915
915
  //
916
916
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
917
917
  // Assumption 2: This function is called from the main Ractor (if Ruby has support for Ractors).
918
- // Assumption 3: If allow_prepare_sample is true while profiling is active, during_sample MUST be set by the caller.
919
918
  __attribute__((warn_unused_result))
920
- bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepare_sample) {
919
+ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
921
920
  thread_context_collector_state *state;
922
921
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
923
922
  // This should never fail when the above check passes
@@ -928,11 +927,6 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepa
928
927
  // Context is created eagerly, so this should not normally be NULL (see on_gc_start).
929
928
  if (thread_context == NULL) return false;
930
929
 
931
- if (thread_context->prepare_sample_on_gc_finish) {
932
- thread_context->prepare_sample_on_gc_finish = false;
933
- if (allow_prepare_sample) prepare_sample_thread(rb_thread_current(), &thread_context->sampling_buffer);
934
- }
935
-
936
930
  long cpu_time_at_start_ns = thread_context->gc_tracking.cpu_time_at_start_ns;
937
931
  long wall_time_at_start_ns = thread_context->gc_tracking.wall_time_at_start_ns;
938
932
 
@@ -950,16 +944,13 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepa
950
944
 
951
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
952
946
  long cpu_time_at_finish_ns = cpu_time_now_ns(thread_context);
953
- long wall_time_at_finish_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
954
-
955
- // If our end timestamp is not OK, we bail out
956
- if (wall_time_at_finish_ns == 0) return false;
947
+ long wall_time_at_finish_ns = monotonic_wall_time_now_ns();
957
948
 
958
949
  long gc_cpu_time_elapsed_ns = cpu_time_at_finish_ns - cpu_time_at_start_ns;
959
950
  long gc_wall_time_elapsed_ns = wall_time_at_finish_ns - wall_time_at_start_ns;
960
951
 
961
- // Wall-time can go backwards if the system clock gets changed (and we observed spurious jumps back on macOS as well)
962
- // 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.
963
954
  gc_cpu_time_elapsed_ns = long_max_of(gc_cpu_time_elapsed_ns, 0);
964
955
  gc_wall_time_elapsed_ns = long_max_of(gc_wall_time_elapsed_ns, 0);
965
956
 
@@ -982,10 +973,10 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepa
982
973
  // Let the caller know if it should schedule a flush or not. Returning true every time would cause a lot of overhead
983
974
  // on the application (see GC tracking introduction at the top of the file), so instead we try to accumulate a few
984
975
  // samples first.
985
- bool over_flush_time_treshold =
976
+ bool over_flush_time_threshold =
986
977
  (wall_time_at_finish_ns - state->gc_tracking.wall_time_at_last_flushed_gc_event_ns) >= TIME_BETWEEN_GC_EVENTS_NS;
987
978
 
988
- if (over_flush_time_treshold) {
979
+ if (over_flush_time_threshold) {
989
980
  return true;
990
981
  } else {
991
982
  return gc_profiling_has_major_gc_finished();
@@ -1224,14 +1215,14 @@ static void check_frozen_thread(VALUE thread) {
1224
1215
 
1225
1216
  // See the docs on struct per_thread_context.
1226
1217
  // This allocates a Ruby object and therefore needs the GVL and is not safe to call from RUBY_INTERNAL_EVENT_* hooks.
1227
- 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) {
1228
1219
  per_thread_context *thread_context = get_per_thread_context(thread);
1229
1220
  if (thread_context != NULL) return thread_context;
1230
1221
 
1231
1222
  check_frozen_thread(thread);
1232
1223
 
1233
1224
  thread_context = calloc(1, sizeof(per_thread_context)); // See "note on calloc vs ruby_xcalloc use" in heap_recorder.c
1234
- initialize_context(thread, thread_context);
1225
+ initialize_context(thread, thread_context, current_monotonic_wall_time_ns);
1235
1226
 
1236
1227
  VALUE wrapper = TypedData_Wrap_Struct(rb_cObject, &per_thread_context_typed_data, thread_context);
1237
1228
  rb_ivar_set(thread, dd_per_thread_context_id, wrapper);
@@ -1245,7 +1236,8 @@ static void on_thread_begin_event(VALUE tracepoint_data, DDTRACE_UNUSED void *un
1245
1236
 
1246
1237
  VALUE thread = rb_tracearg_self(rb_tracearg_from_tracepoint(tracepoint_data));
1247
1238
  ENFORCE_THREAD(thread);
1248
- get_or_create_context_for(thread);
1239
+
1240
+ get_or_create_context_for(thread, monotonic_wall_time_now_ns());
1249
1241
  }
1250
1242
 
1251
1243
  #define LOGGING_GEM_PATH "/lib/logging/diagnostic_context.rb"
@@ -1269,7 +1261,7 @@ static bool is_logging_gem_monkey_patch(VALUE invoke_file_location) {
1269
1261
  return strncmp(invoke_file + invoke_file_len - logging_gem_path_len, LOGGING_GEM_PATH, logging_gem_path_len) == 0;
1270
1262
  }
1271
1263
 
1272
- 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) {
1273
1265
  // We always create per_thread_context's with latest_max_frames; that value is kept in sync with the
1274
1266
  // active profiler's max_frames by the global reset that runs when profiling starts
1275
1267
  // so we expect to always see here the latest correct value to be used.
@@ -1306,7 +1298,7 @@ static void initialize_context(VALUE thread, per_thread_context *thread_context)
1306
1298
 
1307
1299
  thread_context->thread_cpu_time_id = thread_cpu_time_id_for(thread);
1308
1300
 
1309
- 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;
1310
1302
  thread_context->cpu_time_at_previous_sample_ns = cpu_time_now_ns(thread_context);
1311
1303
 
1312
1304
  // These will only be used during a GC operation
@@ -1336,6 +1328,8 @@ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance)
1336
1328
 
1337
1329
  latest_max_frames = state->locations.len;
1338
1330
 
1331
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns();
1332
+
1339
1333
  VALUE threads = thread_list(state);
1340
1334
  const long thread_count = RARRAY_LEN(threads);
1341
1335
  for (long i = 0; i < thread_count; i++) {
@@ -1348,12 +1342,12 @@ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance)
1348
1342
 
1349
1343
  sampling_buffer_free(&thread_context->sampling_buffer);
1350
1344
  memset(thread_context, 0, sizeof(per_thread_context));
1351
- initialize_context(thread, thread_context);
1345
+ initialize_context(thread, thread_context, current_monotonic_wall_time_ns);
1352
1346
 
1353
1347
  thread_context->is_profiler_internal_thread = is_profiler_internal_thread;
1354
1348
  } else {
1355
1349
  // If thread didn't have a context, let's trigger its creation
1356
- get_or_create_context_for(thread);
1350
+ get_or_create_context_for(thread, current_monotonic_wall_time_ns);
1357
1351
  }
1358
1352
  }
1359
1353
  }
@@ -1410,7 +1404,6 @@ static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context)
1410
1404
  ID2SYM(rb_intern("gvl_state_change_count")), /* => */ ULL2NUM(thread_context->gvl_state_change_count),
1411
1405
  ID2SYM(rb_intern("gvl_state_change_count_at_previous_sample")), /* => */ ULL2NUM(thread_context->gvl_state_change_count_at_previous_sample),
1412
1406
  ID2SYM(rb_intern("was_skipped_at_last_sample")), /* => */ thread_context->was_skipped_at_last_sample ? Qtrue : Qfalse,
1413
- ID2SYM(rb_intern("prepare_sample_on_gc_finish")), /* => */ thread_context->prepare_sample_on_gc_finish ? Qtrue : Qfalse,
1414
1407
  ID2SYM(rb_intern("is_profiler_internal_thread")), /* => */ thread_context->is_profiler_internal_thread ? Qtrue : Qfalse,
1415
1408
  };
1416
1409
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(context_as_hash, arguments[i], arguments[i+1]);
@@ -1479,7 +1472,7 @@ static long update_time_since_previous_sample(long *time_at_previous_sample_ns,
1479
1472
  thread_context->thread_id,
1480
1473
  thread_context->gc_tracking.cpu_time_at_start_ns,
1481
1474
  thread_context->gc_tracking.wall_time_at_start_ns,
1482
- monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
1475
+ monotonic_wall_time_now_ns()
1483
1476
  );
1484
1477
  }
1485
1478
 
@@ -1503,10 +1496,7 @@ static long update_cpu_time_since_previous_sample(per_thread_context *thread_con
1503
1496
  thread_context
1504
1497
  );
1505
1498
 
1506
- // We don't expect cpu-time to go backwards, so let's flag this as a bug
1507
- if (elapsed_time_ns < 0) {
1508
- raise_error(rb_eRuntimeError, "BUG: Unexpected CPU time going backwards between samples");
1509
- }
1499
+ if (elapsed_time_ns < 0) raise_error(rb_eRuntimeError, "BUG: Unexpected CPU time going backwards between samples");
1510
1500
 
1511
1501
  return elapsed_time_ns;
1512
1502
  }
@@ -1518,11 +1508,9 @@ static long update_wall_time_since_previous_sample(per_thread_context *thread_co
1518
1508
  thread_context
1519
1509
  );
1520
1510
 
1521
- // Wall-time can actually go backwards (e.g. when the system clock gets set) so we can't assume time going backwards
1522
- // was a bug.
1523
- // @ivoanjo: I've also observed time going backwards spuriously on macOS, see discussion on
1524
- // https://github.com/DataDog/dd-trace-rb/pull/2336.
1525
- 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;
1526
1514
  }
1527
1515
 
1528
1516
  // Safety: This function is assumed never to raise exceptions by callers
@@ -1695,14 +1683,7 @@ static VALUE thread_list(thread_context_collector_state *state) {
1695
1683
  return result;
1696
1684
  }
1697
1685
 
1698
- // Inside a signal handler, we don't want to do the whole work of recording a sample, but we only record the stack of
1699
- // the current thread.
1700
- //
1701
- // Assumptions for this function are same as for `thread_context_collector_sample` except that this function is
1702
- // expected to be called from a signal handler and to be async-signal-safe, and `during_sample` MUST be unset.
1703
- //
1704
- // Also, no allocation (Ruby or malloc) can happen.
1705
- bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1686
+ static inline bool thread_context_collector_prepare_sample_internal(void) {
1706
1687
  VALUE current_thread = rb_thread_current();
1707
1688
  per_thread_context *thread_context = get_per_thread_context(current_thread);
1708
1689
  if (thread_context == NULL) return false;
@@ -1710,10 +1691,16 @@ bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1710
1691
  return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1711
1692
  }
1712
1693
 
1713
- // Called from the sampling signal handler; must not allocate or inspect the Ruby stack during GC.
1714
- void thread_context_collector_request_prepare_on_gc_finish(void) {
1715
- per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
1716
- if (thread_context != NULL) thread_context->prepare_sample_on_gc_finish = true;
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();
1717
1704
  }
1718
1705
 
1719
1706
  // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
@@ -2169,19 +2156,19 @@ static void mark_thread_as_profiler_internal(per_thread_context *ctx) {
2169
2156
  }
2170
2157
 
2171
2158
  void thread_context_collector_profiler_internal_thread_started(void) {
2172
- 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());
2173
2160
  mark_thread_as_profiler_internal(ctx);
2174
2161
  }
2175
2162
 
2176
2163
  static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread) {
2177
- 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());
2178
2165
  mark_thread_as_profiler_internal(ctx);
2179
2166
  return Qnil;
2180
2167
  }
2181
2168
 
2182
2169
  // Called via rb_ensure when a profiler-internal thread (worker or idle helper) is about to exit.
2183
- // Records a final sample so the thread's accumulated cpu/wall time since the last on_serialize
2184
- // 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
2185
2172
  // serialization, but it can't help at shutdown: by the time the final serialize runs, these
2186
2173
  // threads are already dead and absent from thread_list.
2187
2174
  //
@@ -2191,13 +2178,12 @@ VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance
2191
2178
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2192
2179
 
2193
2180
  VALUE current_thread = rb_thread_current();
2194
- 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);
2195
2183
  if (!thread_context->is_profiler_internal_thread) {
2196
2184
  rb_raise(rb_eRuntimeError, "current thread %"PRIsVALUE" is not profiler-internal thread", current_thread);
2197
2185
  }
2198
2186
 
2199
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2200
-
2201
2187
  update_metrics_and_sample(
2202
2188
  state,
2203
2189
  current_thread,
@@ -2210,14 +2196,14 @@ VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance
2210
2196
 
2211
2197
  // Flushes threads whose last per-tick sample was skipped (either by the SUSPENDED-skip
2212
2198
  // optimization, or by is_profiler_internal_thread) so their accumulated time is recorded.
2213
- // Called by the stack recorder at the start of _native_serialize (regular periodic flush).
2199
+ // Called before serialization so that threads suspended across the whole profile period still get sampled.
2214
2200
  //
2215
2201
  // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2216
- void thread_context_collector_on_serialize(VALUE self_instance) {
2202
+ VALUE thread_context_prepare_serialize(VALUE self_instance) {
2217
2203
  thread_context_collector_state *state;
2218
2204
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2219
2205
 
2220
- 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();
2221
2207
  VALUE threads = thread_list(state);
2222
2208
  const long thread_count = RARRAY_LEN(threads);
2223
2209
 
@@ -2237,6 +2223,12 @@ void thread_context_collector_on_serialize(VALUE self_instance) {
2237
2223
  );
2238
2224
  }
2239
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);
2240
2232
  }
2241
2233
 
2242
2234
  void thread_context_collector_on_gvl_released(per_thread_context *thread_context) {
@@ -2244,10 +2236,7 @@ void thread_context_collector_on_gvl_released(per_thread_context *thread_context
2244
2236
  }
2245
2237
 
2246
2238
  void thread_context_collector_on_gvl_waiting(per_thread_context *thread_context) {
2247
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
2248
- if (current_monotonic_wall_time_ns <= 0) return;
2249
-
2250
- thread_context->gvl_waiting_at = current_monotonic_wall_time_ns;
2239
+ thread_context->gvl_waiting_at = monotonic_wall_time_now_ns();
2251
2240
  }
2252
2241
 
2253
2242
  static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
@@ -2296,7 +2285,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2296
2285
  return (on_gvl_running_result) {.action = ON_GVL_RUNNING_SAMPLE, .waiting_for_gvl_duration_ns = 0};
2297
2286
  }
2298
2287
 
2299
- 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;
2300
2289
 
2301
2290
  bool should_sample = waiting_for_gvl_duration_ns >= waiting_for_gvl_threshold_ns;
2302
2291
 
@@ -2365,7 +2354,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2365
2354
  thread_context_collector_state *state;
2366
2355
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2367
2356
 
2368
- 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);
2369
2358
 
2370
2359
  long gvl_waiting_at = thread_context->gvl_waiting_at;
2371
2360
 
@@ -2517,7 +2506,7 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2517
2506
  VALUE result = thread_context_collector_sample_after_gvl_running(
2518
2507
  collector_instance,
2519
2508
  thread,
2520
- monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
2509
+ monotonic_wall_time_now_ns()
2521
2510
  );
2522
2511
 
2523
2512
  debug_leave_unsafe_context();
@@ -2535,13 +2524,33 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2535
2524
 
2536
2525
  #endif // NO_GVL_INSTRUMENTATION
2537
2526
 
2538
- 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
+
2539
2536
  ENFORCE_THREAD(thread);
2540
2537
 
2541
2538
  per_thread_context *thread_context = get_per_thread_context(thread);
2542
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");
2543
2540
 
2544
- thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
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) {
2548
+ ENFORCE_THREAD(thread);
2549
+
2550
+ per_thread_context *thread_context = get_per_thread_context(thread);
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");
2552
+
2553
+ thread_context->gvl_waiting_at += NUM2LONG(delta_ns);
2545
2554
 
2546
2555
  return Qtrue;
2547
2556
  }
@@ -2584,8 +2593,7 @@ static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE c
2584
2593
  thread_context_collector_state *state;
2585
2594
  TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2586
2595
 
2587
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2588
- 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());
2589
2597
 
2590
2598
  return LONG2NUM(system_epoch_time_ns);
2591
2599
  }
@@ -2593,8 +2601,3 @@ static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE c
2593
2601
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self) {
2594
2602
  return thread_context_collector_prepare_sample_inside_signal_handler() ? Qtrue : Qfalse;
2595
2603
  }
2596
-
2597
- static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self) {
2598
- thread_context_collector_request_prepare_on_gc_finish();
2599
- return Qnil;
2600
- }
@@ -13,7 +13,7 @@ __attribute__((warn_unused_result)) bool thread_context_collector_sample(
13
13
  long current_monotonic_wall_time_ns
14
14
  );
15
15
  __attribute__((warn_unused_result)) bool thread_context_collector_prepare_sample_inside_signal_handler(void);
16
- void thread_context_collector_request_prepare_on_gc_finish(void);
16
+ void thread_context_collector_prepare_sample_on_gc_finish(void);
17
17
  __attribute__((warn_unused_result)) bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object);
18
18
  void thread_context_collector_commit_heap_recordings_may_lose_gvl(VALUE self_instance);
19
19
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples);
@@ -21,11 +21,11 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance);
21
21
  VALUE thread_context_collector_heap_update_may_lose_gvl(VALUE self_instance);
22
22
  void thread_context_collector_resolve_otel_span_key_may_lose_gvl(VALUE self_instance);
23
23
  void thread_context_collector_on_gc_start(VALUE self_instance);
24
- __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepare_sample);
24
+ __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance);
25
25
  VALUE enforce_thread_context_collector_instance(VALUE object);
26
26
  void thread_context_collector_stats(VALUE self_instance, VALUE stats_hash);
27
27
  void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance);
28
- void thread_context_collector_on_serialize(VALUE self_instance);
28
+ VALUE thread_context_prepare_serialize(VALUE self_instance);
29
29
  void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance);
30
30
  void thread_context_collector_profiler_internal_thread_started(void);
31
31
  VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance);
@@ -139,6 +139,8 @@ end
139
139
 
140
140
  have_func "malloc_stats"
141
141
 
142
+ $defs << "-DHAVE_ZJIT_FRAME" if RUBY_VERSION >= "4.1"
143
+
142
144
  # On older Rubies, there was no primitive mutex and condition variable implemented in `thread_sync.rb` (internal)
143
145
  $defs << "-DNO_PRIMITIVE_MUTEX_AND_CONDITION_VARIABLE" if RUBY_VERSION < "4"
144
146
 
@@ -627,7 +627,7 @@ static VALUE heap_recorder_update_locked(VALUE heap_recorder_update_locked_args_
627
627
  }
628
628
 
629
629
  size_t current_gc_gen = rb_gc_count();
630
- long now_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
630
+ long now_ns = monotonic_wall_time_now_ns();
631
631
 
632
632
  if (!full_update) {
633
633
  if (current_gc_gen == heap_recorder->update_gen) {
@@ -641,7 +641,7 @@ static VALUE heap_recorder_update_locked(VALUE heap_recorder_update_locked_args_
641
641
  return Qnil;
642
642
  }
643
643
 
644
- if (now_ns > 0 && (now_ns - heap_recorder->last_update_ns) < MIN_TIME_BETWEEN_HEAP_RECORDER_UPDATES_NS) {
644
+ if ((now_ns - heap_recorder->last_update_ns) < MIN_TIME_BETWEEN_HEAP_RECORDER_UPDATES_NS) {
645
645
  // We did an update not too long ago. Let's skip this one to avoid over-taxing the system.
646
646
  heap_recorder->stats_lifetime.updates_skipped_time++;
647
647
  return Qnil;