datadog 2.36.0 → 2.37.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +6 -3
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +219 -98
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +2 -0
  8. data/ext/datadog_profiling_native_extension/extconf.rb +11 -0
  9. data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
  10. data/ext/libdatadog_api/di.c +76 -0
  11. data/ext/libdatadog_api/extconf.rb +6 -0
  12. data/ext/libdatadog_extconf_helpers.rb +1 -1
  13. data/lib/datadog/ai_guard/api_client.rb +6 -4
  14. data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
  15. data/lib/datadog/appsec/configuration.rb +6 -0
  16. data/lib/datadog/appsec/context.rb +4 -2
  17. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  18. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  19. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
  20. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
  21. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
  22. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  23. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +2 -1
  24. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  25. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
  26. data/lib/datadog/appsec/ext.rb +1 -0
  27. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  28. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  29. data/lib/datadog/appsec/remote.rb +9 -3
  30. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  31. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  32. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  33. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  34. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  35. data/lib/datadog/core/configuration/components.rb +82 -2
  36. data/lib/datadog/core/configuration/components_state.rb +6 -1
  37. data/lib/datadog/core/configuration/settings.rb +0 -3
  38. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  39. data/lib/datadog/core/evp.rb +11 -0
  40. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  41. data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
  42. data/lib/datadog/di/base.rb +3 -1
  43. data/lib/datadog/di/code_tracker.rb +5 -2
  44. data/lib/datadog/di/component.rb +110 -36
  45. data/lib/datadog/di/error.rb +10 -0
  46. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  47. data/lib/datadog/di/instrumenter.rb +381 -165
  48. data/lib/datadog/di/probe.rb +14 -0
  49. data/lib/datadog/di/probe_file_loader.rb +8 -3
  50. data/lib/datadog/di/probe_manager.rb +67 -9
  51. data/lib/datadog/di/probe_notification_builder.rb +4 -1
  52. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  53. data/lib/datadog/di/redactor.rb +16 -1
  54. data/lib/datadog/di/remote.rb +175 -14
  55. data/lib/datadog/di/serializer.rb +12 -5
  56. data/lib/datadog/di/transport/input.rb +8 -4
  57. data/lib/datadog/di.rb +63 -0
  58. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  59. data/lib/datadog/open_feature/component.rb +29 -6
  60. data/lib/datadog/open_feature/configuration.rb +8 -0
  61. data/lib/datadog/open_feature/ext.rb +2 -0
  62. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  63. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  64. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  65. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  66. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  67. data/lib/datadog/open_feature/provider.rb +59 -27
  68. data/lib/datadog/open_feature/transport.rb +55 -1
  69. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  70. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  71. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  72. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  73. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  74. data/lib/datadog/symbol_database/component.rb +164 -47
  75. data/lib/datadog/symbol_database/configuration.rb +12 -4
  76. data/lib/datadog/symbol_database/extensions.rb +19 -0
  77. data/lib/datadog/symbol_database/extractor.rb +124 -87
  78. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  79. data/lib/datadog/symbol_database/remote.rb +8 -3
  80. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  81. data/lib/datadog/symbol_database/uploader.rb +9 -2
  82. data/lib/datadog/symbol_database.rb +22 -0
  83. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  84. data/lib/datadog/tracing/ext.rb +9 -0
  85. data/lib/datadog/tracing/remote.rb +34 -3
  86. data/lib/datadog/version.rb +1 -1
  87. data/lib/datadog.rb +1 -0
  88. metadata +19 -6
@@ -1,4 +1,5 @@
1
1
  #include <ruby.h>
2
+ #include <ruby/debug.h>
2
3
 
3
4
  #include "datadog_ruby_common.h"
4
5
  #include "collectors_thread_context.h"
@@ -108,6 +109,15 @@ static ID server_id; // id of :server in Ruby
108
109
  static ID otel_context_storage_id; // id of :__opentelemetry_context_storage__ in Ruby
109
110
  static ID otel_fiber_context_storage_id; // id of :@opentelemetry_context in Ruby
110
111
 
112
+ // This is mutable and gets set last-writer-wins style whenever a new `ThreadContext` is created.
113
+ //
114
+ // The initial value should be kept in sync with the default for DD_PROFILING_MAX_FRAMES
115
+ // in settings.rb. See `initialize_context` for details on why this is needed/used.
116
+ static uint16_t latest_max_frames = 400;
117
+
118
+ // Global tracepoint for RUBY_EVENT_THREAD_BEGIN. Created and enabled once when the first ThreadContext collector is initialized.
119
+ static VALUE thread_begin_tracepoint = Qnil;
120
+
111
121
 
112
122
  typedef enum { OTEL_CONTEXT_ENABLED_FALSE, OTEL_CONTEXT_ENABLED_ONLY, OTEL_CONTEXT_ENABLED_BOTH } otel_context_enabled;
113
123
  typedef enum { OTEL_CONTEXT_SOURCE_UNKNOWN, OTEL_CONTEXT_SOURCE_FIBER_IVAR, OTEL_CONTEXT_SOURCE_FIBER_LOCAL } otel_context_source;
