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
@@ -25,6 +25,9 @@
25
25
  #pragma GCC diagnostic push
26
26
  #pragma GCC diagnostic ignored "-Wunused-parameter"
27
27
  #include <iseq.h>
28
+ #ifdef HAVE_ZJIT_FRAME
29
+ #include <zjit.h>
30
+ #endif
28
31
  #pragma GCC diagnostic pop
29
32
 
30
33
  #ifndef NO_INTERNAL_CLASS_HEADER_INCLUDE
@@ -53,23 +56,45 @@
53
56
  #define PRIVATE_VM_API_ACCESS_SKIP_RUBY_INCLUDES
54
57
  #include "private_vm_api_access.h"
55
58
 
59
+ #ifdef HAVE_RUBY_RACTOR_H
60
+ typedef rb_ractor_t gvl_owner_context_t;
61
+ #else
62
+ typedef rb_vm_t gvl_owner_context_t;
63
+ #endif
64
+
56
65
  static inline const rb_callable_method_entry_t* get_cfunc_method_entry(const rb_control_frame_t *cfp);
57
66
  static const rb_callable_method_entry_t* safe_vm_frame_method_entry(const rb_control_frame_t *cfp);
67
+ static gvl_owner_context_t *get_gvl_owner_context(void);
68
+
69
+ typedef struct {
70
+ gvl_owner_context_t *main_ractor_gvl_owner_context;
71
+ const rb_data_type_t *thread_data_type;
72
+ } private_vm_api_global_t;
73
+
74
+ // Initialized once on the main ractor with the GVL held, before sampling starts; never rewritten.
75
+ // The native context stays at the same address across GC compaction and forks from the main ractor.
76
+ static private_vm_api_global_t private_vm_api_global;
77
+
78
+ void private_vm_api_access_init(void) {
79
+ private_vm_api_global.thread_data_type = RTYPEDDATA_TYPE(rb_thread_current());
80
+ private_vm_api_global.main_ractor_gvl_owner_context = get_gvl_owner_context();
81
+ }
82
+
83
+ void private_vm_api_access_self_test(void) {
84
+ if (private_vm_api_global.main_ractor_gvl_owner_context != get_gvl_owner_context()) {
85
+ rb_raise(rb_eRuntimeError, "BUG: main_ractor_gvl_owner_context changed unexpectedly");
86
+ }
87
+ }
58
88
 
59
89
  // MRI has a similar rb_thread_ptr() function which we can't call it directly
60
90
  // because Ruby does not expose the thread_data_type publicly.
61
- // Instead, we have our own version of that function, and we lazily initialize the thread_data_type pointer
91
+ // Instead, we have our own version of that function, and we initialize the thread_data_type pointer
62
92
  // from a known-correct object: the current thread.
63
93
  //
64
94
  // Note that beyond returning the rb_thread_struct*, rb_check_typeddata() raises an exception
65
95
  // if the argument passed in is not actually a `Thread` instance.
66
96
  static inline rb_thread_t *thread_struct_from_object(VALUE thread) {
67
- static const rb_data_type_t *thread_data_type = NULL;
68
- if (UNLIKELY(thread_data_type == NULL)) {
69
- thread_data_type = RTYPEDDATA_TYPE(rb_thread_current());
70
- }
71
-
72
- return (rb_thread_t *) rb_check_typeddata(thread, thread_data_type);
97
+ return (rb_thread_t *) rb_check_typeddata(thread, private_vm_api_global.thread_data_type);
73
98
  }
74
99
 
