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
@@ -16,6 +16,7 @@
16
16
  #include "private_vm_api_access.h"
17
17
  #include "setup_signal_handler.h"
18
18
  #include "time_helpers.h"
19
+ #include "unsafe_api_calls_check.h"
19
20
 
20
21
  #ifdef __APPLE__
21
22
  #include "macos_sampler_thread.h"
@@ -122,6 +123,7 @@ typedef struct {
122
123
  VALUE self_instance;
123
124
  VALUE thread_context_collector_instance;
124
125
  VALUE idle_sampling_helper_instance;
126
+ idle_sampling_loop_state *idle_sampling_helper_state;
125
127
  VALUE owner_thread;
126
128
  dynamic_sampling_rate_state cpu_dynamic_sampling_rate;
127
129
  discrete_dynamic_sampler allocation_sampler;
@@ -145,6 +147,7 @@ typedef struct {
145
147
  //
146
148
  // It's not an actual lock because we rely on the GVL for correct synchronization
147
149
  // (and thus this flag is only valid when we know we have the GVL).
150
+ // Exception: `on_gvl_event` sets it for RUBY_INTERNAL_THREAD_EVENT_RESUMED without the GVL -- see the notes there.
148
151
  //
149
152
  // Similar to a lock, it should not be held across long-running operations,
150
153
  // **in particular it MUST NEVER be held during operations where we might lose the GVL**
@@ -157,6 +160,10 @@ typedef struct {
157
160
  // @ivoanjo: Right now we always sample inside `safely_call`; if that ever changes, this flag may need to become
158
161
  // volatile/atomic/have some barriers to ensure it's visible during e.g. signal handlers.
159
162
  bool during_sample;
163
+ // Used from the signal handler to ask for a sample at the end of GC.
164
+ // The GC_EXIT event is emitted by the thread that had the GVL + was doing GC, e.g. Ruby won't switch threads between
165
+ // this flag getting set in the signal handler and the GC_EXIT event.
166
+ bool prepare_sample_on_gc_finish;
160
167
 
161
168
  #ifndef NO_GVL_INSTRUMENTATION
162
169
  // Only set when sampling is active (gets created at start and cleaned on stop)
@@ -228,6 +235,8 @@ static VALUE _native_new(VALUE klass);
228
235
  static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
229
236
  static void cpu_and_wall_time_worker_typed_data_mark(void *state_ptr);
230
237
  static VALUE _native_sampling_loop(VALUE self, VALUE instance);
238
+ static VALUE _native_profiler_internal_thread_done(VALUE self_instance);
239
+ static VALUE _native_prepare_serialize(DDTRACE_UNUSED VALUE self, VALUE self_instance);
231
240
  static VALUE _native_stop(DDTRACE_UNUSED VALUE _self, VALUE self_instance, VALUE worker_thread);
232
241
  static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *optional_exception_during_operation);
233
242
  static void stop_state(cpu_and_wall_time_worker_state *state, VALUE optional_exception, const char *optional_operation_name);
@@ -243,10 +252,12 @@ static VALUE _native_failure_exception_during_operation(DDTRACE_UNUSED VALUE sel
243
252
  static void testing_signal_handler(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, DDTRACE_UNUSED void *_ucontext);
244
253
  static VALUE _native_install_testing_signal_handler(DDTRACE_UNUSED VALUE self);
245
254
  static VALUE _native_remove_testing_signal_handler(DDTRACE_UNUSED VALUE self);
246
- static VALUE _native_install_sigprof_handler_on_altstack(DDTRACE_UNUSED VALUE self);
247
255
  static VALUE _native_trigger_sample(DDTRACE_UNUSED VALUE self);
248
256
  static VALUE _native_gc_tracepoint(DDTRACE_UNUSED VALUE self, VALUE instance);
249
257
  static void on_gc_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused);
258
+ static void handle_gc_event(cpu_and_wall_time_worker_state *state, rb_event_flag_t event);
259
+ static VALUE _native_on_gc_event(VALUE self, VALUE instance, VALUE event, VALUE during_sample);
260
+ static VALUE _native_request_prepare_on_gc_finish(VALUE self, VALUE instance);
250
261
  static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused);
251
262
  static VALUE safely_call(
252
263
  VALUE (*function_to_call_safely)(VALUE),
@@ -271,7 +282,6 @@ static VALUE _native_with_blocked_sigprof(DDTRACE_UNUSED VALUE self);
271
282
  static VALUE rescued_sample_allocation(VALUE tracepoint_data);
272
283
  static VALUE rescued_commit_heap_recordings_may_lose_gvl(VALUE self_instance);
273
284
  static void delayed_error(cpu_and_wall_time_worker_state *state, const char *error);
274
- static void delayed_error_clock_failure(cpu_and_wall_time_worker_state *state);
275
285
  static VALUE _native_delayed_error(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE error_msg);
276
286
  static VALUE _native_hold_signals(DDTRACE_UNUSED VALUE self);
277
287
  static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self);
@@ -315,16 +325,6 @@ static void commit_heap_recordings_from_postponed_job_may_lose_gvl(DDTRACE_UNUSE
315
325
  // (e.g. signal handler) where it's impossible or just awkward to pass it as an argument.
316
326
  static VALUE active_sampler_instance = Qnil;
317
327
  static cpu_and_wall_time_worker_state *active_sampler_instance_state = NULL;
318
- static VALUE clock_failure_exception_class = Qnil;
319
-
320
- // Stats that live outside of any particular `cpu_and_wall_time_worker_state`, so that they can always be safely
321
- // touched, even when we're not sure it's safe to touch the state (e.g. signal handler, no GVL, etc).
322
- typedef struct {
323
- // How many times we skipped sampling because we were running on the alternate signal stack (e.g. nested inside
324
- // another signal handler, presumably GC compaction).
325
- unsigned int signal_handler_skipped_sample_on_altstack;
326
- } global_stats_t;
327
- static global_stats_t global_stats;
328
328
 
329
329
  // See handle_sampling_signal for details on what this does
330
330
  #ifdef NO_POSTPONED_TRIGGER
@@ -365,8 +365,6 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
365
365
  VALUE collectors_cpu_and_wall_time_worker_class = rb_define_class_under(collectors_module, "CpuAndWallTimeWorker", rb_cObject);
366
366
  // Hosts methods used for testing the native code using RSpec
367
367
  VALUE testing_module = rb_define_module_under(collectors_cpu_and_wall_time_worker_class, "Testing");
368
- clock_failure_exception_class = rb_define_class_under(collectors_cpu_and_wall_time_worker_class, "ClockFailure", rb_eRuntimeError);
369
- rb_gc_register_mark_object(clock_failure_exception_class);
370
368
 
371
369
  // Instances of the CpuAndWallTimeWorker class are "TypedData" objects.
372
370
  // "TypedData" objects are special objects in the Ruby VM that can wrap C structs.
@@ -380,6 +378,8 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
380
378
 
381
379
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_initialize", _native_initialize, -1);
382
380
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_sampling_loop", _native_sampling_loop, 1);
381
+ rb_define_method(collectors_cpu_and_wall_time_worker_class, "_native_profiler_internal_thread_done", _native_profiler_internal_thread_done, 0);
382
+ rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_prepare_serialize", _native_prepare_serialize, 1);
383
383
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_stop", _native_stop, 2);
384
384
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
385
385
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_stats", _native_stats, 1);
@@ -392,9 +392,10 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
392
392
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_resume_signals", _native_resume_signals, 0);
393
393
  rb_define_singleton_method(testing_module, "_native_install_testing_signal_handler", _native_install_testing_signal_handler, 0);
394
394
  rb_define_singleton_method(testing_module, "_native_remove_testing_signal_handler", _native_remove_testing_signal_handler, 0);
395
- rb_define_singleton_method(testing_module, "_native_install_sigprof_handler_on_altstack", _native_install_sigprof_handler_on_altstack, 0);
396
395
  rb_define_singleton_method(testing_module, "_native_trigger_sample", _native_trigger_sample, 0);
397
396
  rb_define_singleton_method(testing_module, "_native_gc_tracepoint", _native_gc_tracepoint, 1);
397
+ rb_define_singleton_method(testing_module, "_native_on_gc_event", _native_on_gc_event, 3);
398
+ rb_define_singleton_method(testing_module, "_native_request_prepare_on_gc_finish", _native_request_prepare_on_gc_finish, 1);
398
399
  rb_define_singleton_method(testing_module, "_native_simulate_handle_sampling_signal", _native_simulate_handle_sampling_signal, 0);
399
400
  rb_define_singleton_method(testing_module, "_native_simulate_sample_from_postponed_job", _native_simulate_sample_from_postponed_job, 0);
400
401
  rb_define_singleton_method(testing_module, "_native_is_sigprof_blocked_in_current_thread", _native_is_sigprof_blocked_in_current_thread, 0);
@@ -417,7 +418,7 @@ static const rb_data_type_t cpu_and_wall_time_worker_typed_data = {
417
418
  };
418
419
 
419
420
  static VALUE _native_new(VALUE klass) {
420
- long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
421
+ long now = monotonic_wall_time_now_ns();
421
422
 
422
423
  cpu_and_wall_time_worker_state *state = ruby_xcalloc(1, sizeof(cpu_and_wall_time_worker_state));
423
424
 
@@ -436,6 +437,7 @@ static VALUE _native_new(VALUE klass) {
436
437
  state->waiting_for_gvl_threshold_ns = 10 * 1000 * 1000;
437
438
  state->thread_context_collector_instance = Qnil;
438
439
  state->idle_sampling_helper_instance = Qnil;
440
+ state->idle_sampling_helper_state = NULL;
439
441
  state->owner_thread = Qnil;
440
442
  dynamic_sampling_rate_init(&state->cpu_dynamic_sampling_rate);
441
443
  state->gc_tracepoint = Qnil;
@@ -514,12 +516,14 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
514
516
  // TODO: May be nice to offer customization here? Distribute available "overhead" margin with a bias towards one or the other
515
517
  // sampler.
516
518
  dynamic_sampling_rate_set_overhead_target_percentage(&state->cpu_dynamic_sampling_rate, total_overhead_target_percentage / 2);
517
- long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
518
- discrete_dynamic_sampler_set_overhead_target_percentage(&state->allocation_sampler, total_overhead_target_percentage / 2, now);
519
+ discrete_dynamic_sampler_set_overhead_target_percentage(
520
+ &state->allocation_sampler, total_overhead_target_percentage / 2, monotonic_wall_time_now_ns()
521
+ );
519
522
  }
520
523
 
521
524
  state->thread_context_collector_instance = enforce_thread_context_collector_instance(thread_context_collector_instance);
522
525
  state->idle_sampling_helper_instance = idle_sampling_helper_instance;
526
+ state->idle_sampling_helper_state = idle_sampling_helper_get_state(idle_sampling_helper_instance);
523
527
  state->gc_tracepoint = rb_tracepoint_new(Qnil, RUBY_INTERNAL_EVENT_GC_ENTER | RUBY_INTERNAL_EVENT_GC_EXIT, on_gc_event, NULL /* unused */);
524
528
 
525
529
  return Qtrue;
@@ -572,8 +576,7 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
572
576
 
573
577
  // Reset the dynamic sampling rate state, if any (reminder: the monotonic clock reference may change after a fork)
574
578
  dynamic_sampling_rate_reset(&state->cpu_dynamic_sampling_rate);
575
- long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
576
- discrete_dynamic_sampler_reset(&state->allocation_sampler, now);
579
+ discrete_dynamic_sampler_reset(&state->allocation_sampler, monotonic_wall_time_now_ns());
577
580
 
578
581
  // Reset per-thread state, if any. This ensures there's no leftover state from a previous profiler run that would
579
582
  // affect or be included in samples taken by this profiler about to run.
@@ -650,6 +653,38 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
650
653
  return Qnil;
651
654
  }
652
655
 
656
+ static VALUE during_sample_exit_ensure(VALUE state_ptr) {
657
+ during_sample_exit((cpu_and_wall_time_worker_state *) state_ptr);
658
+ return Qnil;
659
+ }
660
+
661
+ static VALUE _native_profiler_internal_thread_done(VALUE self_instance) {
662
+ cpu_and_wall_time_worker_state *state;
663
+ TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
664
+
665
+ during_sample_enter(state);
666
+ return rb_ensure(
667
+ thread_context_collector_profiler_internal_thread_done, state->thread_context_collector_instance,
668
+ during_sample_exit_ensure, (VALUE) state
669
+ );
670
+ }
671
+
672
+ // This method exists so that we run `thread_context_prepare_serialize` protected with
673
+ // `during_sample_enter`/`during_sample_exit` before we actually serialize.
674
+ //
675
+ // That's why in normal operation we go through the `CpuAndWallTimeWorker` and not directly to the `ThreadContext` to
676
+ // serialize.
677
+ static VALUE _native_prepare_serialize(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
678
+ cpu_and_wall_time_worker_state *state;
679
+ TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
680
+
681
+ during_sample_enter(state);
682
+ return rb_ensure(
683
+ thread_context_prepare_serialize, state->thread_context_collector_instance,
684
+ during_sample_exit_ensure, (VALUE) state
685
+ );
686
+ }
687
+
653
688
  static VALUE _native_stop(DDTRACE_UNUSED VALUE _self, VALUE self_instance, VALUE worker_thread) {
654
689
  cpu_and_wall_time_worker_state *state;
655
690
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -678,57 +713,19 @@ static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *opt
678
713
  return Qtrue;
679
714
  }
680
715
 
681
- // Our SIGPROF handler is not installed with SA_ONSTACK (see `install_sigprof_signal_handler`).
682
- // If we detect our signal handler is running on the alt stack, it means we interrupted another signal handler
683
- // that IS running on the alt stack -- in practice, Ruby's GC compaction read-barrier handler.
684
- #ifdef __APPLE__
685
- // On macOS, `ucontext->uc_stack.ss_sp` reports the (live) stack pointer near the top of the alt stack rather than the
686
- // alt-stack base like Linux does, and `ss_size` is the full alt-stack size. Thus the linux version of the check
687
- // didn't work for macOS.
688
- // As an alternative, we query `sigaltstack()`; we save/restore errno since `sigaltstack()` may clobber it.
689
- static bool is_running_on_alternate_signal_stack(DDTRACE_UNUSED void *ucontext) {
690
- int old_errno = errno;
691
- stack_t current_alt_stack;
692
- bool on_alt_stack = sigaltstack(NULL, &current_alt_stack) == 0 && (current_alt_stack.ss_flags & SS_ONSTACK) != 0;
693
- errno = old_errno;
694
- return on_alt_stack;
695
- }
696
- #else
697
- // NOTE: On Linux we use the alt-stack bounds from the `ucontext` rather than `sigaltstack()`, allowing us to avoid one syscall.
698
- static bool is_running_on_alternate_signal_stack(void *ucontext) {
699
- if (ucontext == NULL) return false;
700
-
701
- stack_t alt_stack = ((ucontext_t *) ucontext)->uc_stack;
702
- if (alt_stack.ss_size == 0) return false; // No alternate signal stack registered on this thread
703
-
704
- char stack_local;
705
- uintptr_t current_sp = (uintptr_t) &stack_local;
706
- uintptr_t alt_stack_start = (uintptr_t) alt_stack.ss_sp;
707
-
708
- return current_sp >= alt_stack_start && current_sp < alt_stack_start + alt_stack.ss_size;
709
- }
710
- #endif
711
-
712
716
  // NOTE: Remember that this will run in the thread and within the scope of user code, including user C code.
713
717
  // We need to be careful not to change any state that may be observed OR to restore it if we do. For instance, if anything
714
718
  // we do here can set `errno`, then we must be careful to restore the old `errno` after the fact.
715
- static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, void *ucontext) {
716
- // If we're running on the alternate signal stack, we've interrupted another signal handler that's running
717
- // there -- in practice, Ruby's GC compaction read-barrier handler.
718
- // During GC compaction, Ruby protects pages containing Ruby objects, so many of the checks we do below are unsafe
719
- // since they can touch those pages, and thus we just bail out here in this situation to avoid crashes.
720
- if (is_running_on_alternate_signal_stack(ucontext)) {
721
- global_stats.signal_handler_skipped_sample_on_altstack++;
722
- return;
723
- }
724
-
719
+ //
720
+ // Neither this handler nor its callees may dereference Ruby objects before confirming GVL ownership or during GC.
721
+ // Holding the GVL is not enough: we may have interrupted GC itself, with Ruby heap pages protected by compaction.
722
+ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, DDTRACE_UNUSED void *_ucontext) {
725
723
  // We must first check that we landed on the correct thread and can proceed.
726
724
  // We must never touch the state before we confirm that we have landed on the thread that is holding the GVL on the main
727
725
  // ractor as otherwise we may be concurrent with the profiler shutting down and removing its state.
728
726
  if (
729
727
  !ruby_native_thread_p() || // Not a Ruby thread
730
- !is_current_thread_holding_the_gvl() || // Not safe to enqueue a sample from this thread
731
- !ddtrace_rb_ractor_main_p() // We're not on the main Ractor; we currently don't support profiling non-main Ractors
728
+ !is_current_thread_in_main_ractor_and_holding_the_gvl() // Not safe to enqueue a sample from this thread
732
729
  ) return;
733
730
 
734
731
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
@@ -751,11 +748,20 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
751
748
  !state->during_sample;
752
749
 
753
750
  if (sample_from_signal_handler) {
754
- // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
755
- // until the postponed job below gets run.
756
- bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
751
+ if (rb_during_gc()) {
752
+ // During GC Ruby might be marking our buffer or compacting (moving) where iseqs live so we skip preparing the
753
+ // sample immediately, and instead defer preparation until GC_EXIT.
754
+ //
755
+ // When GC profiling is disabled, we just fall back to sampling the stack after GC finishes, e.g. it's the
756
+ // equivalent of disabling `sample_from_signal_handler` for samples that happen during GC.
757
+ state->prepare_sample_on_gc_finish = true;
758
+ } else {
759
+ // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
760
+ // until the postponed job below gets run.
761
+ bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
757
762
 
758
- if (prepared) state->stats.signal_handler_prepared_sample++;
763
+ if (prepared) state->stats.signal_handler_prepared_sample++;
764
+ }
759
765
  }
760
766
 
761
767
  #ifndef NO_POSTPONED_TRIGGER // Ruby 3.3+
@@ -789,7 +795,8 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
789
795
  #endif
790
796
  }
791
797
 
792
- // The actual sampling trigger loop always runs **without** the global vm lock.
798
+ // Runs without the GVL: neither this loop nor its callees may dereference Ruby objects until they reacquire it.
799
+ // GC compaction can protect Ruby heap pages while this loop runs, so even reading an object's header is unsafe.
793
800
  static void *run_sampling_trigger_loop(void *state_ptr) {
794
801
  cpu_and_wall_time_worker_state *state = (cpu_and_wall_time_worker_state *) state_ptr;
795
802
 
@@ -809,7 +816,7 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
809
816
  state->stats.trigger_simulated_signal_delivery_attempts++;
810
817
  grab_gvl_and_sample(); // Note: Can raise exceptions
811
818
  } else {
812
- current_gvl_owner owner = gvl_owner();
819
+ current_gvl_owner owner = main_ractor_gvl_owner();
813
820
  if (owner.valid) {
814
821
  // Note that reading the GVL owner and sending them a signal is a race -- the Ruby VM keeps on executing while
815
822
  // we're doing this, so we may still not signal the correct thread from time to time, but our signal handler
@@ -833,7 +840,7 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
833
840
  // for an uncontrolled amount of time. (This can still happen to the IdleSamplingHelper, but the
834
841
  // CpuAndWallTimeWorker will still be free to interrupt the Ruby VM and keep sampling for the entire blocking period).
835
842
  state->stats.trigger_simulated_signal_delivery_attempts++;
836
- idle_sampling_helper_request_action(state->idle_sampling_helper_instance, grab_gvl_and_sample);
843
+ idle_sampling_helper_request_action(state->idle_sampling_helper_state, grab_gvl_and_sample);
837
844
  }
838
845
  }
839
846
  }