@@ -160,6 +170,7 @@ typedef struct {
160
170
  // How many per-thread samples were skipped because the thread has been continuously suspended
161
171
  // (no GVL) since its previous sample, so its Ruby stack cannot have changed.
162
172
  unsigned int inactive_thread_samples_skipped;
173
+ unsigned int profiler_thread_samples_skipped;
163
174
  } stats;
164
175
 
165
176
  struct {
@@ -171,7 +182,20 @@ typedef struct {
171
182
  } gc_tracking;
172
183
  } thread_context_collector_state;
173
184
 
174
- // Tracks per-thread state
185
+ // Tracks per-thread state.
186
+ // This state is global and lives forever on the Ruby Thread (until the Thread is GC'd).
187
+ // The state is created early on for all threads on the main Ractor
188
+ // (enabling a TracePoint only enables it for the current Ractor).
189
+ // The state is either created when the Thread starts running (via the RUBY_EVENT_THREAD_BEGIN TracePoint),
190
+ // or the first time we create a ThreadContext by iterating Thread.list.
191
+ // Unfortunately that RUBY_EVENT_THREAD_BEGIN TracePoint still fires after some other events:
192
+ // * RUBY_INTERNAL_THREAD_EVENT_RESUMED for the Thread acquiring the GVL for the first time
193
+ // * an early SIGPROF calling handle_sampling_signal()
194
+ // * Ruby might check for interrupts and run postponed jobs (e.g. thread_context_collector_sample)
195
+ // * another RUBY_EVENT_THREAD_BEGIN TracePoint which might run before ours
196
+ // * etc
197
+ // For those cases we have to ignore those events and we cannot assume the state is always set,
198
+ // however this only matters for a very short period when a thread starts.
175
199
  struct per_thread_context {
176
200
  sampling_buffer sampling_buffer;
177
201
  char thread_id[THREAD_ID_LIMIT_CHARS];
@@ -179,8 +203,8 @@ struct per_thread_context {
179
203
  char thread_invoke_location[THREAD_INVOKE_LOCATION_LIMIT_CHARS];
180
204
  ddog_CharSlice thread_invoke_location_char_slice;
181
205
  thread_cpu_time_id thread_cpu_time_id;
182
- long cpu_time_at_previous_sample_ns; // Can be INVALID_TIME until initialized or if getting it fails for another reason
183
- long wall_time_at_previous_sample_ns; // Can be INVALID_TIME until initialized
206
+ long cpu_time_at_previous_sample_ns;
207
+ long wall_time_at_previous_sample_ns;
184
208
 
185
209
  // There are 3 possible states for the GVL (per thread), and 3 transitions for which we receive GVL events:
186
210
  // Thread holds the GVL
@@ -233,6 +257,14 @@ struct per_thread_context {
233
257
  // but this is deemed worth it for this optimization. In any case we don't know exactly
234
258
  // at what time a thread was doing CPU work (unless it's on CPU 100% of the time).
235
259
  bool was_skipped_at_last_sample;
260
+ // Set as true for CpuAndWallTimeWorker and IdleSamplingHelper threads.
261
+ // When true, per-tick samples are skipped entirely; the thread is sampled only once per
262
+ // reporting period during the on_serialize flush.
263
+ //
264
+ // These threads are always in native code so their stacks aren't interesting;
265
+ // the Profiling::Scheduler thread on the other hand does a lot of different
266
+ // things using a mix of Ruby and native code, so that one isn't considered internal.
267
+ bool is_profiler_internal_thread;
236
268
 
237
269
  struct {
238
270
  // Both of these fields are set by on_gc_start and kept until on_gc_finish is called.
@@ -270,7 +302,6 @@ static void update_metrics_and_sample(
270
302
  thread_context_collector_state *state,
271
303
  VALUE thread_being_sampled,
272
304
  per_thread_context *thread_context,
273
- sampling_buffer* sampling_buffer,
274
305
  long current_cpu_time_ns,
275
306
  long current_monotonic_wall_time_ns,
276
307
  bool force_sample
@@ -279,7 +310,6 @@ static void trigger_sample_for_thread(
279
310
  thread_context_collector_state *state,
280
311
  VALUE thread_being_sampled,
281
312
  per_thread_context *thread_context,
282
- sampling_buffer* sampling_buffer,
283
313
  sample_values values,
284
314
  long current_monotonic_wall_time_ns,
285
315
  ddog_CharSlice *ruby_vm_type,
@@ -288,8 +318,9 @@ static void trigger_sample_for_thread(
288
318
  bool is_safe_to_allocate_objects
289
319
  );
290
320
  static VALUE _native_thread_list(VALUE self);
291
- static per_thread_context *get_or_create_context_for(VALUE thread, thread_context_collector_state *state);
292
- static void initialize_context(VALUE thread, per_thread_context *thread_context, thread_context_collector_state *state);
321
+ static void check_frozen_thread(VALUE thread);
322
+ static per_thread_context *get_or_create_context_for(VALUE thread);
323
+ static void initialize_context(VALUE thread, per_thread_context *thread_context);
293
324
  static VALUE _native_inspect(VALUE self, VALUE collector_instance);
294
325
  static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context);
295
326
  static VALUE stats_to_ruby_hash(thread_context_collector_state *state, VALUE hash);
@@ -326,7 +357,6 @@ static bool handle_gvl_waiting(
326
357
  thread_context_collector_state *state,
327
358
  VALUE thread_being_sampled,
328
359
  per_thread_context *thread_context,
329
- sampling_buffer* sampling_buffer,
330
360
  long current_cpu_time_ns
331
361
  );
332
362
  #ifndef NO_GVL_INSTRUMENTATION
@@ -335,8 +365,8 @@ static bool handle_gvl_waiting(
335
365
  static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
336
366
  static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread);
337
367
  static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception);
338
- static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns);
339
368
  #endif
369
+ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns);
340
370
  static void otel_without_ddtrace_trace_identifiers_for(
341
371
  thread_context_collector_state *state,
342
372
  VALUE thread,
@@ -349,11 +379,15 @@ static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE k
349
379
  static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
350
380
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self);
351
381
  static VALUE _native_clear_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
352
- static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample_suspended);
382
+ static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread);
383
+ static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
384
+ static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample);
385
+ static void on_thread_begin_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused);
353
386
 
