datadog 2.37.0 → 2.38.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +23 -15
  4. data/ext/datadog_profiling_native_extension/collectors_stack.c +19 -24
  5. data/ext/datadog_profiling_native_extension/collectors_stack.h +0 -1
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +155 -141
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +1 -0
  8. data/ext/datadog_profiling_native_extension/extconf.rb +0 -3
  9. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -0
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  11. data/ext/datadog_profiling_native_extension/time_helpers.h +12 -7
  12. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +5 -0
  13. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +5 -0
  14. data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
  15. data/lib/datadog/core/configuration/settings.rb +4 -4
  16. data/lib/datadog/core/configuration/supported_configurations.rb +1 -0
  17. data/lib/datadog/core/knuth_sampler.rb +3 -1
  18. data/lib/datadog/core/metrics/client.rb +1 -1
  19. data/lib/datadog/core/transport/http/builder.rb +1 -1
  20. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  21. data/lib/datadog/core/utils/time.rb +5 -1
  22. data/lib/datadog/di/boot.rb +3 -0
  23. data/lib/datadog/di/capture_expression.rb +21 -0
  24. data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
  25. data/lib/datadog/di/capture_limits.rb +41 -0
  26. data/lib/datadog/di/component.rb +2 -2
  27. data/lib/datadog/di/configuration.rb +6 -0
  28. data/lib/datadog/di/context.rb +9 -3
  29. data/lib/datadog/di/el/compiler.rb +1 -1
  30. data/lib/datadog/di/instrumenter.rb +34 -3
  31. data/lib/datadog/di/probe.rb +47 -2
  32. data/lib/datadog/di/probe_builder.rb +83 -1
  33. data/lib/datadog/di/probe_notification_builder.rb +48 -7
  34. data/lib/datadog/di/remote.rb +1 -1
  35. data/lib/datadog/di/serializer.rb +22 -10
  36. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +5 -0
  37. data/lib/datadog/profiling/collectors/thread_context.rb +9 -1
  38. data/lib/datadog/profiling/component.rb +17 -0
  39. data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
  40. data/lib/datadog/tracing/contrib/active_job/patcher.rb +12 -0
  41. data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +2 -3
  42. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -12
  43. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  44. data/lib/datadog/tracing/contrib/sequel/utils.rb +2 -2
  45. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
  46. data/lib/datadog/tracing/contrib/sinatra/framework.rb +1 -1
  47. data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
  48. data/lib/datadog/version.rb +1 -1
  49. metadata +9 -5
@@ -43,11 +43,8 @@
43
43
  // When `thread_context_collector_on_gc_start` gets called, the current cpu and wall-time get recorded to the thread
44
44
  // context: `cpu_time_at_gc_start_ns` and `wall_time_at_gc_start_ns`.
45
45
  //
46
- // While `cpu_time_at_gc_start_ns` is set, regular samples (if any) do not account for cpu-time any time that passes
47
- // after this timestamp. The idea is that this cpu-time will be blamed separately on GC, and not on the user thread.
48
- // Wall-time accounting is not affected by this (e.g. we still record 60 seconds every 60 seconds).
49
- //
50
- // (Regular samples can still account for the cpu-time between the previous sample and the start of GC.)
46
+ // While `cpu_time_at_gc_start_ns` is set, we don't expect the thread to be sampled: the VM is doing GC
47
+ // on the thread holding the GVL so no other samples can/will be triggered until GC finishes.
51
48
  //
52
49
  // When `thread_context_collector_on_gc_finish` gets called, the cpu-time and wall-time spent during GC gets recorded
53
50
  // into the global gc_tracking structure, and further samples are not affected. (The `cpu_time_at_previous_sample_ns`
@@ -76,8 +73,6 @@
76
73
 
77
74
  #define THREAD_ID_LIMIT_CHARS 44 // Why 44? "#{2**64} (#{2**64})".size + 1 for \0
78
75
  #define THREAD_INVOKE_LOCATION_LIMIT_CHARS 512
79
- #define IS_WALL_TIME true
80
- #define IS_CPU_TIME false
81
76
  #define MISSING_TRACER_CONTEXT_KEY 0
82
77
  #define TIME_BETWEEN_GC_EVENTS_NS MILLIS_AS_NS(10)
83
78
  #define GVL_SUSPENDED ((uint64_t)1)
@@ -109,7 +104,13 @@ static ID server_id; // id of :server in Ruby
109
104
  static ID otel_context_storage_id; // id of :__opentelemetry_context_storage__ in Ruby
110
105
  static ID otel_fiber_context_storage_id; // id of :@opentelemetry_context in Ruby
111
106
 
