datadog 2.43.0 → 2.44.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +36 -5
  4. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +65 -31
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  9. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -0
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  12. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +4 -4
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +3 -3
  15. data/ext/libdatadog_api/di.c +8 -3
  16. data/ext/libdatadog_api/extconf.rb +1 -1
  17. data/ext/libdatadog_api/feature_flags.c +8 -0
  18. data/ext/libdatadog_api/init.c +3 -1
  19. data/ext/libdatadog_api/otel_thread_context.c +38 -17
  20. data/ext/libdatadog_extconf_helpers.rb +14 -9
  21. data/lib/datadog/ai_guard/component.rb +17 -8
  22. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  23. data/lib/datadog/ai_guard/configuration.rb +13 -0
  24. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  25. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  26. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  27. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  29. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  30. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  31. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  32. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  33. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  34. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  35. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  36. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  37. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  38. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  39. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  40. data/lib/datadog/ai_guard/ext.rb +1 -0
  41. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  42. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  43. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  44. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  45. data/lib/datadog/ai_guard/redaction.rb +80 -0
  46. data/lib/datadog/ai_guard.rb +58 -57
  47. data/lib/datadog/core/configuration/components.rb +9 -1
  48. data/lib/datadog/core/configuration/settings.rb +5 -2
  49. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  50. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  51. data/lib/datadog/core/remote/component.rb +9 -2
  52. data/lib/datadog/core/remote/worker.rb +8 -0
  53. data/lib/datadog/di/instrumenter.rb +1 -1
  54. data/lib/datadog/di/probe_manager.rb +10 -5
  55. data/lib/datadog/di/proc_responder.rb +1 -1
  56. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  57. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  58. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  59. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  60. data/lib/datadog/open_feature/component.rb +57 -1
  61. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  62. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  63. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  64. data/lib/datadog/open_feature/configuration.rb +118 -0
  65. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  66. data/lib/datadog/open_feature/remote.rb +5 -5
  67. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  68. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  69. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  70. data/lib/datadog/symbol_database/extractor.rb +84 -102
  71. data/lib/datadog/tracing/component.rb +2 -0
  72. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  73. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  74. data/lib/datadog/tracing/context.rb +24 -2
  75. data/lib/datadog/tracing/context_provider.rb +9 -5
  76. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  77. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  78. data/lib/datadog/tracing/otel_thread_context.rb +76 -11
  79. data/lib/datadog/tracing/tracer.rb +15 -2
  80. data/lib/datadog/tracing/transport/native.rb +1 -1
  81. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  82. data/lib/datadog/tracing/transport/traces.rb +1 -1
  83. data/lib/datadog/version.rb +1 -1
  84. metadata +23 -7
  85. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 154c126111f9b92cc59626264fc83b954b36317f96798bc33a0bcb7784516968
4
- data.tar.gz: 4098225f4c88a0c3e31b069ca1a17231af4298ca149ce530d0dc4ccc9a8d8db5
3
+ metadata.gz: c60f4ef57b9508827fac3387bd07b4233666ed9d22684c0d063187d05dcfc449
4
+ data.tar.gz: 6e795bd6f5fc8b5f59d040a4da62e636a15d12024ff4e6b5c2b10d0989c047ec
5
5
  SHA512:
6
- metadata.gz: a566b1ab03efdf8c881ff7a6af93aff5fc946dc213ea1ccfabc554458ae733769ab98002ea14f039e3b2a19c150b2e208083e35fd0519f34b77ff239247412dd
7
- data.tar.gz: 3a9f8574b7325c9f6e77da1a7f8ad65b8b6a74426b9dab829db84c8adbe8757c1f09d57d8232085c51d074c60e4a77e017cbdd9bfaa5d9f84efba0ad66869866
6
+ metadata.gz: 27f955e40d920907b2af84f36f8671f4e306235dce525be84da5221e2c66b852183974095afbbcc4acff4148e9202f585067e676d897b20da08c41e535846e9c
7
+ data.tar.gz: 29f4f51331feb1e0a6e70437490b8067b9e4a6f561b55c7f1b5fff58ae326964d92cf8e2dcf16c31466b5f654b851a25e8b683322795573e5ee3742fb395f519
data/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.44.0] - 2026-10-07
6
+
7
+ ### Added
8
+
9
+ * AI Guard: Add sensitive-data redaction to evaluated messages, enabled by default; configure it with `ai_guard.redaction_enabled` or `DD_AI_GUARD_REDACTION_ENABLED`. ([#6394][])
10
+ * Open Feature: Add agentless Feature Flags configuration delivery and settings for selecting agentless, Remote Configuration, or offline delivery. ([#6291][])
11
+
12
+ ### Changed
13
+
14
+ * AI Guard: Require `ruby_llm` 2.0 or later for the `RubyLLM` integration. ([#6394][])
15
+ * Dynamic Instrumentation: Condition evaluation error snapshots are now subject to the process-wide Dynamic Instrumentation snapshot rate limit in addition to the per-probe limit. ([#6250][])
16
+ * OpenTelemetry: Emit `deployment.environment.name` instead of `deployment.environment` for Logs and Metrics resources, and accept it as the Datadog environment while retaining `deployment.environment` as a fallback. ([#6261][])
17
+
18
+ ### Fixed
19
+
20
+ * Core: Fix native extension build failures when installing gems in a directory whose path contains spaces. ([#6371][])
21
+ * Core: Fix Remote Configuration polling remaining stopped in forked web workers when application warmup started it before the fork. ([#6427][])
22
+ * Tracing: Fix the `grape` integration creating no `grape.endpoint_run` span on Grape 4, which moved the API class, HTTP verb and route path off `Grape::Endpoint#options`. ([#6354][]) ([@dduugg][])
23
+ * Tracing: Fix an empty `http.route` tag on spans for 405 responses when running `grape` versions 2.3 through 3.0. ([#6358][])
24
+ * Profiling: Fix the profiler stopping with `BUG: Unexpected CPU time going backwards between samples`. ([#6364][])
25
+ * Profiling: Fix a sampling race condition during profiler shutdown. ([#6375][])
26
+ * Profiling: Fix a potential crash during garbage collection compaction when profiling is enabled. ([#6395][])
27
+ * Profiling: Stop profiling if `thread_name_for`/`rb_thread_t` changes. ([#6398][])
28
+ * Profiling: Fix native extension build breaking when trying to log %. ([#6407][])
29
+ * Dynamic Instrumentation: Fix methods disappearing from code search and source lookup when method logpoints are already installed. ([#6339][])
30
+ * Dynamic Instrumentation: Fix missing condition evaluation error snapshots for method probes with a `when` condition; errors raised while evaluating the condition are now reported to the backend. ([#6250][])
31
+
5
32
  ## [2.43.0] - 2026-09-22
6
33
 
7
34
  ### Added
@@ -3793,7 +3820,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3793
3820
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3794
3821
 
3795
3822
 
3796
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.43.0...master
3823
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.44.0...master
3824
+ [2.44.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.43.0...v2.44.0
3797
3825
  [2.43.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.42.0...v2.43.0
3798
3826
  [2.42.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.41.0...v2.42.0
3799
3827
  [2.41.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.40.0...v2.41.0
@@ -5628,10 +5656,24 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5628
5656
  [#6232]: https://github.com/DataDog/dd-trace-rb/issues/6232
5629
5657
  [#6234]: https://github.com/DataDog/dd-trace-rb/issues/6234
5630
5658
  [#6242]: https://github.com/DataDog/dd-trace-rb/issues/6242
5659
+ [#6250]: https://github.com/DataDog/dd-trace-rb/issues/6250
5660
+ [#6261]: https://github.com/DataDog/dd-trace-rb/issues/6261
5631
5661
  [#6286]: https://github.com/DataDog/dd-trace-rb/issues/6286
5662
+ [#6291]: https://github.com/DataDog/dd-trace-rb/issues/6291
5632
5663
  [#6316]: https://github.com/DataDog/dd-trace-rb/issues/6316
5633
5664
  [#6320]: https://github.com/DataDog/dd-trace-rb/issues/6320
5665
+ [#6339]: https://github.com/DataDog/dd-trace-rb/issues/6339
5634
5666
  [#6350]: https://github.com/DataDog/dd-trace-rb/issues/6350
5667
+ [#6354]: https://github.com/DataDog/dd-trace-rb/issues/6354
5668
+ [#6358]: https://github.com/DataDog/dd-trace-rb/issues/6358
5669
+ [#6364]: https://github.com/DataDog/dd-trace-rb/issues/6364
5670
+ [#6371]: https://github.com/DataDog/dd-trace-rb/issues/6371
5671
+ [#6375]: https://github.com/DataDog/dd-trace-rb/issues/6375
5672
+ [#6394]: https://github.com/DataDog/dd-trace-rb/issues/6394
5673
+ [#6395]: https://github.com/DataDog/dd-trace-rb/issues/6395
5674
+ [#6398]: https://github.com/DataDog/dd-trace-rb/issues/6398
5675
+ [#6407]: https://github.com/DataDog/dd-trace-rb/issues/6407
5676
+ [#6427]: https://github.com/DataDog/dd-trace-rb/issues/6427
5635
5677
  [@AdrianLC]: https://github.com/AdrianLC
5636
5678
  [@Azure7111]: https://github.com/Azure7111
5637
5679
  [@BabyGroot]: https://github.com/BabyGroot
@@ -5684,6 +5726,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5684
5726
  [@cwoodcox]: https://github.com/cwoodcox
5685
5727
  [@danhodge]: https://github.com/danhodge
5686
5728
  [@dasch]: https://github.com/dasch
5729
+ [@dduugg]: https://github.com/dduugg
5687
5730
  [@dim]: https://github.com/dim
5688
5731
  [@dirk]: https://github.com/dirk
5689
5732
  [@djmb]: https://github.com/djmb
@@ -228,6 +228,7 @@ static VALUE _native_new(VALUE klass);
228
228
  static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
229
229
  static void cpu_and_wall_time_worker_typed_data_mark(void *state_ptr);
230
230
  static VALUE _native_sampling_loop(VALUE self, VALUE instance);
231
+ static VALUE _native_profiler_internal_thread_done(VALUE self_instance);
231
232
  static VALUE _native_stop(DDTRACE_UNUSED VALUE _self, VALUE self_instance, VALUE worker_thread);
232
233
  static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *optional_exception_during_operation);
233
234
  static void stop_state(cpu_and_wall_time_worker_state *state, VALUE optional_exception, const char *optional_operation_name);
@@ -289,6 +290,7 @@ static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instan
289
290
  static VALUE handle_sampling_failure_rescued_commit_heap_recordings(VALUE self_instance, VALUE exception);
290
291
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state);
291
292
  static inline void during_sample_exit(cpu_and_wall_time_worker_state* state);
293
+ static VALUE during_sample_exit_rescue(VALUE state_ptr);
292
294
  static void commit_heap_recordings_from_postponed_job_may_lose_gvl(DDTRACE_UNUSED void *_unused);
293
295
 
294
296
  // We're using `on_newobj_event` function with `rb_add_event_hook2`, which requires in its public signature a function
@@ -380,6 +382,7 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
380
382
 
381
383
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_initialize", _native_initialize, -1);
382
384
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_sampling_loop", _native_sampling_loop, 1);
385
+ rb_define_method(collectors_cpu_and_wall_time_worker_class, "_native_profiler_internal_thread_done", _native_profiler_internal_thread_done, 0);
383
386
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_stop", _native_stop, 2);
384
387
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
385
388
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_stats", _native_stats, 1);
@@ -650,6 +653,17 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
650
653
  return Qnil;
651
654
  }
652
655
 
656
+ static VALUE _native_profiler_internal_thread_done(VALUE self_instance) {
657
+ cpu_and_wall_time_worker_state *state;
658
+ TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
659
+
660
+ during_sample_enter(state);
661
+ return rb_ensure(
662
+ thread_context_collector_profiler_internal_thread_done, state->thread_context_collector_instance,
663
+ during_sample_exit_rescue, (VALUE) state
664
+ );
665
+ }
666
+
653
667
  static VALUE _native_stop(DDTRACE_UNUSED VALUE _self, VALUE self_instance, VALUE worker_thread) {
654
668
  cpu_and_wall_time_worker_state *state;
655
669
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -751,11 +765,20 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
751
765
  !state->during_sample;
752
766
 
753
767
  if (sample_from_signal_handler) {
754
- // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
755
- // until the postponed job below gets run.
756
- bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
768
+ if (rb_during_gc()) {
769
+ // During GC Ruby might be marking our buffer or compacting (moving) where iseqs live so we skip preparing the
770
+ // sample immediately, and instead ask on_gc_finish to do the job when GC is at its end.
771
+ //
772
+ // When GC profiling is disabled, we just fall back to sampling the stack after GC finishes, e.g. it's the
773
+ // equivalent of disabling `sample_from_signal_handler` for samples that happen during GC.
774
+ thread_context_collector_request_prepare_on_gc_finish();
775
+ } else {
776
+ // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
777
+ // until the postponed job below gets run.
778
+ bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
757
779
 
758
- if (prepared) state->stats.signal_handler_prepared_sample++;
780
+ if (prepared) state->stats.signal_handler_prepared_sample++;
781
+ }
759
782
  }
760
783
 
761
784
  #ifndef NO_POSTPONED_TRIGGER // Ruby 3.3+
@@ -1096,7 +1119,10 @@ static void on_gc_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused) {
1096
1119
  if (event == RUBY_INTERNAL_EVENT_GC_ENTER) {
1097
1120
  thread_context_collector_on_gc_start(state->thread_context_collector_instance);
1098
1121
  } else if (event == RUBY_INTERNAL_EVENT_GC_EXIT) {
1099
- bool should_flush = thread_context_collector_on_gc_finish(state->thread_context_collector_instance);
1122
+ bool allow_prepare_sample = !state->during_sample;
1123
+ if (allow_prepare_sample) during_sample_enter(state);
1124
+ bool should_flush = thread_context_collector_on_gc_finish(state->thread_context_collector_instance, allow_prepare_sample);
1125
+ if (allow_prepare_sample) during_sample_exit(state);
1100
1126
 
1101
1127
  // We use rb_postponed_job_register_one to ask Ruby to run thread_context_collector_sample_after_gc when the
1102
1128
  // thread collector flags it's time to flush.
@@ -1775,3 +1801,8 @@ static inline void during_sample_exit(cpu_and_wall_time_worker_state* state) {
1775
1801
  atomic_signal_fence(memory_order_seq_cst);
1776
1802
  state->during_sample = false;
1777
1803
  }
1804
+
1805
+ static VALUE during_sample_exit_rescue(VALUE state_ptr) {
1806
+ during_sample_exit((cpu_and_wall_time_worker_state *) state_ptr);
1807
+ return Qnil;
1808
+ }
@@ -31,9 +31,9 @@ typedef struct {
31
31
 
32
32
  static VALUE _native_new(VALUE klass);
33
33
  static void reset_state(idle_sampling_loop_state *state);
34
- static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE thread_context_collector_instance);
34
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE cpu_and_wall_time_worker_instance);
35
35
  static VALUE idle_sampling_loop_body(VALUE self_instance);
36
- static VALUE idle_sampling_loop_ensure(VALUE thread_context_collector_instance);
36
+ static VALUE idle_sampling_loop_ensure(VALUE cpu_and_wall_time_worker_instance);
37
37
  static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
38
38
  static void *run_idle_sampling_loop(void *state_ptr);
39
39
  static void interrupt_idle_sampling_loop(void *state_ptr);
@@ -112,8 +112,8 @@ static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
112
112
  return Qtrue;
113
113
  }
114
114
 
115
- static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE thread_context_collector_instance) {
116
- return rb_ensure(idle_sampling_loop_body, self_instance, idle_sampling_loop_ensure, thread_context_collector_instance);
115
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE cpu_and_wall_time_worker_instance) {
116
+ return rb_ensure(idle_sampling_loop_body, self_instance, idle_sampling_loop_ensure, cpu_and_wall_time_worker_instance);
117
117
  }
118
118
 
119
119
  static VALUE idle_sampling_loop_body(VALUE self_instance) {
@@ -128,9 +128,8 @@ static VALUE idle_sampling_loop_body(VALUE self_instance) {
128
128
  return Qtrue;
129
129
  }
130
130
 
131
- static VALUE idle_sampling_loop_ensure(VALUE thread_context_collector_instance) {
132
- thread_context_collector_profiler_internal_thread_done(thread_context_collector_instance);
133
- return Qnil;
131
+ static VALUE idle_sampling_loop_ensure(VALUE cpu_and_wall_time_worker_instance) {
132
+ return rb_funcall(cpu_and_wall_time_worker_instance, rb_intern("_native_profiler_internal_thread_done"), 0);
134
133
  }
135
134
 
136
135
  static void *run_idle_sampling_loop(void *state_ptr) {
@@ -642,8 +642,8 @@ void record_placeholder_stack(
642
642
  }
643
643
 
644
644
  bool prepare_sample_thread(VALUE thread, sampling_buffer *buffer) {
645
- // Since this can get called from inside a signal handler, we don't want to touch the buffer if
646
- // the thread was actually in the middle of marking it.
645
+ // While the signal handler checks if there's GC going on, it's actually possible for `sampling_buffer_mark` to
646
+ // still run outside of GC (see comments on that function) and thus we guard for it here.
647
647
  if (buffer->is_marking) return false;
648
648
 
649
649
  buffer->pending_sample = true;
@@ -705,12 +705,11 @@ void sampling_buffer_mark(sampling_buffer *buffer) {
705
705
  }
706
706
 
707
707
  buffer->is_marking = true;
708
- // Tell the compiler it's not allowed to reorder the `is_marking` flag with the iteration below.
709
- //
710
- // Specifically, in the middle of `sampling_buffer_mark` a signal handler may execute and call
711
- // `prepare_sample_thread` to add a new sample to the buffer. This flag is here to prevent that BUT we need to
712
- // make sure the signal handler actually sees the flag being set.
708
+ // The sampling buffer isn't supposed to be touched during GC. Yet, marking can happen outside of GC -- in particular
709
+ // ObjectSpace.dump/dump_all and reachable_objects_from invoke mark callbacks outside GC to enumerate references.
710
+ // Thus we keep this flag to tell the signal handler "hey you're still not allowed to touch the buffer at this time".
713
711
  //
712
+ // The fences ensure the compiler doesn't reorder the flags and a signal handler observes them
714
713
  // See https://github.com/ruby/ruby/pull/11036 for a similar change made to the Ruby VM with more context.
715
714
  atomic_signal_fence(memory_order_seq_cst);
716
715
 
@@ -13,7 +13,7 @@ typedef struct {
13
13
  uint16_t max_frames;
14
14
  frame_info *stack_buffer;
15
15
  bool pending_sample;
16
- bool is_marking; // Used to avoid recording a sample when marking
16
+ bool is_marking; // Used to avoid recording a sample when marking (marking can happen outside of GC as well)
17
17
  int pending_sample_result;
18
18
  } sampling_buffer;
19
19
 
@@ -207,6 +207,7 @@ typedef struct {
207
207
  // however this only matters for a very short period when a thread starts.
208
208
  struct per_thread_context {
209
209
  sampling_buffer sampling_buffer;
210
+ bool prepare_sample_on_gc_finish;
210
211
  char thread_id[THREAD_ID_LIMIT_CHARS];
211
212
  ddog_CharSlice thread_id_char_slice;
212
213
  char thread_invoke_location[THREAD_INVOKE_LOCATION_LIMIT_CHARS];
@@ -311,7 +312,6 @@ static void update_metrics_and_sample(
311
312
  thread_context_collector_state *state,
312
313
  VALUE thread_being_sampled,
313
314
  per_thread_context *thread_context,
314
- long current_cpu_time_ns,
315
315
  long current_monotonic_wall_time_ns,
316
316
  bool force_sample
317
317
  );
@@ -385,6 +385,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
385
385
  static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key);
386
386
  static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
387
387
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self);
388
+ static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self);
388
389
  static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread);
389
390
  static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
390
391
  static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
@@ -424,6 +425,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
424
425
  rb_define_singleton_method(testing_module, "_native_sample_skipped_allocation_samples", _native_sample_skipped_allocation_samples, 2);
425
426
  rb_define_singleton_method(testing_module, "_native_system_epoch_time_now_ns", _native_system_epoch_time_now_ns, 1);
426
427
  rb_define_singleton_method(testing_module, "_native_prepare_sample_inside_signal_handler", _native_prepare_sample_inside_signal_handler, 0);
428
+ rb_define_singleton_method(testing_module, "_native_request_prepare_on_gc_finish", _native_request_prepare_on_gc_finish, 0);
427
429
  rb_define_singleton_method(testing_module, "_native_remove_per_thread_context_for", _native_remove_per_thread_context_for, 1);
428
430
  rb_define_singleton_method(testing_module, "_native_global_reset_per_thread_context", _native_global_reset_per_thread_context, 1);
429
431
  rb_define_singleton_method(testing_module, "_native_mark_thread_as_profiler_internal", _native_mark_thread_as_profiler_internal, 1);
@@ -679,7 +681,7 @@ static VALUE _native_on_gc_start(DDTRACE_UNUSED VALUE self, VALUE collector_inst
679
681
  static VALUE _native_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
680
682
  debug_enter_unsafe_context();
681
683
 
682
- (void) !thread_context_collector_on_gc_finish(collector_instance);
684
+ (void) !thread_context_collector_on_gc_finish(collector_instance, true);
683
685
 
684
686
  debug_leave_unsafe_context();
685
687
 
@@ -741,6 +743,7 @@ static void record_sampling_overhead(thread_context_collector_state *state, per_
741
743
  // Assumption 3: This function IS NOT called from a signal handler. This function is not async-signal-safe.
742
744
  // Assumption 4: This function IS NOT called in a reentrant way.
743
745
  // Assumption 5: This function is called from the main Ractor (if Ruby has support for Ractors).
746
+ // Assumption 6: When called while the profiler is active, `during_sample` MUST be set.
744
747
  //
745
748
  bool thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns) {
746
749
  thread_context_collector_state *state;
@@ -748,25 +751,30 @@ bool thread_context_collector_sample(VALUE self_instance, long current_monotonic
748
751
 
749
752
  VALUE current_thread = rb_thread_current();
750
753
  per_thread_context *current_thread_context = get_or_create_context_for(current_thread);
751
- long cpu_time_at_sample_start_for_current_thread = cpu_time_now_ns(current_thread_context);
754
+
755
+ // Sample the current thread (which will use the current CPU-time) first.
756
+ // This ensures any CPU-time the profiler spends sampling (e.g. the rest of the function) is not wrongly blamed on this thread,
757
+ // and instead is separately accounted by `record_sampling_overhead()` below.
758
+ update_metrics_and_sample(
759
+ state,
760
+ current_thread,
761
+ current_thread_context,
762
+ current_monotonic_wall_time_ns,
763
+ false);
752
764
 
753
765
  VALUE threads = thread_list(state);
754
766
 
755
767
  const long thread_count = RARRAY_LEN(threads);
756
768
  for (long i = 0; i < thread_count; i++) {
757
- VALUE thread = RARRAY_AREF(threads, i);
758
- per_thread_context *thread_context = get_or_create_context_for(thread);
769
+ VALUE thread = rb_ary_entry(threads, i);
770
+ ENFORCE_THREAD(thread);
759
771
 
760
- // We account for cpu-time for the current thread in a different way: we use the cpu-time at sampling start,
761
- // to avoid blaming the time the profiler took on whatever is currently running on the thread,
762
- // and instead we report that time the profiler took as sampling overhead below.
763
- long current_cpu_time_ns = (thread == current_thread) ? cpu_time_at_sample_start_for_current_thread : cpu_time_now_ns(thread_context);
772
+ if (thread == current_thread) continue; // Already sampled above
764
773
 
765
774
  update_metrics_and_sample(
766
775
  state,
767
776
  thread,
768
- thread_context,
769
- current_cpu_time_ns,
777
+ get_or_create_context_for(thread),
770
778
  current_monotonic_wall_time_ns,
771
779
  false);
772
780
  }
@@ -790,10 +798,10 @@ static void update_metrics_and_sample(
790
798
  thread_context_collector_state *state,
791
799
  VALUE thread_being_sampled,
792
800
  per_thread_context *thread_context,
793
- long current_cpu_time_ns,
794
801
  long current_monotonic_wall_time_ns,
795
802
  bool force_sample
796
803
  ) {
804
+ long current_cpu_time_ns = cpu_time_now_ns(thread_context);
797
805
  bool is_gvl_waiting_state =
798
806
  handle_gvl_waiting(state, thread_being_sampled, thread_context, current_cpu_time_ns);
799
807
 
@@ -907,8 +915,9 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
907
915
  //
908
916
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
909
917
  // Assumption 2: This function is called from the main Ractor (if Ruby has support for Ractors).
918
+ // Assumption 3: If allow_prepare_sample is true while profiling is active, during_sample MUST be set by the caller.
910
919
  __attribute__((warn_unused_result))
911
- bool thread_context_collector_on_gc_finish(VALUE self_instance) {
920
+ bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepare_sample) {
912
921
  thread_context_collector_state *state;
913
922
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
914
923
  // This should never fail when the above check passes
@@ -919,6 +928,11 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
919
928
  // Context is created eagerly, so this should not normally be NULL (see on_gc_start).
920
929
  if (thread_context == NULL) return false;
921
930
 
931
+ if (thread_context->prepare_sample_on_gc_finish) {
932
+ thread_context->prepare_sample_on_gc_finish = false;
933
+ if (allow_prepare_sample) prepare_sample_thread(rb_thread_current(), &thread_context->sampling_buffer);
934
+ }
935
+
922
936
  long cpu_time_at_start_ns = thread_context->gc_tracking.cpu_time_at_start_ns;
923
937
  long wall_time_at_start_ns = thread_context->gc_tracking.wall_time_at_start_ns;
924
938
 
@@ -986,6 +1000,7 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
986
1000
  // Assumption 2: This function is allowed to raise exceptions. Caller is responsible for handling them, if needed.
987
1001
  // Assumption 3: Unlike `on_gc_start` and `on_gc_finish`, this method is allowed to allocate memory as needed.
988
1002
  // Assumption 4: This function is called from the main Ractor (if Ruby has support for Ractors).
1003
+ // Assumption 5: When called while the profiler is active, `during_sample` MUST be set.
989
1004
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
990
1005
  thread_context_collector_state *state;
991
1006
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -1325,6 +1340,8 @@ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance)
1325
1340
  const long thread_count = RARRAY_LEN(threads);
1326
1341
  for (long i = 0; i < thread_count; i++) {
1327
1342
  VALUE thread = rb_ary_entry(threads, i);
1343
+ ENFORCE_THREAD(thread);
1344
+
1328
1345
  per_thread_context *thread_context = get_per_thread_context(thread);
1329
1346
  if (thread_context != NULL) {
1330
1347
  bool is_profiler_internal_thread = thread_context->is_profiler_internal_thread;
@@ -1393,6 +1410,7 @@ static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context)
1393
1410
  ID2SYM(rb_intern("gvl_state_change_count")), /* => */ ULL2NUM(thread_context->gvl_state_change_count),
1394
1411
  ID2SYM(rb_intern("gvl_state_change_count_at_previous_sample")), /* => */ ULL2NUM(thread_context->gvl_state_change_count_at_previous_sample),
1395
1412
  ID2SYM(rb_intern("was_skipped_at_last_sample")), /* => */ thread_context->was_skipped_at_last_sample ? Qtrue : Qfalse,
1413
+ ID2SYM(rb_intern("prepare_sample_on_gc_finish")), /* => */ thread_context->prepare_sample_on_gc_finish ? Qtrue : Qfalse,
1396
1414
  ID2SYM(rb_intern("is_profiler_internal_thread")), /* => */ thread_context->is_profiler_internal_thread ? Qtrue : Qfalse,
1397
1415
  };
1398
1416
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(context_as_hash, arguments[i], arguments[i+1]);
@@ -1439,7 +1457,9 @@ static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collec
1439
1457
  VALUE threads = thread_list(state);
1440
1458
  const long thread_count = RARRAY_LEN(threads);
1441
1459
  for (long i = 0; i < thread_count; i++) {
1442
- VALUE thread = RARRAY_AREF(threads, i);
1460
+ VALUE thread = rb_ary_entry(threads, i);
1461
+ ENFORCE_THREAD(thread);
1462
+
1443
1463
  per_thread_context *thread_context = get_per_thread_context(thread);
1444
1464
  if (thread_context != NULL) {
1445
1465
  rb_hash_aset(result, thread, per_thread_context_to_ruby_hash(thread_context));
@@ -1679,7 +1699,7 @@ static VALUE thread_list(thread_context_collector_state *state) {
1679
1699
  // the current thread.
1680
1700
  //
1681
1701
  // Assumptions for this function are same as for `thread_context_collector_sample` except that this function is
1682
- // expected to be called from a signal handler and to be async-signal-safe.
1702
+ // expected to be called from a signal handler and to be async-signal-safe, and `during_sample` MUST be unset.
1683
1703
  //
1684
1704
  // Also, no allocation (Ruby or malloc) can happen.
1685
1705
  bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
@@ -1690,13 +1710,20 @@ bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1690
1710
  return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1691
1711
  }
1692
1712
 
1713
+ // Called from the sampling signal handler; must not allocate or inspect the Ruby stack during GC.
1714
+ void thread_context_collector_request_prepare_on_gc_finish(void) {
1715
+ per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
1716
+ if (thread_context != NULL) thread_context->prepare_sample_on_gc_finish = true;
1717
+ }
1718
+
1693
1719
  // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1694
1720
  // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1695
1721
  //
1696
1722
  // Returns true if `thread_context_collector_commit_heap_recordings_may_lose_gvl` needs to be called (to do work
1697
1723
  // that can't be done from inside the tracepoint, such as allocate new objects), and false if it doesn't
1698
1724
  //
1699
- // The callers must ensure thread_context is non-NULL.
1725
+ // Assumption 1: The callers must ensure thread_context is non-NULL.
1726
+ // Assumption 2: When called while the profiler is active, `during_sample` MUST be set.
1700
1727
  bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object) {
1701
1728
  thread_context_collector_state *state;
1702
1729
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -1942,6 +1969,7 @@ static void ddtrace_otel_trace_identifiers_for(
1942
1969
  *numeric_span_id = resolved_numeric_span_id;
1943
1970
  }
1944
1971
 
1972
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
1945
1973
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples) {
1946
1974
  thread_context_collector_state *state;
1947
1975
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -2156,7 +2184,9 @@ static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self,
2156
2184
  // flush is not lost. on_serialize (below) also flushes profiler-internal threads during periodic
2157
2185
  // serialization, but it can't help at shutdown: by the time the final serialize runs, these
2158
2186
  // threads are already dead and absent from thread_list.
2159
- void thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2187
+ //
2188
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2189
+ VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2160
2190
  thread_context_collector_state *state;
2161
2191
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2162
2192
 
@@ -2166,21 +2196,23 @@ void thread_context_collector_profiler_internal_thread_done(VALUE self_instance)
2166
2196
  rb_raise(rb_eRuntimeError, "current thread %"PRIsVALUE" is not profiler-internal thread", current_thread);
2167
2197
  }
2168
2198
 
2169
- long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2170
2199
  long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2171
2200
 
2172
2201
  update_metrics_and_sample(
2173
2202
  state,
2174
2203
  current_thread,
2175
2204
  thread_context,
2176
- current_cpu_time_ns,
2177
2205
  current_monotonic_wall_time_ns,
2178
2206
  true);
2207
+
2208
+ return Qnil;
2179
2209
  }
2180
2210
 
2181
2211
  // Flushes threads whose last per-tick sample was skipped (either by the SUSPENDED-skip
2182
2212
  // optimization, or by is_profiler_internal_thread) so their accumulated time is recorded.
2183
2213
  // Called by the stack recorder at the start of _native_serialize (regular periodic flush).
2214
+ //
2215
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2184
2216
  void thread_context_collector_on_serialize(VALUE self_instance) {
2185
2217
  thread_context_collector_state *state;
2186
2218
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -2190,19 +2222,19 @@ void thread_context_collector_on_serialize(VALUE self_instance) {
2190
2222
  const long thread_count = RARRAY_LEN(threads);
2191
2223
 
2192
2224
  for (long i = 0; i < thread_count; i++) {
2193
- VALUE thread = RARRAY_AREF(threads, i);
2225
+ VALUE thread = rb_ary_entry(threads, i);
2226
+ ENFORCE_THREAD(thread);
2227
+
2194
2228
  per_thread_context *thread_context = get_per_thread_context(thread);
2195
2229
 
2196
2230
  if (thread_context != NULL && (thread_context->was_skipped_at_last_sample || thread_context->is_profiler_internal_thread)) {
2197
- long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2198
- // We need to force_sample=true otherwise this sample would be skipped too
2199
2231
  update_metrics_and_sample(
2200
2232
  state,
2201
2233
  thread,
2202
2234
  thread_context,
2203
- current_cpu_time_ns,
2204
2235
  current_monotonic_wall_time_ns,
2205
- true);
2236
+ true // We need to force_sample=true otherwise this sample would be skipped too
2237
+ );
2206
2238
  }
2207
2239
  }
2208
2240
  }
@@ -2246,6 +2278,8 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2246
2278
 
2247
2279
  #ifndef NO_GVL_INSTRUMENTATION
2248
2280
  // We must only use async-signal-safe functions here, see notes in the caller
2281
+ //
2282
+ // Assumption 1: When called while the profiler is active, `during_sample` MUST be set.
2249
2283
  __attribute__((warn_unused_result))
2250
2284
  on_gvl_running_result thread_context_collector_on_gvl_running(VALUE thread, per_thread_context *thread_context, uint32_t waiting_for_gvl_threshold_ns) {
2251
2285
  // Bump the event counter and clears the state bit to "running"
@@ -2322,7 +2356,8 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2322
2356
  //
2323
2357
  // ---
2324
2358
  //
2325
- // Always called with the GVL, either from a postponed_job or from tests.
2359
+ // Assumption 1: Always called with the GVL, either from a postponed_job or from tests.
2360
+ // Assumption 2: When called while the profiler is active, `during_sample` MUST be set.
2326
2361
  //
2327
2362
  // NOTE: In normal use, current_thread is expected to be == rb_thread_current(); the `current_thread` parameter only
2328
2363
  // exists to enable testing.
@@ -2342,18 +2377,12 @@ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2342
2377
  return Qfalse;
2343
2378
  }
2344
2379
 
2345
- // We don't actually account for cpu-time during Waiting for GVL. BUT, we may choose to push an
2346
- // extra sample to represent the period prior to Waiting for GVL. To support that, we retrieve the current
2347
- // cpu-time of the thread and let `update_metrics_and_sample` decide what to do with it.
2348
- long cpu_time_for_thread = cpu_time_now_ns(thread_context);
2349
-
2350
2380
  // TODO: Should we update the dynamic sampling rate overhead tracking with this sample as well?
2351
2381
 
2352
2382
  update_metrics_and_sample(
2353
2383
  state,
2354
2384
  current_thread,
2355
2385
  thread_context,
2356
- cpu_time_for_thread,
2357
2386
  current_monotonic_wall_time_ns,
2358
2387
  false);
2359
2388
 
@@ -2564,3 +2593,8 @@ static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE c
2564
2593
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self) {
2565
2594
  return thread_context_collector_prepare_sample_inside_signal_handler() ? Qtrue : Qfalse;
2566
2595
  }
2596
+
2597
+ static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self) {
2598
+ thread_context_collector_request_prepare_on_gc_finish();
2599
+ return Qnil;
2600
+ }
@@ -13,6 +13,7 @@ __attribute__((warn_unused_result)) bool thread_context_collector_sample(
13
13
  long current_monotonic_wall_time_ns
14
14
  );
15
15
  __attribute__((warn_unused_result)) bool thread_context_collector_prepare_sample_inside_signal_handler(void);
16
+ void thread_context_collector_request_prepare_on_gc_finish(void);
16
17
  __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);
17
18
  void thread_context_collector_commit_heap_recordings_may_lose_gvl(VALUE self_instance);
18
19
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples);
@@ -20,14 +21,14 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance);
20
21
  VALUE thread_context_collector_heap_update_may_lose_gvl(VALUE self_instance);
21
22
  void thread_context_collector_resolve_otel_span_key_may_lose_gvl(VALUE self_instance);
22
23
  void thread_context_collector_on_gc_start(VALUE self_instance);
23
- __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance);
24
+ __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance, bool allow_prepare_sample);
24
25
  VALUE enforce_thread_context_collector_instance(VALUE object);
25
26
  void thread_context_collector_stats(VALUE self_instance, VALUE stats_hash);
26
27
  void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance);
27
28
  void thread_context_collector_on_serialize(VALUE self_instance);
28
29
  void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance);
29
30
  void thread_context_collector_profiler_internal_thread_started(void);
30
- void thread_context_collector_profiler_internal_thread_done(VALUE self_instance);
31
+ VALUE thread_context_collector_profiler_internal_thread_done(VALUE self_instance);
31
32
 
32
33
  #ifndef NO_GVL_INSTRUMENTATION
33
34
  typedef enum {
@@ -260,7 +260,9 @@ dir_config("ruby") # allow user to pass in non-standard core include directory
260
260
  #
261
261
  # To workaround this issue, we override `with_cppflags` for datadog-ruby_core_source to still include `$CPPFLAGS`.
262
262
  Datadog::RubyCoreSource.define_singleton_method(:with_cppflags) do |newflags, &block|
263
- super("#{newflags} #{$CPPFLAGS}", &block)
263
+ # Note: The `Shellwords.escape` is an additional workaround for ruby_core_source supplying
264
+ # one unescaped -I argument containing the complete header path, not the CPPFLAGS thing above.
265
+ super("#{Shellwords.escape(newflags)} #{$CPPFLAGS}", &block)
264
266
  end
265
267
 
266
268
  Datadog::RubyCoreSource
@@ -270,7 +272,7 @@ Datadog::RubyCoreSource
270
272
  have_header("vm_core.h") &&
271
273
  have_header("iseq.h", "vm_core.h") &&
272
274
  # These are only used on Ruby 3+
273
- (RUBY_VERSION < "3" || have_header("ractor_core.h") && have_header("internal/class.h"))
275
+ (RUBY_VERSION < "3" || have_header("ractor_core.h") && have_header("internal/class.h", "vm_core.h"))
274
276
 
275
277
  if headers_available
276
278
  # Warn on unused parameters to functions. Use `DDTRACE_UNUSED` to mark things as known-to-not-be-used.
@@ -278,6 +278,23 @@ VALUE thread_name_for(VALUE thread) {
278
278
  return thread_struct_from_object(thread)->name;
279
279
  }
280
280
 
281
+ // This test doubles as a validation for the layout of the rb_thread_t structure. Because the `name` is almost at
282
+ // the end of the structure, if somehow we can't read the name, this probably means we're getting the structure wrong
283
+ // and we should stop immediately (this includes covering the use of `stat_insn_usage` on Ruby <= 3.2).
284
+ //
285
+ // One example where this happens is in patches such as
286
+ // https://github.com/gitlabhq/omnibus-gitlab/blob/master/config/patches/ruby/thread-memory-allocations-3.2.patch
287
+ // that add items to the structure and thus shift its position from where we expect.
288
+ void self_test_thread_name_for(void) {
289
+ VALUE thread = rb_eval_string("Thread.new { Thread.current.name = 'dd-prof-test' }.join");
290
+ VALUE expected_name = rb_funcall(thread, rb_intern("name"), 0);
291
+ if (thread_name_for(thread) != expected_name) {
292
+ rb_raise(rb_eRuntimeError,
293
+ "thread_name_for() self-test failed; this usually means the `rb_thread_t` on this Ruby doesn't match what the profiler expects"
294
+ );
295
+ }
296
+ }
297
+
281
298
  // -----------------------------------------------------------------------------
282
299
  // The sources below are modified versions of code extracted from the Ruby project.
283
300
  // Each function is annotated with its origin, why we imported it, and the changes made.
@@ -539,6 +556,14 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
539
556
  cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
540
557
 
541
558
  for (i=0; i<limit && cfp != top_sentinel; cfp = RUBY_VM_NEXT_CONTROL_FRAME(cfp)) {
559
+ #ifndef NO_T_MOVED
560
+ if (cfp->iseq && RB_TYPE_P((VALUE) cfp->iseq, T_MOVED)) {
561
+ // The profiler is not supposed to sample during GC compaction, so T_MOVED is not expected here.
562
+ // Yet, crash tracking also uses this walker and may run at any time. For now, we choose to skip these frames.
563
+ continue;
564
+ }
565
+ #endif
566
+
542
567
  if (cfp->iseq && !cfp->pc) {
543
568
  // Fix: Do nothing -- this frame should not be used
544
569
  //