354
387
  void collectors_thread_context_init(VALUE profiling_module) {
355
388
  VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
356
389
  VALUE collectors_thread_context_class = rb_define_class_under(collectors_module, "ThreadContext", rb_cObject);
390
+
357
391
  // Hosts methods used for testing the native code using RSpec
358
392
  VALUE testing_module = rb_define_module_under(collectors_thread_context_class, "Testing");
359
393
 
@@ -384,14 +418,16 @@ void collectors_thread_context_init(VALUE profiling_module) {
384
418
  rb_define_singleton_method(testing_module, "_native_system_epoch_time_now_ns", _native_system_epoch_time_now_ns, 1);
385
419
  rb_define_singleton_method(testing_module, "_native_prepare_sample_inside_signal_handler", _native_prepare_sample_inside_signal_handler, 0);
386
420
  rb_define_singleton_method(testing_module, "_native_clear_per_thread_context_for", _native_clear_per_thread_context_for, 1);
421
+ rb_define_singleton_method(testing_module, "_native_remove_per_thread_context_for", _native_remove_per_thread_context_for, 1);
422
+ rb_define_singleton_method(testing_module, "_native_mark_thread_as_profiler_internal", _native_mark_thread_as_profiler_internal, 1);
387
423
  #ifndef NO_GVL_INSTRUMENTATION
388
424
  rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
389
425
  rb_define_singleton_method(testing_module, "_native_gvl_waiting_at_for", _native_gvl_waiting_at_for, 1);
390
426
  rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 2);
391
427
  rb_define_singleton_method(testing_module, "_native_on_gvl_released", _native_on_gvl_released, 1);
392
428
  rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 3);
393
- rb_define_singleton_method(testing_module, "_native_apply_delta_to_cpu_time_at_previous_sample_ns", _native_apply_delta_to_cpu_time_at_previous_sample_ns, 2);
394
429
  #endif
430
+ rb_define_singleton_method(testing_module, "_native_apply_delta_to_cpu_time_at_previous_sample_ns", _native_apply_delta_to_cpu_time_at_previous_sample_ns, 2);
395
431
 
396
432
  at_active_span_id = rb_intern_const("@active_span");
397
433
  at_active_trace_id = rb_intern_const("@active_trace");
@@ -417,6 +453,8 @@ void collectors_thread_context_init(VALUE profiling_module) {
417
453
  // This will raise if Ruby already ran out of thread-local keys
418
454
  per_thread_context_tls_init();
419
455
 
456
+ rb_global_variable(&thread_begin_tracepoint);
457
+
420
458
  gc_profiling_init();
421
459
  }
422
460
 
@@ -485,13 +523,20 @@ static void per_thread_context_typed_data_free(void *ctx_ptr) {
485
523
  free(ctx);
486
524
  }
487
525
 
488
- static VALUE _native_clear_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
526
+ static VALUE _native_clear_per_thread_context_for(VALUE self, VALUE thread) {
527
+ _native_remove_per_thread_context_for(self, thread);
528
+ get_or_create_context_for(thread);
529
+ return Qnil;
530
+ }
531
+
532
+ // Only for testing: removes the per-thread context without recreating it, so the thread has no context.
533
+ // This simulates a thread that starts running before the RUBY_EVENT_THREAD_BEGIN tracepoint fires.
534
+ static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
535
+ check_frozen_thread(thread);
489
536
  per_thread_context *ctx = get_per_thread_context(thread);
490
537
  if (ctx != NULL) {
491
538
  set_per_thread_context(thread, NULL);
492
- if (!RB_OBJ_FROZEN(thread)) {
493
- rb_ivar_set(thread, dd_per_thread_context_id, Qnil);
494
- }
539
+ rb_ivar_set(thread, dd_per_thread_context_id, Qnil);
495
540
  }
496
541
  return Qnil;
497
542
  }
@@ -554,12 +599,15 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
554
599
  ENFORCE_BOOLEAN(native_filenames_enabled);
555
600
  ENFORCE_TYPE(overhead_filename, T_STRING);
556
601
 
602
+ uint16_t max_frame_int = sampling_buffer_check_max_frames(NUM2INT(max_frames));
603
+ latest_max_frames = max_frame_int;
604
+
557
605
  thread_context_collector_state *state;