@@ -845,7 +852,7 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
845
852
  // Note that we deliberately should NOT combine this sleep_for with the one above because the result of
846
853
  // `dynamic_sampling_rate_get_sleep` may have changed while the above sleep was ongoing.
847
854
  uint64_t extra_sleep =
848
- dynamic_sampling_rate_get_sleep(&state->cpu_dynamic_sampling_rate, monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE));
855
+ dynamic_sampling_rate_get_sleep(&state->cpu_dynamic_sampling_rate, monotonic_wall_time_now_ns());
849
856
  if (state->dynamic_sampling_rate_enabled && extra_sleep > 0) {
850
857
  state->stats.trigger_sample_extra_sleep++;
851
858
  sleep_for(extra_sleep);
@@ -899,7 +906,7 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
899
906
  cpu_and_wall_time_worker_state *state;
900
907
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
901
908
 
902
- long wall_time_ns_before_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
909
+ long wall_time_ns_before_sample = monotonic_wall_time_now_ns();
903
910
 
904
911
  if (state->dynamic_sampling_rate_enabled && !dynamic_sampling_rate_should_sample(&state->cpu_dynamic_sampling_rate, wall_time_ns_before_sample)) {
905
912
  state->stats.cpu_skipped++;
@@ -911,11 +918,13 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
911
918
  bool needs_otel_span_key =
912
919
  thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
913
920
 
914
- long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
915
- long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
921
+ long wall_time_ns_after_sample = monotonic_wall_time_now_ns();
916
922
 
917
- // Guard against wall-time going backwards, see https://github.com/DataDog/dd-trace-rb/pull/2336 for discussion.
918
- uint64_t sampling_time_ns = delta_ns < 0 ? 0 : delta_ns;
923
+ if (wall_time_ns_after_sample < wall_time_ns_before_sample) {
924
+ raise_error(rb_eRuntimeError, "BUG: Unexpected wall time going backwards when tracking overhead");
925
+ }
926
+
927
+ uint64_t sampling_time_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
919
928
 
920
929
  state->stats.cpu_sampling_time_ns_min = uint64_min_of(sampling_time_ns, state->stats.cpu_sampling_time_ns_min);
921
930
  state->stats.cpu_sampling_time_ns_max = uint64_max_of(sampling_time_ns, state->stats.cpu_sampling_time_ns_max);
@@ -988,7 +997,7 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
988
997
  // If we stopped sampling due to an exception, re-raise it (now in the worker thread)
989
998
  if (state->failure_exception != Qnil) rb_exc_raise(state->failure_exception);
990
999
 
991
- thread_context_collector_profiler_internal_thread_done(state->thread_context_collector_instance);
1000
+ _native_profiler_internal_thread_done(instance);
992
1001
 
993
1002
  return Qnil;
994
1003
  }
@@ -1028,28 +1037,6 @@ static VALUE _native_remove_testing_signal_handler(DDTRACE_UNUSED VALUE self) {
1028
1037
  return Qtrue;
1029
1038
  }
1030
1039
 
1031
- // Reproduces a profiler sample being taken in the middle of GC compaction: during compaction a SIGPROF can be
1032
- // delivered while nested inside Ruby's read-barrier signal handler, which runs on the alternate signal stack. We
1033
- // simulate that here by re-installing the production `handle_sampling_signal` with the `SA_ONSTACK` flag, so it runs
1034
- // on the alternate signal stack just like it would in that scenario.
1035
- //
1036
- // The `SA_ONSTACK` will be undone by any next `replace_sigprof_signal_handler_with_empty_handler` /
1037
- // `install_sigprof_signal_handler_internal` / `remove_sigprof_signal_handle` so this doesn't need a specific, symmetric
1038
- // `uninstall` to undo this.
1039
- static VALUE _native_install_sigprof_handler_on_altstack(DDTRACE_UNUSED VALUE self) {
1040
- struct sigaction signal_handler_config = {
1041
- .sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK,
1042
- .sa_sigaction = handle_sampling_signal,
1043
- };
1044
- sigemptyset(&signal_handler_config.sa_mask);
1045
-
1046
- if (sigaction(SIGPROF, &signal_handler_config, NULL) != 0) {
1047
- rb_sys_fail("Failed to install SA_ONSTACK SIGPROF signal handler for testing");
1048
- }
1049
-
1050
- return Qtrue;
1051
- }
1052
-
1053
1040
  // This method exists only to enable testing Datadog::Profiling::Collectors::CpuAndWallTimeWorker behavior using RSpec.
1054
1041
  // It SHOULD NOT be used for other purposes.
1055
1042
  static VALUE _native_trigger_sample(DDTRACE_UNUSED VALUE self) {
@@ -1093,9 +1080,20 @@ static void on_gc_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused) {
1093
1080
  // and disabled before it is cleared, but just in case...
1094
1081
  if (state == NULL) return;
1095
1082
 
1083
+ handle_gc_event(state, event);
1084
+ }
1085
+
1086
+ static void handle_gc_event(cpu_and_wall_time_worker_state *state, rb_event_flag_t event) {
1096
1087
  if (event == RUBY_INTERNAL_EVENT_GC_ENTER) {
1088
+ // A signal can arrive after GC_EXIT while `rb_during_gc` is still true; discard requests from the previous GC step.
1089
+ state->prepare_sample_on_gc_finish = false;
1097
1090
  thread_context_collector_on_gc_start(state->thread_context_collector_instance);
1098
1091
  } else if (event == RUBY_INTERNAL_EVENT_GC_EXIT) {
1092
+ if (state->prepare_sample_on_gc_finish && !state->during_sample) {
1093
+ during_sample_enter(state);
1094
+ thread_context_collector_prepare_sample_on_gc_finish();
1095
+ during_sample_exit(state);
1096
+ }
1099
1097
  bool should_flush = thread_context_collector_on_gc_finish(state->thread_context_collector_instance);
1100
1098
 
1101
1099
  // We use rb_postponed_job_register_one to ask Ruby to run thread_context_collector_sample_after_gc when the
@@ -1110,6 +1108,31 @@ static void on_gc_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused) {
1110
1108
  }
1111
1109
  }
1112
1110
 
1111
+ static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE instance) {
1112
+ cpu_and_wall_time_worker_state *state;
1113
+ TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1114
+ state->prepare_sample_on_gc_finish = true;
1115
+ return Qnil;
1116
+ }
1117
+
1118
+ static VALUE _native_on_gc_event(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE event, VALUE during_sample) {
1119
+ cpu_and_wall_time_worker_state *state;
1120
+ TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1121
+ ENFORCE_BOOLEAN(during_sample);
1122
+ rb_event_flag_t event_flag;
1123
+ if (event == ID2SYM(rb_intern("enter"))) event_flag = RUBY_INTERNAL_EVENT_GC_ENTER;
1124
+ else if (event == ID2SYM(rb_intern("exit"))) event_flag = RUBY_INTERNAL_EVENT_GC_EXIT;
1125
+ else rb_raise(rb_eArgError, "Expected :enter or :exit");
1126
+
1127
+ bool was_during_sample = state->during_sample;
1128
+ if (during_sample == Qtrue) during_sample_enter(state);
1129
+ debug_enter_unsafe_context();
1130
+ handle_gc_event(state, event_flag);
1131
+ debug_leave_unsafe_context();
1132
+ if (!was_during_sample) during_sample_exit(state);
1133
+ return Qnil;
1134
+ }
1135
+
1113
1136
  static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1114
