datadog 2.35.0 → 2.36.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +68 -31
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +434 -300
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +9 -7
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
  12. data/ext/datadog_profiling_native_extension/extconf.rb +2 -2
  13. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
  15. data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
  16. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
  17. data/ext/datadog_profiling_native_extension/profiling.c +41 -4
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.c +33 -34
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +24 -3
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  21. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
  22. data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
  23. data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/appsec/api_security/route_extractor.rb +6 -0
  26. data/lib/datadog/appsec/component.rb +1 -1
  27. data/lib/datadog/appsec/configuration.rb +7 -0
  28. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
  29. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +64 -19
  30. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
  31. data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
  32. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
  33. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +20 -7
  34. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +92 -0
  35. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +33 -0
  36. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
  37. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +20 -3
  38. data/lib/datadog/appsec/default_header_tags.rb +10 -6
  39. data/lib/datadog/core/configuration/components.rb +1 -0
  40. data/lib/datadog/core/configuration/settings.rb +1 -2
  41. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  42. data/lib/datadog/core/remote/component.rb +1 -1
  43. data/lib/datadog/core/telemetry/event/app_started.rb +0 -21
  44. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  45. data/lib/datadog/core/utils/forking.rb +3 -1
  46. data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
  47. data/lib/datadog/core.rb +3 -0
  48. data/lib/datadog/di/base.rb +4 -1
  49. data/lib/datadog/di/component.rb +1 -1
  50. data/lib/datadog/error_tracking/collector.rb +2 -1
  51. data/lib/datadog/error_tracking/component.rb +2 -2
  52. data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
  53. data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
  54. data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
  55. data/lib/datadog/profiling/collectors/thread_context.rb +1 -0
  56. data/lib/datadog/profiling/component.rb +13 -15
  57. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
  58. data/lib/datadog/ruby_version.rb +25 -0
  59. data/lib/datadog/symbol_database/component.rb +306 -98
  60. data/lib/datadog/symbol_database/extractor.rb +223 -84
  61. data/lib/datadog/tracing/configuration/ext.rb +13 -0
  62. data/lib/datadog/tracing/configuration/settings.rb +17 -0
  63. data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
  64. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
  65. data/lib/datadog/tracing/contrib/grpc.rb +1 -0
  66. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
  67. data/lib/datadog/tracing/contrib/http.rb +1 -0
  68. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
  69. data/lib/datadog/tracing/contrib/karafka.rb +1 -0
  70. data/lib/datadog/tracing/contrib/rack/middlewares.rb +3 -1
  71. data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
  72. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
  73. data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
  74. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
  75. data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
  76. data/lib/datadog/tracing/distributed/propagation.rb +33 -1
  77. data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
  78. data/lib/datadog/tracing/trace_digest.rb +7 -0
  79. data/lib/datadog/tracing/trace_operation.rb +4 -1
  80. data/lib/datadog/tracing/tracer.rb +1 -0
  81. data/lib/datadog/version.rb +1 -1
  82. data/lib/datadog.rb +4 -1
  83. metadata +8 -5
@@ -8,18 +8,19 @@
8
8
 
9
9
  void thread_context_collector_sample(
10
10
  VALUE self_instance,
11
- long current_monotonic_wall_time_ns,
12
- VALUE profiler_overhead_stack_thread
11
+ long current_monotonic_wall_time_ns
13
12
  );
14
- __attribute__((warn_unused_result)) bool thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_instance);
15
- __attribute__((warn_unused_result)) bool thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object);
13
+ __attribute__((warn_unused_result)) bool thread_context_collector_prepare_sample_inside_signal_handler(void);
14
+ __attribute__((warn_unused_result)) bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object);
16
15
  void thread_context_collector_after_allocation(VALUE self_instance);
17
16
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples);
18
17
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance);
19
18
  void thread_context_collector_on_gc_start(VALUE self_instance);
20
19
  __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance);
21
20
  VALUE enforce_thread_context_collector_instance(VALUE object);
22
-
21
+ void thread_context_collector_stats(VALUE self_instance, VALUE stats_hash);
22
+ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance);
23
+ void thread_context_collector_on_serialize(VALUE self_instance);
23
24
 
24
25
  #ifndef NO_GVL_INSTRUMENTATION
25
26
  typedef enum {
@@ -33,7 +34,8 @@ VALUE enforce_thread_context_collector_instance(VALUE object);
33
34
  long waiting_for_gvl_duration_ns;
34
35
  } on_gvl_running_result;
35
36
 