112
- // This is mutable and gets set last-writer-wins style whenever a new `ThreadContext` is created.
107
+ // This is mutable and gets set last-writer-wins style by
108
+ // `thread_context_collector_reset_all_per_thread_contexts`, which is called whenever
109
+ // profiling is starting or restating.
110
+ //
111
+ // Note: We must be careful to not change this value while the profiler is still running,
112
+ // otherwise new threads can get the new value and cause profiling to stop with an
113
+ // exception due to the mismatched size.
113
114
  //
114
115
  // The initial value should be kept in sync with the default for DD_PROFILING_MAX_FRAMES
115
116
  // in settings.rb. See `initialize_context` for details on why this is needed/used.
@@ -118,7 +119,6 @@ static uint16_t latest_max_frames = 400;
118
119
  // Global tracepoint for RUBY_EVENT_THREAD_BEGIN. Created and enabled once when the first ThreadContext collector is initialized.
119
120
  static VALUE thread_begin_tracepoint = Qnil;
120
121
 
121
-
122
122
  typedef enum { OTEL_CONTEXT_ENABLED_FALSE, OTEL_CONTEXT_ENABLED_ONLY, OTEL_CONTEXT_ENABLED_BOTH } otel_context_enabled;
123
123
  typedef enum { OTEL_CONTEXT_SOURCE_UNKNOWN, OTEL_CONTEXT_SOURCE_FIBER_IVAR, OTEL_CONTEXT_SOURCE_FIBER_LOCAL } otel_context_source;
124
124
 
@@ -187,7 +187,8 @@ typedef struct {
187
187
  // The state is created early on for all threads on the main Ractor
188
188
  // (enabling a TracePoint only enables it for the current Ractor).
189
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.
190
+ // or via `thread_context_collector_reset_all_per_thread_contexts` when the profiler starts.
191
+ //
191
192
  // Unfortunately that RUBY_EVENT_THREAD_BEGIN TracePoint still fires after some other events:
192
193
  // * RUBY_INTERNAL_THREAD_EVENT_RESUMED for the Thread acquiring the GVL for the first time
193
194
  // * an early SIGPROF calling handle_sampling_signal()
@@ -326,7 +327,8 @@ static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context)
326
327
  static VALUE stats_to_ruby_hash(thread_context_collector_state *state, VALUE hash);
327
328
  static VALUE gc_tracking_as_ruby_hash(thread_context_collector_state *state);
328
329
  static VALUE _native_per_thread_context(VALUE self, VALUE collector_instance);
329
- static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, long gc_start_time_ns, bool is_wall_time);
330
+ static long update_cpu_time_since_previous_sample(per_thread_context *thread_context, long current_cpu_time_ns);
331
+ static long update_wall_time_since_previous_sample(per_thread_context *thread_context, long current_wall_time_ns);
330
332
  static long cpu_time_now_ns(per_thread_context *thread_context);
331
333
  static long thread_id_for(VALUE thread);
332
334
  static VALUE _native_stats(VALUE self, VALUE collector_instance);
@@ -359,11 +361,11 @@ static bool handle_gvl_waiting(
359
361
  per_thread_context *thread_context,
360
362
  long current_cpu_time_ns
361
363
  );
364
+ static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
365
+ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread);
362
366
  #ifndef NO_GVL_INSTRUMENTATION
363
- static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
364
367
  static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread);
365
368
  static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
366
- static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread);
367
369
  static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception);
368
370
  #endif
369
371
  static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns);
@@ -378,9 +380,9 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
378
380
  static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key);
379
381
  static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
380
382
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self);
381
- static VALUE _native_clear_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
382
383
  static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread);
383
384
  static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
385
+ static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
384
386
  static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample);
385
387
  static void on_thread_begin_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused);
386
388
 
@@ -417,14 +419,14 @@ void collectors_thread_context_init(VALUE profiling_module) {
417
419
  rb_define_singleton_method(testing_module, "_native_sample_skipped_allocation_samples", _native_sample_skipped_allocation_samples, 2);
418
420
  rb_define_singleton_method(testing_module, "_native_system_epoch_time_now_ns", _native_system_epoch_time_now_ns, 1);
419
421
  rb_define_singleton_method(testing_module, "_native_prepare_sample_inside_signal_handler", _native_prepare_sample_inside_signal_handler, 0);
420
- rb_define_singleton_method(testing_module, "_native_clear_per_thread_context_for", _native_clear_per_thread_context_for, 1);
421
422
  rb_define_singleton_method(testing_module, "_native_remove_per_thread_context_for", _native_remove_per_thread_context_for, 1);
423
+ rb_define_singleton_method(testing_module, "_native_global_reset_per_thread_context", _native_global_reset_per_thread_context, 1);
422
424
  rb_define_singleton_method(testing_module, "_native_mark_thread_as_profiler_internal", _native_mark_thread_as_profiler_internal, 1);
425
+ rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
426
+ rb_define_singleton_method(testing_module, "_native_on_gvl_released", _native_on_gvl_released, 1);
423
427
  #ifndef NO_GVL_INSTRUMENTATION
424
- rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
425
428
  rb_define_singleton_method(testing_module, "_native_gvl_waiting_at_for", _native_gvl_waiting_at_for, 1);
426
429
  rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 2);