1137
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1115
1138
 
@@ -1194,6 +1217,8 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE instance)
1194
1217
  // Disable all hooks, so that there are no more attempts to mutate the profile
1195
1218
  disable_hooks(state);
1196
1219
 
1220
+ private_vm_api_access_self_test();
1221
+
1197
1222
  reset_stats_not_thread_safe(state);
1198
1223
 
1199
1224
  // Remove all state from the `Collectors::ThreadState` and connected downstream components
@@ -1228,7 +1253,6 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
1228
1253
  ID2SYM(rb_intern("simulated_signal_delivery")), /* => */ UINT2NUM(state->stats.simulated_signal_delivery),
1229
1254
  ID2SYM(rb_intern("signal_handler_enqueued_sample")), /* => */ UINT2NUM(state->stats.signal_handler_enqueued_sample),
1230
1255
  ID2SYM(rb_intern("signal_handler_prepared_sample")), /* => */ UINT2NUM(state->stats.signal_handler_prepared_sample),
1231
- ID2SYM(rb_intern("signal_handler_skipped_sample_on_altstack")), /* => */ UINT2NUM(global_stats.signal_handler_skipped_sample_on_altstack),
1232
1256
  ID2SYM(rb_intern("interrupt_thread_attempts")), /* => */ UINT2NUM(state->stats.interrupt_thread_attempts),