36
- void thread_context_collector_on_gvl_waiting(gvl_profiling_thread thread);
37
- __attribute__((warn_unused_result)) on_gvl_running_result thread_context_collector_on_gvl_running(gvl_profiling_thread thread);
37
+ void thread_context_collector_on_gvl_waiting(per_thread_context *thread_context);
38
+ __attribute__((warn_unused_result)) on_gvl_running_result thread_context_collector_on_gvl_running(VALUE self_instance, VALUE thread, per_thread_context *thread_context);
38
39
  VALUE thread_context_collector_sample_after_gvl_running(VALUE self_instance, VALUE current_thread, long current_monotonic_wall_time_ns);
40
+ void thread_context_collector_on_gvl_released(per_thread_context *thread_context);
39
41
  #endif
@@ -29,16 +29,15 @@ void private_raise_exception(VALUE exception, const char *static_message) {
29
29
  rb_exc_raise(exception);
30
30
  }
31
31
 
32
- // Helper for raising pre-formatted exceptions
33
- void private_raise_error_formatted(VALUE exception_class, const char *detailed_message, const char *static_message) {
34
- VALUE exception = rb_exc_new_cstr(exception_class, detailed_message);
35
- private_raise_exception(exception, static_message);
36
- }
37
-
38
32
  // Use `raise_error` the macro instead, as it provides additional argument checks.
39
33
  void private_raise_error(VALUE exception_class, const char *fmt, ...) {
40
- FORMAT_VA_ERROR_MESSAGE(detailed_message, fmt);
41
- private_raise_error_formatted(exception_class, detailed_message, fmt);
34
+ va_list args;
35
+ va_start(args, fmt);
36
+ VALUE detailed_message = rb_vsprintf(fmt, args);
37
+ va_end(args);
38
+
39
+ VALUE exception = rb_exc_new_str(exception_class, detailed_message);
40
+ private_raise_exception(exception, fmt);
42
41
  }
43
42
 