427
- rb_define_singleton_method(testing_module, "_native_on_gvl_released", _native_on_gvl_released, 1);
428
430
  rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 3);
429
431
  #endif
430
432
  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);
@@ -523,14 +525,9 @@ static void per_thread_context_typed_data_free(void *ctx_ptr) {
523
525
  free(ctx);
524
526
  }
525
527
 
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
528
  // 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.
529
+ // This simulates situations where we observe a thread running before our RUBY_EVENT_THREAD_BEGIN tracepoint fires,
530
+ // so that we can test our `get_per_thread_context(...) => null` code paths.
534
531
  static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
535
532
  check_frozen_thread(thread);
536
533
  per_thread_context *ctx = get_per_thread_context(thread);
@@ -541,6 +538,11 @@ static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VA
541
538
  return Qnil;
542
539
  }
543
540
 
541
+ static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
542
+ thread_context_collector_reset_all_per_thread_contexts(collector_instance);
543
+ return Qnil;
544
+ }
545
+
544
546
  static VALUE _native_new(VALUE klass) {
545
547
  thread_context_collector_state *state = ruby_xcalloc(1, sizeof(thread_context_collector_state));
546
548
 
@@ -600,7 +602,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
600
602
  ENFORCE_TYPE(overhead_filename, T_STRING);
601
603
 
602
604
  uint16_t max_frame_int = sampling_buffer_check_max_frames(NUM2INT(max_frames));
603
- latest_max_frames = max_frame_int;
604
605
 
605
606
  thread_context_collector_state *state;
606
607
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -636,14 +637,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
636
637
  if (thread_begin_tracepoint == Qnil) {
637
638
  thread_begin_tracepoint = rb_tracepoint_new(Qnil, RUBY_EVENT_THREAD_BEGIN, on_thread_begin_event, NULL);
638
639
  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
640
  }
648
641
 
649
642
  return Qtrue;
@@ -704,17 +697,8 @@ static void record_sampling_overhead(thread_context_collector_state *state, per_
704
697
  long wall_time_after_sampling = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
705
698
  long cpu_time_after_sampling = cpu_time_now_ns(current_thread_context);
706
699
 
707
- long overhead_cpu_time_ns = update_time_since_previous_sample(
708
- &current_thread_context->cpu_time_at_previous_sample_ns,
709
- cpu_time_after_sampling,
710
- current_thread_context->gc_tracking.cpu_time_at_start_ns,
711
- IS_CPU_TIME);
712
-
713
- long overhead_wall_time_ns = update_time_since_previous_sample(
714
- &current_thread_context->wall_time_at_previous_sample_ns,
715
- wall_time_after_sampling,
716
- INVALID_TIME,
717
- IS_WALL_TIME);
700
+ long overhead_cpu_time_ns = update_cpu_time_since_previous_sample(current_thread_context, cpu_time_after_sampling);
701
+ long overhead_wall_time_ns = update_wall_time_since_previous_sample(current_thread_context, wall_time_after_sampling);
718
702
 
719
703
  ddog_prof_Label overhead_labels[] = {
720
704
  {.key = DDOG_CHARSLICE_C("thread id"), .str = DDOG_CHARSLICE_C("0"), .num = 0},
@@ -810,19 +794,9 @@ static void update_metrics_and_sample(
810
794
  if (skip_sample(state, thread_context, is_gvl_waiting_state, force_sample)) return;
811
795
 
812
796
  // Don't assign/update cpu during "Waiting for GVL"
813
- long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_time_since_previous_sample(
814
- &thread_context->cpu_time_at_previous_sample_ns,
815
- current_cpu_time_ns,
816
- thread_context->gc_tracking.cpu_time_at_start_ns,
817
- IS_CPU_TIME
818
- );
797
+ long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_cpu_time_since_previous_sample(thread_context, current_cpu_time_ns);
819
798
 
820
- long wall_time_elapsed_ns = update_time_since_previous_sample(
821
- &thread_context->wall_time_at_previous_sample_ns,
822
- current_monotonic_wall_time_ns,
823
- INVALID_TIME,
824
- IS_WALL_TIME
825
- );
799
+ long wall_time_elapsed_ns = update_wall_time_since_previous_sample(thread_context, current_monotonic_wall_time_ns);
826
800
 
827
801
  // A thread enters "Waiting for GVL", well, as the name implies, without the GVL.
828
802
  //
@@ -973,9 +947,9 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
973
947
  state->gc_tracking.accumulated_wall_time_ns += gc_wall_time_elapsed_ns;
974
948
  state->gc_tracking.wall_time_at_previous_gc_ns = wall_time_at_finish_ns;
975
949
 
976
- // Update cpu-time accounting so it doesn't include the cpu-time spent in GC during the next sample
977
- // We don't update the wall-time because we don't subtract the wall-time spent in GC (see call to
978
- // `update_time_since_previous_sample` for wall-time in `update_metrics_and_sample`).
950
+ // Update cpu-time accounting so it doesn't include the cpu-time spent in GC during the next sample.
951
+ // We don't do the same for wall-time, because GC is just like any other reason a thread didn't make
952
+ // progress -- time always goes forward regardless of the thread making progress on what it wanted.
979
953
  if (thread_context->cpu_time_at_previous_sample_ns != INVALID_TIME) {
980
954
  thread_context->cpu_time_at_previous_sample_ns += gc_cpu_time_elapsed_ns;
981
955
  }
@@ -1255,10 +1229,9 @@ static bool is_logging_gem_monkey_patch(VALUE invoke_file_location) {
1255
1229
  }
1256
1230
 
1257
1231
  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.
1232
+ // We always create per_thread_context's with latest_max_frames; that value is kept in sync with the
1233
+ // active profiler's max_frames by the global reset that runs when profiling starts
1234
+ // so we expect to always see here the latest correct value to be used.
1262
1235
  sampling_buffer_initialize(&thread_context->sampling_buffer, latest_max_frames);
1263
1236
 
1264
1237
  snprintf(thread_context->thread_id, THREAD_ID_LIMIT_CHARS, "%"PRIu64" (%lu)", native_thread_id_for(thread), (unsigned long) thread_id_for(thread));
@@ -1303,6 +1276,45 @@ static void initialize_context(VALUE thread, per_thread_context *thread_context)
1303
1276
  thread_context->gvl_state_change_count = 0;
1304
1277
  }
1305
1278
 
1279
+ // This MUST be called before profiling starts, so that a new profiler session starts from a fresh state and never
1280
+ // observes or includes any leftover stale state from a previous session.
1281
+ // Such a call MUST happen while the CpuAndWallTimeWorker is stopped (e.g. no tracepoints active, no signals
1282
+ // triggering samples, no gvl hooks, etc).
1283
+ //
1284
+ // It updates the global `latest_max_frames` from the given (latest) ThreadContext and (re)creates every per-thread
1285
+ // context's sampling buffer sized accordingly, so the buffers always match the collector that's about to start
1286
+ // sampling -- even if a previous session used a different max_frames.
1287
+ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance) {
1288
+ thread_context_collector_state *state;
1289
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1290
+
1291
+ // Update global max frames to be used when allocating sampling buffers
1292
+ if (state->locations.len == 0) {
1293
+ raise_error(rb_eRuntimeError, "BUG: Unexpected locations.len == 0. Is this ThreadContext not initialized?");
1294
+ }
1295
+
1296
+ latest_max_frames = state->locations.len;
1297
+
1298
+ VALUE threads = thread_list(state);
1299
+ const long thread_count = RARRAY_LEN(threads);
1300
+ for (long i = 0; i < thread_count; i++) {
1301
+ VALUE thread = rb_ary_entry(threads, i);
1302
+ per_thread_context *thread_context = get_per_thread_context(thread);
1303
+ if (thread_context != NULL) {
1304
+ bool is_profiler_internal_thread = thread_context->is_profiler_internal_thread;
1305
+
1306
+ sampling_buffer_free(&thread_context->sampling_buffer);
1307
+ memset(thread_context, 0, sizeof(per_thread_context));
1308
+ initialize_context(thread, thread_context);
1309
+
1310
+ thread_context->is_profiler_internal_thread = is_profiler_internal_thread;
1311
+ } else {
1312
+ // If thread didn't have a context, let's trigger its creation
1313
+ get_or_create_context_for(thread);
1314
+ }
1315
+ }
1316
+ }
1317
+
1306
1318
  static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
1307
1319
  thread_context_collector_state *state;
1308
1320
  TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
@@ -1409,48 +1421,59 @@ static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collec
1409
1421
  return result;
1410
1422
  }
1411
1423
 
1412
- // gc_start_time_ns should only be passed if IS_CPU_TIME
1413
- static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, long gc_start_time_ns, bool is_wall_time) {
1424
+ static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, per_thread_context *thread_context) {
1414
1425
  // If we didn't have a time for the previous sample, we use the current one
1415
1426
  if (*time_at_previous_sample_ns == INVALID_TIME) *time_at_previous_sample_ns = current_time_ns;
1416
1427
 
1417
- // We don't want wall-time accounting to change during GC.
1418
- // E.g. if 60 seconds pass in the real world, 60 seconds of wall-time are recorded, regardless of the thread doing GC or not.
1419
- bool is_thread_doing_gc = !is_wall_time && gc_start_time_ns != INVALID_TIME;
1420
- long elapsed_time_ns = -1;
1428
+ // We don't expect to be sampling a thread (and thus updating these counters) while Ruby is doing GC (between
1429
+ // `thread_context_collector_on_gc_start` and `thread_context_collector_on_gc_finish`)
1430
+ if (thread_context->gc_tracking.cpu_time_at_start_ns != INVALID_TIME) {
1431
+ raise_error(
1432
+ rb_eRuntimeError,
1433
+ "BUG: Unexpected sample during GC (thread_id=%s, gc_tracking.cpu_time_at_start_ns=%ld, "
1434
+ "gc_tracking.wall_time_at_start_ns=%ld, monotonic_wall_time_now_ns=%ld)",
1435
+ thread_context->thread_id,
1436
+ thread_context->gc_tracking.cpu_time_at_start_ns,
1437
+ thread_context->gc_tracking.wall_time_at_start_ns,
1438
+ monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
1439
+ );
1440
+ }
1421
1441
 
1422
- if (is_thread_doing_gc) {
1423
- bool previous_sample_was_during_gc = gc_start_time_ns <= *time_at_previous_sample_ns;
1442
+ long elapsed_time_ns = current_time_ns - *time_at_previous_sample_ns; // Capture all time since previous sample
1443
+ *time_at_previous_sample_ns = current_time_ns;
1424
1444
 
1425
- if (previous_sample_was_during_gc) {
1426
- elapsed_time_ns = 0; // No time to account for -- any time since the last sample is going to get assigned to GC separately
1427
- } else {
1428
- elapsed_time_ns = gc_start_time_ns - *time_at_previous_sample_ns; // Capture time between previous sample and start of GC only
1429
- }
1445
+ return elapsed_time_ns;
1446
+ }
1430
1447
 
1431
- // Remaining time (from gc_start_time to current_time_ns) will be accounted for inside `sample_after_gc`
1432
- *time_at_previous_sample_ns = gc_start_time_ns;
1433
- } else {
1434
- elapsed_time_ns = current_time_ns - *time_at_previous_sample_ns; // Capture all time since previous sample
1435
- *time_at_previous_sample_ns = current_time_ns;
1436
- }
1448
+ static long update_cpu_time_since_previous_sample(per_thread_context *thread_context, long current_cpu_time_ns) {
1449
+ long elapsed_time_ns = update_time_since_previous_sample(
1450
+ &thread_context->cpu_time_at_previous_sample_ns,
1451
+ current_cpu_time_ns,
1452
+ thread_context
1453
+ );
1437
1454
 
1455
+ // We don't expect cpu-time to go backwards, so let's flag this as a bug
1438
1456
  if (elapsed_time_ns < 0) {
1439
- if (is_wall_time) {
1440
- // Wall-time can actually go backwards (e.g. when the system clock gets set) so we can't assume time going backwards
1441
- // was a bug.
1442
- // @ivoanjo: I've also observed time going backwards spuriously on macOS, see discussion on
1443
- // https://github.com/DataDog/dd-trace-rb/pull/2336.
1444
- elapsed_time_ns = 0;
1445
- } else {
1446
- // We don't expect non-wall time to go backwards, so let's flag this as a bug
1447
- raise_error(rb_eRuntimeError, "BUG: Unexpected negative elapsed_time_ns between samples");
1448
- }
1457
+ raise_error(rb_eRuntimeError, "BUG: Unexpected CPU time going backwards between samples");
1449
1458
  }
1450
1459
 
1451
1460
  return elapsed_time_ns;
1452
1461
  }