1233
1257
 
1234
1258
  // CPU Stats
@@ -1314,7 +1338,6 @@ static void reset_stats_not_thread_safe(cpu_and_wall_time_worker_state *state) {
1314
1338
  .gvl_sampling_time_ns_min = UINT64_MAX,
1315
1339
  // Other fields are reset to 0
1316
1340
  };
1317
- global_stats = (global_stats_t) {0};
1318
1341
  }
1319
1342
 
1320
1343
  static void sleep_for(uint64_t time_ns) {
@@ -1343,15 +1366,6 @@ static VALUE _native_allocation_count(DDTRACE_UNUSED VALUE self) {
1343
1366
  return are_allocations_being_tracked ? ULL2NUM(allocation_count) : Qnil;
1344
1367
  }
1345
1368
 
1346
- #define HANDLE_CLOCK_FAILURE(call) ({ \
1347
- long _result = (call); \
1348
- if (_result == 0) { \
1349
- delayed_error_clock_failure(state); \
1350
- return; \
1351
- } \
1352
- _result; \
1353
- })
1354
-
1355
1369
  // Implements memory-related profiling events. This function is called by Ruby via the `rb_add_event_hook2`
1356
1370
  // when the RUBY_INTERNAL_EVENT_NEWOBJ event is triggered.