75
100
  rb_nativethread_id_t pthread_id_for(VALUE thread) {
@@ -86,19 +111,16 @@ rb_nativethread_id_t pthread_id_for(VALUE thread) {
86
111
  #endif
87
112
  }
88
113
 
89
- // Queries if the current thread is the owner of the global VM lock.
114
+ // Combines checking for main ractor and GVL in one check that can be done safely from a signal handler, even
115
+ // if the thread is not a Ruby thread or if we're in the middle of GC.
90
116
  //
91
- // @ivoanjo: Ruby has a similarly-named `ruby_thread_has_gvl_p` but that API is insufficient for our needs because it can
92
- // still return `true` even when a thread DOES NOT HAVE the global VM lock.
93
- // In particular, looking at the implementation, that API assumes that if a thread is not in a "blocking region" then it
94
- // will have the GVL which is probably true for the situations that API was designed to be called from BUT this assumption
95
- // does not hold true when calling `ruby_thread_has_gvl_p` from a signal handler. (Because the thread may have lost the
96
- // GVL due to a scheduler decision, not because it decided to block.)
97
- // I have also submitted https://bugs.ruby-lang.org/issues/19172 to discuss this with upstream Ruby developers.
117
+ // Note that Ruby has `ruby_thread_has_gvl_p`, but unfortunately it can return true in a signal
118
+ // handler even when the thread doesn't hold the GVL. It assumes that a thread outside a "blocking region" holds
119
+ // the GVL, but the scheduler can also take it away (https://bugs.ruby-lang.org/issues/19172).
98
120
  //
99
- // Thus we need our own gvl-checking method which actually looks at the gvl structure to determine if it is the owner.
100
- bool is_current_thread_holding_the_gvl(void) {
101
- current_gvl_owner owner = gvl_owner();
121
+ // This must not touch Ruby objects, since GC compaction may have protected their heap pages.
122
+ bool is_current_thread_in_main_ractor_and_holding_the_gvl(void) {
123
+ current_gvl_owner owner = main_ractor_gvl_owner();
102
124
  return owner.valid && pthread_equal(pthread_self(), owner.owner);
103
125
  }
104
126
 
@@ -112,23 +134,37 @@ static inline rb_ractor_t *ddtrace_get_ractor(void) {
112
134
  }
113
135
  #endif
114
136
 
137
+ static gvl_owner_context_t *get_gvl_owner_context(void) {
138
+ if (!ddtrace_rb_ractor_main_p()) {
139
+ rb_raise(rb_eRuntimeError, "BUG: get_gvl_owner_context must be called from the main ractor");
140
+ }
141
+
142
+ #ifdef HAVE_RUBY_RACTOR_H
143
+ return ddtrace_get_ractor();
144
+ #else
145
+ return GET_VM();
146
+ #endif
147
+ }
148
+
115
149
  #ifndef NO_GVL_OWNER // Ruby < 2.6 doesn't have the owner/running field
116
150
  // NOTE: Reading the owner in this is a racy read, because we're not grabbing the lock that Ruby uses to protect it.
117
151
  //
118
152
  // While we could potentially grab this lock, I (@ivoanjo) think we actually don't need it because:
119
- // * In the case where a thread owns the GVL and calls `gvl_owner`, it will always see the correct value. That's
153
+ // * In the case where a thread owns the GVL and calls `main_ractor_gvl_owner`, it will always see the correct value. That's
120
154
  // because every thread sets itself as the owner when it grabs the GVL and unsets itself at the end.
121
- // That means that `is_current_thread_holding_the_gvl` is always accurate.
155
+ // That means that `is_current_thread_in_main_ractor_and_holding_the_gvl` is always accurate.
122
156
  // * In a case where we observe a different thread, then this may change by the time we do something with this value
123
157
  // anyway. So unless we want to prevent the Ruby scheduler from switching threads, we need to deal with races here.
124
- current_gvl_owner gvl_owner(void) {
158
+ current_gvl_owner main_ractor_gvl_owner(void) {
159
+ if (private_vm_api_global.main_ractor_gvl_owner_context == NULL) return (current_gvl_owner) {.valid = false};
160
+
125
161
  const rb_thread_t *current_owner =
126
162
  #ifndef NO_RB_THREAD_SCHED // Introduced in Ruby 3.2 as a replacement for struct rb_global_vm_lock_struct
127
- ddtrace_get_ractor()->threads.sched.running;
163
+ private_vm_api_global.main_ractor_gvl_owner_context->threads.sched.running;
128
164
  #elif HAVE_RUBY_RACTOR_H
129
- ddtrace_get_ractor()->threads.gvl.owner;
165
+ private_vm_api_global.main_ractor_gvl_owner_context->threads.gvl.owner;
130
166
  #else
131
- GET_VM()->gvl.owner;
167
+ private_vm_api_global.main_ractor_gvl_owner_context->gvl.owner;
132
168
  #endif
133
169
 
134
170
  if (current_owner == NULL) {
@@ -149,8 +185,9 @@ current_gvl_owner gvl_owner(void) {
149
185
  #endif
150
186
  }
151
187
  #else
152
- current_gvl_owner gvl_owner(void) {
153
- rb_vm_t *vm = GET_VM();
188
+ current_gvl_owner main_ractor_gvl_owner(void) {
189
+ rb_vm_t *vm = private_vm_api_global.main_ractor_gvl_owner_context;
190
+ if (vm == NULL) return (current_gvl_owner) {.valid = false};
154
191
 
155
192
  // BIG Issue: Ruby < 2.6 did not have the owner field. The really nice thing about the owner field is that it's
156
193
  // "atomic" -- when a thread sets it, it "declares" two things in a single step
@@ -169,15 +206,15 @@ current_gvl_owner gvl_owner(void) {
169
206
  // * Thread A sets `running_thread` (`gvl.acquired == 1` + `running_thread == Thread A`)
170
207
  // * Thread A releases the GVL (`gvl.acquired == 0` + `running_thread == Thread A`)
171
208
  // * Thread B grabs the GVL (`gvl.acquired == 1` + `running_thread == Thread A`)
172
- // * Thread A calls gvl_owner. Due to the current state (`gvl.acquired == 1` + `running_thread == Thread A`), this
209
+ // * Thread A calls main_ractor_gvl_owner. Due to the current state (`gvl.acquired == 1` + `running_thread == Thread A`), this
173
210
  // function returns an incorrect result.
174
211
  // * Thread B finally sets `running_thread` (`gvl.acquired == 1` + `running_thread == Thread B`)
175
212
  //
176
- // This is especially problematic because we use `gvl_owner` to implement `is_current_thread_holding_the_gvl` which
213
+ // This is especially problematic because we use `main_ractor_gvl_owner` to implement `is_current_thread_in_main_ractor_and_holding_the_gvl` which
177
214
  // is called in a signal handler to decide "is it safe for me to call `rb_postponed_job_register_one` or not".
178
215
  // (See constraints in `collectors_cpu_and_wall_time_worker.c` comments for why).
179
216
  //
180
- // Thus an incorrect `is_current_thread_holding_the_gvl` result may lead to issues inside `rb_postponed_job_register_one`.
217
+ // Thus an incorrect `is_current_thread_in_main_ractor_and_holding_the_gvl` result may lead to issues inside `rb_postponed_job_register_one`.
181
218
  //
182
219
  // For this reason we default to use the "no signals workaround" on Ruby 2.5 by default, and we print a
183
220
  // warning when customers force-enable it.
@@ -373,7 +410,6 @@ calc_pos(const rb_iseq_t *iseq, const VALUE *pc, int *lineno, int *node_id) {
373
410
  ptrdiff_t n = pc - ISEQ_BODY(iseq)->iseq_encoded;
374
411
  VM_ASSERT(n <= ISEQ_BODY(iseq)->iseq_size);
375
412
  VM_ASSERT(n >= 0);
376
- ASSUME(n >= 0);
377
413
  size_t pos = n; /* no overflow */
378
414
  if (LIKELY(pos)) {
379
415
  /* use pos-1 because PC points next instruction at the beginning of instruction */
@@ -556,15 +592,38 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
556
592
  cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
557
593
 
558
594
  for (i=0; i<limit && cfp != top_sentinel; cfp = RUBY_VM_NEXT_CONTROL_FRAME(cfp)) {
595
+ const VALUE *pc = cfp->pc;
596
+ #ifdef HAVE_ZJIT_FRAME
597
+ const rb_iseq_t *iseq = cfp->_iseq;
598
+ #else
599
+ const rb_iseq_t *iseq = cfp->iseq;
600
+ #endif
601
+
602
+ #if defined(HAVE_ZJIT_FRAME) && USE_ZJIT
603
+ // Based on CFP_ZJIT_FRAME, which we currently can't use directly.
604
+ if (CFP_ZJIT_FRAME_P(cfp)) {
605
+ if ((VALUE)cfp->jit_return == ZJIT_JIT_RETURN_C_FRAME) {
606
+ // ZJIT uses this sentinel for C method frames, which have no Ruby ISEQ or bytecode PC.
607
+ // Set both to NULL explicitly because ZJIT leaves those fields untouched, so they may contain stale data.
608
+ iseq = NULL;
609
+ pc = NULL;
610
+ } else {
611
+ const zjit_jit_frame_t *jit_frame = (const zjit_jit_frame_t *)((VALUE *)cfp->jit_return)[-1];
612
+ iseq = jit_frame->iseq;
613
+ pc = jit_frame->pc;
614
+ }
615
+ }
616
+ #endif
617
+
559
618
  #ifndef NO_T_MOVED
560
- if (cfp->iseq && RB_TYPE_P((VALUE) cfp->iseq, T_MOVED)) {
619
+ if (iseq && RB_TYPE_P((VALUE) iseq, T_MOVED)) {
561
620
  // The profiler is not supposed to sample during GC compaction, so T_MOVED is not expected here.
562
621
  // Yet, crash tracking also uses this walker and may run at any time. For now, we choose to skip these frames.
563
622
  continue;
564
623
  }
565
624
  #endif
566
625
 
567
- if (cfp->iseq && !cfp->pc) {
626
+ if (iseq && !pc) {
568
627
  // Fix: Do nothing -- this frame should not be used
569
628
  //
570
629
  // rb_profile_frames does not do this check, but `backtrace_each` (`vm_backtrace.c`) does. This frame is not
@@ -593,8 +652,8 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
593
652
 
594
653
  stack_buffer[i].same_frame =
595
654
  stack_buffer[i].is_ruby_frame &&
596
- stack_buffer[i].as.ruby_frame.iseq == cfp->iseq &&
597
- stack_buffer[i].as.ruby_frame.caching_pc == cfp->pc &&
655
+ stack_buffer[i].as.ruby_frame.iseq == iseq &&
656
+ stack_buffer[i].as.ruby_frame.caching_pc == pc &&
598
657
  stack_buffer[i].cme == cme;
599
658
 
600
659
  if (stack_buffer[i].same_frame) { // Nothing to do, buffer already contains this frame
@@ -602,8 +661,8 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
602
661
  continue;
603
662
  }
604
663
 
605
- stack_buffer[i].as.ruby_frame.iseq = cfp->iseq;
606
- stack_buffer[i].as.ruby_frame.caching_pc = (void *) cfp->pc;
664
+ stack_buffer[i].as.ruby_frame.iseq = iseq;
665
+ stack_buffer[i].as.ruby_frame.caching_pc = (void *) pc;
607
666
  stack_buffer[i].cme = cme;
608
667
 
609
668
  // The topmost frame may not have an updated PC because the JIT
@@ -614,10 +673,10 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
614
673
  if (cfp == top && cfp->jit_return) {
615
674
  stack_buffer[i].as.ruby_frame.line = 0;
616
675
  } else {
617
- stack_buffer[i].as.ruby_frame.line = calc_lineno(cfp->iseq, cfp->pc);
676
+ stack_buffer[i].as.ruby_frame.line = calc_lineno(iseq, pc);
618
677
  }
619
678
  #else // Ruby < 3.1
620
- stack_buffer[i].as.ruby_frame.line = calc_lineno(cfp->iseq, cfp->pc);
679
+ stack_buffer[i].as.ruby_frame.line = calc_lineno(iseq, pc);
621
680
  #endif
622
681
 
623
682
  stack_buffer[i].is_ruby_frame = true;
@@ -822,26 +881,11 @@ void self_test_mn_enabled(void) {
822
881
  #endif
823
882
  }
824
883
 
825
- // Taken from upstream imemo.h at commit 6ebcf25de2859b5b6402b7e8b181066c32d0e0bf (November 2023, master branch)
826
- // (See the Ruby project copyright and license above)
827
- // to enable calling rb_imemo_name
828
- //
829
- // Modifications:
830
- // * Added IMEMO_MASK define
831
- // * Changed return type to int to avoid having to define `enum imemo_type`
832
- static inline int ddtrace_imemo_type(VALUE imemo) {
833
- // This mask is the same between Ruby 2.5 and 3.3-preview3. Furthermore, the intention of this method is to be used
834
- // to call `rb_imemo_name` which correctly handles invalid numbers so even if the mask changes in the future, at most
835
- // we'll get incorrect results (and never a VM crash)
836
- #define IMEMO_MASK 0x0f
837
- return (RBASIC(imemo)->flags >> FL_USHIFT) & IMEMO_MASK;
838
- }
839
-
840
884
  // Safety: This function assumes the object passed in is of the imemo type. But in the worst case, you'll just get
841
885
  // a string that doesn't make any sense.
842
886
  #ifndef NO_IMEMO_NAME
843
887
  const char *imemo_kind(VALUE imemo) {
844
- return rb_imemo_name(ddtrace_imemo_type(imemo));
888
+ return rb_imemo_name(imemo_type(imemo));
845
889
  }
846
890
  #else
847
891
  const char *imemo_kind(__attribute__((unused)) VALUE imemo) {
@@ -39,8 +39,11 @@ typedef struct {
39
39
  } frame_info;
40
40
 
41
41
  rb_nativethread_id_t pthread_id_for(VALUE thread);
42
- bool is_current_thread_holding_the_gvl(void);
43
- current_gvl_owner gvl_owner(void);
42
+ // Must be called from the main ractor while holding the GVL
43
+ void private_vm_api_access_init(void);
44
+ void private_vm_api_access_self_test(void);
45
+ current_gvl_owner main_ractor_gvl_owner(void);
46
+ bool is_current_thread_in_main_ractor_and_holding_the_gvl(void);
44
47
  uint64_t native_thread_id_for(VALUE thread);
45
48
  void ddtrace_thread_list(VALUE result_array);
46
49
  bool is_thread_alive(VALUE thread);
@@ -37,9 +37,9 @@ static void *trigger_grab_gvl_and_raise_value_arg(void *trigger_args);
37
37
  static VALUE _native_grab_gvl_and_raise_syserr(DDTRACE_UNUSED VALUE _self, VALUE syserr_errno, VALUE test_message, VALUE test_message_arg, VALUE release_gvl);
38
38
  static void *trigger_grab_gvl_and_raise_syserr(void *trigger_args);
39
39
  static VALUE _native_ddtrace_rb_ractor_main_p(DDTRACE_UNUSED VALUE _self);
40
- static VALUE _native_is_current_thread_holding_the_gvl(DDTRACE_UNUSED VALUE _self);
41
- static VALUE _native_release_gvl_and_call_is_current_thread_holding_the_gvl(DDTRACE_UNUSED VALUE _self);
42
- static void *testing_is_current_thread_holding_the_gvl(DDTRACE_UNUSED void *_unused);
40
+ static VALUE _native_is_current_thread_in_main_ractor_and_holding_the_gvl(DDTRACE_UNUSED VALUE _self);
41
+ static VALUE _native_release_gvl_and_call_is_current_thread_in_main_ractor_and_holding_the_gvl(DDTRACE_UNUSED VALUE _self);
42
+ static void *testing_is_current_thread_in_main_ractor_and_holding_the_gvl(void *unused);
43
43
  static VALUE _native_install_holding_the_gvl_signal_handler(DDTRACE_UNUSED VALUE _self);
44
44
  static void holding_the_gvl_signal_handler(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, DDTRACE_UNUSED void *_ucontext);
45
45
  static VALUE _native_trigger_holding_the_gvl_signal_handler_on(DDTRACE_UNUSED VALUE _self, VALUE background_thread);
@@ -66,6 +66,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
66
66
  rb_define_singleton_method(native_extension_module, "native_working?", native_working_p, 0);
67
67
  rb_funcall(native_extension_module, rb_intern("private_class_method"), 1, ID2SYM(rb_intern("native_working?")));
68
68
 
69
+ private_vm_api_access_init();
69
70
  ruby_helpers_init();
70
71
  setup_signal_handler_init(profiling_module);
71
72
  collectors_cpu_and_wall_time_worker_init(profiling_module);
@@ -88,11 +89,11 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
88
89
  rb_define_singleton_method(testing_module, "_native_grab_gvl_and_raise_value_arg", _native_grab_gvl_and_raise_value_arg, 3);
89
90
  rb_define_singleton_method(testing_module, "_native_grab_gvl_and_raise_syserr", _native_grab_gvl_and_raise_syserr, 4);
90
91
  rb_define_singleton_method(testing_module, "_native_ddtrace_rb_ractor_main_p", _native_ddtrace_rb_ractor_main_p, 0);
91
- rb_define_singleton_method(testing_module, "_native_is_current_thread_holding_the_gvl", _native_is_current_thread_holding_the_gvl, 0);
92
+ rb_define_singleton_method(testing_module, "_native_is_current_thread_in_main_ractor_and_holding_the_gvl", _native_is_current_thread_in_main_ractor_and_holding_the_gvl, 0);
92
93
  rb_define_singleton_method(
93
94
  testing_module,
94
- "_native_release_gvl_and_call_is_current_thread_holding_the_gvl",
95
- _native_release_gvl_and_call_is_current_thread_holding_the_gvl,
95
+ "_native_release_gvl_and_call_is_current_thread_in_main_ractor_and_holding_the_gvl",
96
+ _native_release_gvl_and_call_is_current_thread_in_main_ractor_and_holding_the_gvl,
96
97
  0
97
98
  );
98
99
  rb_define_singleton_method(testing_module, "_native_install_holding_the_gvl_signal_handler", _native_install_holding_the_gvl_signal_handler, 0);
@@ -103,6 +104,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
103
104
  }
104
105
 
105
106
  static VALUE native_working_p(DDTRACE_UNUSED VALUE _self) {
107
+ private_vm_api_access_self_test();
106
108
  self_test_thread_name_for();
107
109
  self_test_clock_id();
108
110
  self_test_current_fiber_for();
@@ -219,41 +221,37 @@ static VALUE _native_ddtrace_rb_ractor_main_p(DDTRACE_UNUSED VALUE _self) {
219
221
  return ddtrace_rb_ractor_main_p() ? Qtrue : Qfalse;
220
222
  }
221
223
 
222
- static VALUE _native_is_current_thread_holding_the_gvl(DDTRACE_UNUSED VALUE _self) {
223
- return ((bool) testing_is_current_thread_holding_the_gvl(NULL)) ? Qtrue : Qfalse;
224
+ static VALUE _native_is_current_thread_in_main_ractor_and_holding_the_gvl(DDTRACE_UNUSED VALUE _self) {
225
+ return is_current_thread_in_main_ractor_and_holding_the_gvl() ? Qtrue : Qfalse;
224
226
  }
225
227
 
226
- static VALUE _native_release_gvl_and_call_is_current_thread_holding_the_gvl(DDTRACE_UNUSED VALUE _self) {
227
- return ((bool) rb_thread_call_without_gvl(testing_is_current_thread_holding_the_gvl, NULL, NULL, NULL)) ? Qtrue : Qfalse;
228
+ static VALUE _native_release_gvl_and_call_is_current_thread_in_main_ractor_and_holding_the_gvl(DDTRACE_UNUSED VALUE _self) {
229
+ return ((bool) rb_thread_call_without_gvl(testing_is_current_thread_in_main_ractor_and_holding_the_gvl, NULL, NULL, NULL)) ? Qtrue : Qfalse;
228
230
  }
229
231
 
230
- static void *testing_is_current_thread_holding_the_gvl(DDTRACE_UNUSED void *_unused) {
231
- return (void *) is_current_thread_holding_the_gvl();
232
- }
233
-
234
- static VALUE _native_install_holding_the_gvl_signal_handler(DDTRACE_UNUSED VALUE _self) {
235
- install_sigprof_signal_handler(holding_the_gvl_signal_handler, "holding_the_gvl_signal_handler");
236
- return Qtrue;
232
+ static void *testing_is_current_thread_in_main_ractor_and_holding_the_gvl(DDTRACE_UNUSED void *unused) {
233
+ return (void *) is_current_thread_in_main_ractor_and_holding_the_gvl();
237
234
  }
238
235
 
239
236
  static pthread_mutex_t holding_the_gvl_signal_handler_mutex = PTHREAD_MUTEX_INITIALIZER;
240
237
  static pthread_cond_t holding_the_gvl_signal_handler_executed = PTHREAD_COND_INITIALIZER;
241
238
  static VALUE holding_the_gvl_signal_handler_result[3];
242
239
 
243
- // Ruby VM API that is exported but not present in the header files. Only used by holding_the_gvl_signal_handler below and SHOULD NOT
244
- // be used in any other situation. See the comments on is_current_thread_holding_the_gvl for details.
245
- int ruby_thread_has_gvl_p(void);
240
+ static VALUE _native_install_holding_the_gvl_signal_handler(DDTRACE_UNUSED VALUE _self) {
241
+ install_sigprof_signal_handler(holding_the_gvl_signal_handler, "holding_the_gvl_signal_handler");
242
+ return Qtrue;
243
+ }
246
244
 
247
245
  static void holding_the_gvl_signal_handler(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, DDTRACE_UNUSED void *_ucontext) {
248
246
  pthread_mutex_lock(&holding_the_gvl_signal_handler_mutex);
249
247
 
250
248
  VALUE test_executed = Qtrue;
251
249
  VALUE ruby_thread_has_gvl_p_result = ruby_thread_has_gvl_p() ? Qtrue : Qfalse;
252
- VALUE is_current_thread_holding_the_gvl_result = is_current_thread_holding_the_gvl() ? Qtrue : Qfalse;
250
+ VALUE is_current_thread_in_main_ractor_and_holding_the_gvl_result = is_current_thread_in_main_ractor_and_holding_the_gvl() ? Qtrue : Qfalse;
253
251
 
254
252
  holding_the_gvl_signal_handler_result[0] = test_executed;
255
253
  holding_the_gvl_signal_handler_result[1] = ruby_thread_has_gvl_p_result;
256
- holding_the_gvl_signal_handler_result[2] = is_current_thread_holding_the_gvl_result;
254
+ holding_the_gvl_signal_handler_result[2] = is_current_thread_in_main_ractor_and_holding_the_gvl_result;
257
255
 
258
256
  pthread_cond_broadcast(&holding_the_gvl_signal_handler_executed);
259
257
  pthread_mutex_unlock(&holding_the_gvl_signal_handler_mutex);
@@ -299,7 +297,7 @@ static VALUE _native_trigger_holding_the_gvl_signal_handler_on(DDTRACE_UNUSED VA
299
297
 
300
298
  VALUE result = rb_hash_new();
301
299
  rb_hash_aset(result, ID2SYM(rb_intern("ruby_thread_has_gvl_p")), holding_the_gvl_signal_handler_result[1]);
302
- rb_hash_aset(result, ID2SYM(rb_intern("is_current_thread_holding_the_gvl")), holding_the_gvl_signal_handler_result[2]);
300
+ rb_hash_aset(result, ID2SYM(rb_intern("is_current_thread_in_main_ractor_and_holding_the_gvl")), holding_the_gvl_signal_handler_result[2]);
303
301
  return result;
304
302
  }
305
303
 
@@ -66,7 +66,9 @@ void private_grab_gvl_and_raise(VALUE exception_class, int syserr_errno, const c
66
66
  args.format_string = format_string;
67
67
  va_start(args.va_args, format_string);
68
68
 
69
- if (is_current_thread_holding_the_gvl()) {
69
+ // Callers run with the GVL or inside an explicit no-GVL region, so Ruby's blocking-region check is sufficient.
70
+ // We don't run in signal handlers or scheduler hooks, so we don't need is_current_thread_in_main_ractor_and_holding_the_gvl().
71
+ if (ruby_thread_has_gvl_p()) {
70
72
  VALUE detailed_message = rb_vsprintf(format_string, args.va_args);
71
73
  va_end(args.va_args);
72
74
 
@@ -4,6 +4,10 @@
4
4
  #include <stdarg.h>
5
5
  #include "datadog_ruby_common.h"
6
6
 
7
+ // Public since Ruby 4.0 (https://bugs.ruby-lang.org/issues/20877); older Rubies export it without a header declaration.
8
+ // Be careful about using this in the profiler; see notes on is_current_thread_in_main_ractor_and_holding_the_gvl().
9
+ int ruby_thread_has_gvl_p(void);
10
+
7
11
  // Initialize internal data needed by some ruby helpers. Should be called during start, before any actual
8
12
  // usage of ruby helpers.
9
13
  void ruby_helpers_init(void);
@@ -36,8 +36,6 @@ static void install_sigprof_signal_handler_internal(
36
36
  ) {
37
37
  struct sigaction existing_signal_handler_config = {.sa_sigaction = NULL};
38
38
  struct sigaction signal_handler_config = {
39
- // NOTE: Deliberately not SA_ONSTACK. `handle_sampling_signal` uses "am I running on the alternate signal stack?"
40
- // to detect that if it interrupted another signal handler from the Ruby VM, see `is_running_on_alternate_signal_stack` for details.
41
39
  .sa_flags = SA_RESTART | SA_SIGINFO,
42
40
  .sa_sigaction = signal_handler_function
43
41
  };
@@ -9,7 +9,6 @@
9
9
  #include "time_helpers.h"
10
10
  #include "heap_recorder.h"
11
11
  #include "encoded_profile.h"
12
- #include "collectors_thread_context.h"
13
12
 
14
13
  // Used to wrap a ddog_prof_Profile in a Ruby object and expose Ruby-level serialization APIs
15
14
  // This file implements the native bits of the Datadog::Profiling::StackRecorder class
@@ -178,10 +177,6 @@ typedef struct {
178
177
  heap_recorder *heap_recorder;
179
178
  bool heap_clean_after_gc_enabled;
180
179
 
181
- // When set, _native_serialize will call thread_context_collector_on_serialize on this instance
182
- // before serializing, so that threads suspended across the whole profile period still get sampled.
183
- VALUE thread_context_collector_instance;
184
-
185
180
  pthread_mutex_t mutex_slot_one;
186
181
  profile_slot profile_slot_one;
187
182
  pthread_mutex_t mutex_slot_two;
@@ -343,7 +338,6 @@ static VALUE _native_new(VALUE klass) {
343
338
  // being leaked.
344
339
 
345
340
  state->heap_clean_after_gc_enabled = false;
346
- state->thread_context_collector_instance = Qnil;
347
341
 
348
342
  ddog_prof_Slice_SampleType sample_types = {.ptr = all_sample_types, .len = ALL_VALUE_TYPES_COUNT};
349
343
 
@@ -415,7 +409,6 @@ static void initialize_profiles(stack_recorder_state *state, ddog_prof_Slice_Sam
415
409
  static void stack_recorder_typed_data_mark(void *state_ptr) {
416
410
  stack_recorder_state *state = (stack_recorder_state *) state_ptr;
417
411
 
418
- rb_gc_mark(state->thread_context_collector_instance);
419
412
  heap_recorder_mark(state->heap_recorder);
420
413
  }
421
414
 
@@ -539,17 +532,13 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
539
532
  stack_recorder_state *state;
540
533
  TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
541
534
 
542
- if (state->thread_context_collector_instance != Qnil) {
543
- thread_context_collector_on_serialize(state->thread_context_collector_instance);
544
- }
545
-
546
- long heap_iteration_prep_start_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
535
+ long heap_iteration_prep_start_time_ns = monotonic_wall_time_now_ns();
547
536
  // Prepare the iteration on heap recorder we'll be doing outside the GVL. The preparation needs to
548
537
  // happen while holding the GVL.
549
538
  // NOTE: While rare, it's possible for the GVL to be released inside this function (see comments on `heap_recorder_update`)
550
539
  // and thus don't assume this is an "atomic" step -- other threads may get some running time in the meanwhile.
551
540
  heap_recorder_prepare_iteration(state->heap_recorder);
552
- long heap_iteration_prep_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - heap_iteration_prep_start_time_ns;
541
+ long heap_iteration_prep_time_ns = monotonic_wall_time_now_ns() - heap_iteration_prep_start_time_ns;
553
542
 
554
543
  ddog_Timespec finish_timestamp = system_epoch_now_timespec();
555
544
  // Need to do this while still holding the Global VM Lock; see comments on method for why
@@ -811,7 +800,7 @@ static void build_heap_profile_without_gvl(stack_recorder_state *state, profile_
811
800
  static void *call_serialize_without_gvl(void *call_args) {
812
801
  call_serialize_without_gvl_arguments *args = (call_serialize_without_gvl_arguments *) call_args;
813
802
 
814
- long serialize_no_gvl_start_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
803
+ long serialize_no_gvl_start_time_ns = monotonic_wall_time_now_ns();
815
804
 
816
805
  profile_slot *slot_now_inactive = serializer_flip_active_and_inactive_slots(args->state);
817
806
  args->slot = slot_now_inactive;
@@ -819,13 +808,13 @@ static void *call_serialize_without_gvl(void *call_args) {
819
808
  // Now that we have the inactive profile with all but heap samples, lets fill it with heap data
820
809
  // without needing to race with the active sampler
821
810
  build_heap_profile_without_gvl(args->state, args->slot);
822
- args->heap_profile_build_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - serialize_no_gvl_start_time_ns;
811
+ args->heap_profile_build_time_ns = monotonic_wall_time_now_ns() - serialize_no_gvl_start_time_ns;
823
812
 
824
813
  // Note: The profile gets reset by the serialize call
825
814
  args->result = ddog_prof_Profile_serialize(&args->slot->profile, &args->slot->start_timestamp, &args->finish_timestamp);
826
815
  args->advance_gen_result = ddog_prof_ManagedStringStorage_advance_gen(args->state->string_storage);
827
816
  args->serialize_ran = true;
828
- args->serialize_no_gvl_time_ns = long_max_of(0, monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - serialize_no_gvl_start_time_ns);
817
+ args->serialize_no_gvl_time_ns = long_max_of(0, monotonic_wall_time_now_ns() - serialize_no_gvl_start_time_ns);
829
818
 
830
819
  return NULL; // Unused
831
820
  }
@@ -925,7 +914,7 @@ static VALUE test_slot_mutex_state(VALUE recorder_instance, int slot) {
925
914
  }
926
915
 
927
916
  static ddog_Timespec system_epoch_now_timespec(void) {
928
- long now_ns = system_epoch_time_now_ns(RAISE_ON_FAILURE);
917
+ long now_ns = system_epoch_time_now_ns();
929
918
  return (ddog_Timespec) {.seconds = now_ns / SECONDS_AS_NS(1), .nanoseconds = now_ns % SECONDS_AS_NS(1)};
930
919
  }
931
920
 
@@ -1206,10 +1195,3 @@ static VALUE _native_commit_heap_recordings(DDTRACE_UNUSED VALUE _self, VALUE re
1206
1195
 
1207
1196
  return Qtrue;
1208
1197
  }
1209
-
1210
- void recorder_install_on_serialize(VALUE recorder_instance, VALUE thread_context_collector_instance) {
1211
- stack_recorder_state *state;
1212
- TypedData_Get_Struct(recorder_instance, stack_recorder_state, &stack_recorder_typed_data, state);
1213
-
1214
- state->thread_context_collector_instance = enforce_thread_context_collector_instance(thread_context_collector_instance);
1215
- }
@@ -37,5 +37,4 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
37
37
  void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_CharSlice endpoint);
38
38
  void recorder_commit_heap_recordings_may_lose_gvl(VALUE recorder_instance);
39
39
  void recorder_heap_update_may_lose_gvl(VALUE recorder_instance);
40
- void recorder_install_on_serialize(VALUE recorder_instance, VALUE thread_context_collector_instance);
41
40
  VALUE enforce_recorder_instance(VALUE object);
@@ -28,10 +28,8 @@ long monotonic_to_system_epoch_ns(monotonic_to_system_epoch_state *state, long m
28
28
  (state->delta_to_epoch_ns + monotonic_wall_time_ns > state->system_epoch_ns_reference + SECONDS_AS_NS(60));
29
29
 
30
30
  if (reference_needs_update) {
31
- state->system_epoch_ns_reference = system_epoch_time_now_ns(RAISE_ON_FAILURE);
32
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
33
-
34
- state->delta_to_epoch_ns = state->system_epoch_ns_reference - current_monotonic_wall_time_ns;
31
+ state->system_epoch_ns_reference = system_epoch_time_now_ns();
32
+ state->delta_to_epoch_ns = state->system_epoch_ns_reference - monotonic_wall_time_now_ns();
35
33
  }
36
34
 
37
35
  return state->delta_to_epoch_ns + monotonic_wall_time_ns;
@@ -1,18 +1,16 @@
1
1
  #pragma once
2
2
 
3
- #include <stdbool.h>
3
+ #include <ruby.h>
4
4
  #include <errno.h>
5
+ #include <stdbool.h>
5
6
  #include <time.h>
6
7
 
7
8
  #include "extconf.h"
8
- #include "ruby_helpers.h"
9
9
 
10
10
  #define SECONDS_AS_NS(value) ((value) * 1000L * 1000L * 1000L)
11
11
  #define MILLIS_AS_NS(value) ((value) * 1000L * 1000L)
12
12
  #define MICROS_AS_NS(value) ((value) * 1000L)
13
13
 
14
- typedef enum { RAISE_ON_FAILURE, DO_NOT_RAISE_ON_FAILURE } raise_on_failure_setting;
15
-
16
14
  #define INVALID_TIME -1
17
15
 
18
16
  typedef struct {
@@ -23,12 +21,13 @@ typedef struct {
23
21
 
24
22
  #define MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER {.system_epoch_ns_reference = INVALID_TIME}
25
23
 
26
- static inline long retrieve_clock_as_ns(clockid_t clock_id, raise_on_failure_setting raise_on_failure) {
24
+ // clock_id MUST be one of CLOCK_REALTIME/CLOCK_MONOTONIC_FOR_PROFILING/CLOCK_MONOTONIC_COARSE_FOR_PROFILING
25
+ // as for those we're guaranteed to never see failures here unless something is really off
26
+ static inline long retrieve_clock_as_ns(clockid_t clock_id) {
27
27
  struct timespec clock_value;
28
28
 
29
- if (clock_gettime(clock_id, &clock_value) != 0) {
30
- if (raise_on_failure == RAISE_ON_FAILURE) ENFORCE_SUCCESS_GVL(errno);
31
- return 0;
29
+ if (RB_UNLIKELY(clock_gettime(clock_id, &clock_value) != 0)) {
30
+ rb_bug("[ddtrace] clock_gettime(%d) unexpectedly failed (errno=%d) but it should always succeed, libc/kernel bug?", (int) clock_id, errno);
32
31
  }
33
32
 
34
33
  return clock_value.tv_nsec + SECONDS_AS_NS(clock_value.tv_sec);
@@ -43,8 +42,8 @@ static inline long retrieve_clock_as_ns(clockid_t clock_id, raise_on_failure_set
43
42
  #define CLOCK_MONOTONIC_COARSE_FOR_PROFILING CLOCK_MONOTONIC_COARSE
44
43
  #endif
45
44
 
46
- static inline long monotonic_wall_time_now_ns(raise_on_failure_setting raise_on_failure) { return retrieve_clock_as_ns(CLOCK_MONOTONIC_FOR_PROFILING, raise_on_failure); }
47
- static inline long system_epoch_time_now_ns(raise_on_failure_setting raise_on_failure) { return retrieve_clock_as_ns(CLOCK_REALTIME, raise_on_failure); }
45
+ static inline long monotonic_wall_time_now_ns(void) { return retrieve_clock_as_ns(CLOCK_MONOTONIC_FOR_PROFILING); }
46
+ static inline long system_epoch_time_now_ns(void) { return retrieve_clock_as_ns(CLOCK_REALTIME); }
48
47
 
49
48
  // Coarse instants use CLOCK_MONOTONIC_COARSE on Linux which is expected to provide resolution in the millisecond range:
50
49
  // https://docs.redhat.com/en/documentation/red_hat_enterprise_linux_for_real_time/7/html/reference_guide/sect-posix_clocks#Using_clock_getres_to_compare_clock_resolution
@@ -57,7 +56,7 @@ typedef struct {
57
56
  static inline coarse_instant to_coarse_instant(long timestamp_ns) { return (coarse_instant) {.timestamp_ns = timestamp_ns}; }
58
57
 
59
58
  static inline coarse_instant monotonic_coarse_wall_time_now_ns(void) {
60
- return to_coarse_instant(retrieve_clock_as_ns(CLOCK_MONOTONIC_COARSE_FOR_PROFILING, DO_NOT_RAISE_ON_FAILURE));
59
+ return to_coarse_instant(retrieve_clock_as_ns(CLOCK_MONOTONIC_COARSE_FOR_PROFILING));
61
60
  }
62
61
 
63
62
  long monotonic_to_system_epoch_ns(monotonic_to_system_epoch_state *state, long monotonic_wall_time_ns);