1453
1462
 
1463
+ static long update_wall_time_since_previous_sample(per_thread_context *thread_context, long current_wall_time_ns) {
1464
+ long elapsed_time_ns = update_time_since_previous_sample(
1465
+ &thread_context->wall_time_at_previous_sample_ns,
1466
+ current_wall_time_ns,
1467
+ thread_context
1468
+ );
1469
+
1470
+ // Wall-time can actually go backwards (e.g. when the system clock gets set) so we can't assume time going backwards
1471
+ // was a bug.
1472
+ // @ivoanjo: I've also observed time going backwards spuriously on macOS, see discussion on
1473
+ // https://github.com/DataDog/dd-trace-rb/pull/2336.
1474
+ return long_max_of(elapsed_time_ns, 0);
1475
+ }
1476
+
1454
1477
  // Safety: This function is assumed never to raise exceptions by callers
1455
1478
  static long cpu_time_now_ns(per_thread_context *thread_context) {
1456
1479
  thread_cpu_time cpu_time = thread_cpu_time_for(thread_context->thread_cpu_time_id);
@@ -1607,8 +1630,8 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector
1607
1630
 
1608
1631
  state->stats = (struct stats) {}; // Resets all stats back to zero
1609
1632
 
1610
- // Clear any leftover state from parent process in the current thread; all other threads are assumed dead
1611
- _native_clear_per_thread_context_for(Qnil, rb_thread_current());
1633
+ // No need to clean-up the per-thread context because the CpuAndWallTimeWorker always cleans
1634
+ // it up unconditionally on every start/restart and that includes after a fork.
1612
1635
 
1613
1636
  rb_funcall(state->recorder_instance, rb_intern("reset_after_fork"), 0);
1614
1637
 
@@ -2129,18 +2152,44 @@ void thread_context_collector_on_serialize(VALUE self_instance) {
2129
2152
  }
2130
2153
  }