1357
1371
  //
@@ -1417,16 +1431,13 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1417
1431
  if (RB_LIKELY(state->dynamic_sampling_rate_enabled && !discrete_dynamic_sampler_should_sample(&state->allocation_sampler))) {
1418
1432
  state->stats.allocation_skipped++;
1419
1433
 
1420
- coarse_instant now = monotonic_coarse_wall_time_now_ns();
1421
- HANDLE_CLOCK_FAILURE(now.timestamp_ns);
1422
-
1423
- bool needs_readjust = discrete_dynamic_sampler_skipped_sample(&state->allocation_sampler, now);
1434
+ bool needs_readjust = discrete_dynamic_sampler_skipped_sample(&state->allocation_sampler, monotonic_coarse_wall_time_now_ns());
1424
1435
  if (RB_UNLIKELY(needs_readjust)) {
1425
1436
  // We rarely readjust, so this is a cold path
1426
1437
  // Also, while above we used the cheaper monotonic_coarse, for this call we want the regular monotonic call,
1427
1438
  // which is why we end up getting time "again".
1428
1439
  discrete_dynamic_sampler_readjust(
1429
- &state->allocation_sampler, HANDLE_CLOCK_FAILURE(monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE))
1440
+ &state->allocation_sampler, monotonic_wall_time_now_ns()
1430
1441
  );
1431
1442
  }