558
606
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
559
607
 
560
608
  // Update this when modifying state struct
561
- state->locations.len = sampling_buffer_check_max_frames(NUM2INT(max_frames));
562
- state->locations.ptr = ruby_xcalloc(state->locations.len, sizeof(ddog_prof_Location));
609
+ state->locations.len = max_frame_int;
610
+ state->locations.ptr = ruby_xcalloc(max_frame_int, sizeof(ddog_prof_Location));
563
611
  state->recorder_instance = enforce_recorder_instance(recorder_instance);
564
612
  recorder_install_on_serialize(recorder_instance, self_instance);
565
613
  state->endpoint_collection_enabled = (endpoint_collection_enabled == Qtrue);
@@ -585,6 +633,19 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
585
633
  state->tracer_context_key = rb_to_id(tracer_context_key);
586
634
  }
587
635
 
636
+ if (thread_begin_tracepoint == Qnil) {
637
+ thread_begin_tracepoint = rb_tracepoint_new(Qnil, RUBY_EVENT_THREAD_BEGIN, on_thread_begin_event, NULL);
638
+ rb_tracepoint_enable(thread_begin_tracepoint);
639
+
640
+ VALUE thread_list = rb_ary_new();
641
+ ddtrace_thread_list(thread_list);
642
+ long thread_count = RARRAY_LEN(thread_list);
643
+ for (long i = 0; i < thread_count; i++) {
644
+ get_or_create_context_for(RARRAY_AREF(thread_list, i));
645
+ }
646
+ RB_GC_GUARD(thread_list);
647
+ }
648
+
588
649
  return Qtrue;
589
650
  }
590
651
 
@@ -698,7 +759,7 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
698
759
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
699
760
 
700
761
  VALUE current_thread = rb_thread_current();
701
- per_thread_context *current_thread_context = get_or_create_context_for(current_thread, state);
762
+ per_thread_context *current_thread_context = get_or_create_context_for(current_thread);
702
763
  long cpu_time_at_sample_start_for_current_thread = cpu_time_now_ns(current_thread_context);
703
764
 
704
765
  VALUE threads = thread_list(state);
@@ -706,7 +767,7 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
706
767
  const long thread_count = RARRAY_LEN(threads);
707
768
  for (long i = 0; i < thread_count; i++) {
708
769
  VALUE thread = RARRAY_AREF(threads, i);
709
- per_thread_context *thread_context = get_or_create_context_for(thread, state);
770
+ per_thread_context *thread_context = get_or_create_context_for(thread);
710
771
 
711
772
  // We account for cpu-time for the current thread in a different way: we use the cpu-time at sampling start,
712
773
  // to avoid blaming the time the profiler took on whatever is currently running on the thread,
@@ -717,29 +778,36 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
717
778
  state,
718
779
  thread,
719
780
  thread_context,
720
- &thread_context->sampling_buffer,
721
781
  current_cpu_time_ns,
722
782
  current_monotonic_wall_time_ns,
723
783
  false);
724
784
  }
725
785
 
726
786
  state->stats.sample_count++;
727
- record_sampling_overhead(state, current_thread_context);
787
+
788
+ // If the current thread is a profiler-internal thread, we don't use record_sampling_overhead()
789
+ // and accept the sampling overhead is attributed to the profiler-internal thread
790
+ // (both are under the same datadog group in the flamegraph).
791
+ // The reason we need to do this is profiler-internal threads are skipped during per-tick sampling
792
+ // so their timestamps are not updated. record_sampling_overhead() would see the stale previous-sample timestamps and
793
+ // attribute the entire sleep interval incorrectly as overhead instead of just the sampling work.
794
+ if (!current_thread_context->is_profiler_internal_thread) {
795
+ record_sampling_overhead(state, current_thread_context);
796
+ }
728
797
  }
729
798
 