2131
2154
 
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
- }
2155
+ void thread_context_collector_on_gvl_released(per_thread_context *thread_context) {
2156
+ thread_context->gvl_state_change_count |= GVL_SUSPENDED;
2157
+ }
2136
2158
 
2137
- void thread_context_collector_on_gvl_waiting(per_thread_context *thread_context) {
2138
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
2139
- if (current_monotonic_wall_time_ns <= 0) return;
2159
+ void thread_context_collector_on_gvl_waiting(per_thread_context *thread_context) {
2160
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
2161
+ if (current_monotonic_wall_time_ns <= 0) return;
2140
2162
 
2141
- thread_context->gvl_waiting_at = current_monotonic_wall_time_ns;
2142
- }
2163
+ thread_context->gvl_waiting_at = current_monotonic_wall_time_ns;
2164
+ }
2165
+
2166
+ static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
2167
+ ENFORCE_THREAD(thread);
2168
+
2169
+ debug_enter_unsafe_context();
2170
+
2171
+ per_thread_context *thread_context = get_per_thread_context(thread);
2172
+ if (thread_context) thread_context_collector_on_gvl_waiting(thread_context);
2173
+
2174
+ debug_leave_unsafe_context();
2175
+
2176
+ return Qnil;
2177
+ }
2178
+
2179
+ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2180
+ ENFORCE_THREAD(thread);
2181
+
2182
+ debug_enter_unsafe_context();
2183
+
2184
+ per_thread_context *thread_context = get_per_thread_context(thread);
2185
+ if (thread_context) thread_context_collector_on_gvl_released(thread_context);
2186
+
2187
+ debug_leave_unsafe_context();
2188
+
2189
+ return Qnil;
2190
+ }
2143
2191
 
