datadog 2.41.0 → 2.43.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  5. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +120 -61
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +156 -108
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  9. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  10. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +351 -255
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +50 -31
  13. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +518 -442
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  16. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +106 -61
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  21. data/ext/datadog_profiling_native_extension/time_helpers.h +2 -1
  22. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  23. data/ext/libdatadog_api/di.c +10 -0
  24. data/ext/libdatadog_api/extconf.rb +3 -0
  25. data/ext/libdatadog_api/init.c +2 -0
  26. data/ext/libdatadog_api/otel_thread_context.c +247 -0
  27. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  28. data/ext/libdatadog_api/trace_exporter.c +642 -22
  29. data/ext/libdatadog_extconf_helpers.rb +1 -1
  30. data/lib/datadog/appsec/assets/blocked.html +1 -108
  31. data/lib/datadog/core/configuration/components.rb +1 -0
  32. data/lib/datadog/core/crashtracking/component.rb +5 -1
  33. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  34. data/lib/datadog/data_streams/processor.rb +31 -0
  35. data/lib/datadog/di/instrumenter.rb +41 -1
  36. data/lib/datadog/di/logger.rb +2 -2
  37. data/lib/datadog/di/probe.rb +9 -1
  38. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  39. data/lib/datadog/di/remote.rb +3 -3
  40. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  41. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  42. data/lib/datadog/open_feature/ext.rb +19 -0
  43. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  44. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  45. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  46. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  47. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  48. data/lib/datadog/open_feature/provider.rb +11 -2
  49. data/lib/datadog/open_feature/transport.rb +5 -0
  50. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  51. data/lib/datadog/opentelemetry/trace.rb +3 -0
  52. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  53. data/lib/datadog/profiling/collectors/info.rb +2 -6
  54. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  55. data/lib/datadog/profiling/component.rb +8 -16
  56. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  57. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  58. data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
  59. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  60. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  61. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  62. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  63. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  64. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  65. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  66. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  67. data/lib/datadog/tracing/otel_thread_context.rb +34 -0
  68. data/lib/datadog/tracing/remote.rb +195 -27
  69. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  70. data/lib/datadog/tracing/trace_digest.rb +22 -4
  71. data/lib/datadog/tracing/trace_operation.rb +22 -10
  72. data/lib/datadog/tracing/tracer.rb +5 -5
  73. data/lib/datadog/tracing/transport/native.rb +17 -25
  74. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  75. data/lib/datadog/tracing/transport/traces.rb +2 -22
  76. data/lib/datadog/version.rb +1 -1
  77. metadata +16 -8
  78. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9770ef0d02b2ecf56734c823b89ea642beb8512dae612904f0bc3d050852b44
4
- data.tar.gz: b1e359c8eb8d26528b21c963c775b7cf8e5e227ca2cf7b88b322337d32c7283c
3
+ metadata.gz: 154c126111f9b92cc59626264fc83b954b36317f96798bc33a0bcb7784516968
4
+ data.tar.gz: 4098225f4c88a0c3e31b069ca1a17231af4298ca149ce530d0dc4ccc9a8d8db5
5
5
  SHA512:
6
- metadata.gz: 21335b21c2d0be965d3f3c972efab822767ddd6f28e3a1263497335300cf7a34b3fe1872d9037c1e40af909a897296e3946fac7ecd7b8e7b92c21c1ecdfc7a77
7
- data.tar.gz: a2ee7baf6f4e6e45f6116629d09806db7d0bd171bb5aaa36f4cf65e2f7c53038be18e04b84b25f1a26bd87031ea223a8e6ced8f91cceb62f8a292bfd4fd76875
6
+ metadata.gz: a566b1ab03efdf8c881ff7a6af93aff5fc946dc213ea1ccfabc554458ae733769ab98002ea14f039e3b2a19c150b2e208083e35fd0519f34b77ff239247412dd
7
+ data.tar.gz: 3a9f8574b7325c9f6e77da1a7f8ad65b8b6a74426b9dab829db84c8adbe8757c1f09d57d8232085c51d074c60e4a77e017cbdd9bfaa5d9f84efba0ad66869866
data/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.43.0] - 2026-09-22
6
+
7
+ ### Added
8
+
9
+ * Core: Add an experimental `libdatadog`-backed native trace transport with full feature parity. Opt in with `DD_EXPERIMENTAL_NATIVE_TRANSPORT_ENABLED=true`. ([#5971][])
10
+ * SSI: Support `force_ruby_platform` during Ruby dependency resolution. ([#6316][])
11
+
12
+ ### Fixed
13
+
14
+ * SSI: Respect transitive application dependencies during Ruby injection. ([#6316][])
15
+ * SSI: Preserve checksums in injected Ruby lockfiles. ([#6320][])
16
+ * SSI: Preserve Bundler selection and `RUBYOPT` options during injection. ([#6316][])
17
+ * SSI: Support versioned and symlinked Bundler launchers. ([#6316][])
18
+ * SSI: Restore Ruby injection for deployment and vendored bundles. ([#6316][])
19
+ * Profiling: Fix a potential sampling race when Ruby is idle. ([#6286][])
20
+ * Profiling: Fix profiler not starting on Ruby 4 due to `undefined symbol: rb_current_box`. ([#6350][])
21
+
22
+ ## [2.42.0] - 2026-08-31
23
+
24
+ ### Added
25
+
26
+ * Tracing: Add sampling probability compatibility with OTel-instrumented services ([#6111][])
27
+ * Dynamic Instrumentation: Honor organization- and environment-level remote enablement for Ruby services. ([#6234][])
28
+ * Dynamic Instrumentation: Add per-process runtime id for snapshots to distinguish process restarts within a container ([#6232][])
29
+ * Open Feature: OpenFeature provider sends the serial ID of the assigned split on exposure events ([#6220][])
30
+
31
+ ### Changed
32
+
33
+ * Profiling: Drop usage of MJIT headers for Ruby 2.6 to 3.1 ([#6135][])
34
+ * Profiling: Re-enable heap live size profiling for Ruby 4+ ([#6176][])
35
+ * Dynamic Instrumentation: Enforce process-wide rate limit across all probes ([#6043][])
36
+
37
+ ### Fixed
38
+
39
+ * Core: Ignore `SignalException` from crashtracker as unhandled exception errors ([#6219][])
40
+ * Tracing: Fix missing peer tags for database queries traced through `ActiveRecord` ([#6213][])
41
+ * Profiling: Fix bug in `sample_after_gc` leading to profiler stopping ([#6242][])
42
+ * Profiling: Improve stability of heap profiling by using `ObjectSpace::WeakMap` instead of `_id2ref` ([#6176][])
43
+ * Profiling: VM-internal objects are no longer sampled for heap profiling ([#6176][])
44
+
5
45
  ## [2.41.0] - 2026-08-13
6
46
 
7
47
  ### Added
@@ -3753,7 +3793,9 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3753
3793
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3754
3794
 
3755
3795
 
3756
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.41.0...master
3796
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.43.0...master
3797
+ [2.43.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.42.0...v2.43.0
3798
+ [2.42.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.41.0...v2.42.0
3757
3799
  [2.41.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.40.0...v2.41.0
3758
3800
  [2.40.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.39.0...v2.40.0
3759
3801
  [2.39.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.38.0...v2.39.0
@@ -5560,6 +5602,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5560
5602
  [#6022]: https://github.com/DataDog/dd-trace-rb/issues/6022
5561
5603
  [#6023]: https://github.com/DataDog/dd-trace-rb/issues/6023
5562
5604
  [#6024]: https://github.com/DataDog/dd-trace-rb/issues/6024
5605
+ [#6043]: https://github.com/DataDog/dd-trace-rb/issues/6043
5563
5606
  [#6047]: https://github.com/DataDog/dd-trace-rb/issues/6047
5564
5607
  [#6050]: https://github.com/DataDog/dd-trace-rb/issues/6050
5565
5608
  [#6056]: https://github.com/DataDog/dd-trace-rb/issues/6056
@@ -5568,14 +5611,27 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5568
5611
  [#6072]: https://github.com/DataDog/dd-trace-rb/issues/6072
5569
5612
  [#6097]: https://github.com/DataDog/dd-trace-rb/issues/6097
5570
5613
  [#6104]: https://github.com/DataDog/dd-trace-rb/issues/6104
5614
+ [#6111]: https://github.com/DataDog/dd-trace-rb/issues/6111
5571
5615
  [#6116]: https://github.com/DataDog/dd-trace-rb/issues/6116
5572
5616
  [#6126]: https://github.com/DataDog/dd-trace-rb/issues/6126
5573
5617
  [#6130]: https://github.com/DataDog/dd-trace-rb/issues/6130
5574
5618
  [#6132]: https://github.com/DataDog/dd-trace-rb/issues/6132
5575
5619
  [#6133]: https://github.com/DataDog/dd-trace-rb/issues/6133
5620
+ [#6135]: https://github.com/DataDog/dd-trace-rb/issues/6135
5576
5621
  [#6138]: https://github.com/DataDog/dd-trace-rb/issues/6138
5577
5622
  [#6142]: https://github.com/DataDog/dd-trace-rb/issues/6142
5578
5623
  [#6173]: https://github.com/DataDog/dd-trace-rb/issues/6173
5624
+ [#6176]: https://github.com/DataDog/dd-trace-rb/issues/6176
5625
+ [#6213]: https://github.com/DataDog/dd-trace-rb/issues/6213
5626
+ [#6219]: https://github.com/DataDog/dd-trace-rb/issues/6219
5627
+ [#6220]: https://github.com/DataDog/dd-trace-rb/issues/6220
5628
+ [#6232]: https://github.com/DataDog/dd-trace-rb/issues/6232
5629
+ [#6234]: https://github.com/DataDog/dd-trace-rb/issues/6234
5630
+ [#6242]: https://github.com/DataDog/dd-trace-rb/issues/6242
5631
+ [#6286]: https://github.com/DataDog/dd-trace-rb/issues/6286
5632
+ [#6316]: https://github.com/DataDog/dd-trace-rb/issues/6316
5633
+ [#6320]: https://github.com/DataDog/dd-trace-rb/issues/6320
5634
+ [#6350]: https://github.com/DataDog/dd-trace-rb/issues/6350
5579
5635
  [@AdrianLC]: https://github.com/AdrianLC
5580
5636
  [@Azure7111]: https://github.com/Azure7111
5581
5637
  [@BabyGroot]: https://github.com/BabyGroot
data/README.md CHANGED
@@ -17,8 +17,11 @@ For descriptions of terminology used in APM, take a look at the [APM Terms and C
17
17
 
18
18
  For contributing, checkout the [contribution guidelines][contribution docs] and [development guide][development docs].
19
19
 
20
+ To check that a downloaded gem file was published by this repository, see [how to verify gem provenance][provenance docs].
21
+
20
22
  [public docs]: https://docs.datadoghq.com/tracing/setup/ruby/
21
23
  [api docs]: https://datadog.github.io/dd-trace-rb/
22
24
  [APM glossary]: https://docs.datadoghq.com/tracing/glossary/
23
25
  [contribution docs]: https://github.com/DataDog/dd-trace-rb/blob/master/CONTRIBUTING.md
24
26
  [development docs]: https://github.com/DataDog/dd-trace-rb/blob/master/docs/DevelopmentGuide.md
27
+ [provenance docs]: https://github.com/DataDog/dd-trace-rb/blob/master/docs/Provenance.md
@@ -65,38 +65,26 @@ Non-exhaustive list of APIs that cause exceptions to be raised:
65
65
  To implement some of the features below, we sometimes require access to private Ruby header files (that describe VM
66
66
  internal types, structures and functions).
67
67
 
68
- Because these private header files are not included in regular Ruby installations, we have two different workarounds:
69
-
70
- 1. for Ruby versions 2.6 to 3.2 we make use use the Ruby private MJIT header
71
- 2. for Ruby versions < 2.6 and > 3.2 we make use of the `datadog-ruby_core_source` gem
68
+ Because these private header files are not included in regular Ruby installations, we make use of the
69
+ [`datadog-ruby_core_source`](https://github.com/DataDog/datadog-ruby_core_source) gem, which contains almost no code
70
+ of its own; instead, it just contains per-Ruby-version folders with the private VM headers (`.h`) files for that
71
+ version. Thus, even though a regular Ruby installation does not include these files, we can access the copy inside
72
+ this gem.
72
73
 
73
74
  Functions which make use of these headers are defined in the <private_vm_api_acccess.c> file.
74
75
 
75
- There is currently no way for disabling usage of the private MJIT header for Ruby 2.6 to 3.2.
76
-
77
76
  **Important Note**: Our medium/long-term plan is to stop relying on all private Ruby headers, and instead request and
78
77
  contribute upstream changes so that they become official public VM APIs.
79
78
 
80
- ### Approach 1: Using the Ruby private MJIT header
81
-
82
- Ruby versions 2.6 to 3.2 shipped a JIT compiler called MJIT. This compiler does not directly generate machine code;
83
- instead it generates C code and uses the system C compiler to turn it into machine code.
84
-
85
- The generated C code `#include`s a private header -- which we call "the MJIT header".
86
- The MJIT header gets shipped with all MJIT-enabled Rubies and includes the layout of many internal VM structures;
87
- and of course the intention is that it is only used by the Ruby MJIT compiler.
88
-
89
- This header is placed inside the `include/` directory in a Ruby installation, and is named for that specific Ruby
90
- version. e.g. `rb_mjit_min_header-2.7.4.h`.
91
-
92
- This header was removed in Ruby 3.3.
93
-
94
- ### Approach 2: Using the `datadog-ruby_core_source` gem
79
+ ### Historical note: the Ruby private MJIT header
95
80
 
96
- The [`datadog-ruby_core_source`](https://github.com/DataDog/datadog-ruby_core_source) contains almost no code;
97
- instead, it just contains per-Ruby-version folders with the private VM headers (`.h`) files for that version.
81
+ Ruby versions 2.6 to 3.2 shipped a JIT compiler called MJIT. This compiler did not directly generate machine code;
82
+ instead it generated C code and used the system C compiler to turn it into machine code.
98
83
 
99
- Thus, even though a regular Ruby installation does not include these files, we can access the copy inside this gem.
84
+ The generated C code `#include`d a private header -- which we called "the MJIT header". In the past we used to
85
+ rely on it for Ruby versions 2.6 to 3.2.
86
+ This header was removed in Ruby 3.3, and we have since moved all supported Ruby versions over to using the
87
+ `datadog-ruby_core_source` gem instead.
100
88
 
101
89
  ## Feature: Getting thread CPU-time clock_ids
102
90
 
@@ -68,14 +68,24 @@
68
68
  // 4. The Ruby VM calls our `sample_from_postponed_job` from a thread holding the global VM lock. A sample is recorded by
69
69
  // calling `thread_context_collector_sample`.
70
70
  //
71
- // ### TracePoints and Forking
71
+ //
72
+ // ### Hooks and TracePoints
73
+ //
74
+ // This class uses various hooks:
75
+ // * A RUBY_INTERNAL_EVENT_GC_ENTER & RUBY_INTERNAL_EVENT_GC_EXIT TracePoint
76
+ // * A RUBY_INTERNAL_EVENT_NEWOBJ "event hook"/internal tracepoint
77
+ // * A GVL thread event hook
78
+ //
79
+ // We refer to those collectively as "hooks".
80
+ //
81
+ // ### Hooks and Forking
72
82
  //
73
83
  // When the Ruby VM forks, the CPU/Wall-time profiling stops naturally because it's triggered by a background thread
74
84
  // that doesn't get automatically restarted by the VM on the child process. (The profiler does trigger its restart at
75
85
  // some point -- see `Profiling::Tasks::Setup` for details).
76
86
  //
77
- // But this doesn't apply to any `TracePoint`s this class may use, which will continue to be active. Thus, we need to
78
- // always remember consider this case of -- the worker thread may not be alive but the `TracePoint`s can continue to
87
+ // But this doesn't apply to any hooks this class may use, which will continue to be active. Thus, we need to
88
+ // always remember consider this case of -- the worker thread may not be alive but the hooks can continue to
79
89
  // trigger samples.
80
90
  //
81
91
  // ---
@@ -91,7 +101,7 @@ unsigned int MAX_ALLOC_WEIGHT = 10000;
91
101
  static rb_postponed_job_handle_t sample_from_postponed_job_handle;
92
102
  static rb_postponed_job_handle_t after_gc_from_postponed_job_handle;
93
103
  static rb_postponed_job_handle_t after_gvl_running_from_postponed_job_handle;
94
- static rb_postponed_job_handle_t after_allocation_from_postponed_job_handle;
104
+ static rb_postponed_job_handle_t commit_heap_recordings_from_postponed_job_may_lose_gvl_handle;
95
105
  #endif
96
106
 
97
107
  // Contains state for a single CpuAndWallTimeWorker instance
@@ -107,6 +117,8 @@ typedef struct {
107
117
  bool skip_idle_samples_for_testing;
108
118
  bool sighandler_sampling_enabled;
109
119
  uint32_t cpu_sampling_interval_ms;
120
+ // Minimum duration of a "Waiting for GVL" period to trigger a sample
121
+ uint32_t waiting_for_gvl_threshold_ns;
110
122
  VALUE self_instance;
111
123
  VALUE thread_context_collector_instance;
112
124
  VALUE idle_sampling_helper_instance;
@@ -127,9 +139,20 @@ typedef struct {
127
139
 
128
140
  // Others
129
141
 
130
- // Used to detect/avoid nested sampling, e.g. when on_newobj_event gets triggered by a memory allocation
131
- // that happens during another sample, or when the signal handler gets triggered while we're already in the middle of
132
- // sampling.
142
+ // Used to detect/avoid nested sampling, and intended to behave as a lock to ensure the profiler doesn't recurse on itself,
143
+ // e.g. when on_newobj_event gets triggered by a memory allocation that happens during another sample, or when the
144
+ // signal handler gets triggered while we're already in the middle of sampling.
145
+ //
146
+ // It's not an actual lock because we rely on the GVL for correct synchronization
147
+ // (and thus this flag is only valid when we know we have the GVL).
148
+ //
149
+ // Similar to a lock, it should not be held across long-running operations,
150
+ // **in particular it MUST NEVER be held during operations where we might lose the GVL**
151
+ // because effectively that would stop the profiler from working until control
152
+ // goes back to that special thread, which on a contended Ruby app, can take hundreds of ms (or more).
153
+ //
154
+ // Because what we want is "profiler doesn't recurse on itself" we want this to behave as a non-reentrant lock
155
+ // (FIXME: We should have checks for this)
133
156
  //
134
157
  // @ivoanjo: Right now we always sample inside `safely_call`; if that ever changes, this flag may need to become
135
158
  // volatile/atomic/have some barriers to ensure it's visible during e.g. signal handlers.
@@ -243,10 +266,10 @@ static void reset_stats_not_thread_safe(cpu_and_wall_time_worker_state *state);
243
266
  static void sleep_for(uint64_t time_ns);
244
267
  static VALUE _native_allocation_count(DDTRACE_UNUSED VALUE self);
245
268
  static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *unused2);
246
- static void disable_tracepoints(cpu_and_wall_time_worker_state *state);
269
+ static void disable_hooks(cpu_and_wall_time_worker_state *state);
247
270
  static VALUE _native_with_blocked_sigprof(DDTRACE_UNUSED VALUE self);
248
271
  static VALUE rescued_sample_allocation(VALUE tracepoint_data);
249
- static VALUE rescued_after_allocation(VALUE self_instance);
272
+ static VALUE rescued_commit_heap_recordings_may_lose_gvl(VALUE self_instance);
250
273
  static void delayed_error(cpu_and_wall_time_worker_state *state, const char *error);
251
274
  static void delayed_error_clock_failure(cpu_and_wall_time_worker_state *state);
252
275
  static VALUE _native_delayed_error(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE error_msg);
@@ -261,11 +284,12 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self);
261
284
  static VALUE _native_gvl_profiling_hook_active(DDTRACE_UNUSED VALUE self, VALUE instance);
262
285
  static VALUE handle_sampling_failure_rescued_sample_from_postponed_job(VALUE self_instance, VALUE exception);
263
286
  static VALUE handle_sampling_failure_thread_context_collector_sample_after_gc(VALUE self_instance, VALUE exception);
287
+ static VALUE handle_sampling_failure_thread_context_collector_heap_update(VALUE self_instance, VALUE exception);
264
288
  static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instance, VALUE exception);
265
- static VALUE handle_sampling_failure_rescued_after_allocation(VALUE self_instance, VALUE exception);
289
+ static VALUE handle_sampling_failure_rescued_commit_heap_recordings(VALUE self_instance, VALUE exception);
266
290
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state);
267
291
  static inline void during_sample_exit(cpu_and_wall_time_worker_state* state);
268
- static void after_allocation_from_postponed_job(DDTRACE_UNUSED void *_unused);
292
+ static void commit_heap_recordings_from_postponed_job_may_lose_gvl(DDTRACE_UNUSED void *_unused);
269
293
 
270
294
  // We're using `on_newobj_event` function with `rb_add_event_hook2`, which requires in its public signature a function
271
295
  // with signature `rb_event_hook_func_t` which doesn't match `on_newobj_event`.
@@ -322,13 +346,14 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
322
346
  sample_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, sample_from_postponed_job, NULL);
323
347
  after_gc_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_gc_from_postponed_job, NULL);
324
348
  after_gvl_running_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_gvl_running_from_postponed_job, NULL);
325
- after_allocation_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_allocation_from_postponed_job, NULL);
349
+ commit_heap_recordings_from_postponed_job_may_lose_gvl_handle =
350
+ rb_postponed_job_preregister(unused_flags, commit_heap_recordings_from_postponed_job_may_lose_gvl, NULL);
326
351
 
327
352
  if (
328
353
  sample_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
329
354
  after_gc_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
330
355
  after_gvl_running_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
331
- after_allocation_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID
356
+ commit_heap_recordings_from_postponed_job_may_lose_gvl_handle == POSTPONED_JOB_HANDLE_INVALID
332
357
  ) {
333
358
  raise_error(rb_eRuntimeError, "Failed to register profiler postponed jobs (got POSTPONED_JOB_HANDLE_INVALID)");
334
359
  }
@@ -408,6 +433,7 @@ static VALUE _native_new(VALUE klass) {
408
433
  state->skip_idle_samples_for_testing = false;
409
434
  state->sighandler_sampling_enabled = false;
410
435
  state->cpu_sampling_interval_ms = 10;
436
+ state->waiting_for_gvl_threshold_ns = 10 * 1000 * 1000;
411
437
  state->thread_context_collector_instance = Qnil;
412
438
  state->idle_sampling_helper_instance = Qnil;
413
439
  state->owner_thread = Qnil;
@@ -453,6 +479,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
453
479
  VALUE skip_idle_samples_for_testing = rb_hash_fetch(options, ID2SYM(rb_intern("skip_idle_samples_for_testing")));
454
480
  VALUE sighandler_sampling_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("sighandler_sampling_enabled")));
455
481
  VALUE cpu_sampling_interval_ms = rb_hash_fetch(options, ID2SYM(rb_intern("cpu_sampling_interval_ms")));
482
+ VALUE waiting_for_gvl_threshold_ns = rb_hash_fetch(options, ID2SYM(rb_intern("waiting_for_gvl_threshold_ns")));
456
483
 
457
484
  ENFORCE_BOOLEAN(gc_profiling_enabled);
458
485
  ENFORCE_BOOLEAN(no_signals_workaround_enabled);
@@ -464,6 +491,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
464
491
  ENFORCE_BOOLEAN(skip_idle_samples_for_testing)
465
492
  ENFORCE_BOOLEAN(sighandler_sampling_enabled)
466
493
  ENFORCE_TYPE(cpu_sampling_interval_ms, T_FIXNUM);
494
+ ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
467
495
 
468
496
  cpu_and_wall_time_worker_state *state;
469
497
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -477,6 +505,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
477
505
  state->skip_idle_samples_for_testing = (skip_idle_samples_for_testing == Qtrue);
478
506
  state->sighandler_sampling_enabled = (sighandler_sampling_enabled == Qtrue);
479
507
  state->cpu_sampling_interval_ms = NUM2INT(cpu_sampling_interval_ms);
508
+ state->waiting_for_gvl_threshold_ns = NUM2UINT(waiting_for_gvl_threshold_ns);
480
509
 
481
510
  double total_overhead_target_percentage = NUM2DBL(dynamic_sampling_rate_overhead_target_percentage);
482
511
  if (!state->allocation_profiling_enabled) {
@@ -515,7 +544,7 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
515
544
 
516
545
  // If we already got a delayed exception registered even before starting, raise before starting
517
546
  if (state->failure_exception != Qnil) {
518
- disable_tracepoints(state);
547
+ disable_hooks(state);
519
548
  rb_exc_raise(state->failure_exception);
520
549
  }
521
550
 
@@ -525,13 +554,13 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
525
554
  raise_error(rb_eRuntimeError, "Could not start CpuAndWallTimeWorker: There's already another instance of CpuAndWallTimeWorker active in a different thread");
526
555
  } else {
527
556
  // The previously active thread seems to have died without cleaning up after itself.
528
- // In this case, we can still go ahead and start the profiler BUT we make sure to disable any existing tracepoint
557
+ // In this case, we can still go ahead and start the profiler BUT we make sure to disable any existing hooks
529
558
  // first as:
530
- // a) If this is a new instance of the CpuAndWallTimeWorker, we don't want the tracepoint from the old instance
559
+ // a) If this is a new instance of the CpuAndWallTimeWorker, we don't want the hooks from the old instance
531
560
  // being kept around
532
561
  // b) If this is the same instance of the CpuAndWallTimeWorker if we call enable on a tracepoint that is already
533
562
  // enabled, it will start firing more than once, see https://bugs.ruby-lang.org/issues/19114 for details.
534
- disable_tracepoints(old_state);
563
+ disable_hooks(old_state);
535
564
  }
536
565
  }
537
566
 
@@ -549,7 +578,7 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
549
578
  // Reset per-thread state, if any. This ensures there's no leftover state from a previous profiler run that would
550
579
  // affect or be included in samples taken by this profiler about to run.
551
580
  //
552
- // NOTE: This needs to be called before we enable any tracepoints or anything that could trigger samples (e.g.
581
+ // NOTE: This needs to be called before we enable any hooks or anything that could trigger samples (e.g.
553
582
  // reset cannot be concurrent with any sampling activity)
554
583
  thread_context_collector_reset_all_per_thread_contexts(state->thread_context_collector_instance);
555
584
 
@@ -576,7 +605,7 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
576
605
 
577
606
  // The sample trigger loop finished (either cleanly or with an error); let's clean up
578
607
 
579
- disable_tracepoints(state);
608
+ disable_hooks(state);
580
609
 
581
610
  active_sampler_instance_state = NULL;
582
611
  active_sampler_instance = Qnil;
@@ -636,8 +665,8 @@ static void stop_state(cpu_and_wall_time_worker_state *state, VALUE optional_exc
636
665
  state->failure_exception = optional_exception;
637
666
  state->failure_exception_during_operation = optional_exception_during_operation;
638
667
 
639
- // Disable the tracepoints as soon as possible, so the VM doesn't keep on calling them
640
- disable_tracepoints(state);
668
+ // Disable the hooks as soon as possible, so the VM doesn't keep on calling them
669
+ disable_hooks(state);
641
670
  }
642
671
 
643
672
  static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *optional_exception_during_operation) {
@@ -711,6 +740,8 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
711
740
  // a) we get triggered using SIGPROF, and the docs state a second SIGPROF will not interrupt an existing one (see sigaction docs on sa_mask)
712
741
  // b) we validate we are in the thread that has the global VM lock; if a different thread gets a signal, it will return early
713
742
  // because it will not have the global VM lock
743
+ // c) `simulate_sampling_signal_delivery` calls us directly instead of via a real SIGPROF, and blocks SIGPROF delivery on its
744
+ // thread for the duration of that call, so it can't be nested into by a real SIGPROF either
714
745
 
715
746
  state->stats.signal_handler_enqueued_sample++;
716
747
 
@@ -848,7 +879,7 @@ static void sample_from_postponed_job(DDTRACE_UNUSED void *_unused) {
848
879
  during_sample_enter(state);
849
880
 
850
881
  // Rescue against any exceptions that happen during sampling
851
- safely_call(
882
+ VALUE needs_otel_span_key = safely_call(
852
883
  rescued_sample_from_postponed_job,
853
884
  state->self_instance,
854
885
  state->self_instance,
@@ -856,6 +887,12 @@ static void sample_from_postponed_job(DDTRACE_UNUSED void *_unused) {
856
887
  );
857
888
 
858
889
  during_sample_exit(state);
890
+
891
+ // Extracting the otel span key can lose the GVL, so we move it outside `during_sample`
892
+ // (It can't raise: it rescues its own exceptions)
893
+ if (needs_otel_span_key == Qtrue) {
894
+ thread_context_collector_resolve_otel_span_key_may_lose_gvl(state->thread_context_collector_instance);
895
+ }
859
896
  }
860
897
 
861
898
  static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
@@ -866,12 +903,13 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
866
903
 
867
904
  if (state->dynamic_sampling_rate_enabled && !dynamic_sampling_rate_should_sample(&state->cpu_dynamic_sampling_rate, wall_time_ns_before_sample)) {
868
905
  state->stats.cpu_skipped++;
869
- return Qnil;
906
+ return Qfalse;
870
907
  }
871
908
 
872
909
  state->stats.cpu_sampled++;
873
910
 
874
- thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
911
+ bool needs_otel_span_key =
912
+ thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
875
913
 
876
914
  long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
877
915
  long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
@@ -885,8 +923,7 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
885
923
 
886
924
  dynamic_sampling_rate_after_sample(&state->cpu_dynamic_sampling_rate, wall_time_ns_after_sample, sampling_time_ns);
887
925
 
888
- // Return a dummy VALUE because we're called from rb_rescue2 which requires it
889
- return Qnil;
926
+ return needs_otel_span_key ? Qtrue : Qfalse;
890
927
  }
891
928
 
892
929
  // This method exists only to enable testing Datadog::Profiling::Collectors::CpuAndWallTimeWorker behavior using RSpec.
@@ -912,7 +949,7 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
912
949
  cpu_and_wall_time_worker_state *state;
913
950
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
914
951
 
915
- // Final preparations: Setup signal handler and enable tracepoints. We run these here and not in `_native_sampling_loop`
952
+ // Final preparations: Setup signal handler and enable hooks. We run these here and not in `_native_sampling_loop`
916
953
  // because they may raise exceptions.
917
954
  install_sigprof_signal_handler(handle_sampling_signal, "handle_sampling_signal");
918
955
  if (state->gc_profiling_enabled) rb_tracepoint_enable(state->gc_tracepoint);
@@ -1095,6 +1132,15 @@ static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1095
1132
  );
1096
1133
 
1097
1134
  during_sample_exit(state);
1135
+
1136
+ // This part runs separately from above because it may lose the GVL and we don't want `during_sample` to be set in
1137
+ // such a situation
1138
+ safely_call(
1139
+ thread_context_collector_heap_update_may_lose_gvl,
1140
+ state->thread_context_collector_instance,
1141
+ state->self_instance,
1142
+ handle_sampling_failure_thread_context_collector_heap_update
1143
+ );
1098
1144
  }
1099
1145
 
1100
1146
  // Equivalent to Ruby begin/rescue call, where we call a C function and jump to the exception handler if an
@@ -1132,12 +1178,12 @@ static VALUE _native_simulate_sample_from_postponed_job(DDTRACE_UNUSED VALUE sel
1132
1178
 
1133
1179
  // After the Ruby VM forks, this method gets called in the child process to clean up any leftover state from the parent.
1134
1180
  //
1135
- // Assumption: This method gets called BEFORE restarting profiling. Note that profiling-related tracepoints may still
1181
+ // Assumption: This method gets called BEFORE restarting profiling. Note that profiling-related hooks may still
1136
1182
  // be active, so we make sure to disable them before calling into anything else, so that there are no components
1137
1183
  // attempting to trigger samples at the same time as the reset is done.
1138
1184
  //
1139
- // In the future, if we add more other components with tracepoints, we will need to coordinate stopping all such
1140
- // tracepoints before doing the other cleaning steps.
1185
+ // In the future, if we add more other components with hooks, we will need to coordinate stopping all such
1186
+ // hooks before doing the other cleaning steps.
1141
1187
  //
1142
1188
  // Note that tests call this method directly in the same process without forking,
1143
1189
  // and in such a case non-current Threads keep running.
@@ -1145,8 +1191,8 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE instance)
1145
1191
  cpu_and_wall_time_worker_state *state;
1146
1192
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1147
1193
 
1148
- // Disable all tracepoints, so that there are no more attempts to mutate the profile
1149
- disable_tracepoints(state);
1194
+ // Disable all hooks, so that there are no more attempts to mutate the profile
1195
+ disable_hooks(state);
1150
1196
 
1151
1197
  reset_stats_not_thread_safe(state);
1152
1198
 
@@ -1235,11 +1281,19 @@ void *simulate_sampling_signal_delivery(DDTRACE_UNUSED void *_unused) {
1235
1281
  // This can potentially happen if the CpuAndWallTimeWorker was stopped while the IdleSamplingHelper was trying to execute this action
1236
1282
  if (state == NULL) return NULL;
1237
1283
 
1284
+ // Since this is not a real signal firing, we need to block SIGPROF delivery on this thread to avoid an actual SIGPROF
1285
+ // signal coming in nested and interrupting us on this thread. Thus we respect the invariant of "no nesting" for `handle_sampling_signal`.
1286
+ //
1287
+ // Not needed when `no_signals_workaround_enabled` is set: no SIGPROFs are ever sent in that mode, so there's nothing to mask.
1288
+ if (!state->no_signals_workaround_enabled) block_sigprof_signal_handler_from_running_in_current_thread();
1289
+
1238
1290
  state->stats.simulated_signal_delivery++;
1239
1291
 
1240
1292
  // `handle_sampling_signal` does a few things extra on top of `sample_from_postponed_job` so that's why we don't shortcut here
1241
1293
  handle_sampling_signal(0, NULL, NULL);
1242
1294
 
1295
+ if (!state->no_signals_workaround_enabled) unblock_sigprof_signal_handler_from_running_in_current_thread();
1296
+
1243
1297
  return NULL; // Unused
1244
1298
  }
1245
1299
 
@@ -1301,6 +1355,9 @@ static VALUE _native_allocation_count(DDTRACE_UNUSED VALUE self) {
1301
1355
  // Implements memory-related profiling events. This function is called by Ruby via the `rb_add_event_hook2`
1302
1356
  // when the RUBY_INTERNAL_EVENT_NEWOBJ event is triggered.
1303
1357
  //
1358
+ // This function is called from the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1359
+ // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1360
+ //
1304
1361
  // When allocation sampling is enabled, this function gets called for almost all* objects allocated by the Ruby VM.
1305
1362
  // (*In some weird cases the VM may skip this tracepoint.)
1306
1363
  //
@@ -1411,7 +1468,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1411
1468
  during_sample_exit(state);
1412
1469
  }
1413
1470
 
1414
- static void disable_tracepoints(cpu_and_wall_time_worker_state *state) {
1471
+ static void disable_hooks(cpu_and_wall_time_worker_state *state) {
1415
1472
  if (state->gc_tracepoint != Qnil) {
1416
1473
  rb_tracepoint_disable(state->gc_tracepoint);
1417
1474
  }
@@ -1459,18 +1516,18 @@ static VALUE rescued_sample_allocation(VALUE arg) {
1459
1516
  // To control bias from sampling, we clamp the maximum weight attributed to a single allocation sample. This avoids
1460
1517
  // assigning a very large number to a sample, if for instance the dynamic sampling mechanism chose a really big interval.
1461
1518
  unsigned int weight = allocations_since_last_sample > MAX_ALLOC_WEIGHT ? MAX_ALLOC_WEIGHT : (unsigned int) allocations_since_last_sample;
1462
- bool needs_after_allocation = thread_context_collector_sample_allocation(state->thread_context_collector_instance, thread_context, weight, new_object);
1519
+ bool needs_commit = thread_context_collector_sample_allocation(state->thread_context_collector_instance, thread_context, weight, new_object);
1463
1520
  // ...but we still represent the skipped samples in the profile, thus the data will account for all allocations.
1464
1521
  if (weight < allocations_since_last_sample) {
1465
1522
  uint32_t skipped_samples = (uint32_t) uint64_min_of(allocations_since_last_sample - weight, UINT32_MAX);
1466
1523
  thread_context_collector_sample_skipped_allocation_samples(state->thread_context_collector_instance, skipped_samples);
1467
1524
  }
1468
1525
 
1469
- if (needs_after_allocation) {
1526
+ if (needs_commit) {
1470
1527
  #ifndef NO_POSTPONED_TRIGGER
1471
- rb_postponed_job_trigger(after_allocation_from_postponed_job_handle);
1528
+ rb_postponed_job_trigger(commit_heap_recordings_from_postponed_job_may_lose_gvl_handle);
1472
1529
  #else
1473
- // Not needed on legacy rubies
1530
+ rb_postponed_job_register_one(0, commit_heap_recordings_from_postponed_job_may_lose_gvl, NULL);
1474
1531
  #endif
1475
1532
  }
1476
1533
 
@@ -1545,10 +1602,11 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1545
1602
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_READY) { /* waiting for gvl */
1546
1603
  thread_context_collector_on_gvl_waiting(thread_context);
1547
1604
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) { /* running/runnable */
1548
- // Interesting note: A RUBY_INTERNAL_THREAD_EVENT_RESUMED is guaranteed to be called with the GVL being acquired
1549
- // and on the event thread.
1550
- // However, on_gvl_event() is called while holding the scheduler lock, so we do as little work as possible here,
1551
- // and perform the sample in a postponed_job.
1605
+ // We must only use async-signal-safe functions here and not call arbitrary Ruby APIs and not allocate!
1606
+ // One might assume RUBY_INTERNAL_THREAD_EVENT_RESUMED means having the GVL and running that thread.
1607
+ // However, the reality is more complicated (https://bugs.ruby-lang.org/issues/22098),
1608
+ // it only "sort of" has the GVL but not fully, and it's called while holding the scheduler lock,
1609
+ // so we do as little work as possible here, and perform the sample in a postponed_job.
1552
1610
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1553
1611
  if (state == NULL) return; // This should not happen, but just in case...
1554
1612
 
@@ -1560,7 +1618,8 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1560
1618
  // that next.
1561
1619
  during_sample_enter(state);
1562
1620
 
1563
- on_gvl_running_result result = thread_context_collector_on_gvl_running(state->thread_context_collector_instance, target_thread, thread_context);
1621
+ on_gvl_running_result result =
1622
+ thread_context_collector_on_gvl_running(target_thread, thread_context, state->waiting_for_gvl_threshold_ns);
1564
1623
 
1565
1624
  during_sample_exit(state);
1566
1625
 
@@ -1650,54 +1709,54 @@ static VALUE handle_sampling_failure_thread_context_collector_sample_after_gc(VA
1650
1709
  return Qnil;
1651
1710
  }
1652
1711
 
1712
+ static VALUE handle_sampling_failure_thread_context_collector_heap_update(VALUE self_instance, VALUE exception) {
1713
+ stop(self_instance, exception, "thread_context_collector_heap_update_may_lose_gvl");
1714
+ return Qnil;
1715
+ }
1716
+
1653
1717
  static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instance, VALUE exception) {
1654
1718
  stop(self_instance, exception, "rescued_sample_allocation");
1655
1719
  return Qnil;
1656
1720
  }
1657
1721
 
1658
- static VALUE handle_sampling_failure_rescued_after_allocation(VALUE self_instance, VALUE exception) {
1659
- stop(self_instance, exception, "rescued_after_allocation");
1722
+ static VALUE handle_sampling_failure_rescued_commit_heap_recordings(VALUE self_instance, VALUE exception) {
1723
+ stop(self_instance, exception, "rescued_commit_heap_recordings_may_lose_gvl");
1660
1724
  return Qnil;
1661
1725
  }
1662
1726
 
1663
- static VALUE rescued_after_allocation(VALUE self_instance) {
1727
+ static VALUE rescued_commit_heap_recordings_may_lose_gvl(VALUE self_instance) {
1664
1728
  cpu_and_wall_time_worker_state *state;
1665
1729
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1666
1730
 
1667
- thread_context_collector_after_allocation(state->thread_context_collector_instance);
1731
+ thread_context_collector_commit_heap_recordings_may_lose_gvl(state->thread_context_collector_instance);
1668
1732
 
1669
1733
  // Return a dummy VALUE because we're called from rb_rescue2 which requires it
1670
1734
  return Qnil;
1671
1735
  }
1672
1736
 
1673
- // This postponed job callback is used to finalize heap allocation recordings on Ruby 4+.
1674
- // During on_newobj_event, calling rb_obj_id() is unsafe because it mutates the object.
1675
- // So we defer getting the object_id until after the event completes.
1676
- #pragma GCC diagnostic push
1677
- #pragma GCC diagnostic ignored "-Wunused-function" // This is only used for some Rubies, but we want to build on all to make it easier to dev
1678
- static void after_allocation_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1737
+ // This postponed job callback is used to commit heap allocation recordings.
1738
+ // During on_newobj_event, we can't take the weak reference the heap recorder needs to track the object, so we defer
1739
+ // that until after the event completes.
1740
+ static void commit_heap_recordings_from_postponed_job_may_lose_gvl(DDTRACE_UNUSED void *_unused) {
1679
1741
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state;
1680
1742
 
1681
1743
  if (state == NULL || !ddtrace_rb_ractor_main_p()) return;
1682
1744
 
1683
- // Protect against nested operations
1684
- if (state->during_sample) return;
1685
-
1686
- during_sample_enter(state);
1745
+ if (state->during_sample) {
1746
+ delayed_error(state, "commit_heap_recordings_from_postponed_job_may_lose_gvl called during_sample");
1747
+ return;
1748
+ }
1687
1749
 
1688
1750
  // NOTE: We're not updating the allocation_sampler here.
1689
1751
  // This means work done in this function isn't accounted for as profiler overhead.
1690
1752
  // This is acceptable as the amount of work done here is expected to be small.
1691
1753
  safely_call(
1692
- rescued_after_allocation,
1754
+ rescued_commit_heap_recordings_may_lose_gvl,
1693
1755
  state->self_instance,
1694
1756
  state->self_instance,
1695
- handle_sampling_failure_rescued_after_allocation
1757
+ handle_sampling_failure_rescued_commit_heap_recordings
1696
1758
  );
1697
-
1698
- during_sample_exit(state);
1699
1759
  }
1700
- #pragma GCC diagnostic pop
1701
1760
 
1702
1761
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state) {
1703
1762
  // Tell the compiler it's not allowed to reorder the `during_sample` flag with anything that happens after.