44
43
  VALUE datadog_gem_version(void) {
@@ -47,11 +47,6 @@ NORETURN(
47
47
  __attribute__ ((format (printf, 2, 3)));
48
48
  );
49
49
 
50
- // Internal helper for raising pre-formatted exceptions
51
- NORETURN(
52
- void private_raise_error_formatted(VALUE exception_class, const char *detailed_message, const char *static_message)
53
- );
54
-
55
50
  // Raises an exception with separate telemetry-safe and detailed messages.
56
51
  // NOTE: Raising an exception always invokes Ruby code so it requires the GVL and is not compatible with "debug_enter_unsafe_context".
57
52
  // @see debug_enter_unsafe_context
@@ -61,13 +56,6 @@ NORETURN(
61
56
 
62
57
  #define MAX_RAISE_MESSAGE_SIZE 256
63
58
 
64
- #define FORMAT_VA_ERROR_MESSAGE(buf, fmt) \
65
- char buf[MAX_RAISE_MESSAGE_SIZE]; \
66
- va_list buf##_args; \
67
- va_start(buf##_args, fmt); \
68
- vsnprintf(buf, MAX_RAISE_MESSAGE_SIZE, fmt, buf##_args); \
69
- va_end(buf##_args);
70
-
71
59
  // Helper to retrieve Datadog::VERSION::STRING
72
60
  VALUE datadog_gem_version(void);
73
61
 
@@ -174,8 +174,8 @@ $defs << "-DUSE_RACTOR_INTERNAL_APIS_DIRECTLY" if RUBY_VERSION < "3.3"
174
174
  # On older Rubies, there was no GVL instrumentation API and APIs created to support it
175
175
  $defs << "-DNO_GVL_INSTRUMENTATION" if RUBY_VERSION < "3.2"
176
176
 
177
- # Supporting GVL instrumentation on 3.2 needs some workarounds
178
- $defs << "-DUSE_GVL_PROFILING_3_2_WORKAROUNDS" if RUBY_VERSION.start_with?("3.2")
177
+ # rb_internal_thread_specific_*()
178
+ $defs << "-DHAVE_RUBY_THREAD_STORAGE_API" if RUBY_VERSION >= "3.3"
179
179
 
180
180
  # On older Rubies, there was no struct rb_native_thread. See private_vm_api_acccess.c for details.
181
181
  $defs << "-DNO_RB_NATIVE_THREAD" if RUBY_VERSION < "3.2"
@@ -4,49 +4,10 @@
4
4
  #include "datadog_ruby_common.h"
5
5
  #include "gvl_profiling_helper.h"
6
6
 
7
- #if !defined(NO_GVL_INSTRUMENTATION) && !defined(USE_GVL_PROFILING_3_2_WORKAROUNDS) // Ruby 3.3+
8
- rb_internal_thread_specific_key_t gvl_waiting_tls_key;
7
+ #ifdef HAVE_RUBY_THREAD_STORAGE_API
8
+ rb_internal_thread_specific_key_t per_thread_context_key;
9
9
 
10
- void gvl_profiling_init(void) {
11
- gvl_waiting_tls_key = rb_internal_thread_specific_key_create();
12
- }
13
-
14
- #endif
15
-
16
- #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS // Ruby 3.2
17
- __thread gvl_profiling_thread gvl_waiting_tls;
18
- static bool gvl_profiling_state_thread_tracking_workaround_installed = false;
19
-
20
- static void on_thread_start(
21
- DDTRACE_UNUSED rb_event_flag_t _unused1,
22
- DDTRACE_UNUSED const rb_internal_thread_event_data_t *_unused2,
23
- DDTRACE_UNUSED void *_unused3
24
- ) {
25
- gvl_waiting_tls = (gvl_profiling_thread) {.thread = NULL};
26
- }
27
-
28
- // Hack: We're using the gvl_waiting_tls native thread-local to store per-thread information. Unfortunately, Ruby puts a big hole
29
- // in our plan because it reuses native threads -- specifically, in Ruby 3.2, native threads are still 1:1 to Ruby
30
- // threads (M:N wasn't a thing yet) BUT once a Ruby thread dies, the VM will keep the native thread around for a
31
- // bit, and if another Ruby thread starts right after, Ruby will reuse the native thread, rather than create a new one.
32
- //
33
- // This will mean that the new Ruby thread will still have the same native thread-local data that we set on the
34
- // old thread. For the purposes of our tracking, where we're keeping a pointer to the current thread object in
35
- // thread-local storage **this is disastrous** since it means we'll be pointing at the wrong thread (and its
36
- // memory may have been freed or reused since!)
37
- //
38
- // To work around this issue, once GVL profiling is enabled, we install an event hook on thread start
39
- // events that clears the thread-local data. This guarantees that there will be no stale data -- any existing
40
- // data will be cleared at thread start.
41
- //
42
- // Note that once installed, this event hook becomes permanent -- stopping the profiler does not stop this event
43
- // hook, unlike all others. This is because we can't afford to miss any thread start events while the
44
- // profiler is stopped (e.g. during reconfiguration) as that would mean stale data once the profiler starts again.
45
- void gvl_profiling_state_thread_tracking_workaround(void) {
46
- if (gvl_profiling_state_thread_tracking_workaround_installed) return;
47
-
48
- rb_internal_thread_add_event_hook(on_thread_start, RUBY_INTERNAL_THREAD_EVENT_STARTED, NULL);
49
-
50
- gvl_profiling_state_thread_tracking_workaround_installed = true;
10
+ void per_thread_context_tls_init(void) {
11
+ per_thread_context_key = rb_internal_thread_specific_key_create();
51
12
  }
52
13
  #endif
@@ -6,62 +6,30 @@
6
6
 
7
7
  #include "extconf.h"
8
8
 
9
- #if !defined(NO_GVL_INSTRUMENTATION) && !defined(USE_GVL_PROFILING_3_2_WORKAROUNDS) // Ruby 3.3+
10
- #include <ruby.h>
11
- #include <ruby/thread.h>
12
- #include "datadog_ruby_common.h"
9
+ #include <ruby.h>
13
10
 
14
- typedef struct { VALUE thread; } gvl_profiling_thread;
15
- extern rb_internal_thread_specific_key_t gvl_waiting_tls_key;
11
+ // Opaque to this header; the full definition lives in collectors_thread_context.c.
12
+ typedef struct per_thread_context per_thread_context;
16
13
 
17
- void gvl_profiling_init(void);
14
+ #ifdef HAVE_RUBY_THREAD_STORAGE_API
15
+ #include <ruby/thread.h>
18
16
 
19
- static inline gvl_profiling_thread thread_from_thread_object(VALUE thread) {
20
- return (gvl_profiling_thread) {.thread = thread};
21
- }
17
+ extern rb_internal_thread_specific_key_t per_thread_context_key;
22
18
 
23
- static inline gvl_profiling_thread thread_from_event(const rb_internal_thread_event_data_t *event_data) {
24
- return thread_from_thread_object(event_data->thread);
25
- }
19
+ void per_thread_context_tls_init(void);
26
20
 
27
- static inline intptr_t gvl_profiling_state_get(gvl_profiling_thread thread) {
28
- return (intptr_t) rb_internal_thread_specific_get(thread.thread, gvl_waiting_tls_key);
21
+ static inline per_thread_context* get_per_thread_context(VALUE thread) {
22
+ return rb_internal_thread_specific_get(thread, per_thread_context_key);
29
23
  }
30
24
 
31
- static inline void gvl_profiling_state_set(gvl_profiling_thread thread, intptr_t value) {
32
- rb_internal_thread_specific_set(thread.thread, gvl_waiting_tls_key, (void *) value);
25
+ static inline void set_per_thread_context(VALUE thread, per_thread_context* value) {
26
+ rb_internal_thread_specific_set(thread, per_thread_context_key, value);
33
27
  }
34
- #endif
35
-
36
- #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS // Ruby 3.2
37
- typedef struct { void *thread; } gvl_profiling_thread;
38
- extern __thread gvl_profiling_thread gvl_waiting_tls;
39
-
40
- static inline void gvl_profiling_init(void) { }
41
-
42
- // NOTE: This is a hack that relies on the knowledge that on Ruby 3.2 the
43
- // RUBY_INTERNAL_THREAD_EVENT_READY and RUBY_INTERNAL_THREAD_EVENT_RESUMED events always get called on the thread they
44
- // are about. Thus, we can use our thread local storage hack to get this data, even though the event doesn't include it.
45
- static inline gvl_profiling_thread thread_from_event(DDTRACE_UNUSED const void *event_data) {
46
- return gvl_waiting_tls;
47
- }
48
-
49
- void gvl_profiling_state_thread_tracking_workaround(void);
50
- gvl_profiling_thread gvl_profiling_state_maybe_initialize(void);
28
+ #else
29
+ static inline void per_thread_context_tls_init(void) { }
51
30
 
52
31
  // Implementing these on Ruby 3.2 requires access to private VM things, so the following methods are
53
32
  // implemented in `private_vm_api_access.c`
54
- gvl_profiling_thread thread_from_thread_object(VALUE thread);
55
- intptr_t gvl_profiling_state_get(gvl_profiling_thread thread);
56
- void gvl_profiling_state_set(gvl_profiling_thread thread, intptr_t value);
57
- #endif
58
-
59
- #ifndef NO_GVL_INSTRUMENTATION // For all Rubies supporting GVL profiling (3.2+)
60
- static inline intptr_t gvl_profiling_state_thread_object_get(VALUE thread) {
61
- return gvl_profiling_state_get(thread_from_thread_object(thread));
62
- }
63
-
64
- static inline void gvl_profiling_state_thread_object_set(VALUE thread, intptr_t value) {
65
- gvl_profiling_state_set(thread_from_thread_object(thread), value);
66
- }
33
+ per_thread_context* get_per_thread_context(VALUE thread);
34
+ void set_per_thread_context(VALUE thread, per_thread_context* value);
67
35
  #endif
@@ -5,7 +5,7 @@
5
5
  #include "libdatadog_helpers.h"
6
6
  #include "time_helpers.h"
7
7
 
8
- // Note on calloc vs ruby_xcalloc use:
8
+ // note on calloc vs ruby_xcalloc use:
9
9
  // * Whenever we're allocating memory after being called by the Ruby VM in a "regular" situation (e.g. initializer)
10
10
  // we should use `ruby_xcalloc` to give the VM visibility into what we're doing + give it a chance to manage GC
11
11
  // * BUT, when we're being called during a sample, being in the middle of an object allocation is a very special
@@ -58,7 +58,7 @@ typedef struct {
58
58
  heap_frame frames[];
59
59
  } heap_record;
60
60
  static heap_record* heap_record_new(heap_recorder*, ddog_prof_Slice_Location);
61
- static void heap_record_free(heap_recorder*, heap_record*);
61
+ static void heap_record_free(heap_recorder*, heap_record*, bool should_unintern);
62
62
 
63
63
  #if MAX_FRAMES_LIMIT > UINT16_MAX
64
64
  #error Frames len type not compatible with MAX_FRAMES_LIMIT
@@ -75,7 +75,7 @@ typedef struct {
75
75
  live_object_data object_data;
76
76
  } object_record;
77
77
  static object_record* object_record_new(long, heap_record*, live_object_data);
78
- static void object_record_free(heap_recorder*, object_record*);
78
+ static void object_record_free(heap_recorder*, object_record*, bool should_unintern);
79
79
  static VALUE object_record_inspect(heap_recorder*, object_record*);
80
80
  static object_record SKIPPED_RECORD = {0};
81
81
 
@@ -196,8 +196,8 @@ typedef struct {
196
196
  static heap_record* get_or_create_heap_record(heap_recorder*, ddog_prof_Slice_Location);
197
197
  static void cleanup_heap_record_if_unused(heap_recorder*, heap_record*);
198
198
  static void on_committed_object_record_cleanup(heap_recorder *heap_recorder, object_record *record);
199
- static int st_heap_record_entry_free(st_data_t, st_data_t, st_data_t);
200
- static int st_object_record_entry_free(st_data_t, st_data_t, st_data_t);
199
+ static int st_heap_record_entry_free_no_unintern(st_data_t, st_data_t, st_data_t);
200
+ static int st_object_record_entry_free_no_unintern(st_data_t, st_data_t, st_data_t);
201
201
  static int st_object_record_update(st_data_t, st_data_t, st_data_t);
202
202
  static int st_object_records_iterate(st_data_t, st_data_t, st_data_t);
203
203
  static int st_object_records_debug(st_data_t key, st_data_t value, st_data_t extra);
@@ -250,17 +250,24 @@ void heap_recorder_free(heap_recorder *heap_recorder) {
250
250
  heap_recorder_finish_iteration(heap_recorder);
251
251
  }
252
252
 
253
+ // NOTE: We don't unintern the strings referenced by the records we're about to free, thus we use
254
+ // `..._no_unintern` and `should_unintern: false`. This is intentional: `heap_recorder_free` is only ever called as part of
255
+ // tearing down the entire stack recorder, and the caller drops the whole managed string storage right after
256
+ // (see `stack_recorder_typed_data_free`), so there's no need to spend effort updating the managed string table.
257
+ // Crucially, this also keeps us from crashing: this code runs from the stack recorder's GC free callback, and
258
+ // because uninterning can fail, we can't raise exceptions in the middle of a dfree.
259
+
253
260
  // Clean-up all object records
254
- st_foreach(heap_recorder->object_records, st_object_record_entry_free, (st_data_t) heap_recorder);
261
+ st_foreach(heap_recorder->object_records, st_object_record_entry_free_no_unintern, (st_data_t) heap_recorder);
255
262
  st_free_table(heap_recorder->object_records);
256
263
 
257
264
  // Clean-up all heap records (this includes those only referred to by queued_samples)
258
- st_foreach(heap_recorder->heap_records, st_heap_record_entry_free, (st_data_t) heap_recorder);
265
+ st_foreach(heap_recorder->heap_records, st_heap_record_entry_free_no_unintern, (st_data_t) heap_recorder);
259
266
  st_free_table(heap_recorder->heap_records);
260
267
 
261
268
  if (heap_recorder->active_recording != NULL && heap_recorder->active_recording != &SKIPPED_RECORD) {
262
269
  // If there's a partial object record, clean it up as well
263
- object_record_free(heap_recorder, heap_recorder->active_recording);
270
+ object_record_free(heap_recorder, heap_recorder->active_recording, false);
264
271
  }
265
272
 
266
273
  ruby_xfree(heap_recorder->reusable_locations);
@@ -306,7 +313,7 @@ void heap_recorder_after_fork(heap_recorder *heap_recorder) {
306
313
  // simply be noticed on next heap_recorder_prepare_iteration.
307
314
  //
308
315
  // There is one small caveat though: fork only preserves one thread and in a Ruby app, that
309
- // will be the thread holding on to the GVL. Since we support iteration on the heap recorder
316
+ // will be the thread holding the GVL. Since we support iteration on the heap recorder
310
317
  // outside of the GVL, any state specific to that interaction may be inconsistent after fork
311
318
  // (e.g. an acquired lock for thread safety). Iteration operates on object_records_snapshot
312
319
  // though and that one will be updated on next heap_recorder_prepare_iteration so we really
@@ -714,15 +721,17 @@ VALUE heap_recorder_testonly_debug(heap_recorder *heap_recorder) {
714
721
  // ==========================
715
722
  // Heap Recorder Internal API
716
723
  // ==========================
717
- static int st_heap_record_entry_free(st_data_t key, DDTRACE_UNUSED st_data_t value, st_data_t extra_arg) {
724
+ // NOTE: Only expected to be used from heap_recorder_free, which will separately destroy the string table
725
+ static int st_heap_record_entry_free_no_unintern(st_data_t key, DDTRACE_UNUSED st_data_t value, st_data_t extra_arg) {
718
726
  heap_recorder *recorder = (heap_recorder *) extra_arg;
719
- heap_record_free(recorder, (heap_record *) key);
727
+ heap_record_free(recorder, (heap_record *) key, false);
720
728
  return ST_DELETE;
721
729
  }
722
730
 
723
- static int st_object_record_entry_free(DDTRACE_UNUSED st_data_t key, st_data_t value, st_data_t extra_arg) {
731
+ // NOTE: Only expected to be used from heap_recorder_free, which will separately destroy the string table
732
+ static int st_object_record_entry_free_no_unintern(DDTRACE_UNUSED st_data_t key, st_data_t value, st_data_t extra_arg) {
724
733
  heap_recorder *recorder = (heap_recorder *) extra_arg;
725
- object_record_free(recorder, (object_record *) value);
734
+ object_record_free(recorder, (object_record *) value, false);
726
735
  return ST_DELETE;
727
736
  }
728
737
 
@@ -888,7 +897,7 @@ static heap_record* get_or_create_heap_record(heap_recorder *heap_recorder, ddog
888
897
  heap_record *new_or_existing_record = NULL; // Will be set inside update_heap_record_entry_with_new_allocation
889
898
  bool existing = st_update(heap_recorder->heap_records, (st_data_t) stack, update_heap_record_entry_with_new_allocation, (st_data_t) &new_or_existing_record);
890
899
  if (existing) {
891
- heap_record_free(heap_recorder, stack);
900
+ heap_record_free(heap_recorder, stack, true);
892
901
  }
893
902
 
894
903
  return new_or_existing_record;
@@ -903,7 +912,7 @@ static void cleanup_heap_record_if_unused(heap_recorder *heap_recorder, heap_rec
903
912
  if (!st_delete(heap_recorder->heap_records, (st_data_t*) &heap_record, NULL)) {
904
913
  raise_error(rb_eRuntimeError, "Attempted to cleanup an untracked heap_record");
905
914
  };
906
- heap_record_free(heap_recorder, heap_record);
915
+ heap_record_free(heap_recorder, heap_record, true);
907
916
  }
908
917
 
909
918
  static void on_committed_object_record_cleanup(heap_recorder *heap_recorder, object_record *record) {
@@ -925,7 +934,7 @@ static void on_committed_object_record_cleanup(heap_recorder *heap_recorder, obj
925
934
  // One less object using this heap record, it may have become unused...
926
935
  cleanup_heap_record_if_unused(heap_recorder, heap_record);
927
936
 
928
- object_record_free(heap_recorder, record);
937
+ object_record_free(heap_recorder, record, true);
929
938
  }
930
939
 
931
940
  // =================
@@ -939,8 +948,12 @@ object_record* object_record_new(long obj_id, heap_record *heap_record, live_obj
939
948
  return record;
940
949
  }
941
950
 
942
- void object_record_free(heap_recorder *recorder, object_record *record) {
943
- unintern_or_raise(recorder, record->object_data.class);
951
+ void object_record_free(heap_recorder *recorder, object_record *record, bool should_unintern) {
952
+ // When tearing down the whole recorder state, we skip uninterning as it's not needed (the managed
953
+ // string table is going to be destroyed anyway) and if there's any failures we can't raise
954
+ // in the middle of a dfree callback.
955
+ if (should_unintern) unintern_or_raise(recorder, record->object_data.class);
956
+
944
957
  free(record); // See "note on calloc vs ruby_xcalloc use" above
945
958
  }
946
959
 
@@ -1012,15 +1025,20 @@ heap_record* heap_record_new(heap_recorder *recorder, ddog_prof_Slice_Location l
1012
1025
  return stack;
1013
1026
  }
1014
1027
 
1015
- void heap_record_free(heap_recorder *recorder, heap_record *stack) {
1016
- ddog_prof_ManagedStringId *ids = recorder->reusable_ids;
1017
-
1018
- // Put all the ids in the same array; doesn't really matter the order
1019
- for (u_int16_t i = 0; i < stack->frames_len; i++) {
1020
- ids[i] = stack->frames[i].filename;
1021
- ids[i + stack->frames_len] = stack->frames[i].name;
1028
+ void heap_record_free(heap_recorder *recorder, heap_record *stack, bool should_unintern) {
1029
+ // When tearing down the whole recorder state, we skip uninterning as it's not needed (the managed
1030
+ // string table is going to be destroyed anyway) and if there's any failures we can't raise
1031
+ // in the middle of a dfree callback.
1032
+ if (should_unintern) {
1033
+ ddog_prof_ManagedStringId *ids = recorder->reusable_ids;
1034
+
1035
+ // Put all the ids in the same array; doesn't really matter the order
1036
+ for (u_int16_t i = 0; i < stack->frames_len; i++) {
1037
+ ids[i] = stack->frames[i].filename;
1038
+ ids[i + stack->frames_len] = stack->frames[i].name;
1039
+ }
1040
+ unintern_all_or_raise(recorder, (ddog_prof_Slice_ManagedStringId) { .ptr = ids, .len = stack->frames_len * 2 });
1022
1041
  }
1023
- unintern_all_or_raise(recorder, (ddog_prof_Slice_ManagedStringId) { .ptr = ids, .len = stack->frames_len * 2 });
1024
1042
 
1025
1043
  free(stack); // See "note on calloc vs ruby_xcalloc use" above
1026
1044
  }
@@ -790,15 +790,11 @@ static inline int ddtrace_imemo_type(VALUE imemo) {
790
790
  }
791
791
  #endif
792
792
 
793
- #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS // Ruby 3.2
793
+ #ifndef HAVE_RUBY_THREAD_STORAGE_API
794
794
  #include "gvl_profiling_helper.h"
795
795
 
796
- gvl_profiling_thread thread_from_thread_object(VALUE thread) {
797
- return (gvl_profiling_thread) {.thread = thread_struct_from_object(thread)};
798
- }
799
-
800
796
  // Hack: In Ruby 3.3+ we attach gvl profiling state to Ruby threads using the
801
- // rb_internal_thread_specific_* APIs. These APIs did not exist on Ruby 3.2. On Ruby 3.2 we instead store the
797
+ // rb_internal_thread_specific_* APIs. These APIs did not exist on Ruby <= 3.2. On Ruby <= 3.2 we instead store the
802
798
  // needed data inside the `rb_thread_t` structure, specifically in `stat_insn_usage` as a Ruby FIXNUM.
803
799
  //
804
800
  // Why `stat_insn_usage`? We needed some per-thread storage, and while looking at the Ruby VM sources I noticed
@@ -806,38 +802,21 @@ static inline int ddtrace_imemo_type(VALUE imemo) {
806
802
  // code. There's a comment attached to it "/* statistics data for profiler */" but other than marking this
807
803
  // field for GC, I could not find any place in the VM commit history or on GitHub where this has ever been used.
808
804
  //
809
- // Thus, since this hack is only for 3.2, which presumably will never see this field either removed or used
810
- // during its remaining maintenance release period we... kinda take it for our own usage. It's ugly, I know...
811
- intptr_t gvl_profiling_state_get(gvl_profiling_thread thread) {
812
- if (thread.thread == NULL) return 0;
813
-
814
- VALUE current_value = ((rb_thread_t *)thread.thread)->stat_insn_usage;
815
- intptr_t result = current_value == Qnil ? 0 : FIX2LONG(current_value);
816
- return result;
817
- }
818
-
819
- void gvl_profiling_state_set(gvl_profiling_thread thread, intptr_t value) {
820
- if (thread.thread == NULL) return;
821
- ((rb_thread_t *)thread.thread)->stat_insn_usage = LONG2FIX(value);
805
+ // Thus, since this hack is only for Ruby <= 3.2, which presumably will never see this field either removed or used
806
+ // we... kinda take it for our own usage. It's ugly, I know...
807
+ //
808
+ // 64-bit pointers actually use 48-bit virtual addresses (https://muxup.com/2023q4/storing-data-in-pointers),
809
+ // so we are sure the addresses fit in Fixnums.
810
+ per_thread_context *get_per_thread_context(VALUE thread) {
811
+ VALUE current_value = thread_struct_from_object(thread)->stat_insn_usage;
812
+ return RB_FIXNUM_P(current_value) ? (per_thread_context *) FIX2LONG(current_value) : NULL;
822
813
  }
823
814
 
824
- // Because Ruby 3.2 does not give us the current thread when calling the RUBY_INTERNAL_THREAD_EVENT_READY and
825
- // RUBY_INTERNAL_THREAD_EVENT_RESUMED APIs, we need to figure out this info ourselves.
826
- //
827
- // Specifically, this method was created to be called from a RUBY_INTERNAL_THREAD_EVENT_RESUMED callback --
828
- // when it's triggered, we know the thread the code gets executed on is holding the GVL, so we use this
829
- // opportunity to initialize our thread-local value.
830
- gvl_profiling_thread gvl_profiling_state_maybe_initialize(void) {
831
- gvl_profiling_thread current_thread = gvl_waiting_tls;
832
-
833
- if (current_thread.thread == NULL) {
834
- // threads.sched.running is the thread currently holding the GVL, which when this gets executed is the
835
- // current thread!
836
- current_thread = (gvl_profiling_thread) {.thread = (void *) rb_current_ractor()->threads.sched.running};
837
- gvl_waiting_tls = current_thread;
815
+ void set_per_thread_context(VALUE thread, per_thread_context *value) {
816
+ if (!RB_FIXABLE((intptr_t) value)) {
817
+ rb_bug("per_thread_context pointer does not fit in a Fixnum: %p", value);
838
818
  }
839
-
840
- return current_thread;
819
+ thread_struct_from_object(thread)->stat_insn_usage = value ? LONG2FIX((intptr_t) value) : Qfalse;
841
820
  }
842
821
  #endif
843
822
 
@@ -28,8 +28,11 @@ void crashtracking_runtime_stacks_init(void);
28
28
  void setup_signal_handler_init(VALUE profiling_module);
29
29
 
30
30
  static VALUE native_working_p(VALUE self);
31
- static VALUE _native_grab_gvl_and_raise(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message, VALUE test_message_arg, VALUE release_gvl);
31
+ static VALUE _native_raise_error_value_arg(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message_value_arg);
32
+ static VALUE _native_grab_gvl_and_raise_cstr_arg(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message, VALUE test_message_arg, VALUE release_gvl);
32
33
  static void *trigger_grab_gvl_and_raise(void *trigger_args);
34
+ static VALUE _native_grab_gvl_and_raise_value_arg(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message_value_arg, VALUE release_gvl);
35
+ static void *trigger_grab_gvl_and_raise_value_arg(void *trigger_args);
33
36
  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);
34
37
  static void *trigger_grab_gvl_and_raise_syserr(void *trigger_args);
35
38
  static VALUE _native_ddtrace_rb_ractor_main_p(DDTRACE_UNUSED VALUE _self);
@@ -78,7 +81,9 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
78
81
 
79
82
  // Hosts methods used for testing the native code using RSpec
80
83
  VALUE testing_module = rb_define_module_under(native_extension_module, "Testing");
81
- rb_define_singleton_method(testing_module, "_native_grab_gvl_and_raise", _native_grab_gvl_and_raise, 4);
84
+ rb_define_singleton_method(testing_module, "_native_raise_error_value_arg", _native_raise_error_value_arg, 2);
85
+ rb_define_singleton_method(testing_module, "_native_grab_gvl_and_raise_cstr_arg", _native_grab_gvl_and_raise_cstr_arg, 4);
86
+ rb_define_singleton_method(testing_module, "_native_grab_gvl_and_raise_value_arg", _native_grab_gvl_and_raise_value_arg, 3);
82
87
  rb_define_singleton_method(testing_module, "_native_grab_gvl_and_raise_syserr", _native_grab_gvl_and_raise_syserr, 4);
83
88
  rb_define_singleton_method(testing_module, "_native_ddtrace_rb_ractor_main_p", _native_ddtrace_rb_ractor_main_p, 0);
84
89
  rb_define_singleton_method(testing_module, "_native_is_current_thread_holding_the_gvl", _native_is_current_thread_holding_the_gvl, 0);
@@ -103,13 +108,17 @@ static VALUE native_working_p(DDTRACE_UNUSED VALUE _self) {
103
108
  return Qtrue;
104
109
  }
105
110
 
111
+ static VALUE _native_raise_error_value_arg(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message_value_arg) {
112
+ raise_error(exception_class, ">%"PRIsVALUE"<", test_message_value_arg);
113
+ }
114
+
106
115
  typedef struct {
107
116
  VALUE exception_class;
108
117
  char *test_message;
109
118
  char *test_message_arg;
110
119
  } trigger_grab_gvl_and_raise_arguments;
111
120
 
112
- static VALUE _native_grab_gvl_and_raise(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message, VALUE test_message_arg, VALUE release_gvl) {
121
+ static VALUE _native_grab_gvl_and_raise_cstr_arg(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message, VALUE test_message_arg, VALUE release_gvl) {
113
122
  ENFORCE_TYPE(test_message, T_STRING);
114
123
 
115
124
  trigger_grab_gvl_and_raise_arguments args;
@@ -124,7 +133,7 @@ static VALUE _native_grab_gvl_and_raise(DDTRACE_UNUSED VALUE _self, VALUE except
124
133
  private_grab_gvl_and_raise(args.exception_class, 0, args.test_message, args.test_message_arg);
125
134
  }
126
135
 
127
- raise_error(rb_eRuntimeError, "Failed to raise exception in _native_grab_gvl_and_raise; this should never happen");
136
+ raise_error(rb_eRuntimeError, "Failed to raise exception in _native_grab_gvl_and_raise_cstr_arg; this should never happen");
128
137
  }
129
138
 
130
139
  static void *trigger_grab_gvl_and_raise(void *trigger_args) {
@@ -139,6 +148,34 @@ static void *trigger_grab_gvl_and_raise(void *trigger_args) {
139
148
  return NULL;
140
149
  }
141
150
 
151
+ typedef struct {
152
+ VALUE exception_class;
153
+ VALUE test_message_value_arg;
154
+ } trigger_grab_gvl_and_raise_value_arguments;
155
+
156
+ static VALUE _native_grab_gvl_and_raise_value_arg(DDTRACE_UNUSED VALUE _self, VALUE exception_class, VALUE test_message_value_arg, VALUE release_gvl) {
157
+ trigger_grab_gvl_and_raise_value_arguments args;
158
+
159
+ args.exception_class = exception_class;
160
+ args.test_message_value_arg = test_message_value_arg;
161
+
162
+ if (RTEST(release_gvl)) {
163
+ rb_thread_call_without_gvl(trigger_grab_gvl_and_raise_value_arg, &args, NULL, NULL);
164
+ } else {
165
+ private_grab_gvl_and_raise(args.exception_class, 0, ">%"PRIsVALUE"<", args.test_message_value_arg);
166
+ }
167
+
168
+ raise_error(rb_eRuntimeError, "Failed to raise exception in _native_grab_gvl_and_raise_value_arg; this should never happen");
169
+ }
170
+
171
+ static void *trigger_grab_gvl_and_raise_value_arg(void *trigger_args) {
172
+ trigger_grab_gvl_and_raise_value_arguments *args = (trigger_grab_gvl_and_raise_value_arguments *) trigger_args;
173
+
174
+ private_grab_gvl_and_raise(args->exception_class, 0, ">%"PRIsVALUE"<", args->test_message_value_arg);
175
+
176
+ return NULL;
177
+ }
178
+
142
179
  typedef struct {
143
180
  int syserr_errno;
144
181
  char *test_message;