datadog 2.41.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  9. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  10. data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
  11. data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
  12. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  20. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  21. data/ext/libdatadog_api/di.c +10 -0
  22. data/ext/libdatadog_api/extconf.rb +3 -0
  23. data/ext/libdatadog_api/init.c +2 -0
  24. data/ext/libdatadog_api/otel_thread_context.c +232 -0
  25. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  26. data/ext/libdatadog_extconf_helpers.rb +1 -1
  27. data/lib/datadog/appsec/assets/blocked.html +1 -108
  28. data/lib/datadog/core/configuration/components.rb +1 -0
  29. data/lib/datadog/core/crashtracking/component.rb +5 -1
  30. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  31. data/lib/datadog/data_streams/processor.rb +31 -0
  32. data/lib/datadog/di/instrumenter.rb +41 -1
  33. data/lib/datadog/di/logger.rb +2 -2
  34. data/lib/datadog/di/probe.rb +9 -1
  35. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  36. data/lib/datadog/di/remote.rb +3 -3
  37. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  38. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  39. data/lib/datadog/open_feature/ext.rb +19 -0
  40. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  41. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  42. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  43. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  44. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  45. data/lib/datadog/open_feature/provider.rb +11 -2
  46. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  47. data/lib/datadog/opentelemetry/trace.rb +3 -0
  48. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  49. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  50. data/lib/datadog/profiling/component.rb +8 -16
  51. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  52. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  53. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  54. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  55. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  56. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  57. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  58. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  59. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  60. data/lib/datadog/tracing/otel_thread_context.rb +30 -0
  61. data/lib/datadog/tracing/remote.rb +195 -27
  62. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  63. data/lib/datadog/tracing/trace_digest.rb +22 -4
  64. data/lib/datadog/tracing/trace_operation.rb +22 -10
  65. data/lib/datadog/tracing/tracer.rb +5 -5
  66. data/lib/datadog/version.rb +1 -1
  67. metadata +14 -8
  68. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9770ef0d02b2ecf56734c823b89ea642beb8512dae612904f0bc3d050852b44
4
- data.tar.gz: b1e359c8eb8d26528b21c963c775b7cf8e5e227ca2cf7b88b322337d32c7283c
3
+ metadata.gz: dfc982d67dbb768eb43d510dd6322ee484bc0a0fdacf20deb40196aa13821e51
4
+ data.tar.gz: be416fd9f34cbdf7ead90fa03d273f1739e90008cb82b6a0d22d5f62f17e1660
5
5
  SHA512:
6
- metadata.gz: 21335b21c2d0be965d3f3c972efab822767ddd6f28e3a1263497335300cf7a34b3fe1872d9037c1e40af909a897296e3946fac7ecd7b8e7b92c21c1ecdfc7a77
7
- data.tar.gz: a2ee7baf6f4e6e45f6116629d09806db7d0bd171bb5aaa36f4cf65e2f7c53038be18e04b84b25f1a26bd87031ea223a8e6ced8f91cceb62f8a292bfd4fd76875
6
+ metadata.gz: 80d39181feb3e02e4f022d06f0c6bab26e993f48521c2fb5e526d3b81a5b4a68e7069d92af90638700f2e0a0af24efbd0df63de311c7561cb02c1270d63cbaaa
7
+ data.tar.gz: 44932c7f87c3a52e1790b44ae2c4f13ae63006360a0846c240ec0b6ac3ffe4b1c98ff33426b19f27f51ce5105770ab263870601ebe5b33788381debd85a04b37
data/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.42.0] - 2026-08-31
6
+
7
+ ### Added
8
+
9
+ * Tracing: Add sampling probability compatibility with OTel-instrumented services ([#6111][])
10
+ * Dynamic Instrumentation: Honor organization- and environment-level remote enablement for Ruby services. ([#6234][])
11
+ * Dynamic Instrumentation: Add per-process runtime id for snapshots to distinguish process restarts within a container ([#6232][])
12
+ * Open Feature: OpenFeature provider sends the serial ID of the assigned split on exposure events ([#6220][])
13
+
14
+ ### Changed
15
+
16
+ * Profiling: Drop usage of MJIT headers for Ruby 2.6 to 3.1 ([#6135][])
17
+ * Profiling: Re-enable heap live size profiling for Ruby 4+ ([#6176][])
18
+ * Dynamic Instrumentation: Enforce process-wide rate limit across all probes ([#6043][])
19
+
20
+ ### Fixed
21
+
22
+ * Core: Ignore `SignalException` from crashtracker as unhandled exception errors ([#6219][])
23
+ * Tracing: Fix missing peer tags for database queries traced through `ActiveRecord` ([#6213][])
24
+ * Profiling: Fix bug in `sample_after_gc` leading to profiler stopping ([#6242][])
25
+ * Profiling: Improve stability of heap profiling by using `ObjectSpace::WeakMap` instead of `_id2ref` ([#6176][])
26
+ * Profiling: VM-internal objects are no longer sampled for heap profiling ([#6176][])
27
+
5
28
  ## [2.41.0] - 2026-08-13
6
29
 
7
30
  ### Added
@@ -3753,7 +3776,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3753
3776
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3754
3777
 
3755
3778
 
3756
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.41.0...master
3779
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.42.0...master
3780
+ [2.42.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.41.0...v2.42.0
3757
3781
  [2.41.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.40.0...v2.41.0
3758
3782
  [2.40.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.39.0...v2.40.0
3759
3783
  [2.39.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.38.0...v2.39.0
@@ -5560,6 +5584,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5560
5584
  [#6022]: https://github.com/DataDog/dd-trace-rb/issues/6022
5561
5585
  [#6023]: https://github.com/DataDog/dd-trace-rb/issues/6023
5562
5586
  [#6024]: https://github.com/DataDog/dd-trace-rb/issues/6024
5587
+ [#6043]: https://github.com/DataDog/dd-trace-rb/issues/6043
5563
5588
  [#6047]: https://github.com/DataDog/dd-trace-rb/issues/6047
5564
5589
  [#6050]: https://github.com/DataDog/dd-trace-rb/issues/6050
5565
5590
  [#6056]: https://github.com/DataDog/dd-trace-rb/issues/6056
@@ -5568,14 +5593,23 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5568
5593
  [#6072]: https://github.com/DataDog/dd-trace-rb/issues/6072
5569
5594
  [#6097]: https://github.com/DataDog/dd-trace-rb/issues/6097
5570
5595
  [#6104]: https://github.com/DataDog/dd-trace-rb/issues/6104
5596
+ [#6111]: https://github.com/DataDog/dd-trace-rb/issues/6111
5571
5597
  [#6116]: https://github.com/DataDog/dd-trace-rb/issues/6116
5572
5598
  [#6126]: https://github.com/DataDog/dd-trace-rb/issues/6126
5573
5599
  [#6130]: https://github.com/DataDog/dd-trace-rb/issues/6130
5574
5600
  [#6132]: https://github.com/DataDog/dd-trace-rb/issues/6132
5575
5601
  [#6133]: https://github.com/DataDog/dd-trace-rb/issues/6133
5602
+ [#6135]: https://github.com/DataDog/dd-trace-rb/issues/6135
5576
5603
  [#6138]: https://github.com/DataDog/dd-trace-rb/issues/6138
5577
5604
  [#6142]: https://github.com/DataDog/dd-trace-rb/issues/6142
5578
5605
  [#6173]: https://github.com/DataDog/dd-trace-rb/issues/6173
5606
+ [#6176]: https://github.com/DataDog/dd-trace-rb/issues/6176
5607
+ [#6213]: https://github.com/DataDog/dd-trace-rb/issues/6213
5608
+ [#6219]: https://github.com/DataDog/dd-trace-rb/issues/6219
5609
+ [#6220]: https://github.com/DataDog/dd-trace-rb/issues/6220
5610
+ [#6232]: https://github.com/DataDog/dd-trace-rb/issues/6232
5611
+ [#6234]: https://github.com/DataDog/dd-trace-rb/issues/6234
5612
+ [#6242]: https://github.com/DataDog/dd-trace-rb/issues/6242
5579
5613
  [@AdrianLC]: https://github.com/AdrianLC
5580
5614
  [@Azure7111]: https://github.com/Azure7111
5581
5615
  [@BabyGroot]: https://github.com/BabyGroot
@@ -5731,4 +5765,4 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5731
5765
  [@y-yagi]: https://github.com/y-yagi
5732
5766
  [@yujideveloper]: https://github.com/yujideveloper
5733
5767
  [@yukimurasawa]: https://github.com/yukimurasawa
5734
- [@zachmccormick]: https://github.com/zachmccormick
5768
+ [@zachmccormick]: https://github.com/zachmccormick
@@ -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) {
@@ -848,7 +877,7 @@ static void sample_from_postponed_job(DDTRACE_UNUSED void *_unused) {
848
877
  during_sample_enter(state);
849
878
 
850
879
  // Rescue against any exceptions that happen during sampling
851
- safely_call(
880
+ VALUE needs_otel_span_key = safely_call(
852
881
  rescued_sample_from_postponed_job,
853
882
  state->self_instance,
854
883
  state->self_instance,
@@ -856,6 +885,12 @@ static void sample_from_postponed_job(DDTRACE_UNUSED void *_unused) {
856
885
  );
857
886
 
858
887
  during_sample_exit(state);
888
+
889
+ // Extracting the otel span key can lose the GVL, so we move it outside `during_sample`
890
+ // (It can't raise: it rescues its own exceptions)
891
+ if (needs_otel_span_key == Qtrue) {
892
+ thread_context_collector_resolve_otel_span_key_may_lose_gvl(state->thread_context_collector_instance);
893
+ }
859
894
  }
860
895
 
861
896
  static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
@@ -866,12 +901,13 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
866
901
 
867
902
  if (state->dynamic_sampling_rate_enabled && !dynamic_sampling_rate_should_sample(&state->cpu_dynamic_sampling_rate, wall_time_ns_before_sample)) {
868
903
  state->stats.cpu_skipped++;
869
- return Qnil;
904
+ return Qfalse;
870
905
  }
871
906
 
872
907
  state->stats.cpu_sampled++;
873
908
 
874
- thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
909
+ bool needs_otel_span_key =
910
+ thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
875
911
 
876
912
  long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
877
913
  long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
@@ -885,8 +921,7 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
885
921
 
886
922
  dynamic_sampling_rate_after_sample(&state->cpu_dynamic_sampling_rate, wall_time_ns_after_sample, sampling_time_ns);
887
923
 
888
- // Return a dummy VALUE because we're called from rb_rescue2 which requires it
889
- return Qnil;
924
+ return needs_otel_span_key ? Qtrue : Qfalse;
890
925
  }
891
926
 
892
927
  // This method exists only to enable testing Datadog::Profiling::Collectors::CpuAndWallTimeWorker behavior using RSpec.
@@ -912,7 +947,7 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
912
947
  cpu_and_wall_time_worker_state *state;
913
948
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
914
949
 
915
- // Final preparations: Setup signal handler and enable tracepoints. We run these here and not in `_native_sampling_loop`
950
+ // Final preparations: Setup signal handler and enable hooks. We run these here and not in `_native_sampling_loop`
916
951
  // because they may raise exceptions.
917
952
  install_sigprof_signal_handler(handle_sampling_signal, "handle_sampling_signal");
918
953
  if (state->gc_profiling_enabled) rb_tracepoint_enable(state->gc_tracepoint);
@@ -1095,6 +1130,15 @@ static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1095
1130
  );
1096
1131
 
1097
1132
  during_sample_exit(state);
1133
+
1134
+ // This part runs separately from above because it may lose the GVL and we don't want `during_sample` to be set in
1135
+ // such a situation
1136
+ safely_call(
1137
+ thread_context_collector_heap_update_may_lose_gvl,
1138
+ state->thread_context_collector_instance,
1139
+ state->self_instance,
1140
+ handle_sampling_failure_thread_context_collector_heap_update
1141
+ );
1098
1142
  }
1099
1143
 
1100
1144
  // Equivalent to Ruby begin/rescue call, where we call a C function and jump to the exception handler if an
@@ -1132,12 +1176,12 @@ static VALUE _native_simulate_sample_from_postponed_job(DDTRACE_UNUSED VALUE sel
1132
1176
 
1133
1177
  // After the Ruby VM forks, this method gets called in the child process to clean up any leftover state from the parent.
1134
1178
  //
1135
- // Assumption: This method gets called BEFORE restarting profiling. Note that profiling-related tracepoints may still
1179
+ // Assumption: This method gets called BEFORE restarting profiling. Note that profiling-related hooks may still
1136
1180
  // be active, so we make sure to disable them before calling into anything else, so that there are no components
1137
1181
  // attempting to trigger samples at the same time as the reset is done.
1138
1182
  //
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.
1183
+ // In the future, if we add more other components with hooks, we will need to coordinate stopping all such
1184
+ // hooks before doing the other cleaning steps.
1141
1185
  //
1142
1186
  // Note that tests call this method directly in the same process without forking,
1143
1187
  // and in such a case non-current Threads keep running.
@@ -1145,8 +1189,8 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE instance)
1145
1189
  cpu_and_wall_time_worker_state *state;
1146
1190
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1147
1191
 
1148
- // Disable all tracepoints, so that there are no more attempts to mutate the profile
1149
- disable_tracepoints(state);
1192
+ // Disable all hooks, so that there are no more attempts to mutate the profile
1193
+ disable_hooks(state);
1150
1194
 
1151
1195
  reset_stats_not_thread_safe(state);
1152
1196
 
@@ -1301,6 +1345,9 @@ static VALUE _native_allocation_count(DDTRACE_UNUSED VALUE self) {
1301
1345
  // Implements memory-related profiling events. This function is called by Ruby via the `rb_add_event_hook2`
1302
1346
  // when the RUBY_INTERNAL_EVENT_NEWOBJ event is triggered.
1303
1347
  //
1348
+ // This function is called from the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1349
+ // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1350
+ //
1304
1351
  // When allocation sampling is enabled, this function gets called for almost all* objects allocated by the Ruby VM.
1305
1352
  // (*In some weird cases the VM may skip this tracepoint.)
1306
1353
  //
@@ -1411,7 +1458,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1411
1458
  during_sample_exit(state);
1412
1459
  }
1413
1460
 
1414
- static void disable_tracepoints(cpu_and_wall_time_worker_state *state) {
1461
+ static void disable_hooks(cpu_and_wall_time_worker_state *state) {
1415
1462
  if (state->gc_tracepoint != Qnil) {
1416
1463
  rb_tracepoint_disable(state->gc_tracepoint);
1417
1464
  }
@@ -1459,18 +1506,18 @@ static VALUE rescued_sample_allocation(VALUE arg) {
1459
1506
  // To control bias from sampling, we clamp the maximum weight attributed to a single allocation sample. This avoids
1460
1507
  // assigning a very large number to a sample, if for instance the dynamic sampling mechanism chose a really big interval.
1461
1508
  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);
1509
+ bool needs_commit = thread_context_collector_sample_allocation(state->thread_context_collector_instance, thread_context, weight, new_object);
1463
1510
  // ...but we still represent the skipped samples in the profile, thus the data will account for all allocations.
1464
1511
  if (weight < allocations_since_last_sample) {
1465
1512
  uint32_t skipped_samples = (uint32_t) uint64_min_of(allocations_since_last_sample - weight, UINT32_MAX);
1466
1513
  thread_context_collector_sample_skipped_allocation_samples(state->thread_context_collector_instance, skipped_samples);
1467
1514
  }
1468
1515
 
1469
- if (needs_after_allocation) {
1516
+ if (needs_commit) {
1470
1517
  #ifndef NO_POSTPONED_TRIGGER
1471
- rb_postponed_job_trigger(after_allocation_from_postponed_job_handle);
1518
+ rb_postponed_job_trigger(commit_heap_recordings_from_postponed_job_may_lose_gvl_handle);
1472
1519
  #else
1473
- // Not needed on legacy rubies
1520
+ rb_postponed_job_register_one(0, commit_heap_recordings_from_postponed_job_may_lose_gvl, NULL);
1474
1521
  #endif
1475
1522
  }
1476
1523
 
@@ -1545,10 +1592,11 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1545
1592
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_READY) { /* waiting for gvl */
1546
1593
  thread_context_collector_on_gvl_waiting(thread_context);
1547
1594
  } 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.
1595
+ // We must only use async-signal-safe functions here and not call arbitrary Ruby APIs and not allocate!
1596
+ // One might assume RUBY_INTERNAL_THREAD_EVENT_RESUMED means having the GVL and running that thread.
1597
+ // However, the reality is more complicated (https://bugs.ruby-lang.org/issues/22098),
1598
+ // it only "sort of" has the GVL but not fully, and it's called while holding the scheduler lock,
1599
+ // so we do as little work as possible here, and perform the sample in a postponed_job.
1552
1600
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1553
1601
  if (state == NULL) return; // This should not happen, but just in case...
1554
1602
 
@@ -1560,7 +1608,8 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1560
1608
  // that next.
1561
1609
  during_sample_enter(state);
1562
1610
 
1563
- on_gvl_running_result result = thread_context_collector_on_gvl_running(state->thread_context_collector_instance, target_thread, thread_context);
1611
+ on_gvl_running_result result =
1612
+ thread_context_collector_on_gvl_running(target_thread, thread_context, state->waiting_for_gvl_threshold_ns);
1564
1613
 
1565
1614
  during_sample_exit(state);
1566
1615
 
@@ -1650,54 +1699,54 @@ static VALUE handle_sampling_failure_thread_context_collector_sample_after_gc(VA
1650
1699
  return Qnil;
1651
1700
  }
1652
1701
 
1702
+ static VALUE handle_sampling_failure_thread_context_collector_heap_update(VALUE self_instance, VALUE exception) {
1703
+ stop(self_instance, exception, "thread_context_collector_heap_update_may_lose_gvl");
1704
+ return Qnil;
1705
+ }
1706
+
1653
1707
  static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instance, VALUE exception) {
1654
1708
  stop(self_instance, exception, "rescued_sample_allocation");
1655
1709
  return Qnil;
1656
1710
  }
1657
1711
 
1658
- static VALUE handle_sampling_failure_rescued_after_allocation(VALUE self_instance, VALUE exception) {
1659
- stop(self_instance, exception, "rescued_after_allocation");
1712
+ static VALUE handle_sampling_failure_rescued_commit_heap_recordings(VALUE self_instance, VALUE exception) {
1713
+ stop(self_instance, exception, "rescued_commit_heap_recordings_may_lose_gvl");
1660
1714
  return Qnil;
1661
1715
  }
1662
1716
 
1663
- static VALUE rescued_after_allocation(VALUE self_instance) {
1717
+ static VALUE rescued_commit_heap_recordings_may_lose_gvl(VALUE self_instance) {
1664
1718
  cpu_and_wall_time_worker_state *state;
1665
1719
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1666
1720
 
1667
- thread_context_collector_after_allocation(state->thread_context_collector_instance);
1721
+ thread_context_collector_commit_heap_recordings_may_lose_gvl(state->thread_context_collector_instance);
1668
1722
 
1669
1723
  // Return a dummy VALUE because we're called from rb_rescue2 which requires it
1670
1724
  return Qnil;
1671
1725
  }
1672
1726
 
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) {
1727
+ // This postponed job callback is used to commit heap allocation recordings.
1728
+ // During on_newobj_event, we can't take the weak reference the heap recorder needs to track the object, so we defer
1729
+ // that until after the event completes.
1730
+ static void commit_heap_recordings_from_postponed_job_may_lose_gvl(DDTRACE_UNUSED void *_unused) {
1679
1731
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state;
1680
1732
 
1681
1733
  if (state == NULL || !ddtrace_rb_ractor_main_p()) return;
1682
1734
 
1683
- // Protect against nested operations
1684
- if (state->during_sample) return;
1685
-
1686
- during_sample_enter(state);
1735
+ if (state->during_sample) {
1736
+ delayed_error(state, "commit_heap_recordings_from_postponed_job_may_lose_gvl called during_sample");
1737
+ return;
1738
+ }
1687
1739
 
1688
1740
  // NOTE: We're not updating the allocation_sampler here.
1689
1741
  // This means work done in this function isn't accounted for as profiler overhead.
1690
1742
  // This is acceptable as the amount of work done here is expected to be small.
1691
1743
  safely_call(
1692
- rescued_after_allocation,
1744
+ rescued_commit_heap_recordings_may_lose_gvl,
1693
1745
  state->self_instance,
1694
1746
  state->self_instance,
1695
- handle_sampling_failure_rescued_after_allocation
1747
+ handle_sampling_failure_rescued_commit_heap_recordings
1696
1748
  );
1697
-
1698
- during_sample_exit(state);
1699
1749
  }
1700
- #pragma GCC diagnostic pop
1701
1750
 
1702
1751
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state) {
1703
1752
  // Tell the compiler it's not allowed to reorder the `during_sample` flag with anything that happens after.
@@ -130,8 +130,16 @@ static VALUE _native_sample(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
130
130
  ENFORCE_BOOLEAN(show_classes);
131
131
 
132
132
  VALUE zero = INT2NUM(0);
133
- VALUE heap_sample = rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("heap_sample"), Qfalse);
134
- ENFORCE_BOOLEAN(heap_sample);
133
+ // Pass `heap_sample: {new_object: ..., alloc_class: ...}` to also record this sample for heap profiling
134
+ VALUE heap_sample_options = rb_hash_lookup2(options, ID2SYM(rb_intern("heap_sample")), Qnil);
135
+ heap_sample_values heap_sample = {};
136
+ if (heap_sample_options != Qnil) {
137
+ ENFORCE_TYPE(heap_sample_options, T_HASH);
138
+ heap_sample.new_object = rb_hash_fetch(heap_sample_options, ID2SYM(rb_intern("new_object")));
139
+ heap_sample.alloc_class =
140
+ char_slice_from_ruby_string(rb_hash_fetch(heap_sample_options, ID2SYM(rb_intern("alloc_class"))));
141
+ }
142
+
135
143
  sample_values values = {
136
144
  .cpu_time_ns = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("cpu-time"), zero)),
137
145
  .cpu_or_wall_samples = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("cpu-samples"), zero)),
@@ -139,7 +147,7 @@ static VALUE _native_sample(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
139
147
  .alloc_samples = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("alloc-samples"), zero)),
140
148
  .alloc_samples_unscaled = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("alloc-samples-unscaled"), zero)),
141
149
  .timeline_wall_time_ns = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("timeline"), zero)),
142
- .heap_sample = heap_sample == Qtrue,
150
+ .heap_sample = heap_sample_options != Qnil ? &heap_sample : NULL,
143
151
  };
144
152
 
145
153
  long labels_count = RARRAY_LEN(labels_array) + RARRAY_LEN(numeric_labels_array);