2192
+ #ifndef NO_GVL_INSTRUMENTATION
2144
2193
  // This function runs on the passed thread and has the GVL because it gets called just after the Ruby thread acquired the GVL
2145
2194
  __attribute__((warn_unused_result))
2146
2195
  on_gvl_running_result thread_context_collector_on_gvl_running(VALUE self_instance, VALUE thread, per_thread_context *thread_context) {
@@ -2324,19 +2373,10 @@ void thread_context_collector_on_serialize(VALUE self_instance) {
2324
2373
  long gvl_waiting_started_wall_time_ns = labs(gvl_waiting_at);
2325
2374
 
2326
2375
  if (thread_context->wall_time_at_previous_sample_ns < gvl_waiting_started_wall_time_ns) { // situation 1 above
2327
- long cpu_time_elapsed_ns = update_time_since_previous_sample(
2328
- &thread_context->cpu_time_at_previous_sample_ns,
2329
- current_cpu_time_ns,
2330
- thread_context->gc_tracking.cpu_time_at_start_ns,
2331
- IS_CPU_TIME
2332
- );
2376
+ long cpu_time_elapsed_ns = update_cpu_time_since_previous_sample(thread_context, current_cpu_time_ns);
2333
2377
 
2334
- long duration_until_start_of_gvl_waiting_ns = update_time_since_previous_sample(
2335
- &thread_context->wall_time_at_previous_sample_ns,
2336
- gvl_waiting_started_wall_time_ns,
2337
- INVALID_TIME,
2338
- IS_WALL_TIME
2339
- );
2378
+ long duration_until_start_of_gvl_waiting_ns =
2379
+ update_wall_time_since_previous_sample(thread_context, gvl_waiting_started_wall_time_ns);
2340
2380
 
2341
2381
  // Push extra sample
2342
2382
  trigger_sample_for_thread(
@@ -2355,19 +2395,6 @@ void thread_context_collector_on_serialize(VALUE self_instance) {
2355
2395
  return true;
2356
2396
  }
2357
2397
 
2358
- static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
2359
- ENFORCE_THREAD(thread);
2360
-
2361
- debug_enter_unsafe_context();
2362
-
2363
- per_thread_context *thread_context = get_per_thread_context(thread);
2364
- if (thread_context) thread_context_collector_on_gvl_waiting(thread_context);
2365
-
2366
- debug_leave_unsafe_context();
2367
-
2368
- return Qnil;
2369
- }
2370
-
2371
2398
  static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
2372
2399
  ENFORCE_THREAD(thread);
2373
2400
 
@@ -2399,19 +2426,6 @@ void thread_context_collector_on_serialize(VALUE self_instance) {
2399
2426
  return result;
2400
2427
  }
2401
2428
 
2402
- static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2403
- ENFORCE_THREAD(thread);
2404
-
2405
- debug_enter_unsafe_context();
2406
-
2407
- per_thread_context *thread_context = get_per_thread_context(thread);
2408
- if (thread_context) thread_context_collector_on_gvl_released(thread_context);
2409
-
2410
- debug_leave_unsafe_context();
2411
-
2412
- return Qnil;
2413
- }
2414
-
2415
2429
  static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception) {
2416
2430
  ENFORCE_THREAD(thread);
2417
2431
  ENFORCE_BOOLEAN(allow_exception);
@@ -21,6 +21,7 @@ VALUE enforce_thread_context_collector_instance(VALUE object);
21
21
  void thread_context_collector_stats(VALUE self_instance, VALUE stats_hash);
22
22
  void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance);
23
23
  void thread_context_collector_on_serialize(VALUE self_instance);
24
+ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance);
24
25
  void thread_context_collector_profiler_internal_thread_started(void);