1432
1443
 
@@ -1436,7 +1447,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1436
1447
  // From here on, we've decided to go ahead with the sample, which is way less common than skipping it
1437
1448
 
1438
1449
  discrete_dynamic_sampler_before_sample(
1439
- &state->allocation_sampler, HANDLE_CLOCK_FAILURE(monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE))
1450
+ &state->allocation_sampler, monotonic_wall_time_now_ns()
1440
1451
  );
1441
1452
 
1442
1453
  during_sample_enter(state);
@@ -1450,12 +1461,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1450
1461
  );
1451
1462
 
1452
1463
  if (state->dynamic_sampling_rate_enabled) {
1453
- long now = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
1454
- if (now == 0) {
1455
- delayed_error_clock_failure(state);
1456
- // NOTE: Not short-circuiting here to make sure cleanup happens
1457
- }
1458
- uint64_t sampling_time_ns = discrete_dynamic_sampler_after_sample(&state->allocation_sampler, now);
1464
+ uint64_t sampling_time_ns = discrete_dynamic_sampler_after_sample(&state->allocation_sampler, monotonic_wall_time_now_ns());
1459
1465
  // NOTE: To keep things lean when dynamic sampling rate is disabled we skip clock interactions which is
1460
1466
  // why we're fine with having this inside this conditional.
1461
1467
  state->stats.allocation_sampling_time_ns_min = uint64_min_of(sampling_time_ns, state->stats.allocation_sampling_time_ns_min);
@@ -1540,11 +1546,6 @@ static void delayed_error(cpu_and_wall_time_worker_state *state, const char *err
1540
1546
  stop_state(state, rb_exc_new_cstr(rb_eRuntimeError, error), "delayed_error");
1541
1547
  }
1542
1548
 
1543
- static void delayed_error_clock_failure(cpu_and_wall_time_worker_state *state) {
1544
- // If we can't raise an immediate exception at the calling site, use the asynchronous flow through the main worker loop.
1545
- stop_state(state, rb_exc_new_cstr(clock_failure_exception_class, "failed to get clock time"), "delayed_error_clock_failure");
1546
- }
1547
-
1548
1549
  static VALUE _native_delayed_error(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE error_msg) {
1549
1550
  ENFORCE_TYPE(error_msg, T_STRING);
1550
1551
 
@@ -1593,6 +1594,8 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1593
1594
  VALUE target_thread = rb_thread_current();
1594
1595
  #endif
1595
1596
 
1597
+ // TODO: `rb_internal_thread_specific_get` is documented as "async and native thread safe.",
1598
+ // unclear if that holds during GC compaction.
1596
1599
  per_thread_context* thread_context = get_per_thread_context(target_thread);
1597
1600
  if (!thread_context) return;
1598
1601
  // If non-NULL the thread is profiled and from the main Ractor
@@ -1664,14 +1667,15 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1664
1667
  cpu_and_wall_time_worker_state *state;
1665
1668
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1666
1669
 
1667
- long wall_time_ns_before_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1670
+ long wall_time_ns_before_sample = monotonic_wall_time_now_ns();
1668
1671
  thread_context_collector_sample_after_gvl_running(state->thread_context_collector_instance, rb_thread_current(), wall_time_ns_before_sample);
1669
- long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1672
+ long wall_time_ns_after_sample = monotonic_wall_time_now_ns();
1670
1673
 
1671
- long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
1674
+ if (wall_time_ns_after_sample < wall_time_ns_before_sample) {
1675
+ raise_error(rb_eRuntimeError, "BUG: Unexpected wall time going backwards when tracking overhead");
1676
+ }
1672
1677
 
1673
- // Guard against wall-time going backwards, see https://github.com/DataDog/dd-trace-rb/pull/2336 for discussion.
1674
- uint64_t sampling_time_ns = delta_ns < 0 ? 0 : delta_ns;
1678
+ uint64_t sampling_time_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
1675
1679
 
1676
1680
  state->stats.gvl_sampling_time_ns_min = uint64_min_of(sampling_time_ns, state->stats.gvl_sampling_time_ns_min);
1677
1681
  state->stats.gvl_sampling_time_ns_max = uint64_max_of(sampling_time_ns, state->stats.gvl_sampling_time_ns_max);
@@ -367,11 +367,7 @@ static const rb_data_type_t sampler_typed_data = {
367
367
  static VALUE _native_new(VALUE klass) {
368
368
  sampler_state *state = ruby_xcalloc(1, sizeof(sampler_state));
369
369
 
370
- long now_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
371
- if (now_ns == 0) {
372
- raise_error(rb_eRuntimeError, "failed to get clock time");
373
- }
374
- discrete_dynamic_sampler_init(&state->sampler, "test sampler", now_ns);
370
+ discrete_dynamic_sampler_init(&state->sampler, "test sampler", monotonic_wall_time_now_ns());
375
371
 
376
372
  // Note: As of this writing, no new Ruby objects get created and stored in the state. If that ever changes, remember
377
373
  // to keep them on the stack and mark them with RB_GC_GUARD -- otherwise it's possible for a GC to run and
@@ -53,8 +53,6 @@ void gc_profiling_init(void) {
53
53
  shady_sym = ID2SYM(rb_intern_const("shady"));
54
54
  force_sym = ID2SYM(rb_intern_const("force"));
55
55
  oldmalloc_sym = ID2SYM(rb_intern_const("oldmalloc"));
56
- state_sym = ID2SYM(rb_intern_const("state"));
57
- none_sym = ID2SYM(rb_intern_const("none"));
58
56
  }
59
57
 
60
58
  bool gc_profiling_has_major_gc_finished(void) {