730
799
  static void update_metrics_and_sample(
731
800
  thread_context_collector_state *state,
732
801
  VALUE thread_being_sampled,
733
802
  per_thread_context *thread_context,
734
- sampling_buffer* sampling_buffer,
735
803
  long current_cpu_time_ns,
736
804
  long current_monotonic_wall_time_ns,
737
- bool force_sample_suspended
805
+ bool force_sample
738
806
  ) {
739
807
  bool is_gvl_waiting_state =
740
- handle_gvl_waiting(state, thread_being_sampled, thread_context, sampling_buffer, current_cpu_time_ns);
808
+ handle_gvl_waiting(state, thread_being_sampled, thread_context, current_cpu_time_ns);
741
809
 
742
- if (skip_sample(state, thread_context, is_gvl_waiting_state, force_sample_suspended)) return;
810
+ if (skip_sample(state, thread_context, is_gvl_waiting_state, force_sample)) return;
743
811
 
744
812
  // Don't assign/update cpu during "Waiting for GVL"
745
813
  long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_time_since_previous_sample(
@@ -775,7 +843,6 @@ static void update_metrics_and_sample(
775
843
  state,
776
844
  thread_being_sampled,
777
845
  thread_context,
778
- sampling_buffer,
779
846
  (sample_values) {.cpu_time_ns = cpu_time_elapsed_ns, .cpu_or_wall_samples = 1, .wall_time_ns = wall_time_elapsed_ns},
780
847
  current_monotonic_wall_time_ns,
781
848
  NULL,
@@ -785,7 +852,12 @@ static void update_metrics_and_sample(
785
852
  );
786
853
  }
787
854
 
788
- static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample_suspended) {
855
+ static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample) {
856
+ if (!force_sample && thread_context->is_profiler_internal_thread) {
857
+ state->stats.profiler_thread_samples_skipped++;
858
+ return true;
859
+ }
860
+
789
861
  // Racy read but harmless, can only cause an extra sample
790
862
  uint64_t gvl_state_change_count = thread_context->gvl_state_change_count;
791
863
 
@@ -797,7 +869,7 @@ static bool skip_sample(thread_context_collector_state *state, per_thread_contex
797
869
  // in handle_gvl_waiting (situation 1 extra sample, situation 2 regular sample) keeps running.
798
870
  // TODO: we could probably also skip while "Waiting for GVL"
799
871
  if (!is_gvl_waiting_state &&
800
- !force_sample_suspended &&
872
+ !force_sample &&
801
873
  (gvl_state_change_count & GVL_SUSPENDED) &&
802
874
  gvl_state_change_count == thread_context->gvl_state_change_count_at_previous_sample) {
803
875
  state->stats.inactive_thread_samples_skipped++;
@@ -829,10 +901,8 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
829
901
 
830
902
  per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
831
903
 
832
- // If there was no previously-existing context for this thread, we won't allocate one (see safety). For now we just drop
833
- // the GC sample, under the assumption that "a thread that is so new that we never sampled it even once before it triggers
834
- // GC" is a rare enough case that we can just ignore it.
835
- // We can always improve this later if we find that this happens often (and we have the counter to help us figure that out)!
904
+ // Context is created eagerly via on_thread_begin_event, so this should not normally be NULL.
905
+ // We can't get_or_create_context_for() here since we can't allocate (GC context).
836
906
  if (thread_context == NULL) {
837
907
  state->stats.gc_samples_missed_due_to_missing_context++;
838
908
  return;
@@ -862,8 +932,7 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
862
932
 
863
933
  per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
864
934
 
865
- // If there was no previously-existing context for this thread, we won't allocate one (see safety). We keep a metric for
866
- // how often this happens -- see on_gc_start.
935
+ // Context is created eagerly, so this should not normally be NULL (see on_gc_start).
867
936
  if (thread_context == NULL) return false;
868
937
 
869
938
  long cpu_time_at_start_ns = thread_context->gc_tracking.cpu_time_at_start_ns;
@@ -982,7 +1051,6 @@ static void trigger_sample_for_thread(
982
1051
  thread_context_collector_state *state,
983
1052
  VALUE thread_being_sampled,
984
1053
  per_thread_context *thread_context,
985
- sampling_buffer* sampling_buffer,
986
1054
  sample_values values,
987
1055
  long current_monotonic_wall_time_ns,
988
1056
  // These two labels are only used for allocation profiling; @ivoanjo: may want to refactor this at some point?
@@ -1104,7 +1172,7 @@ static void trigger_sample_for_thread(
1104
1172
 
1105
1173
  sample_thread(
1106
1174
  thread_being_sampled,
1107
- sampling_buffer,
1175
+ &thread_context->sampling_buffer,
1108
1176
  state->locations,
1109
1177
  state->recorder_instance,
1110
1178
  values,
@@ -1133,17 +1201,22 @@ static VALUE _native_thread_list(DDTRACE_UNUSED VALUE _self) {
1133
1201
  return result;
1134
1202
  }
1135
1203
 
1204
+ static void check_frozen_thread(VALUE thread) {
1205
+ if (RB_OBJ_FROZEN(thread)) {
1206
+ raise_error(rb_eFrozenError, "Cannot setup profiler state for Thread %"PRIsVALUE" because it is frozen. Please avoid freezing Thread instances and/or report the issue to dd-trace-rb", thread);
1207
+ }
1208
+ }
1209
+
1210
+ // See the docs on struct per_thread_context.
1136
1211
  // This allocates a Ruby object and therefore needs the GVL and is not safe to call from RUBY_INTERNAL_EVENT_* hooks.
1137
- static per_thread_context *get_or_create_context_for(VALUE thread, thread_context_collector_state *state) {
1212
+ static per_thread_context *get_or_create_context_for(VALUE thread) {
1138
1213
  per_thread_context *thread_context = get_per_thread_context(thread);
1139
1214
  if (thread_context != NULL) return thread_context;
1140
1215
 
1141
- if (RB_OBJ_FROZEN(thread)) {
1142
- raise_error(rb_eFrozenError, "Cannot setup profiler state for Thread %"PRIsVALUE" because it is frozen. Please avoid freezing Thread instances and/or report the issue to dd-trace-rb", thread);
1143
- }
1216
+ check_frozen_thread(thread);
1144
1217
 
1145
1218
  thread_context = calloc(1, sizeof(per_thread_context)); // See "note on calloc vs ruby_xcalloc use" in heap_recorder.c
1146
- initialize_context(thread, thread_context, state);
1219
+ initialize_context(thread, thread_context);
1147
1220
 
1148
1221
  VALUE wrapper = TypedData_Wrap_Struct(rb_cObject, &per_thread_context_typed_data, thread_context);
1149
1222
  rb_ivar_set(thread, dd_per_thread_context_id, wrapper);
@@ -1152,6 +1225,14 @@ static per_thread_context *get_or_create_context_for(VALUE thread, thread_contex
1152
1225
  return thread_context;
1153
1226
  }
1154
1227
 
1228
+ static void on_thread_begin_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused) {
1229
+ if (!ddtrace_rb_ractor_main_p()) return;
1230
+
1231
+ VALUE thread = rb_tracearg_self(rb_tracearg_from_tracepoint(tracepoint_data));
1232
+ ENFORCE_THREAD(thread);
1233
+ get_or_create_context_for(thread);
1234
+ }
1235
+
1155
1236
  #define LOGGING_GEM_PATH "/lib/logging/diagnostic_context.rb"
1156
1237
 
1157
1238
  // The `logging` gem monkey patches thread creation, which makes the `invoke_location_for` useless, since every thread
@@ -1173,8 +1254,12 @@ static bool is_logging_gem_monkey_patch(VALUE invoke_file_location) {
1173
1254
  return strncmp(invoke_file + invoke_file_len - logging_gem_path_len, LOGGING_GEM_PATH, logging_gem_path_len) == 0;
1174
1255
  }
1175
1256
 
1176
- static void initialize_context(VALUE thread, per_thread_context *thread_context, thread_context_collector_state *state) {
1177
- sampling_buffer_initialize(&thread_context->sampling_buffer, state->locations.len);
1257
+ static void initialize_context(VALUE thread, per_thread_context *thread_context) {
1258
+ // We always create per_thread_context's with latest_max_frames because
1259
+ // 1) we don't always have access to the ThreadContext object (e.g. in a TracePoint).
1260
+ // 2) the per_thread_context's are global and so might not match the ThreadContext#max_frames anyway.
1261
+ // This is fine because sample_thread() handles when they don't match and resizes as needed.
1262
+ sampling_buffer_initialize(&thread_context->sampling_buffer, latest_max_frames);
1178
1263
 
1179
1264
  snprintf(thread_context->thread_id, THREAD_ID_LIMIT_CHARS, "%"PRIu64" (%lu)", native_thread_id_for(thread), (unsigned long) thread_id_for(thread));
1180
1265
  thread_context->thread_id_char_slice = (ddog_CharSlice) {.ptr = thread_context->thread_id, .len = strlen(thread_context->thread_id)};
@@ -1193,7 +1278,7 @@ static void initialize_context(VALUE thread, per_thread_context *thread_context,
1193
1278
  } else {
1194
1279
  snprintf(thread_context->thread_invoke_location, THREAD_INVOKE_LOCATION_LIMIT_CHARS, "%s", "(Unnamed thread)");
1195
1280
  }
1196
- } else if (thread != state->main_thread) {
1281
+ } else if (thread != rb_thread_main()) {
1197
1282
  // If the first function of a thread is native code, there won't be an invoke location, so we use this fallback.
1198
1283
  // NOTE: In the future, I wonder if we could take the pointer to the native function, and try to see if there's a native
1199
1284
  // symbol attached to it.
@@ -1207,9 +1292,8 @@ static void initialize_context(VALUE thread, per_thread_context *thread_context,
1207
1292
 
1208
1293
  thread_context->thread_cpu_time_id = thread_cpu_time_id_for(thread);
1209
1294
 
1210
- // These will get initialized during actual sampling
1211
- thread_context->cpu_time_at_previous_sample_ns = INVALID_TIME;
1212
- thread_context->wall_time_at_previous_sample_ns = INVALID_TIME;
1295
+ thread_context->wall_time_at_previous_sample_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1296
+ thread_context->cpu_time_at_previous_sample_ns = cpu_time_now_ns(thread_context);
1213
1297
 
1214
1298
  // These will only be used during a GC operation
1215
1299
  thread_context->gc_tracking.cpu_time_at_start_ns = INVALID_TIME;
@@ -1271,6 +1355,7 @@ static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context)
1271
1355
  ID2SYM(rb_intern("gvl_state_change_count")), /* => */ ULL2NUM(thread_context->gvl_state_change_count),
1272
1356
  ID2SYM(rb_intern("gvl_state_change_count_at_previous_sample")), /* => */ ULL2NUM(thread_context->gvl_state_change_count_at_previous_sample),
1273
1357
  ID2SYM(rb_intern("was_skipped_at_last_sample")), /* => */ thread_context->was_skipped_at_last_sample ? Qtrue : Qfalse,
1358
+ ID2SYM(rb_intern("is_profiler_internal_thread")), /* => */ thread_context->is_profiler_internal_thread ? Qtrue : Qfalse,
1274
1359
  };
1275
1360
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(context_as_hash, arguments[i], arguments[i+1]);
1276
1361
 
@@ -1284,6 +1369,7 @@ static VALUE stats_to_ruby_hash(thread_context_collector_state *state, VALUE has
1284
1369
  ID2SYM(rb_intern("gc_samples")), /* => */ UINT2NUM(state->stats.gc_samples),
1285
1370
  ID2SYM(rb_intern("gc_samples_missed_due_to_missing_context")), /* => */ UINT2NUM(state->stats.gc_samples_missed_due_to_missing_context),
1286
1371
  ID2SYM(rb_intern("inactive_thread_samples_skipped")), /* => */ UINT2NUM(state->stats.inactive_thread_samples_skipped),
1372
+ ID2SYM(rb_intern("profiler_thread_samples_skipped")), /* => */ UINT2NUM(state->stats.profiler_thread_samples_skipped),
1287
1373
  };
1288
1374
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(hash, arguments[i], arguments[i+1]);
1289
1375
  return hash;
@@ -1634,11 +1720,10 @@ bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_
1634
1720
  state,
1635
1721
  current_thread,
1636
1722
  thread_context,
1637
- &thread_context->sampling_buffer,
1638
1723
  (sample_values) {.alloc_samples = sample_weight, .alloc_samples_unscaled = 1, .heap_sample = true},
1639
1724
  INVALID_TIME, // For now we're not collecting timestamps for allocation events, as per profiling team internal discussions
1640
1725
  &ruby_vm_type,
1641
- &class_name,
1726
+ &class_name,
1642
1727
  /* is_gvl_waiting_state: */ false,
1643
1728
  /* is_safe_to_allocate_objects: */ false // Not safe to allocate further inside the NEWOBJ tracepoint
1644
1729
  );
@@ -1649,9 +1734,10 @@ bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_
1649
1734
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1650
1735
  // It SHOULD NOT be used for other purposes.
1651
1736
  static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object) {
1652
- thread_context_collector_state *state;
1653
- TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1654
- per_thread_context *thread_context = get_or_create_context_for(rb_thread_current(), state);
1737
+ per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
1738
+ if (thread_context == NULL) {
1739
+ rb_raise(rb_eRuntimeError, "Missing per_thread_context for current thread in _native_sample_allocation");
1740
+ }
1655
1741
 
1656
1742
  debug_enter_unsafe_context();
1657
1743
 
@@ -1972,41 +2058,81 @@ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance) {
1972
2058
  state->stats = (struct stats) {};
1973
2059
  }
1974
2060
 
1975
- #ifndef NO_GVL_INSTRUMENTATION
1976
- void thread_context_collector_on_gvl_released(per_thread_context *thread_context) {
1977
- thread_context->gvl_state_change_count |= GVL_SUSPENDED;
2061
+ static void mark_thread_as_profiler_internal(per_thread_context *ctx) {
2062
+ ctx->is_profiler_internal_thread = true;
2063
+ }
2064
+
2065
+ void thread_context_collector_profiler_internal_thread_started(void) {
2066
+ per_thread_context *ctx = get_or_create_context_for(rb_thread_current());
2067
+ mark_thread_as_profiler_internal(ctx);
2068
+ }
2069
+
2070
+ static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread) {
2071
+ per_thread_context *ctx = get_or_create_context_for(thread);
2072
+ mark_thread_as_profiler_internal(ctx);
2073
+ return Qnil;
2074
+ }
2075
+
2076
+ // Called via rb_ensure when a profiler-internal thread (worker or idle helper) is about to exit.
2077
+ // Records a final sample so the thread's accumulated cpu/wall time since the last on_serialize
2078
+ // flush is not lost. on_serialize (below) also flushes profiler-internal threads during periodic
2079
+ // serialization, but it can't help at shutdown: by the time the final serialize runs, these
2080
+ // threads are already dead and absent from thread_list.
2081
+ void thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2082
+ thread_context_collector_state *state;
2083
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2084
+
2085
+ VALUE current_thread = rb_thread_current();
2086
+ per_thread_context *thread_context = get_or_create_context_for(current_thread);
2087
+ if (!thread_context->is_profiler_internal_thread) {
2088
+ rb_raise(rb_eRuntimeError, "current thread %"PRIsVALUE" is not profiler-internal thread", current_thread);
1978
2089
  }
1979
2090
 
1980
- // Called by the stack recorder at the start of _native_serialize, so that threads whose last
1981
- // per-tick sample was skipped by the SUSPENDED-skip optimization still get their accumulated
1982
- // time recorded in this reporting period. Without this, a thread that sleeps across the whole
1983
- // period would not be reported at all.
1984
- void thread_context_collector_on_serialize(VALUE self_instance) {
1985
- thread_context_collector_state *state;
1986
- TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2091
+ long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2092
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1987
2093
 
1988
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1989
- VALUE threads = thread_list(state);
1990
- const long thread_count = RARRAY_LEN(threads);
2094
+ update_metrics_and_sample(
2095
+ state,
2096
+ current_thread,
2097
+ thread_context,
2098
+ current_cpu_time_ns,
2099
+ current_monotonic_wall_time_ns,
2100
+ true);
2101
+ }
1991
2102
 
1992
- for (long i = 0; i < thread_count; i++) {
1993
- VALUE thread = RARRAY_AREF(threads, i);
1994
- per_thread_context *thread_context = get_per_thread_context(thread);
1995
-
1996
- if (thread_context != NULL && thread_context->was_skipped_at_last_sample) {
1997
- long current_cpu_time_ns = cpu_time_now_ns(thread_context);
1998
- // We need to force_sample_suspended=true otherwise this sample would be skipped too
1999
- update_metrics_and_sample(
2000
- state,
2001
- thread,
2002
- thread_context,
2003
- &thread_context->sampling_buffer,
2004
- current_cpu_time_ns,
2005
- current_monotonic_wall_time_ns,
2006
- true);
2007
- }
2103
+ // Flushes threads whose last per-tick sample was skipped (either by the SUSPENDED-skip
2104
+ // optimization, or by is_profiler_internal_thread) so their accumulated time is recorded.
2105
+ // Called by the stack recorder at the start of _native_serialize (regular periodic flush).
2106
+ void thread_context_collector_on_serialize(VALUE self_instance) {
2107
+ thread_context_collector_state *state;
2108
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2109
+
2110
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2111
+ VALUE threads = thread_list(state);
2112
+ const long thread_count = RARRAY_LEN(threads);
2113
+
2114
+ for (long i = 0; i < thread_count; i++) {
2115
+ VALUE thread = RARRAY_AREF(threads, i);
2116
+ per_thread_context *thread_context = get_per_thread_context(thread);
2117
+
2118
+ if (thread_context != NULL && (thread_context->was_skipped_at_last_sample || thread_context->is_profiler_internal_thread)) {
2119
+ long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2120
+ // We need to force_sample=true otherwise this sample would be skipped too
2121
+ update_metrics_and_sample(
2122
+ state,
2123
+ thread,
2124
+ thread_context,
2125
+ current_cpu_time_ns,
2126
+ current_monotonic_wall_time_ns,
2127
+ true);
2008
2128
  }
2009
2129
  }
2130
+ }
2131
+
2132
+ #ifndef NO_GVL_INSTRUMENTATION
2133
+ void thread_context_collector_on_gvl_released(per_thread_context *thread_context) {
2134
+ thread_context->gvl_state_change_count |= GVL_SUSPENDED;
2135
+ }
2010
2136
 
2011
2137
  void thread_context_collector_on_gvl_waiting(per_thread_context *thread_context) {
2012
2138
  long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
@@ -2103,7 +2229,7 @@ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance) {
2103
2229
  thread_context_collector_state *state;
2104
2230
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2105
2231
 
2106
- per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
2232
+ per_thread_context *thread_context = get_or_create_context_for(current_thread);
2107
2233
 
2108
2234
  long gvl_waiting_at = thread_context->gvl_waiting_at;
2109
2235
 
@@ -2126,7 +2252,6 @@ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance) {
2126
2252
  state,
2127
2253
  current_thread,
2128
2254
  thread_context,
2129
- &thread_context->sampling_buffer,
2130
2255
  cpu_time_for_thread,
2131
2256
  current_monotonic_wall_time_ns,
2132
2257
  false);
@@ -2141,7 +2266,6 @@ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance) {
2141
2266
  thread_context_collector_state *state,
2142
2267
  VALUE thread_being_sampled,
2143
2268
  per_thread_context *thread_context,
2144
- sampling_buffer* sampling_buffer,
2145
2269
  long current_cpu_time_ns
2146
2270
  ) {
2147
2271
  long gvl_waiting_at = thread_context->gvl_waiting_at;
@@ -2219,7 +2343,6 @@ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance) {
2219
2343
  state,
2220
2344
  thread_being_sampled,
2221
2345
  thread_context,
2222
- sampling_buffer,
2223
2346
  (sample_values) {.cpu_time_ns = cpu_time_elapsed_ns, .cpu_or_wall_samples = 1, .wall_time_ns = duration_until_start_of_gvl_waiting_ns},
2224
2347
  gvl_waiting_started_wall_time_ns,
2225
2348
  NULL,
@@ -2306,29 +2429,27 @@ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance) {
2306
2429
  return result;
2307
2430
  }
2308
2431
 
2309
- static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns) {
2310
- ENFORCE_THREAD(thread);
2311
-
2312
- per_thread_context *thread_context = get_per_thread_context(thread);
2313
- if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2314
-
2315
- thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
2316
-
2317
- return Qtrue;
2318
- }
2319
-
2320
2432
  #else
2321
2433
  static bool handle_gvl_waiting(
2322
2434
  DDTRACE_UNUSED thread_context_collector_state *state,
2323
2435
  DDTRACE_UNUSED VALUE thread_being_sampled,
2324
2436
  DDTRACE_UNUSED per_thread_context *thread_context,
2325
- DDTRACE_UNUSED sampling_buffer* sampling_buffer,
2326
2437
  DDTRACE_UNUSED long current_cpu_time_ns
2327
2438
  ) { return false; }
2328
2439
 
2329
- void thread_context_collector_on_serialize(DDTRACE_UNUSED VALUE self_instance) { }
2330
2440
  #endif // NO_GVL_INSTRUMENTATION
2331
2441
 
2442
+ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns) {
2443
+ ENFORCE_THREAD(thread);
2444
+
2445
+ per_thread_context *thread_context = get_per_thread_context(thread);
2446
+ if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2447
+
2448
+ thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
2449
+
2450
+ return Qtrue;
2451
+ }
2452
+
2332
2453
  #define MAX_SAFE_LOOKUP_SIZE 16
2333
2454
 
2334
2455
  typedef struct { VALUE lookup_key; VALUE result; } safe_lookup_hash_state;