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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -1
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +114 -141
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -5
- data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +0 -2
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +13 -13
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
- data/ext/datadog_profiling_native_extension/collectors_stack.c +1 -0
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +106 -103
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -3
- data/ext/datadog_profiling_native_extension/extconf.rb +2 -0
- data/ext/datadog_profiling_native_extension/heap_recorder.c +2 -2
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +98 -54
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +5 -2
- data/ext/datadog_profiling_native_extension/profiling.c +21 -23
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +3 -1
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
- data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
- data/ext/datadog_profiling_native_extension/stack_recorder.c +6 -24
- data/ext/datadog_profiling_native_extension/stack_recorder.h +0 -1
- data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +10 -11
- data/ext/libdatadog_api/trace_exporter.c +77 -11
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +5 -0
- data/lib/datadog/profiling/component.rb +2 -3
- data/lib/datadog/profiling/exporter.rb +7 -5
- data/lib/datadog/tracing/component.rb +1 -1
- data/lib/datadog/tracing/configuration/settings.rb +3 -2
- data/lib/datadog/tracing/contrib/utils/quantization/http.rb +6 -3
- data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -0
- data/lib/datadog/tracing/transport/native.rb +22 -21
- data/lib/datadog/version.rb +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
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
|
-
//
|
|
100
|
-
bool
|
|
101
|
-
current_gvl_owner 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 `
|
|
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 `
|
|
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
|
|
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
|
-
|
|
163
|
+
private_vm_api_global.main_ractor_gvl_owner_context->threads.sched.running;
|
|
128
164
|
#elif HAVE_RUBY_RACTOR_H
|
|
129
|
-
|
|
165
|
+
private_vm_api_global.main_ractor_gvl_owner_context->threads.gvl.owner;
|
|
130
166
|
#else
|
|
131
|
-
|
|
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
|
|
153
|
-
rb_vm_t *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
|
|
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 `
|
|
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 `
|
|
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 (
|
|
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 (
|
|
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 ==
|
|
597
|
-
stack_buffer[i].as.ruby_frame.caching_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 =
|
|
606
|
-
stack_buffer[i].as.ruby_frame.caching_pc = (void *)
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
41
|
-
static VALUE
|
|
42
|
-
static void *
|
|
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, "
|
|
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
|
-
"
|
|
95
|
-
|
|
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
|
|
223
|
-
return (
|
|
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
|
|
227
|
-
return ((bool) rb_thread_call_without_gvl(
|
|
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 *
|
|
231
|
-
return (void *)
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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] =
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
32
|
-
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
47
|
-
static inline long system_epoch_time_now_ns(
|
|
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
|
|
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);
|