25
26
  void thread_context_collector_profiler_internal_thread_done(VALUE self_instance);
26
27
 
@@ -132,9 +132,6 @@ if RUBY_PLATFORM.include?("linux")
132
132
  # but it's slower to build
133
133
  # so instead we just assume that we have the function we need on Linux, and nowhere else
134
134
  $defs << "-DHAVE_PTHREAD_GETCPUCLOCKID"
135
-
136
- # Not available on macOS
137
- $defs << "-DHAVE_CLOCK_MONOTONIC_COARSE"
138
135
  elsif RUBY_PLATFORM.include?("darwin")
139
136
  # On macOS, we use Mach thread APIs to get per-thread CPU time
140
137
  $defs << "-DHAVE_MACH_THREAD_INFO"
@@ -871,3 +871,8 @@ bool is_raised_flag_set(VALUE thread) { return thread_struct_from_object(thread)
871
871
  VALUE current_fiber_for(DDTRACE_UNUSED VALUE thread) { rb_raise(rb_eRuntimeError, "Not implemented for Ruby < 3.1"); }
872
872
  void self_test_current_fiber_for(void) { } // Nothing to do
873
873
  #endif
874
+
875
+ bool pathobj_is_null(VALUE iseq) {
876
+ const rb_iseq_t *iseq_ptr = (const rb_iseq_t *) iseq;
877
+ return ISEQ_BODY(iseq_ptr)->location.pathobj == 0;
878
+ }
@@ -83,3 +83,5 @@ bool is_raised_flag_set(VALUE thread);
83
83
  VALUE current_fiber_for(VALUE thread);
84
84
 
85
85
  void self_test_current_fiber_for(void);
86
+
87
+ bool pathobj_is_null(VALUE iseq);
@@ -4,7 +4,7 @@
4
4
  #include <errno.h>
5
5
  #include <time.h>
6
6
 
7
- #include "extconf.h" // Needed for HAVE_CLOCK_MONOTONIC_COARSE
7
+ #include "extconf.h"
8
8
  #include "ruby_helpers.h"
9
9
 
10
10
  #define SECONDS_AS_NS(value) (value * 1000 * 1000 * 1000L)
@@ -33,7 +33,16 @@ static inline long retrieve_clock_as_ns(clockid_t clock_id, raise_on_failure_set
33
33
  return clock_value.tv_nsec + SECONDS_AS_NS(clock_value.tv_sec);
34
34
  }
35
35
 
36
- static inline long monotonic_wall_time_now_ns(raise_on_failure_setting raise_on_failure) { return retrieve_clock_as_ns(CLOCK_MONOTONIC, raise_on_failure); }
36
+ // CLOCK_MONOTONIC on macOS only has microsecond precision, CLOCK_MONOTONIC_RAW has nanosecond precision
37
+ #ifdef __APPLE__
38
+ #define CLOCK_MONOTONIC_FOR_PROFILING CLOCK_MONOTONIC_RAW
39
+ #define CLOCK_MONOTONIC_COARSE_FOR_PROFILING CLOCK_MONOTONIC_RAW_APPROX
40
+ #else
41
+ #define CLOCK_MONOTONIC_FOR_PROFILING CLOCK_MONOTONIC
42
+ #define CLOCK_MONOTONIC_COARSE_FOR_PROFILING CLOCK_MONOTONIC_COARSE
43
+ #endif
44
+
45
+ static inline long monotonic_wall_time_now_ns(raise_on_failure_setting raise_on_failure) { return retrieve_clock_as_ns(CLOCK_MONOTONIC_FOR_PROFILING, raise_on_failure); }
37
46
  static inline long system_epoch_time_now_ns(raise_on_failure_setting raise_on_failure) { return retrieve_clock_as_ns(CLOCK_REALTIME, raise_on_failure); }
38
47
 
39
48
  // Coarse instants use CLOCK_MONOTONIC_COARSE on Linux which is expected to provide resolution in the millisecond range:
@@ -47,11 +56,7 @@ typedef struct {
47
56
  static inline coarse_instant to_coarse_instant(long timestamp_ns) { return (coarse_instant) {.timestamp_ns = timestamp_ns}; }
48
57
 
49
58
  static inline coarse_instant monotonic_coarse_wall_time_now_ns(void) {
50
- #ifdef HAVE_CLOCK_MONOTONIC_COARSE // Linux
51
- return to_coarse_instant(retrieve_clock_as_ns(CLOCK_MONOTONIC_COARSE, DO_NOT_RAISE_ON_FAILURE));
52
- #else // macOS
53
- return to_coarse_instant(retrieve_clock_as_ns(CLOCK_MONOTONIC, DO_NOT_RAISE_ON_FAILURE));
54
- #endif
59
+ return to_coarse_instant(retrieve_clock_as_ns(CLOCK_MONOTONIC_COARSE_FOR_PROFILING, DO_NOT_RAISE_ON_FAILURE));
55
60
  }
56
61
 
57
62
  long monotonic_to_system_epoch_ns(monotonic_to_system_epoch_state *state, long monotonic_wall_time_ns);
@@ -83,6 +83,11 @@ module Datadog
83
83
  # usually Hash[String, String] but can be a more complex
84
84
  # Hash[String, (String|Array|Hash)] when e.g coming from JSON
85
85
  env['rack.request.form_hash']
86
+ rescue => e
87
+ Datadog.logger.debug { "AppSec: Failed to parse request body: #{e.class}: #{e.message}" }
88
+ AppSec.telemetry.report(e, description: 'AppSec: Failed to parse request body')
89
+
90
+ nil
86
91
  end
87
92
 
88
93
  # Returns the request body size in bytes using all available methods,
@@ -52,6 +52,11 @@ module Datadog
52
52
  body.reject do |k, _v|
53
53
  request.env['action_dispatch.request.path_parameters'].key?(k)
54
54
  end
55
+ rescue => e
56
+ Datadog.logger.debug { "AppSec: Failed to parse request body: #{e.class}: #{e.message}" }
57
+ AppSec.telemetry.report(e, description: 'AppSec: Failed to parse request body')
58
+
59
+ nil
55
60
  end
56
61
 
57
62
  def route_params
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Style/*
3
+ # rubocop:disable Style
4
4
 
5
5
  require 'uri'
6
6
 
@@ -173,4 +173,4 @@ module Datadog
173
173
  end
174
174
  end
175
175
 
176
- # rubocop:enable Style/*
176
+ # rubocop:enable Style
@@ -824,14 +824,14 @@ module Datadog
824
824
  # It must respect the interface of [Datadog::Core::Utils::Time#get_time] method.
825
825
  #
826
826
  # For [Timecop](https://rubygems.org/gems/timecop), for example,
827
- # `->(unit = :float_second) { ::Process.clock_gettime_without_mock(::Process::CLOCK_MONOTONIC, unit) }`
827
+ # `->(unit = :float_second) { ::Process.clock_gettime_without_mock(Datadog::Core::Utils::Time::MONOTONIC_CLOCK_ID, unit) }`
828
828
  # allows Datadog features to use the real monotonic time when time is frozen with
829
829
  # `Timecop.mock_process_clock = true`.
830
830
  #
831
- # @default `->(unit = :float_second) { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, unit)}`
831
+ # @default `->(unit = :float_second) { ::Process.clock_gettime(Core::Utils::Time::MONOTONIC_CLOCK_ID, unit) }`
832
832
  # @return [Proc<Numeric>]
833
833
  option :get_time_provider do |o|
834
- o.default_proc { |unit = :float_second| ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, unit) }
834
+ o.default_proc { |unit = :float_second| ::Process.clock_gettime(Core::Utils::Time::MONOTONIC_CLOCK_ID, unit) }
835
835
  o.type :proc
836
836
 
837
837
  o.after_set do |get_time_provider|
@@ -839,7 +839,7 @@ module Datadog
839
839
  end
840
840
 
841
841
  o.resetter do |_value|
842
- ->(unit = :float_second) { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, unit) }.tap do |default|
842
+ ->(unit = :float_second) { ::Process.clock_gettime(Core::Utils::Time::MONOTONIC_CLOCK_ID, unit) }.tap do |default|
843
843
  Core::Utils::Time.get_time_provider = default
844
844
  end
845
845
  end
@@ -50,6 +50,7 @@ module Datadog
50
50
  "DD_DBM_PROPAGATION_MODE",
51
51
  "DD_DISABLE_DATADOG_RAILS",
52
52
  "DD_DYNAMIC_INSTRUMENTATION_ENABLED",
53
+ "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE",
53
54
  "DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE",
54
55
  "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS",
55
56
  "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES",