datadog 2.41.0 → 2.42.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  9. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  10. data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
  11. data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
  12. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  20. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  21. data/ext/libdatadog_api/di.c +10 -0
  22. data/ext/libdatadog_api/extconf.rb +3 -0
  23. data/ext/libdatadog_api/init.c +2 -0
  24. data/ext/libdatadog_api/otel_thread_context.c +232 -0
  25. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  26. data/ext/libdatadog_extconf_helpers.rb +1 -1
  27. data/lib/datadog/appsec/assets/blocked.html +1 -108
  28. data/lib/datadog/core/configuration/components.rb +1 -0
  29. data/lib/datadog/core/crashtracking/component.rb +5 -1
  30. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  31. data/lib/datadog/data_streams/processor.rb +31 -0
  32. data/lib/datadog/di/instrumenter.rb +41 -1
  33. data/lib/datadog/di/logger.rb +2 -2
  34. data/lib/datadog/di/probe.rb +9 -1
  35. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  36. data/lib/datadog/di/remote.rb +3 -3
  37. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  38. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  39. data/lib/datadog/open_feature/ext.rb +19 -0
  40. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  41. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  42. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  43. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  44. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  45. data/lib/datadog/open_feature/provider.rb +11 -2
  46. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  47. data/lib/datadog/opentelemetry/trace.rb +3 -0
  48. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  49. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  50. data/lib/datadog/profiling/component.rb +8 -16
  51. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  52. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  53. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  54. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  55. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  56. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  57. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  58. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  59. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  60. data/lib/datadog/tracing/otel_thread_context.rb +30 -0
  61. data/lib/datadog/tracing/remote.rb +195 -27
  62. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  63. data/lib/datadog/tracing/trace_digest.rb +22 -4
  64. data/lib/datadog/tracing/trace_operation.rb +22 -10
  65. data/lib/datadog/tracing/tracer.rb +5 -5
  66. data/lib/datadog/version.rb +1 -1
  67. metadata +14 -8
  68. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -2,50 +2,45 @@
2
2
 
3
3
  // This file exports functions used to access private Ruby VM APIs and internals.
4
4
  // To do this, it imports a few VM internal (private) headers.
5
+ // We rely on the datadog-ruby_core_source gem to get access to private VM headers; see
6
+ // https://github.com/DataDog/datadog-ruby_core_source for details.
5
7
  //
6
8
  // **Important Note**: Our medium/long-term plan is to stop relying on all private Ruby headers, and instead request and
7
9
  // contribute upstream changes so that they become official public VM APIs.
8
10
  //
9
11
  // In the meanwhile, be very careful when changing things here :)
10
12
 
11
- #ifdef RUBY_MJIT_HEADER
12
- // Pick up internal structures from the private Ruby MJIT header file
13
- #include RUBY_MJIT_HEADER
14
- #else
15
- // The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on
16
- // the datadog-ruby_core_source gem to get access to private VM headers.
13
+ #include <ruby/defines.h>
14
+
15
+ // We can't do anything about warnings in VM headers, so we just use this technique to suppress them.
16
+ // See https://nelkinda.com/blog/suppress-warnings-in-gcc-and-clang/#d11e364 for details.
17
+ #pragma GCC diagnostic push
18
+ #pragma GCC diagnostic ignored "-Wunused-parameter"
19
+ #pragma GCC diagnostic ignored "-Wattributes"
20
+ #pragma GCC diagnostic ignored "-Wpragmas"
21
+ #pragma GCC diagnostic ignored "-Wexpansion-to-defined"
22
+ #include <vm_core.h>
23
+ #pragma GCC diagnostic pop
17
24
 
18
- // We can't do anything about warnings in VM headers, so we just use this technique to suppress them.
19
- // See https://nelkinda.com/blog/suppress-warnings-in-gcc-and-clang/#d11e364 for details.
25
+ #pragma GCC diagnostic push
26
+ #pragma GCC diagnostic ignored "-Wunused-parameter"
27
+ #include <iseq.h>
28
+ #pragma GCC diagnostic pop
29
+
30
+ #ifndef NO_INTERNAL_CLASS_HEADER_INCLUDE
20
31
  #pragma GCC diagnostic push
21
32
  #pragma GCC diagnostic ignored "-Wunused-parameter"
22
- #pragma GCC diagnostic ignored "-Wattributes"
23
- #pragma GCC diagnostic ignored "-Wpragmas"
24
- #pragma GCC diagnostic ignored "-Wexpansion-to-defined"
25
- #include <vm_core.h>
33
+ #include <internal/class.h>
26
34
  #pragma GCC diagnostic pop
35
+ #endif
36
+
37
+ #include <ruby.h>
27
38
 
39
+ #ifndef NO_RACTOR_HEADER_INCLUDE
28
40
  #pragma GCC diagnostic push
29
41
  #pragma GCC diagnostic ignored "-Wunused-parameter"
30
- #include <iseq.h>
42
+ #include <ractor_core.h>
31
43
  #pragma GCC diagnostic pop
32
-
33
- #ifndef NO_INTERNAL_CLASS_HEADER_INCLUDE
34
- #pragma GCC diagnostic push
35
- #pragma GCC diagnostic ignored "-Wunused-parameter"
36
- #include <internal/class.h>
37
- #pragma GCC diagnostic pop
38
- #endif
39
-
40
- #include <ruby.h>
41
-
42
- #ifndef NO_RACTOR_HEADER_INCLUDE
43
- #pragma GCC diagnostic push
44
- #pragma GCC diagnostic ignored "-Wunused-parameter"
45
- #include <ractor_core.h>
46
- #pragma GCC diagnostic pop
47
- #endif
48
-
49
44
  #endif
50
45
 
51
46
  // This file can't include datadog_ruby_common.h so we replicate this here
@@ -70,7 +65,9 @@ static const rb_callable_method_entry_t* safe_vm_frame_method_entry(const rb_con
70
65
  // if the argument passed in is not actually a `Thread` instance.
71
66
  static inline rb_thread_t *thread_struct_from_object(VALUE thread) {
72
67
  static const rb_data_type_t *thread_data_type = NULL;
73
- if (UNLIKELY(thread_data_type == NULL)) thread_data_type = RTYPEDDATA_TYPE(rb_thread_current());
68
+ if (UNLIKELY(thread_data_type == NULL)) {
69
+ thread_data_type = RTYPEDDATA_TYPE(rb_thread_current());
70
+ }
74
71
 
75
72
  return (rb_thread_t *) rb_check_typeddata(thread, thread_data_type);
76
73
  }
@@ -80,7 +77,9 @@ rb_nativethread_id_t pthread_id_for(VALUE thread) {
80
77
  #ifndef NO_RB_NATIVE_THREAD
81
78
  struct rb_native_thread* native_thread = thread_struct_from_object(thread)->nt;
82
79
  // This can be NULL on Ruby 3.3 with MN threads (RUBY_MN_THREADS=1)
83
- if (native_thread == NULL) return 0;
80
+ if (native_thread == NULL) {
81
+ return 0;
82
+ }
84
83
  return native_thread->thread_id;
85
84
  #else
86
85
  return thread_struct_from_object(thread)->thread_id;
@@ -104,87 +103,93 @@ bool is_current_thread_holding_the_gvl(void) {
104
103
  }
105
104
 
106
105
  #ifdef HAVE_RUBY_RACTOR_H
107
- static inline rb_ractor_t *ddtrace_get_ractor(void) {
108
- #ifndef USE_RACTOR_INTERNAL_APIS_DIRECTLY // Ruby >= 3.3
109
- return thread_struct_from_object(rb_thread_current())->ractor;
110
- #else
111
- return GET_RACTOR();
112
- #endif
113
- }
106
+ static inline rb_ractor_t *ddtrace_get_ractor(void) {
107
+ #ifndef USE_RACTOR_INTERNAL_APIS_DIRECTLY // Ruby >= 3.3
108
+ return thread_struct_from_object(rb_thread_current())->ractor;
109
+ #else
110
+ return GET_RACTOR();
111
+ #endif
112
+ }
114
113
  #endif
115
114
 
116
115
  #ifndef NO_GVL_OWNER // Ruby < 2.6 doesn't have the owner/running field
117
- // NOTE: Reading the owner in this is a racy read, because we're not grabbing the lock that Ruby uses to protect it.
118
- //
119
- // While we could potentially grab this lock, I (@ivoanjo) think we actually don't need it because:
120
- // * In the case where a thread owns the GVL and calls `gvl_owner`, it will always see the correct value. That's
121
- // because every thread sets itself as the owner when it grabs the GVL and unsets itself at the end.
122
- // That means that `is_current_thread_holding_the_gvl` is always accurate.
123
- // * In a case where we observe a different thread, then this may change by the time we do something with this value
124
- // anyway. So unless we want to prevent the Ruby scheduler from switching threads, we need to deal with races here.
125
- current_gvl_owner gvl_owner(void) {
126
- const rb_thread_t *current_owner =
127
- #ifndef NO_RB_THREAD_SCHED // Introduced in Ruby 3.2 as a replacement for struct rb_global_vm_lock_struct
128
- ddtrace_get_ractor()->threads.sched.running;
129
- #elif HAVE_RUBY_RACTOR_H
130
- ddtrace_get_ractor()->threads.gvl.owner;
131
- #else
132
- GET_VM()->gvl.owner;
133
- #endif
134
-
135
- if (current_owner == NULL) return (current_gvl_owner) {.valid = false};
136
-
137
- #ifndef NO_RB_NATIVE_THREAD
138
- struct rb_native_thread* current_owner_native_thread = current_owner->nt;
139
-
140
- // This can be NULL on Ruby 3.3 with MN threads (RUBY_MN_THREADS=1)
141
- if (current_owner_native_thread == NULL) return (current_gvl_owner) {.valid = false};
142
-
143
- return (current_gvl_owner) {.valid = true, .owner = current_owner_native_thread->thread_id};
116
+ // NOTE: Reading the owner in this is a racy read, because we're not grabbing the lock that Ruby uses to protect it.
117
+ //
118
+ // 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
120
+ // 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.
122
+ // * In a case where we observe a different thread, then this may change by the time we do something with this value
123
+ // 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) {
125
+ const rb_thread_t *current_owner =
126
+ #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;
128
+ #elif HAVE_RUBY_RACTOR_H
129
+ ddtrace_get_ractor()->threads.gvl.owner;
144
130
  #else
145
- return (current_gvl_owner) {.valid = true, .owner = current_owner->thread_id};
131
+ GET_VM()->gvl.owner;
146
132
  #endif
133
+
134
+ if (current_owner == NULL) {
135
+ return (current_gvl_owner) {.valid = false};
147
136
  }
148
- #else
149
- current_gvl_owner gvl_owner(void) {
150
- rb_vm_t *vm = GET_VM();
151
137
 
152
- // BIG Issue: Ruby < 2.6 did not have the owner field. The really nice thing about the owner field is that it's
153
- // "atomic" -- when a thread sets it, it "declares" two things in a single step
154
- // * Declaration 1: Someone has the GVL
155
- // * Declaration 2: That someone is the specific thread
156
- //
157
- // Observation 1: On older versions of Ruby, this ownership concept is actually split. Specifically, `gvl.acquired`
158
- // is a boolean that represents declaration 1 above, and `vm->running_thread` (or `ruby_current_thread`/
159
- // `ruby_current_execution_context_ptr`) represents declaration 2.
160
- //
161
- // Observation 2: In addition, when a thread releases the GVL, it only sets `gvl.acquired` back to 0 **BUT CRUCIALLY
162
- // DOES NOT CHANGE THE OTHER global variables**.
163
- //
164
- // Observation 1+2 above lead to the following possible race:
165
- // * Thread A grabs the GVL (`gvl.acquired == 1`)
166
- // * Thread A sets `running_thread` (`gvl.acquired == 1` + `running_thread == Thread A`)
167
- // * Thread A releases the GVL (`gvl.acquired == 0` + `running_thread == Thread A`)
168
- // * Thread B grabs the GVL (`gvl.acquired == 1` + `running_thread == Thread A`)
169
- // * Thread A calls gvl_owner. Due to the current state (`gvl.acquired == 1` + `running_thread == Thread A`), this
170
- // function returns an incorrect result.
171
- // * Thread B finally sets `running_thread` (`gvl.acquired == 1` + `running_thread == Thread B`)
172
- //
173
- // This is especially problematic because we use `gvl_owner` to implement `is_current_thread_holding_the_gvl` which
174
- // is called in a signal handler to decide "is it safe for me to call `rb_postponed_job_register_one` or not".
175
- // (See constraints in `collectors_cpu_and_wall_time_worker.c` comments for why).
176
- //
177
- // Thus an incorrect `is_current_thread_holding_the_gvl` result may lead to issues inside `rb_postponed_job_register_one`.
178
- //
179
- // For this reason we default to use the "no signals workaround" on Ruby 2.5 by default, and we print a
180
- // warning when customers force-enable it.
181
- bool gvl_acquired = vm->gvl.acquired != 0;
182
- rb_thread_t *current_owner = vm->running_thread;
138
+ #ifndef NO_RB_NATIVE_THREAD
139
+ struct rb_native_thread* current_owner_native_thread = current_owner->nt;
183
140
 
184
- if (!gvl_acquired || current_owner == NULL) return (current_gvl_owner) {.valid = false};
141
+ // This can be NULL on Ruby 3.3 with MN threads (RUBY_MN_THREADS=1)
142
+ if (current_owner_native_thread == NULL) {
143
+ return (current_gvl_owner) {.valid = false};
144
+ }
185
145
 
146
+ return (current_gvl_owner) {.valid = true, .owner = current_owner_native_thread->thread_id};
147
+ #else
186
148
  return (current_gvl_owner) {.valid = true, .owner = current_owner->thread_id};
149
+ #endif
150
+ }
151
+ #else
152
+ current_gvl_owner gvl_owner(void) {
153
+ rb_vm_t *vm = GET_VM();
154
+
155
+ // BIG Issue: Ruby < 2.6 did not have the owner field. The really nice thing about the owner field is that it's
156
+ // "atomic" -- when a thread sets it, it "declares" two things in a single step
157
+ // * Declaration 1: Someone has the GVL
158
+ // * Declaration 2: That someone is the specific thread
159
+ //
160
+ // Observation 1: On older versions of Ruby, this ownership concept is actually split. Specifically, `gvl.acquired`
161
+ // is a boolean that represents declaration 1 above, and `vm->running_thread` (or `ruby_current_thread`/
162
+ // `ruby_current_execution_context_ptr`) represents declaration 2.
163
+ //
164
+ // Observation 2: In addition, when a thread releases the GVL, it only sets `gvl.acquired` back to 0 **BUT CRUCIALLY
165
+ // DOES NOT CHANGE THE OTHER global variables**.
166
+ //
167
+ // Observation 1+2 above lead to the following possible race:
168
+ // * Thread A grabs the GVL (`gvl.acquired == 1`)
169
+ // * Thread A sets `running_thread` (`gvl.acquired == 1` + `running_thread == Thread A`)
170
+ // * Thread A releases the GVL (`gvl.acquired == 0` + `running_thread == Thread A`)
171
+ // * 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
173
+ // function returns an incorrect result.
174
+ // * Thread B finally sets `running_thread` (`gvl.acquired == 1` + `running_thread == Thread B`)
175
+ //
176
+ // This is especially problematic because we use `gvl_owner` to implement `is_current_thread_holding_the_gvl` which
177
+ // is called in a signal handler to decide "is it safe for me to call `rb_postponed_job_register_one` or not".
178
+ // (See constraints in `collectors_cpu_and_wall_time_worker.c` comments for why).
179
+ //
180
+ // Thus an incorrect `is_current_thread_holding_the_gvl` result may lead to issues inside `rb_postponed_job_register_one`.
181
+ //
182
+ // For this reason we default to use the "no signals workaround" on Ruby 2.5 by default, and we print a
183
+ // warning when customers force-enable it.
184
+ bool gvl_acquired = vm->gvl.acquired != 0;
185
+ rb_thread_t *current_owner = vm->running_thread;
186
+
187
+ if (!gvl_acquired || current_owner == NULL) {
188
+ return (current_gvl_owner) {.valid = false};
187
189
  }
190
+
191
+ return (current_gvl_owner) {.valid = true, .owner = current_owner->thread_id};
192
+ }
188
193
  #endif // NO_GVL_OWNER
189
194
 
190
195
  // Taken from upstream vm_core.h at commit d9cf0388599a3234b9f3c06ddd006cd59a58ab8b (November 2022, Ruby 3.2 trunk)
@@ -192,7 +197,7 @@ bool is_current_thread_holding_the_gvl(void) {
192
197
  // to support tid_for (see below)
193
198
  // Modifications: None
194
199
  #if defined(__linux__) || defined(__FreeBSD__)
195
- # define RB_THREAD_T_HAS_NATIVE_ID
200
+ #define RB_THREAD_T_HAS_NATIVE_ID
196
201
  #endif
197
202
 
198
203
  uint64_t native_thread_id_for(VALUE thread) {
@@ -200,7 +205,9 @@ uint64_t native_thread_id_for(VALUE thread) {
200
205
  #if !defined(NO_THREAD_TID) && defined(RB_THREAD_T_HAS_NATIVE_ID)
201
206
  #ifndef NO_RB_NATIVE_THREAD
202
207
  struct rb_native_thread* native_thread = thread_struct_from_object(thread)->nt;
203
- if (native_thread == NULL) return 0;
208
+ if (native_thread == NULL) {
209
+ return 0;
210
+ }
204
211
  return native_thread->tid;
205
212
  #else
206
213
  return thread_struct_from_object(thread)->tid;
@@ -212,7 +219,9 @@ uint64_t native_thread_id_for(VALUE thread) {
212
219
  uint64_t result;
213
220
  // On macOS, this gives us the same identifier that shows up in activity monitor
214
221
  int error = pthread_threadid_np(pthread_id, &result);
215
- if (error) rb_syserr_fail(error, "Unexpected failure in pthread_threadid_np");
222
+ if (error) {
223
+ rb_syserr_fail(error, "Unexpected failure in pthread_threadid_np");
224
+ }
216
225
  return result;
217
226
  #else
218
227
  // Fallback, when we have nothing better (e.g. on Ruby < 3.1 on Linux)
@@ -244,11 +253,12 @@ void ddtrace_thread_list(VALUE result_array) {
244
253
  // called from a different Ractor, but I'm not sure...
245
254
  #ifdef HAVE_RUBY_RACTOR_H
246
255
  rb_ractor_t *current_ractor = ddtrace_get_ractor();
247
- ccan_list_for_each(&current_ractor->threads.set, thread, lt_node) {
256
+ ccan_list_for_each(&current_ractor->threads.set, thread, lt_node)
248
257
  #else
249
258
  rb_vm_t *vm = GET_VM();
250
- list_for_each(&vm->living_threads, thread, vmlt_node) {
259
+ list_for_each(&vm->living_threads, thread, vmlt_node)
251
260
  #endif
261
+ {
252
262
  switch (thread->status) {
253
263
  case THREAD_RUNNABLE:
254
264
  case THREAD_STOPPED:
@@ -316,63 +326,72 @@ VALUE thread_name_for(VALUE thread) {
316
326
  #pragma GCC diagnostic push
317
327
  #pragma GCC diagnostic ignored "-Wunused-parameter"
318
328
  static inline int
319
- calc_pos(const rb_iseq_t *iseq, const VALUE *pc, int *lineno, int *node_id)
320
- {
321
- VM_ASSERT(iseq);
322
- VM_ASSERT(ISEQ_BODY(iseq));
323
- VM_ASSERT(ISEQ_BODY(iseq)->iseq_encoded);
324
- VM_ASSERT(ISEQ_BODY(iseq)->iseq_size);
325
- if (! pc) {
326
- if (ISEQ_BODY(iseq)->type == ISEQ_TYPE_TOP) {
327
- VM_ASSERT(! ISEQ_BODY(iseq)->local_table);
328
- VM_ASSERT(! ISEQ_BODY(iseq)->local_table_size);
329
- return 0;
330
- }
331
- # ifndef NO_INT_FIRST_LINENO // Ruby 3.2+
332
- if (lineno) *lineno = ISEQ_BODY(iseq)->location.first_lineno;
333
- # else
334
- if (lineno) *lineno = FIX2INT(ISEQ_BODY(iseq)->location.first_lineno);
335
- #endif
329
+ calc_pos(const rb_iseq_t *iseq, const VALUE *pc, int *lineno, int *node_id) {
330
+ VM_ASSERT(iseq);
331
+ VM_ASSERT(ISEQ_BODY(iseq));
332
+ VM_ASSERT(ISEQ_BODY(iseq)->iseq_encoded);
333
+ VM_ASSERT(ISEQ_BODY(iseq)->iseq_size);
334
+ if (pc == NULL) {
335
+ if (ISEQ_BODY(iseq)->type == ISEQ_TYPE_TOP) {
336
+ VM_ASSERT(! ISEQ_BODY(iseq)->local_table);
337
+ VM_ASSERT(! ISEQ_BODY(iseq)->local_table_size);
338
+ return 0;
339
+ }
340
+ #ifndef NO_INT_FIRST_LINENO // Ruby 3.2+
341
+ if (lineno) {
342
+ *lineno = ISEQ_BODY(iseq)->location.first_lineno;
343
+ }
344
+ #else
345
+ if (lineno) {
346
+ *lineno = FIX2INT(ISEQ_BODY(iseq)->location.first_lineno);
347
+ }
348
+ #endif
336
349
  #ifdef USE_ISEQ_NODE_ID
337
- if (node_id) *node_id = -1;
338
- #endif
339
- return 1;
350
+ if (node_id) {
351
+ *node_id = -1;
340
352
  }
341
- else {
342
- ptrdiff_t n = pc - ISEQ_BODY(iseq)->iseq_encoded;
343
- VM_ASSERT(n <= ISEQ_BODY(iseq)->iseq_size);
344
- VM_ASSERT(n >= 0);
345
- ASSUME(n >= 0);
346
- size_t pos = n; /* no overflow */
347
- if (LIKELY(pos)) {
348
- /* use pos-1 because PC points next instruction at the beginning of instruction */
349
- pos--;
350
- }
353
+ #endif
354
+ return 1;
355
+ } else {
356
+ ptrdiff_t n = pc - ISEQ_BODY(iseq)->iseq_encoded;
357
+ VM_ASSERT(n <= ISEQ_BODY(iseq)->iseq_size);
358
+ VM_ASSERT(n >= 0);
359
+ ASSUME(n >= 0);
360
+ size_t pos = n; /* no overflow */
361
+ if (LIKELY(pos)) {
362
+ /* use pos-1 because PC points next instruction at the beginning of instruction */
363
+ pos--;
364
+ } else {
351
365
  #if VMDEBUG && defined(HAVE_BUILTIN___BUILTIN_TRAP)
352
- else {
353
- /* SDR() is not possible; that causes infinite loop. */
354
- rb_print_backtrace();
355
- __builtin_trap();
356
- }
366
+ /* SDR() is not possible; that causes infinite loop. */
367
+ rb_print_backtrace();
368
+ __builtin_trap();
357
369
  #endif
370
+ }
371
+
372
+ // In PROF-11475 we spotted a crash when calling `rb_iseq_line_no` from this method.
373
+ // We were only able to reproduce this issue on Ruby 2.6 and 2.7, not 2.5 or the 3.x series (tried 3.0, 3.2 and 3.4).
374
+ // Note that going out of bounds doesn't crash every time, as usual with C we may just read garbage or get lucky.
375
+ //
376
+ // For those problematic Rubies, we observed that when we try to take a sample in the middle of processing the
377
+ // VM `LEAVE` instruction, the value of `n` can violate the documented assumptions above and be
378
+ // `n > ISEQ_BODY(iseq)->iseq_size)`.
379
+ //
380
+ // To work around this and any other potential issues, we validate here that the bytecode position is sane.
381
+ if (RB_UNLIKELY(n < 0 || n > ISEQ_BODY(iseq)->iseq_size)) {
382
+ return 0;
383
+ }
358
384
 
359
- // In PROF-11475 we spotted a crash when calling `rb_iseq_line_no` from this method.
360
- // We were only able to reproduce this issue on Ruby 2.6 and 2.7, not 2.5 or the 3.x series (tried 3.0, 3.2 and 3.4).
361
- // Note that going out of bounds doesn't crash every time, as usual with C we may just read garbage or get lucky.
362
- //
363
- // For those problematic Rubies, we observed that when we try to take a sample in the middle of processing the
364
- // VM `LEAVE` instruction, the value of `n` can violate the documented assumptions above and be
365
- // `n > ISEQ_BODY(iseq)->iseq_size)`.
366
- //
367
- // To work around this and any other potential issues, we validate here that the bytecode position is sane.
368
- if (RB_UNLIKELY(n < 0 || n > ISEQ_BODY(iseq)->iseq_size)) return 0;
369
-
370
- if (lineno) *lineno = rb_iseq_line_no(iseq, pos);
385
+ if (lineno) {
386
+ *lineno = rb_iseq_line_no(iseq, pos);
387
+ }
371
388
  #ifdef USE_ISEQ_NODE_ID
372
- if (node_id) *node_id = rb_iseq_node_id(iseq, pos);
373
- #endif
374
- return 1;
389
+ if (node_id) {
390
+ *node_id = rb_iseq_node_id(iseq, pos);
375
391
  }
392
+ #endif
393
+ return 1;
394
+ }
376
395
  }
377
396
  #pragma GCC diagnostic pop
378
397
 
@@ -381,11 +400,12 @@ calc_pos(const rb_iseq_t *iseq, const VALUE *pc, int *lineno, int *node_id)
381
400
  // to support our custom rb_profile_frames (see below)
382
401
  // Modifications: None
383
402
  static inline int
384
- calc_lineno(const rb_iseq_t *iseq, const VALUE *pc)
385
- {
386
- int lineno;
387
- if (calc_pos(iseq, pc, &lineno, NULL)) return lineno;
388
- return 0;
403
+ calc_lineno(const rb_iseq_t *iseq, const VALUE *pc) {
404
+ int lineno;
405
+ if (calc_pos(iseq, pc, &lineno, NULL)) {
406
+ return lineno;
407
+ }
408
+ return 0;
389
409
  }
390
410
 
391
411
  // Taken from upstream vm_backtrace.c at commit 5f10bd634fb6ae8f74a4ea730176233b0ca96954 (March 2022, Ruby 3.2 trunk)
@@ -444,176 +464,180 @@ calc_lineno(const rb_iseq_t *iseq, const VALUE *pc)
444
464
  // disagree, and quite a few of them seem oversights/bugs (speculation from my part) rather than deliberate
445
465
  // decisions.
446
466
  int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *stack_buffer) {
447
- int i;
448
- // Modified from upstream: Instead of using `GET_EC` to collect info from the current thread,
449
- // support sampling any thread (including the current) passed as an argument
450
- rb_thread_t *th = thread_struct_from_object(thread);
451
- const rb_execution_context_t *ec = th->ec;
467
+ int i;
468
+ // Modified from upstream: Instead of using `GET_EC` to collect info from the current thread,
469
+ // support sampling any thread (including the current) passed as an argument
470
+ rb_thread_t *th = thread_struct_from_object(thread);
471
+ const rb_execution_context_t *ec = th->ec;
472
+
473
+ // As of this writing, we don't support profiling with MN enabled, and this only happens in that mode, but as we
474
+ // probably want to experiment with it in the future, I've decided to import https://github.com/ruby/ruby/pull/9310
475
+ // here.
476
+ if (ec == NULL) {
477
+ return 0;
478
+ }
479
+
480
+ // Avoid sampling dead threads
481
+ if (th->status == THREAD_KILLED) {
482
+ return 0;
483
+ }
452
484
 
453
- // As of this writing, we don't support profiling with MN enabled, and this only happens in that mode, but as we
454
- // probably want to experiment with it in the future, I've decided to import https://github.com/ruby/ruby/pull/9310
455
- // here.
456
- if (ec == NULL) return 0;
485
+ const rb_control_frame_t *cfp = ec->cfp;
457
486
 
458
- // Avoid sampling dead threads
459
- if (th->status == THREAD_KILLED) return 0;
487
+ // This happens on newly-created threads (we even had a flaky test because of it)
488
+ if (cfp == NULL) {
489
+ return PLACEHOLDER_STACK_IN_NATIVE_CODE;
490
+ }
460
491
 
461
- const rb_control_frame_t *cfp = ec->cfp;
492
+ // I suspect this won't happen for ddtrace, but just-in-case we've imported a potential fix for
493
+ // https://github.com/ruby/ruby/pull/13643 by assuming that these can be NULL/zero with the cfp being non-NULL yet.
494
+ if (ec->vm_stack == NULL || ec->vm_stack_size == 0) {
495
+ return 0;
496
+ }
462
497
 
463
- // This happens on newly-created threads (we even had a flaky test because of it)
464
- if (cfp == NULL) return PLACEHOLDER_STACK_IN_NATIVE_CODE;
498
+ const rb_control_frame_t *end_cfp = RUBY_VM_END_CONTROL_FRAME(ec);
499
+ #ifndef NO_JIT_RETURN
500
+ const rb_control_frame_t *top = cfp;
501
+ #endif
502
+ const rb_callable_method_entry_t *cme;
465
503
 
466
- // I suspect this won't happen for ddtrace, but just-in-case we've imported a potential fix for
467
- // https://github.com/ruby/ruby/pull/13643 by assuming that these can be NULL/zero with the cfp being non-NULL yet.
468
- if (ec->vm_stack == NULL || ec->vm_stack_size == 0) return 0;
504
+ // `vm_backtrace.c` includes this check in several methods. This happens on newly-created threads, and may
505
+ // also (not entirely sure) happen on dead threads
506
+ if (end_cfp == NULL) {
507
+ return PLACEHOLDER_STACK_IN_NATIVE_CODE;
508
+ }
469
509
 
470
- const rb_control_frame_t *end_cfp = RUBY_VM_END_CONTROL_FRAME(ec);
471
- #ifndef NO_JIT_RETURN
472
- const rb_control_frame_t *top = cfp;
473
- #endif
474
- const rb_callable_method_entry_t *cme;
510
+ // Fix: Skip dummy frame that shows up in main thread.
511
+ //
512
+ // According to a comment in `backtrace_each` (`vm_backtrace.c`), there's two dummy frames that we should ignore
513
+ // at the base of every thread's stack.
514
+ // (see https://github.com/ruby/ruby/blob/4bd38e8120f2fdfdd47a34211720e048502377f1/vm_backtrace.c#L890-L914 )
515
+ //
516
+ // One is being pointed to by `RUBY_VM_END_CONTROL_FRAME(ec)`, and so we need to advance to the next one, and
517
+ // reaching it will be used as a condition to break out of the loop below.
518
+ //
519
+ // Note that in `backtrace_each` there's two calls to `RUBY_VM_NEXT_CONTROL_FRAME`, but the loop bounds there
520
+ // are computed in a different way, so the two calls really are equivalent to one here.
521
+ end_cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
475
522
 
476
- // `vm_backtrace.c` includes this check in several methods. This happens on newly-created threads, and may
477
- // also (not entirely sure) happen on dead threads
478
- if (end_cfp == NULL) return PLACEHOLDER_STACK_IN_NATIVE_CODE;
523
+ // See comment on `record_placeholder_stack_in_native_code` for a full explanation of what this means (and why we don't just return 0)
524
+ if (end_cfp <= cfp) {
525
+ return PLACEHOLDER_STACK_IN_NATIVE_CODE;
526
+ }
479
527
 
480
- // Fix: Skip dummy frame that shows up in main thread.
481
- //
482
- // According to a comment in `backtrace_each` (`vm_backtrace.c`), there's two dummy frames that we should ignore
483
- // at the base of every thread's stack.
484
- // (see https://github.com/ruby/ruby/blob/4bd38e8120f2fdfdd47a34211720e048502377f1/vm_backtrace.c#L890-L914 )
485
- //
486
- // One is being pointed to by `RUBY_VM_END_CONTROL_FRAME(ec)`, and so we need to advance to the next one, and
487
- // reaching it will be used as a condition to break out of the loop below.
488
- //
489
- // Note that in `backtrace_each` there's two calls to `RUBY_VM_NEXT_CONTROL_FRAME`, but the loop bounds there
490
- // are computed in a different way, so the two calls really are equivalent to one here.
491
- end_cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
492
-
493
- // See comment on `record_placeholder_stack_in_native_code` for a full explanation of what this means (and why we don't just return 0)
494
- if (end_cfp <= cfp) return PLACEHOLDER_STACK_IN_NATIVE_CODE;
495
-
496
- // This is the position just after the top of the stack -- e.g. where a new frame pushed on the stack would end up.
497
- const rb_control_frame_t *top_sentinel = RUBY_VM_NEXT_CONTROL_FRAME(cfp);
498
-
499
- // We iterate the stack from bottom (beginning of thread) to the top (currently-active frame). This is different
500
- // from upstream rb_profile_frames, but actually matches what `backtrace_each` does (yes, different Ruby VM APIs
501
- // iterate in different directions).
502
- // We do this to better take advantage of the `same_frame` caching mechanism: By starting from the bottom of the
503
- // stack towards the top, we can usually keep most of the stack intact when the code is only going up and down
504
- // a few methods at the top. Before this change, the cache was really only useful if between samples the app had
505
- // not moved from the current stack, as adding or removing one frame would invalidate the existing cache (because
506
- // every position would shift).
507
- cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
508
-
509
- for (i=0; i<limit && cfp != top_sentinel; cfp = RUBY_VM_NEXT_CONTROL_FRAME(cfp)) {
510
- if (cfp->iseq && !cfp->pc) {
511
- // Fix: Do nothing -- this frame should not be used
512
- //
513
- // rb_profile_frames does not do this check, but `backtrace_each` (`vm_backtrace.c`) does. This frame is not
514
- // exposed by the Ruby backtrace APIs and for now we want to match its behavior 1:1
515
- }
516
- else if (cfp->ep == NULL) {
517
- // Do nothing -- this frame should not be used
518
- //
519
- // We're not sure this can ever happen, but we've seen a crash inside `VM_FRAME_RUBYFRAME_P` below (which
520
- // dereferences `cfp->ep`), so "just in case" we're adding this extra sanity check to avoid crashing on a
521
- // NULL `ep`.
528
+ // This is the position just after the top of the stack -- e.g. where a new frame pushed on the stack would end up.
529
+ const rb_control_frame_t *top_sentinel = RUBY_VM_NEXT_CONTROL_FRAME(cfp);
530
+
531
+ // We iterate the stack from bottom (beginning of thread) to the top (currently-active frame). This is different
532
+ // from upstream rb_profile_frames, but actually matches what `backtrace_each` does (yes, different Ruby VM APIs
533
+ // iterate in different directions).
534
+ // We do this to better take advantage of the `same_frame` caching mechanism: By starting from the bottom of the
535
+ // stack towards the top, we can usually keep most of the stack intact when the code is only going up and down
536
+ // a few methods at the top. Before this change, the cache was really only useful if between samples the app had
537
+ // not moved from the current stack, as adding or removing one frame would invalidate the existing cache (because
538
+ // every position would shift).
539
+ cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
540
+
541
+ for (i=0; i<limit && cfp != top_sentinel; cfp = RUBY_VM_NEXT_CONTROL_FRAME(cfp)) {
542
+ if (cfp->iseq && !cfp->pc) {
543
+ // Fix: Do nothing -- this frame should not be used
544
+ //
545
+ // rb_profile_frames does not do this check, but `backtrace_each` (`vm_backtrace.c`) does. This frame is not
546
+ // exposed by the Ruby backtrace APIs and for now we want to match its behavior 1:1
547
+ } else if (cfp->ep == NULL) {
548
+ // Do nothing -- this frame should not be used
549
+ //
550
+ // We're not sure this can ever happen, but we've seen a crash inside `VM_FRAME_RUBYFRAME_P` below (which
551
+ // dereferences `cfp->ep`), so "just in case" we're adding this extra sanity check to avoid crashing on a
552
+ // NULL `ep`.
553
+ } else if (VM_FRAME_RUBYFRAME_P(cfp)) {
554
+ if (start > 0) {
555
+ start--;
556
+ continue;
557
+ }
558
+
559
+ cme = safe_vm_frame_method_entry(cfp);
560
+
561
+ // Upstream (Ruby 4.0) does:
562
+ // if (cme && cme->def->type == VM_METHOD_TYPE_ISEQ) {
563
+ // buff[i] = (VALUE)cme;
564
+ // } else {
565
+ // buff[i] = (VALUE)cfp->iseq;
566
+ // }
567
+ // We get both the iseq and CME because we need both to format like Ruby backtraces
568
+
569
+ stack_buffer[i].same_frame =
570
+ stack_buffer[i].is_ruby_frame &&
571
+ stack_buffer[i].as.ruby_frame.iseq == cfp->iseq &&
572
+ stack_buffer[i].as.ruby_frame.caching_pc == cfp->pc &&
573
+ stack_buffer[i].cme == cme;
574
+
575
+ if (stack_buffer[i].same_frame) { // Nothing to do, buffer already contains this frame
576
+ i++;
577
+ continue;
578
+ }
579
+
580
+ stack_buffer[i].as.ruby_frame.iseq = cfp->iseq;
581
+ stack_buffer[i].as.ruby_frame.caching_pc = (void *) cfp->pc;
582
+ stack_buffer[i].cme = cme;
583
+
584
+ // The topmost frame may not have an updated PC because the JIT
585
+ // may not have set one. The JIT compiler will update the PC
586
+ // before entering a new function (so that `caller` will work),
587
+ // so only the topmost frame could possibly have an out of date PC
588
+ #ifndef NO_JIT_RETURN
589
+ if (cfp == top && cfp->jit_return) {
590
+ stack_buffer[i].as.ruby_frame.line = 0;
591
+ } else {
592
+ stack_buffer[i].as.ruby_frame.line = calc_lineno(cfp->iseq, cfp->pc);
522
593
  }
523
- else if (VM_FRAME_RUBYFRAME_P(cfp)) {
524
- if (start > 0) {
525
- start--;
526
- continue;
527
- }
528
-
529
- cme = safe_vm_frame_method_entry(cfp);
530
-
531
- // Upstream (Ruby 4.0) does:
532
- // if (cme && cme->def->type == VM_METHOD_TYPE_ISEQ) {
533
- // buff[i] = (VALUE)cme;
534
- // } else {
535
- // buff[i] = (VALUE)cfp->iseq;
536
- // }
537
- // We get both the iseq and CME because we need both to format like Ruby backtraces
538
-
539
- stack_buffer[i].same_frame =
540
- stack_buffer[i].is_ruby_frame &&
541
- stack_buffer[i].as.ruby_frame.iseq == cfp->iseq &&
542
- stack_buffer[i].as.ruby_frame.caching_pc == cfp->pc &&
543
- stack_buffer[i].cme == cme;
544
-
545
- if (stack_buffer[i].same_frame) { // Nothing to do, buffer already contains this frame
546
- i++;
547
- continue;
548
- }
549
-
550
- stack_buffer[i].as.ruby_frame.iseq = cfp->iseq;
551
- stack_buffer[i].as.ruby_frame.caching_pc = (void *) cfp->pc;
552
- stack_buffer[i].cme = cme;
553
-
554
- // The topmost frame may not have an updated PC because the JIT
555
- // may not have set one. The JIT compiler will update the PC
556
- // before entering a new function (so that `caller` will work),
557
- // so only the topmost frame could possibly have an out of date PC
558
- #ifndef NO_JIT_RETURN
559
- if (cfp == top && cfp->jit_return) {
560
- stack_buffer[i].as.ruby_frame.line = 0;
561
- } else {
562
- stack_buffer[i].as.ruby_frame.line = calc_lineno(cfp->iseq, cfp->pc);
563
- }
564
- #else // Ruby < 3.1
565
- stack_buffer[i].as.ruby_frame.line = calc_lineno(cfp->iseq, cfp->pc);
566
- #endif
567
-
568
- stack_buffer[i].is_ruby_frame = true;
569
- i++;
594
+ #else // Ruby < 3.1
595
+ stack_buffer[i].as.ruby_frame.line = calc_lineno(cfp->iseq, cfp->pc);
596
+ #endif
597
+
598
+ stack_buffer[i].is_ruby_frame = true;
599
+ i++;
600
+ } else {
601
+ cme = get_cfunc_method_entry(cfp);
602
+ if (cme && cme->def->type == VM_METHOD_TYPE_CFUNC) {
603
+ if (start > 0) {
604
+ start--;
605
+ continue;
570
606
  }
571
- else {
572
- cme = get_cfunc_method_entry(cfp);
573
- if (cme && cme->def->type == VM_METHOD_TYPE_CFUNC) {
574
- if (start > 0) {
575
- start--;
576
- continue;
577
- }
578
-
579
- stack_buffer[i].same_frame =
580
- !stack_buffer[i].is_ruby_frame &&
581
- stack_buffer[i].cme == cme;
582
-
583
- if (stack_buffer[i].same_frame) { // Nothing to do, buffer already contains this frame
584
- i++;
585
- continue;
586
- }
587
-
588
- stack_buffer[i].cme = cme;
589
- stack_buffer[i].is_ruby_frame = false;
590
- i++;
591
- }
607
+
608
+ stack_buffer[i].same_frame =
609
+ !stack_buffer[i].is_ruby_frame &&
610
+ stack_buffer[i].cme == cme;
611
+
612
+ if (stack_buffer[i].same_frame) { // Nothing to do, buffer already contains this frame
613
+ i++;
614
+ continue;
592
615
  }
616
+
617
+ stack_buffer[i].cme = cme;
618
+ stack_buffer[i].is_ruby_frame = false;
619
+ i++;
620
+ }
593
621
  }
622
+ }
594
623
 
595
- return i;
624
+ return i;
596
625
  }
597
626
 
598
- // Support code for older Rubies that cannot use the MJIT header
599
- #ifndef RUBY_MJIT_HEADER
600
-
601
- #define MJIT_STATIC // No-op on older Rubies
602
-
603
627
  // Taken from upstream include/ruby/backward/2/bool.h at commit 5f10bd634fb6ae8f74a4ea730176233b0ca96954 (March 2022, Ruby 3.2 trunk)
604
628
  // Copyright (C) Ruby developers <ruby-core@ruby-lang.org>
605
629
  // to support our custom rb_profile_frames (see above)
606
630
  // Modifications: None
607
631
  #ifndef FALSE
608
- # define FALSE false
632
+ #define FALSE false
609
633
  #elif FALSE
610
- # error FALSE must be false
634
+ #error FALSE must be false
611
635
  #endif
612
636
 
613
637
  #ifndef TRUE
614
- # define TRUE true
638
+ #define TRUE true
615
639
  #elif ! TRUE
616
- # error TRUE must be true
640
+ #error TRUE must be true
617
641
  #endif
618
642
 
619
643
  // Taken from upstream vm_insnhelper.c at commit 5f10bd634fb6ae8f74a4ea730176233b0ca96954 (March 2022, Ruby 3.2 trunk)
@@ -640,7 +664,6 @@ check_method_entry(VALUE obj, int can_be_svar) {
640
664
 
641
665
  return NULL;
642
666
  }
643
- #endif // RUBY_MJIT_HEADER
644
667
 
645
668
  // Identical to upstream rb_vm_frame_method_entry (vm_insnhelper.c) with two additions:
646
669
  // 1. FIXNUM_P check on ep[FLAGS] before each iteration to detect torn EPs
@@ -650,25 +673,28 @@ check_method_entry(VALUE obj, int can_be_svar) {
650
673
  // a child frame's SPECVAL can still point to the parent's old stack EP whose flags
651
674
  // slot has been overwritten with (VALUE)env for GC marking.
652
675
  static const rb_callable_method_entry_t *
653
- safe_vm_frame_method_entry(const rb_control_frame_t *cfp)
654
- {
655
- const VALUE *ep = cfp->ep;
656
- rb_callable_method_entry_t *me;
657
-
658
- // Torn-EP check before VM_ENV_LOCAL_P, check_method_entry, and VM_ENV_PREV_EP
659
- // dereference ep
660
- while (FIXNUM_P(ep[VM_ENV_DATA_INDEX_FLAGS]) && !VM_ENV_LOCAL_P(ep)) {
661
- if ((me = check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], FALSE)) != NULL) {
662
- return me;
663
- }
664
- ep = VM_ENV_PREV_EP(ep);
665
- if (ep == NULL) return NULL;
676
+ safe_vm_frame_method_entry(const rb_control_frame_t *cfp) {
677
+ const VALUE *ep = cfp->ep;
678
+ rb_callable_method_entry_t *me;
679
+
680
+ // Torn-EP check before VM_ENV_LOCAL_P, check_method_entry, and VM_ENV_PREV_EP
681
+ // dereference ep
682
+ while (FIXNUM_P(ep[VM_ENV_DATA_INDEX_FLAGS]) && !VM_ENV_LOCAL_P(ep)) {
683
+ if ((me = check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], FALSE)) != NULL) {
684
+ return me;
666
685
  }
686
+ ep = VM_ENV_PREV_EP(ep);
687
+ if (ep == NULL) {
688
+ return NULL;
689
+ }
690
+ }
667
691
 
668
- // If we exited because of a torn EP (failed FIXNUM_P), bail out
669
- if (!FIXNUM_P(ep[VM_ENV_DATA_INDEX_FLAGS])) return NULL;
692
+ // If we exited because of a torn EP (failed FIXNUM_P), bail out
693
+ if (!FIXNUM_P(ep[VM_ENV_DATA_INDEX_FLAGS])) {
694
+ return NULL;
695
+ }
670
696
 
671
- return check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], TRUE);
697
+ return check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], TRUE);
672
698
  }
673
699
 
674
700
  // Optimized version of rb_vm_frame_method_entry() for cfunc frames.
@@ -682,43 +708,41 @@ get_cfunc_method_entry(const rb_control_frame_t *cfp) {
682
708
  }
683
709
 
684
710
  #ifndef NO_RACTORS
685
- // This API and definition are exported as a public symbol by the VM BUT the function header is not defined in any public header, so we
686
- // repeat it here to be able to use in our code.
687
- #ifndef USE_RACTOR_INTERNAL_APIS_DIRECTLY
688
- // Disable fast path for detecting multiple Ractors. Unfortunately this symbol is no longer visible on modern Ruby
689
- // versions, so we need to do a bit more work.
690
- struct rb_ractor_struct *ruby_single_main_ractor = NULL;
691
-
692
- // Alternative implementation of rb_ractor_main_p_ that avoids relying on non-public symbols
693
- bool rb_ractor_main_p_(void) {
694
- // We need to get the main ractor in a bit of a roundabout way, since Ruby >= 3.3 hid `GET_VM()`
695
- return ddtrace_get_ractor() == thread_struct_from_object(rb_thread_current())->vm->ractor.main_ractor;
696
- }
697
- #else
698
- // Directly access Ruby internal fast path for detecting multiple Ractors.
699
- extern struct rb_ractor_struct *ruby_single_main_ractor;
711
+ // This API and definition are exported as a public symbol by the VM BUT the function header is not defined in any public header, so we
712
+ // repeat it here to be able to use in our code.
713
+ #ifndef USE_RACTOR_INTERNAL_APIS_DIRECTLY
714
+ // Disable fast path for detecting multiple Ractors. Unfortunately this symbol is no longer visible on modern Ruby
715
+ // versions, so we need to do a bit more work.
716
+ struct rb_ractor_struct *ruby_single_main_ractor = NULL;
717
+
718
+ // Alternative implementation of rb_ractor_main_p_ that avoids relying on non-public symbols
719
+ bool rb_ractor_main_p_(void) {
720
+ // We need to get the main ractor in a bit of a roundabout way, since Ruby >= 3.3 hid `GET_VM()`
721
+ return ddtrace_get_ractor() == thread_struct_from_object(rb_thread_current())->vm->ractor.main_ractor;
722
+ }
723
+ #else
724
+ // Directly access Ruby internal fast path for detecting multiple Ractors.
725
+ extern struct rb_ractor_struct *ruby_single_main_ractor;
700
726
 
701
- // Ruby 3.0 to 3.2 directly expose this symbol, we just need to tell the compiler it exists.
702
- bool rb_ractor_main_p_(void);
703
- #endif
727
+ // Ruby 3.0 to 3.2 directly expose this symbol, we just need to tell the compiler it exists.
728
+ bool rb_ractor_main_p_(void);
729
+ #endif
704
730
 
705
- // Taken from upstream ractor_core.h at commit d9cf0388599a3234b9f3c06ddd006cd59a58ab8b (November 2022, Ruby 3.2 trunk)
706
- // to allow us to ensure that we're always operating on the main ractor (if Ruby has ractors)
707
- // Modifications:
708
- // * None
709
- bool ddtrace_rb_ractor_main_p(void)
710
- {
711
- if (ruby_single_main_ractor) {
712
- return true;
713
- }
714
- else {
715
- return rb_ractor_main_p_();
716
- }
731
+ // Taken from upstream ractor_core.h at commit d9cf0388599a3234b9f3c06ddd006cd59a58ab8b (November 2022, Ruby 3.2 trunk)
732
+ // to allow us to ensure that we're always operating on the main ractor (if Ruby has ractors)
733
+ // Modifications:
734
+ // * None
735
+ bool ddtrace_rb_ractor_main_p(void) {
736
+ if (ruby_single_main_ractor) {
737
+ return true;
738
+ } else {
739
+ return rb_ractor_main_p_();
717
740
  }
741
+ }
718
742
  #else
719
- // Simplify callers on older Rubies, instead of having them probe if the VM supports Ractors we just tell them that yes
720
- // they're always on the main Ractor
721
- bool ddtrace_rb_ractor_main_p(void) { return true; }
743
+ // Simplify callers on older Rubies, instead of having them probe if the VM supports Ractors we just tell them that yes
744
+ // they're always on the main Ractor
745
+ bool ddtrace_rb_ractor_main_p(void) { return true; }
722
746
  #endif // NO_RACTORS
723
747
 
724
748
  // This is a tweaked and inlined version of
@@ -730,17 +754,23 @@ static const rb_iseq_t *maybe_thread_invoke_proc_iseq(VALUE thread_value) {
730
754
  rb_thread_t *thread = thread_struct_from_object(thread_value);
731
755
 
732
756
  #ifndef NO_THREAD_INVOKE_ARG // Ruby 2.6+
733
- if (thread->invoke_type != thread_invoke_type_proc) return NULL;
757
+ if (thread->invoke_type != thread_invoke_type_proc) {
758
+ return NULL;
759
+ }
734
760
 
735
761
  VALUE proc = thread->invoke_arg.proc.proc;
736
762
  #else
737
- if (thread->first_func || !thread->first_proc) return NULL;
763
+ if (thread->first_func || !thread->first_proc) {
764
+ return NULL;
765
+ }
738
766
 
739
767
  VALUE proc = thread->first_proc;
740
768
  #endif
741
769
 
742
770
  const rb_iseq_t *iseq = rb_proc_get_iseq(proc, 0);
743
- if (iseq == NULL) return NULL;
771
+ if (iseq == NULL) {
772
+ return NULL;
773
+ }
744
774
 
745
775
  rb_iseq_check(iseq);
746
776
  return iseq;
@@ -749,7 +779,9 @@ static const rb_iseq_t *maybe_thread_invoke_proc_iseq(VALUE thread_value) {
749
779
  VALUE invoke_location_for(VALUE thread, int *line_location) {
750
780
  const rb_iseq_t *iseq = maybe_thread_invoke_proc_iseq(thread);
751
781
 
752
- if (iseq == NULL) return Qnil;
782
+ if (iseq == NULL) {
783
+ return Qnil;
784
+ }
753
785
 
754
786
  *line_location = NUM2INT(rb_iseq_first_lineno(iseq));
755
787
  return ddtrace_iseq_path(iseq);
@@ -783,102 +815,104 @@ static inline int ddtrace_imemo_type(VALUE imemo) {
783
815
  // Safety: This function assumes the object passed in is of the imemo type. But in the worst case, you'll just get
784
816
  // a string that doesn't make any sense.
785
817
  #ifndef NO_IMEMO_NAME
786
- const char *imemo_kind(VALUE imemo) {
787
- return rb_imemo_name(ddtrace_imemo_type(imemo));
788
- }
818
+ const char *imemo_kind(VALUE imemo) {
819
+ return rb_imemo_name(ddtrace_imemo_type(imemo));
820
+ }
789
821
  #else
790
- const char *imemo_kind(__attribute__((unused)) VALUE imemo) {
791
- return NULL;
792
- }
822
+ const char *imemo_kind(__attribute__((unused)) VALUE imemo) {
823
+ return NULL;
824
+ }
793
825
  #endif
794
826
 
795
827
  // This is used to workaround a VM bug. See "handle_sampling_signal" in "collectors_cpu_and_wall_time_worker" for details.
796
828
  #ifdef NO_POSTPONED_TRIGGER
797
- void *objspace_ptr_for_gc_finalize_deferred_workaround(void) {
798
- return GET_VM()->objspace;
799
- }
829
+ void *objspace_ptr_for_gc_finalize_deferred_workaround(void) {
830
+ return GET_VM()->objspace;
831
+ }
800
832
  #endif
801
833
 
802
834
  #ifndef HAVE_RUBY_THREAD_STORAGE_API
803
- #include "gvl_profiling_helper.h"
835
+ #include "gvl_profiling_helper.h"
804
836
 
805
- // Hack: In Ruby 3.3+ we attach gvl profiling state to Ruby threads using the
806
- // rb_internal_thread_specific_* APIs. These APIs did not exist on Ruby <= 3.2. On Ruby <= 3.2 we instead store the
807
- // needed data inside the `rb_thread_t` structure, specifically in `stat_insn_usage` as a Ruby FIXNUM.
808
- //
809
- // Why `stat_insn_usage`? We needed some per-thread storage, and while looking at the Ruby VM sources I noticed
810
- // that `stat_insn_usage` has been in `rb_thread_t` for a long time, but is not used anywhere in the VM
811
- // code. There's a comment attached to it "/* statistics data for profiler */" but other than marking this
812
- // field for GC, I could not find any place in the VM commit history or on GitHub where this has ever been used.
813
- //
814
- // Thus, since this hack is only for Ruby <= 3.2, which presumably will never see this field either removed or used
815
- // we... kinda take it for our own usage. It's ugly, I know...
816
- //
817
- // 64-bit pointers actually use 48-bit virtual addresses (https://muxup.com/2023q4/storing-data-in-pointers),
818
- // so we are sure the addresses fit in Fixnums.
819
- per_thread_context *get_per_thread_context(VALUE thread) {
820
- VALUE current_value = thread_struct_from_object(thread)->stat_insn_usage;
821
- return RB_FIXNUM_P(current_value) ? (per_thread_context *) FIX2LONG(current_value) : NULL;
822
- }
837
+ // Hack: In Ruby 3.3+ we attach gvl profiling state to Ruby threads using the
838
+ // rb_internal_thread_specific_* APIs. These APIs did not exist on Ruby <= 3.2. On Ruby <= 3.2 we instead store the
839
+ // needed data inside the `rb_thread_t` structure, specifically in `stat_insn_usage` as a Ruby FIXNUM.
840
+ //
841
+ // Why `stat_insn_usage`? We needed some per-thread storage, and while looking at the Ruby VM sources I noticed
842
+ // that `stat_insn_usage` has been in `rb_thread_t` for a long time, but is not used anywhere in the VM
843
+ // code. There's a comment attached to it "/* statistics data for profiler */" but other than marking this
844
+ // field for GC, I could not find any place in the VM commit history or on GitHub where this has ever been used.
845
+ //
846
+ // Thus, since this hack is only for Ruby <= 3.2, which presumably will never see this field either removed or used
847
+ // we... kinda take it for our own usage. It's ugly, I know...
848
+ //
849
+ // 64-bit pointers actually use 48-bit virtual addresses (https://muxup.com/2023q4/storing-data-in-pointers),
850
+ // so we are sure the addresses fit in Fixnums.
851
+ per_thread_context *get_per_thread_context(VALUE thread) {
852
+ VALUE current_value = thread_struct_from_object(thread)->stat_insn_usage;
853
+ return RB_FIXNUM_P(current_value) ? (per_thread_context *) FIX2LONG(current_value) : NULL;
854
+ }
823
855
 
824
- void set_per_thread_context(VALUE thread, per_thread_context *value) {
825
- if (!RB_FIXABLE((intptr_t) value)) {
826
- rb_bug("per_thread_context pointer does not fit in a Fixnum: %p", value);
827
- }
828
- thread_struct_from_object(thread)->stat_insn_usage = value ? LONG2FIX((intptr_t) value) : Qfalse;
856
+ void set_per_thread_context(VALUE thread, per_thread_context *value) {
857
+ if (!RB_FIXABLE((intptr_t) value)) {
858
+ rb_bug("per_thread_context pointer does not fit in a Fixnum: %p", value);
829
859
  }
860
+ thread_struct_from_object(thread)->stat_insn_usage = value ? LONG2FIX((intptr_t) value) : Qfalse;
861
+ }
830
862
  #endif
831
863
 
832
864
  // Is the VM smack in the middle of raising an exception?
833
865
  bool is_raised_flag_set(VALUE thread) { return thread_struct_from_object(thread)->ec->raised_flag > 0; }
834
866
 
835
867
  #ifndef NO_CURRENT_FIBER_FOR
836
- // The following three declarations are all
837
- // taken from upstream cont.c at commit d97884a58be32e829fd03a80cd521f4733d65c79 (February 2025, master branch)
838
- // (See the Ruby project copyright and license above)
839
- // to enable building `current_fiber_for`.
840
- //
841
- // We needed to copy them because they aren't otherwise exposed in any VM APIs or headers.
842
- // @ivoanjo: I manually checked the Ruby 3.1, 3.2, 3.3 and 3.4 branches + master, and the parts we care about in these
843
- // structures have not changed in many years (in fact, last change I spotted was for 2.7).
844
- enum context_type {
845
- CONTINUATION_CONTEXT = 0,
846
- FIBER_CONTEXT = 1
847
- };
848
-
849
- typedef struct rb_context_struct { // This declaration is incomplete -- only contains up to `self` which is the part we care about
850
- enum context_type type;
851
- int argc;
852
- int kw_splat;
853
- VALUE self;
854
- } rb_context_t;
855
-
856
- struct rb_fiber_struct { // This declaration is incomplete -- only contains the first entry which is the part we care about
857
- rb_context_t cont;
858
- };
859
-
860
- VALUE current_fiber_for(VALUE thread) {
861
- VALUE self = thread_struct_from_object(thread)->ec->fiber_ptr->cont.self;
862
- return self == 0 ? Qnil : self;
863
- }
868
+ // The following three declarations are all
869
+ // taken from upstream cont.c at commit d97884a58be32e829fd03a80cd521f4733d65c79 (February 2025, master branch)
870
+ // (See the Ruby project copyright and license above)
871
+ // to enable building `current_fiber_for`.
872
+ //
873
+ // We needed to copy them because they aren't otherwise exposed in any VM APIs or headers.
874
+ // @ivoanjo: I manually checked the Ruby 3.1, 3.2, 3.3 and 3.4 branches + master, and the parts we care about in these
875
+ // structures have not changed in many years (in fact, last change I spotted was for 2.7).
876
+ enum context_type {
877
+ CONTINUATION_CONTEXT = 0,
878
+ FIBER_CONTEXT = 1
879
+ };
880
+
881
+ typedef struct rb_context_struct { // This declaration is incomplete -- only contains up to `self` which is the part we care about
882
+ enum context_type type;
883
+ int argc;
884
+ int kw_splat;
885
+ VALUE self;
886
+ } rb_context_t;
887
+
888
+ struct rb_fiber_struct { // This declaration is incomplete -- only contains the first entry which is the part we care about
889
+ rb_context_t cont;
890
+ };
891
+
892
+ VALUE current_fiber_for(VALUE thread) {
893
+ VALUE self = thread_struct_from_object(thread)->ec->fiber_ptr->cont.self;
894
+ return self == 0 ? Qnil : self;
895
+ }
864
896
 
865
- void self_test_current_fiber_for(void) {
866
- VALUE expected_current_fiber = current_fiber_for(rb_thread_current());
867
- VALUE actual_current_fiber = rb_fiber_current();
897
+ void self_test_current_fiber_for(void) {
898
+ VALUE expected_current_fiber = current_fiber_for(rb_thread_current());
899
+ VALUE actual_current_fiber = rb_fiber_current();
868
900
 
869
- if (expected_current_fiber == Qnil) {
870
- // On purpose above we tried reading before calling `rb_fiber_current()` so the fiber may have not existed yet.
871
- // But now it should be there.
872
- expected_current_fiber = current_fiber_for(rb_thread_current());
873
- }
901
+ if (expected_current_fiber == Qnil) {
902
+ // On purpose above we tried reading before calling `rb_fiber_current()` so the fiber may have not existed yet.
903
+ // But now it should be there.
904
+ expected_current_fiber = current_fiber_for(rb_thread_current());
905
+ }
874
906
 
875
- if (expected_current_fiber != actual_current_fiber) rb_raise(rb_eRuntimeError, "current_fiber_for() self-test failed");
907
+ if (expected_current_fiber != actual_current_fiber) {
908
+ rb_raise(rb_eRuntimeError, "current_fiber_for() self-test failed");
876
909
  }
910
+ }
877
911
  #else
878
- NORETURN(VALUE current_fiber_for(DDTRACE_UNUSED VALUE thread));
912
+ NORETURN(VALUE current_fiber_for(DDTRACE_UNUSED VALUE thread));
879
913
 
880
- VALUE current_fiber_for(DDTRACE_UNUSED VALUE thread) { rb_raise(rb_eRuntimeError, "Not implemented for Ruby < 3.1"); }
881
- void self_test_current_fiber_for(void) { } // Nothing to do
914
+ VALUE current_fiber_for(DDTRACE_UNUSED VALUE thread) { rb_raise(rb_eRuntimeError, "Not implemented for Ruby < 3.1"); }
915
+ void self_test_current_fiber_for(void) { } // Nothing to do
882
916
  #endif
883
917
 
884
918
  // Variant of functions related to Thread::Backtrace::Location#label in Ruby 4.0
@@ -889,7 +923,9 @@ bool is_raised_flag_set(VALUE thread) { return thread_struct_from_object(thread)
889
923
 
890
924
  // Return true if a given location is a C method or supposed to behave like one.
891
925
  static bool location_cfunc_p(const rb_callable_method_entry_t *cme) {
892
- if (!cme) return false;
926
+ if (!cme) {
927
+ return false;
928
+ }
893
929
 
894
930
  switch (cme->def->type) {
895
931
  case VM_METHOD_TYPE_CFUNC:
@@ -926,12 +962,20 @@ static bool is_metaclass(VALUE mod, VALUE* attached) {
926
962
  return false;
927
963
  }
928
964
 
929
- static VALUE alloc_free_rb_mod_name(VALUE mod) {
965
+ VALUE ddtrace_alloc_free_rb_mod_name(VALUE mod) {
930
966
  #ifdef NO_ALLOC_FREE_MOD_NAME
931
- return rb_attr_get(mod, rb_intern("__classpath__"));
967
+ VALUE name = rb_attr_get(mod, rb_intern("__classpath__"));
932
968
  #else
933
- return rb_mod_name(mod);
969
+ VALUE name = rb_mod_name(mod);
934
970
  #endif
971
+ // While Module#const_set rejects empty strings,
972
+ // an empty String is possible if `rb_const_set(mod, "", val)` was used
973
+ // but that's not understandable so consider those anonymous too.
974
+ if (name == Qnil || RSTRING_LEN(name) == 0) {
975
+ return Qnil;
976
+ } else {
977
+ return name;
978
+ }
935
979
  }
936
980
 
937
981
  // Ruby 3.3+ has a `permanent_classpath` flag on rb_classext_struct.
@@ -947,6 +991,16 @@ static bool has_permanent_classpath(DDTRACE_UNUSED VALUE mod, DDTRACE_UNUSED VAL
947
991
  #endif
948
992
  }
949
993
 
994
+ VALUE ddtrace_permanent_mod_name(VALUE mod) {
995
+ VALUE name = ddtrace_alloc_free_rb_mod_name(mod);
996
+
997
+ if (NIL_P(name) || !has_permanent_classpath(mod, name)) {
998
+ return Qnil;
999
+ } else {
1000
+ return name;
1001
+ }
1002
+ }
1003
+
950
1004
  #define ONLY_METHOD_NAME ((ssize_t) -1)
951
1005
  #define BUFFER_OUT_OF_SPACE ((ssize_t) -2)
952
1006
  #define NO_METHOD_NAME ((ssize_t) -3)
@@ -961,11 +1015,12 @@ static ssize_t rb_gen_method_name(VALUE owner, VALUE method_name, char *buf, siz
961
1015
  if (is_metaclass(owner, &mod)) {
962
1016
  separator = '.';
963
1017
  }
964
- VALUE mod_name = alloc_free_rb_mod_name(mod);
1018
+
1019
+ VALUE mod_name = ddtrace_permanent_mod_name(mod);
965
1020
 
966
1021
  // Exclude non-permanent names (e.g. `#<Module:0x0123>::Foo`) which break flamegraph aggregation
967
1022
  // since they contain addresses that differ across processes/runs.
968
- if (NIL_P(mod_name) || !has_permanent_classpath(mod, mod_name)) {
1023
+ if (NIL_P(mod_name)) {
969
1024
  return ONLY_METHOD_NAME;
970
1025
  }
971
1026
 
@@ -1084,3 +1139,17 @@ void* ddtrace_cme_cfunc_func(const rb_callable_method_entry_t *cme) {
1084
1139
  const char *ddtrace_cme_original_method_name(const rb_callable_method_entry_t *cme) {
1085
1140
  return rb_id2name(cme->def->original_id);
1086
1141
  }
1142
+
1143
+ // This function is not present in the VM headers, but is a public symbol that can be invoked.
1144
+ int rb_objspace_internal_object_p(VALUE obj);
1145
+
1146
+ bool ddtrace_is_internal_object_p(VALUE obj) {
1147
+ if (RB_SPECIAL_CONST_P(obj)) {
1148
+ // Ruby special constants are not internal, except Qundef.
1149
+ // See enum ruby_special_consts in CRuby.
1150
+ return obj == Qundef;
1151
+ } else {
1152
+ // rb_objspace_internal_object_p() assumes non-immediate, so check that first above
1153
+ return rb_objspace_internal_object_p(obj);
1154
+ }
1155
+ }