datadog 2.35.0 → 2.36.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +68 -31
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +434 -300
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +9 -7
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
  12. data/ext/datadog_profiling_native_extension/extconf.rb +2 -2
  13. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
  15. data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
  16. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
  17. data/ext/datadog_profiling_native_extension/profiling.c +41 -4
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.c +33 -34
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +24 -3
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  21. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
  22. data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
  23. data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/appsec/api_security/route_extractor.rb +6 -0
  26. data/lib/datadog/appsec/component.rb +1 -1
  27. data/lib/datadog/appsec/configuration.rb +7 -0
  28. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
  29. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +64 -19
  30. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
  31. data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
  32. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
  33. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +20 -7
  34. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +92 -0
  35. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +33 -0
  36. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
  37. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +20 -3
  38. data/lib/datadog/appsec/default_header_tags.rb +10 -6
  39. data/lib/datadog/core/configuration/components.rb +1 -0
  40. data/lib/datadog/core/configuration/settings.rb +1 -2
  41. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  42. data/lib/datadog/core/remote/component.rb +1 -1
  43. data/lib/datadog/core/telemetry/event/app_started.rb +0 -21
  44. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  45. data/lib/datadog/core/utils/forking.rb +3 -1
  46. data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
  47. data/lib/datadog/core.rb +3 -0
  48. data/lib/datadog/di/base.rb +4 -1
  49. data/lib/datadog/di/component.rb +1 -1
  50. data/lib/datadog/error_tracking/collector.rb +2 -1
  51. data/lib/datadog/error_tracking/component.rb +2 -2
  52. data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
  53. data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
  54. data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
  55. data/lib/datadog/profiling/collectors/thread_context.rb +1 -0
  56. data/lib/datadog/profiling/component.rb +13 -15
  57. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
  58. data/lib/datadog/ruby_version.rb +25 -0
  59. data/lib/datadog/symbol_database/component.rb +306 -98
  60. data/lib/datadog/symbol_database/extractor.rb +223 -84
  61. data/lib/datadog/tracing/configuration/ext.rb +13 -0
  62. data/lib/datadog/tracing/configuration/settings.rb +17 -0
  63. data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
  64. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
  65. data/lib/datadog/tracing/contrib/grpc.rb +1 -0
  66. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
  67. data/lib/datadog/tracing/contrib/http.rb +1 -0
  68. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
  69. data/lib/datadog/tracing/contrib/karafka.rb +1 -0
  70. data/lib/datadog/tracing/contrib/rack/middlewares.rb +3 -1
  71. data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
  72. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
  73. data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
  74. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
  75. data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
  76. data/lib/datadog/tracing/distributed/propagation.rb +33 -1
  77. data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
  78. data/lib/datadog/tracing/trace_digest.rb +7 -0
  79. data/lib/datadog/tracing/trace_operation.rb +4 -1
  80. data/lib/datadog/tracing/tracer.rb +1 -0
  81. data/lib/datadog/version.rb +1 -1
  82. data/lib/datadog.rb +4 -1
  83. metadata +8 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4029cb3d97dee5a685f9f34027d3f46c960d452eb82339f8a167227f597881aa
4
- data.tar.gz: 3a108d7cb90cb00326d2f9ec563e0370855161ea21c270e31cfe8bbd16b9e154
3
+ metadata.gz: fd8fcd94fa20eb591676abd0f96d6ef69f475f10c08e53d7e4dad1c0cab35e7c
4
+ data.tar.gz: 6e3036aedb4b47ccb9d8c7ec74d070f52e755796e729d99f83c171ca1879ba59
5
5
  SHA512:
6
- metadata.gz: c10991420b5026c39d0ca76529be13c276479236ebf01ab8e5050c110f687266620bfa04127f76cdb55c87b54e84c3cee5ffa39f1de46492f2dd90a58f8ed484
7
- data.tar.gz: f7508958b880c20197ab687ff2eb98c29af51aadbc1835f8ae88a88b8858a520518aefb0d73797ae0728018fe871056c4fa4baccc86aa77b9ec0e1d20a8df298
6
+ metadata.gz: 8de4af357d5807e1855eb4dc078d291af8b601bda3fac1d44d442634dd1b241c7a6b27397095f54a9d931110b503398a43de70878e62c9525629c2b17bcbd97a
7
+ data.tar.gz: 6f8acf814a862cfe261b6a3e97f40c37d0cb86b36deeadf253b14c0b17647e570eca9fca2a4d3dc86126a63d9fa5c0d83f750d495d0f6df0e1d23f43493dad6b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.36.0] - 2026-06-24
6
+
7
+ ### Added
8
+
9
+ * Tracing: Add `DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT` to control trace extraction behavior with `continue`, `restart`, and `ignore` modes ([#5844][])
10
+ * AppSec: Add `DD_APPSEC_BODY_PARSING_SIZE_LIMIT` to control processing of request and response body size; set to 0 to disable ([#5877][])
11
+ * AppSec: Detect attacks from inline fragments in GraphQL queries ([#5916][])
12
+ * Dynamic Instrumentation: Show lazily loaded classes in UI ([#5697][])
13
+
14
+ ### Changed
15
+
16
+ * Dynamic Instrumentation: Reduce peak memory usage during Symbol Database extraction ([#5883][])
17
+ * Profiling: Reduce profiler overhead by up to 50% by skipping redundant samples for threads without the GVL ([#5777][])
18
+ * Profiling: Remove overhead when cleaning up dead threads ([#5816][])
19
+
20
+ ### Fixed
21
+
22
+ * Tracing: Workaround Ruby VM bug causing segmentation faults inside CachingResolver ([#5719][], [#5890][])
23
+ * Dynamic Instrumentation: Prevent uploading stale class definitions for apps using `remove_const`-then-redefine patterns ([#5872][])
24
+ * Profiling: Fix GC profiling being incorrectly disabled on Ruby 3.2.10 and 3.2.11 ([#5894][])
25
+ * Profiling: Fix over-counting of the first allocation sample at profiler startup ([#5881][])
26
+ * Profiling: Fix rare profiler crash during shutdown in heap profiling cleanup ([#5920][])
27
+ * Core: Fix exception message formatting from native extensions ([#5857][])
28
+
5
29
  ## [2.35.0] - 2026-06-03
6
30
 
7
31
  ### Added
@@ -3634,7 +3658,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3634
3658
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3635
3659
 
3636
3660
 
3637
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.35.0...master
3661
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.36.0...master
3662
+ [2.36.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.35.0...v2.36.0
3638
3663
  [2.35.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.34.0...v2.35.0
3639
3664
  [2.34.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.33.0...v2.34.0
3640
3665
  [2.33.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.32.0...v2.33.0
@@ -5379,8 +5404,10 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5379
5404
  [#5681]: https://github.com/DataDog/dd-trace-rb/issues/5681
5380
5405
  [#5687]: https://github.com/DataDog/dd-trace-rb/issues/5687
5381
5406
  [#5689]: https://github.com/DataDog/dd-trace-rb/issues/5689
5407
+ [#5697]: https://github.com/DataDog/dd-trace-rb/issues/5697
5382
5408
  [#5705]: https://github.com/DataDog/dd-trace-rb/issues/5705
5383
5409
  [#5717]: https://github.com/DataDog/dd-trace-rb/issues/5717
5410
+ [#5719]: https://github.com/DataDog/dd-trace-rb/issues/5719
5384
5411
  [#5723]: https://github.com/DataDog/dd-trace-rb/issues/5723
5385
5412
  [#5724]: https://github.com/DataDog/dd-trace-rb/issues/5724
5386
5413
  [#5750]: https://github.com/DataDog/dd-trace-rb/issues/5750
@@ -5389,10 +5416,22 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5389
5416
  [#5762]: https://github.com/DataDog/dd-trace-rb/issues/5762
5390
5417
  [#5768]: https://github.com/DataDog/dd-trace-rb/issues/5768
5391
5418
  [#5773]: https://github.com/DataDog/dd-trace-rb/issues/5773
5419
+ [#5777]: https://github.com/DataDog/dd-trace-rb/issues/5777
5392
5420
  [#5811]: https://github.com/DataDog/dd-trace-rb/issues/5811
5393
5421
  [#5812]: https://github.com/DataDog/dd-trace-rb/issues/5812
5422
+ [#5816]: https://github.com/DataDog/dd-trace-rb/issues/5816
5394
5423
  [#5830]: https://github.com/DataDog/dd-trace-rb/issues/5830
5395
5424
  [#5836]: https://github.com/DataDog/dd-trace-rb/issues/5836
5425
+ [#5844]: https://github.com/DataDog/dd-trace-rb/issues/5844
5426
+ [#5857]: https://github.com/DataDog/dd-trace-rb/issues/5857
5427
+ [#5872]: https://github.com/DataDog/dd-trace-rb/issues/5872
5428
+ [#5877]: https://github.com/DataDog/dd-trace-rb/issues/5877
5429
+ [#5881]: https://github.com/DataDog/dd-trace-rb/issues/5881
5430
+ [#5883]: https://github.com/DataDog/dd-trace-rb/issues/5883
5431
+ [#5890]: https://github.com/DataDog/dd-trace-rb/issues/5890
5432
+ [#5894]: https://github.com/DataDog/dd-trace-rb/issues/5894
5433
+ [#5916]: https://github.com/DataDog/dd-trace-rb/issues/5916
5434
+ [#5920]: https://github.com/DataDog/dd-trace-rb/issues/5920
5396
5435
  [@AdrianLC]: https://github.com/AdrianLC
5397
5436
  [@Azure7111]: https://github.com/Azure7111
5398
5437
  [@BabyGroot]: https://github.com/BabyGroot
@@ -182,6 +182,11 @@ typedef struct {
182
182
  unsigned int allocations_during_sample;
183
183
 
184
184
  // # GVL profiling stats
185
+ // Note that this tracks two kind of samples from RESUMED:
186
+ // * samples when Waiting for GVL for more than the threshold
187
+ // * samples for the skip-samples-while-without-GVL optimization,
188
+ // which are needed to attribute the time without GVL to the correct Ruby stack
189
+
185
190
  // How many times we triggered the after_gvl_running sampling
186
191
  unsigned int after_gvl_running;
187
192
  // How many times we skipped the after_gvl_running sampling
@@ -659,7 +664,7 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
659
664
  if (sample_from_signal_handler) {
660
665
  // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
661
666
  // until the postponed job below gets run.
662
- bool prepared = thread_context_collector_prepare_sample_inside_signal_handler(state->thread_context_collector_instance);
667
+ bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
663
668
 
664
669
  if (prepared) state->stats.signal_handler_prepared_sample++;
665
670
  }
@@ -808,8 +813,7 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
808
813
 
809
814
  state->stats.cpu_sampled++;
810
815
 
811
- VALUE profiler_overhead_stack_thread = state->owner_thread; // Used to attribute profiler overhead to a different stack
812
- thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample, profiler_overhead_stack_thread);
816
+ thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
813
817
 
814
818
  long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
815
819
  long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
@@ -865,15 +869,12 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
865
869
 
866
870
  if (state->gvl_profiling_enabled) {
867
871
  #ifndef NO_GVL_INSTRUMENTATION
868
- #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS
869
- gvl_profiling_state_thread_tracking_workaround();
870
- #endif
871
-
872
872
  state->gvl_profiling_hook = rb_internal_thread_add_event_hook(
873
873
  on_gvl_event,
874
874
  (
875
875
  // For now we're only asking for these events, even though there's more
876
876
  // (e.g. check docs or gvl-tracing gem)
877
+ RUBY_INTERNAL_THREAD_EVENT_SUSPENDED | /* released gvl */
877
878
  RUBY_INTERNAL_THREAD_EVENT_READY /* waiting for gvl */ |
878
879
  RUBY_INTERNAL_THREAD_EVENT_RESUMED /* running/runnable */
879
880
  ),
@@ -1055,6 +1056,9 @@ static VALUE _native_simulate_sample_from_postponed_job(DDTRACE_UNUSED VALUE sel
1055
1056
  //
1056
1057
  // In the future, if we add more other components with tracepoints, we will need to coordinate stopping all such
1057
1058
  // tracepoints before doing the other cleaning steps.
1059
+ //
1060
+ // Note that tests call this method directly in the same process without forking,
1061
+ // and in such a case non-current Threads keep running.
1058
1062
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE instance) {
1059
1063
  cpu_and_wall_time_worker_state *state;
1060
1064
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -1129,6 +1133,9 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
1129
1133
  ID2SYM(rb_intern("gvl_waiting_time_ns_total")), /* => */ state->gvl_profiling_enabled ? ULL2NUM(state->stats.vm_metrics.gvl_waiting_time_ns_total) : Qnil,
1130
1134
  };
1131
1135
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(stats_as_hash, arguments[i], arguments[i+1]);
1136
+
1137
+ thread_context_collector_stats(state->thread_context_collector_instance, stats_as_hash);
1138
+
1132
1139
  return stats_as_hash;
1133
1140
  }
1134
1141
 
@@ -1136,6 +1143,7 @@ static VALUE _native_stats_reset_not_thread_safe(DDTRACE_UNUSED VALUE self, VALU
1136
1143
  cpu_and_wall_time_worker_state *state;
1137
1144
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1138
1145
  reset_stats_not_thread_safe(state);
1146
+ thread_context_collector_stats_reset_not_thread_safe(state->thread_context_collector_instance);
1139
1147
  return Qnil;
1140
1148
  }
1141
1149
 
@@ -1245,13 +1253,23 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1245
1253
  return;
1246
1254
  }
1247
1255
 
1256
+ VALUE current_thread = rb_thread_current();
1257
+
1248
1258
  // If Ruby is in the middle of raising an exception, we don't want to try to sample. This is because if we accidentally
1249
1259
  // trigger an exception inside the profiler code, bad things will happen (specifically, Ruby will try to kill off the
1250
1260
  // thread even though we may try to catch the exception).
1251
1261
  //
1252
1262
  // Note that "in the middle of raising an exception" means the exception itself has already been allocated.
1253
1263
  // What's getting allocated now is probably the backtrace objects (@ivoanjo or at least that's what I've observed)
1254
- if (is_raised_flag_set(rb_thread_current())) {
1264
+ if (is_raised_flag_set(current_thread)) {
1265
+ return;
1266
+ }
1267
+
1268
+ per_thread_context *thread_context = get_per_thread_context(current_thread);
1269
+ if (!thread_context) {
1270
+ // No per_thread_context yet on this Thread, we can't use get_or_create_context_for() as that allocates,
1271
+ // and we are inside on_newobj_event where we MUST NOT allocate.
1272
+ // So we don't sample allocations until another hook allocates the per_thread_context.
1255
1273
  return;
1256
1274
  }
1257
1275
 
@@ -1286,7 +1304,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1286
1304
  // Rescue against any exceptions that happen during sampling
1287
1305
  safely_call(
1288
1306
  rescued_sample_allocation,
1289
- Qnil,
1307
+ (VALUE) thread_context,
1290
1308
  state->self_instance,
1291
1309
  handle_sampling_failure_rescued_sample_allocation
1292
1310
  );
@@ -1338,7 +1356,8 @@ static VALUE _native_with_blocked_sigprof(DDTRACE_UNUSED VALUE self) {
1338
1356
  }
1339
1357
  }
1340
1358
 
1341
- static VALUE rescued_sample_allocation(DDTRACE_UNUSED VALUE unused) {
1359
+ static VALUE rescued_sample_allocation(VALUE arg) {
1360
+ per_thread_context *thread_context = (per_thread_context*) arg;
1342
1361
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1343
1362
 
1344
1363
  // This should not happen in a normal situation because on_newobj_event already checked for this, but just in case...
@@ -1357,7 +1376,7 @@ static VALUE rescued_sample_allocation(DDTRACE_UNUSED VALUE unused) {
1357
1376
  // To control bias from sampling, we clamp the maximum weight attributed to a single allocation sample. This avoids
1358
1377
  // assigning a very large number to a sample, if for instance the dynamic sampling mechanism chose a really big interval.
1359
1378
  unsigned int weight = allocations_since_last_sample > MAX_ALLOC_WEIGHT ? MAX_ALLOC_WEIGHT : (unsigned int) allocations_since_last_sample;
1360
- bool needs_after_allocation = thread_context_collector_sample_allocation(state->thread_context_collector_instance, weight, new_object);
1379
+ bool needs_after_allocation = thread_context_collector_sample_allocation(state->thread_context_collector_instance, thread_context, weight, new_object);
1361
1380
  // ...but we still represent the skipped samples in the profile, thus the data will account for all allocations.
1362
1381
  if (weight < allocations_since_last_sample) {
1363
1382
  uint32_t skipped_samples = (uint32_t) uint64_min_of(allocations_since_last_sample - weight, UINT32_MAX);
@@ -1414,30 +1433,49 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1414
1433
  #ifndef NO_GVL_INSTRUMENTATION
1415
1434
  static void on_gvl_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *event_data, DDTRACE_UNUSED void *_unused) {
1416
1435
  // Be very careful about touching the `state` here or doing anything at all:
1417
- // This function gets called without the GVL, and potentially from background Ractors!
1418
- //
1419
- // In fact, the `target_thread` that this event is about may not even be the current thread. (So be careful with thread locals that
1420
- // are not directly tied to the `target_thread` object and the like)
1421
- gvl_profiling_thread target_thread = thread_from_event(event_data);
1436
+ // This function gets called without the GVL, and potentially from non-main Ractors!
1422
1437
 
1423
- if (event_id == RUBY_INTERNAL_THREAD_EVENT_READY) { /* waiting for gvl */
1424
- thread_context_collector_on_gvl_waiting(target_thread);
1425
- } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) { /* running/runnable */
1426
- // Interesting note: A RUBY_INTERNAL_THREAD_EVENT_RESUMED is guaranteed to be called with the GVL being acquired.
1427
- // (And... I think target_thread will be == rb_thread_current()?)
1428
- //
1429
- // But we're not sure if we're on the main Ractor yet. The thread context collector can actually help here:
1430
- // it tags threads it's tracking, so if a thread is tagged then by definition we know that thread belongs to the main
1431
- // Ractor. Thus, if we get a ON_GVL_RUNNING_UNKNOWN result we shouldn't touch any state, but otherwise we're good to go.
1438
+ // The thread that this event is about may not be the current thread
1439
+ // (as documented on rb_internal_thread_add_event_hook(), and this is notably the case for READY on Ruby 4.0),
1440
+ // so be careful with native thread locals that are not directly tied to the thread object and the like.
1432
1441
 
1433
- #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS
1434
- target_thread = gvl_profiling_state_maybe_initialize();
1435
- #endif
1442
+ // On Ruby 3.2 the event does not carry the thread, but all events always fire on the event thread on Ruby 3.2.
1443
+ // However, during early thread startup rb_thread_current() can crash because the execution context (Fiber) isn't
1444
+ // stored in TLS yet; ruby_native_thread_p() guards against this.
1445
+ #ifdef HAVE_RUBY_THREAD_STORAGE_API
1446
+ VALUE target_thread = event_data->thread;
1447
+ #else
1448
+ if (!ruby_native_thread_p()) return;
1449
+ VALUE target_thread = rb_thread_current();
1450
+ #endif
1436
1451
 
1452
+ per_thread_context* thread_context = get_per_thread_context(target_thread);
1453
+ if (!thread_context) return;
1454
+ // If non-NULL the thread is profiled and from the main Ractor
1455
+
1456
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED) { /* released gvl */
1457
+ thread_context_collector_on_gvl_released(thread_context);
1458
+ } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_READY) { /* waiting for gvl */
1459
+ thread_context_collector_on_gvl_waiting(thread_context);
1460
+ } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) { /* running/runnable */
1461
+ // Interesting note: A RUBY_INTERNAL_THREAD_EVENT_RESUMED is guaranteed to be called with the GVL being acquired
1462
+ // and on the event thread.
1463
+ // However, on_gvl_event() is called while holding the scheduler lock, so we do as little work as possible here,
1464
+ // and perform the sample in a postponed_job.
1437
1465
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1438
1466
  if (state == NULL) return; // This should not happen, but just in case...
1439
1467
 
1440
- on_gvl_running_result result = thread_context_collector_on_gvl_running(target_thread);
1468
+ // on_gvl_running prepares a stack sample so we need to gate it with `during_sample_enter` to avoid a
1469
+ // SIGPROF signal coming in during that function and potentially causing a corrupted final state.
1470
+ //
1471
+ // TODO: Currently, the sample prepared in on_gvl_running can still be clobbered if the signal handler runs
1472
+ // after `during_sample_exit` but before `after_gvl_running_from_postponed_job` gets to run. We'll need to fix
1473
+ // that next.
1474
+ during_sample_enter(state);
1475
+
1476
+ on_gvl_running_result result = thread_context_collector_on_gvl_running(state->thread_context_collector_instance, target_thread, thread_context);
1477
+
1478
+ during_sample_exit(state);
1441
1479
 
1442
1480
  if (result.waiting_for_gvl_duration_ns > 0) {
1443
1481
  state->stats.vm_metrics.gvl_waiting_time_ns_total += (uint64_t) result.waiting_for_gvl_duration_ns;
@@ -1453,8 +1491,7 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1453
1491
  state->stats.gvl_dont_sample++;
1454
1492
  }
1455
1493
  } else {
1456
- // This is a very delicate time and it's hard for us to raise an exception so let's at least complain to stderr
1457
- fprintf(stderr, "[ddtrace] Unexpected value in on_gvl_event (%d)\n", event_id);
1494
+ rb_bug("[ddtrace] Unexpected value in on_gvl_event (%d)\n", event_id);
1458
1495
  }
1459
1496
  }
1460
1497
 
@@ -6,7 +6,7 @@
6
6
  #include "ruby_helpers.h"
7
7
 
8
8
  #define BASE_OVERHEAD_PCT 1.0
9
- #define BASE_SAMPLING_INTERVAL 50
9
+ #define BASE_SAMPLING_INTERVAL 1
10
10
 
11
11
  #define ADJUSTMENT_WINDOW_NS SECONDS_AS_NS(1)
12
12
  #define ADJUSTMENT_WINDOW_SAMPLES 100
@@ -208,7 +208,7 @@ void idle_sampling_helper_request_action(VALUE self_instance, void (*run_action_
208
208
  if (!rb_typeddata_is_kind_of(self_instance, &idle_sampling_helper_typed_data)) {
209
209
  grab_gvl_and_raise(rb_eTypeError, "Wrong argument for idle_sampling_helper_request_action");
210
210
  }
211
- // This should never fail the the above check passes
211
+ // This should never fail when the above check passes
212
212
  TypedData_Get_Struct(self_instance, idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
213
213
 
214
214
  ENFORCE_SUCCESS_NO_GVL(pthread_mutex_lock(&state->wakeup_mutex));
@@ -40,7 +40,7 @@ static void set_file_info_for_cfunc(
40
40
  st_table *native_filenames_cache
41
41
  );
42
42
  static const char *get_or_compute_native_filename(void *function, st_table *native_filenames_cache);
43
- static void add_truncated_frames_placeholder(sampling_buffer* buffer);
43
+ static void add_truncated_frames_placeholder(ddog_prof_Location *locations);
44
44
  static void record_placeholder_stack_in_native_code(VALUE recorder_instance, sample_values values, sample_labels labels);
45
45
  static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_CharSlice *filename_slice);
46
46
 
@@ -98,7 +98,7 @@ typedef struct {
98
98
  sample_values values;
99
99
  sample_labels labels;
100
100
  VALUE thread;
101
- ddog_prof_Location *locations;
101
+ sample_locations locations;
102
102
  sampling_buffer *buffer;
103
103
  bool native_filenames_enabled;
104
104
  st_table *native_filenames_cache;
@@ -175,7 +175,7 @@ static VALUE _native_sample(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
175
175
 
176
176
  ddog_prof_Location *locations = ruby_xcalloc(max_frames_requested, sizeof(ddog_prof_Location));
177
177
  sampling_buffer buffer;
178
- sampling_buffer_initialize(&buffer, max_frames_requested, locations);
178
+ sampling_buffer_initialize(&buffer, max_frames_requested);
179
179
 
180
180
  ddog_prof_Slice_Label slice_labels = {.ptr = labels, .len = labels_count};
181
181
 
@@ -185,7 +185,7 @@ static VALUE _native_sample(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
185
185
  .values = values,
186
186
  .labels = (sample_labels) {.labels = slice_labels, .state_label = state_label, .is_gvl_waiting_state = is_gvl_waiting_state == Qtrue},
187
187
  .thread = thread,
188
- .locations = locations,
188
+ .locations = (sample_locations) {.ptr = locations, .len = max_frames_requested},
189
189
  .buffer = &buffer,
190
190
  .native_filenames_enabled = native_filenames_enabled == Qtrue,
191
191
  .native_filenames_cache = st_init_numtable(),
@@ -208,6 +208,7 @@ static VALUE native_sample_do(VALUE args) {
208
208
  sample_thread(
209
209
  args_struct->thread,
210
210
  args_struct->buffer,
211
+ args_struct->locations,
211
212
  args_struct->recorder_instance,
212
213
  args_struct->values,
213
214
  args_struct->labels,
@@ -222,7 +223,7 @@ static VALUE native_sample_do(VALUE args) {
222
223
  static VALUE native_sample_ensure(VALUE args) {
223
224
  native_sample_args *args_struct = (native_sample_args *) args;
224
225
 
225
- ruby_xfree(args_struct->locations);
226
+ ruby_xfree(args_struct->locations.ptr);
226
227
  sampling_buffer_free(args_struct->buffer);
227
228
  st_free_table(args_struct->native_filenames_cache);
228
229
 
@@ -243,6 +244,7 @@ static VALUE native_sample_ensure(VALUE args) {
243
244
  void sample_thread(
244
245
  VALUE thread,
245
246
  sampling_buffer* buffer,
247
+ sample_locations locations,
246
248
  VALUE recorder_instance,
247
249
  sample_values values,
248
250
  sample_labels labels,
@@ -250,11 +252,21 @@ void sample_thread(
250
252
  st_table *native_filenames_cache
251
253
  ) {
252
254
  // If we already prepared a sample, we use it below; if not, we prepare it now.
253
- if (!buffer->pending_sample) prepare_sample_thread(thread, buffer);
255
+ if (!buffer->pending_sample) {
256
+ // Reconcile the sampling_buffer's max_frames with the locations size
257
+ if (buffer->max_frames != locations.len) {
258
+ sampling_buffer_reinitialize(buffer, locations.len);
259
+ }
260
+ prepare_sample_thread(thread, buffer);
261
+ }
254
262
 
255
263
  buffer->pending_sample = false;
256
264
  int captured_frames = buffer->pending_sample_result;
257
265
 
266
+ // The per_thread_context's sampling_buffer may have been created by a previous collector with a
267
+ // different (larger) max_frames. Cap to the locations array size to prevent out-of-bounds writes.
268
+ if (captured_frames > (int) locations.len) captured_frames = (int) locations.len;
269
+
258
270
  if (captured_frames == PLACEHOLDER_STACK_IN_NATIVE_CODE) {
259
271
  record_placeholder_stack_in_native_code(recorder_instance, values, labels);
260
272
  return;
@@ -389,25 +401,30 @@ void sample_thread(
389
401
 
390
402
  int libdatadog_stores_stacks_flipped_from_rb_profile_frames_index = top_of_stack_position - i;
391
403
 
392
- buffer->locations[libdatadog_stores_stacks_flipped_from_rb_profile_frames_index] = (ddog_prof_Location) {
404
+ locations.ptr[libdatadog_stores_stacks_flipped_from_rb_profile_frames_index] = (ddog_prof_Location) {
393
405
  .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C(""), .build_id_id = {}},
394
406
  .function = (ddog_prof_Function) {.name = name_slice, .filename = filename_slice},
395
407
  .line = line,
396
408
  };
397
409
  }
398
410
 
399
- // If we filled up the buffer, some frames may have been omitted. In that case, we'll add a placeholder frame
411
+ // If we filled up the locations, some frames may have been omitted. In that case, we'll add a placeholder frame
400
412
  // with that info.
401
- if (captured_frames == (long) buffer->max_frames) {
402
- add_truncated_frames_placeholder(buffer);
413
+ if (captured_frames == (long) locations.len) {
414
+ add_truncated_frames_placeholder(locations.ptr);
403
415
  }
404
416
 
405
417
  record_sample(
406
418
  recorder_instance,
407
- (ddog_prof_Slice_Location) {.ptr = buffer->locations, .len = captured_frames},
419
+ (ddog_prof_Slice_Location) {.ptr = locations.ptr, .len = captured_frames},
408
420
  values,
409
421
  labels
410
422
  );
423
+
424
+ // Reconcile the sampling_buffer's max_frames with the locations size for future samples
425
+ if (buffer->max_frames != locations.len) {
426
+ sampling_buffer_reinitialize(buffer, locations.len);
427
+ }
411
428
  }
412
429
 
413
430
  #if (defined(HAVE_DLADDR1) && HAVE_DLADDR1) || (defined(HAVE_DLADDR) && HAVE_DLADDR)
@@ -538,10 +555,10 @@ static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_Char
538
555
  name_slice->len = pos;
539
556
  }
540
557
 
541
- static void add_truncated_frames_placeholder(sampling_buffer* buffer) {
558
+ static void add_truncated_frames_placeholder(ddog_prof_Location *locations) {
542
559
  // Important note: The strings below are static so we don't need to worry about their lifetime. If we ever want to change
543
560
  // this to non-static strings, don't forget to check that lifetimes are properly respected.
544
- buffer->locations[0] = (ddog_prof_Location) {
561
+ locations[0] = (ddog_prof_Location) {
545
562
  .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C(""), .build_id_id = {}},
546
563
  .function = {.name = DDOG_CHARSLICE_C("Truncated Frames"), .filename = DDOG_CHARSLICE_C(""), .filename_id = {}},
547
564
  .line = 0,
@@ -618,27 +635,29 @@ uint16_t sampling_buffer_check_max_frames(int max_frames) {
618
635
  return max_frames;
619
636
  }
620
637
 
621
- void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames, ddog_prof_Location *locations) {
638
+ void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames) {
622
639
  sampling_buffer_check_max_frames(max_frames);
623
640
 
624
641
  buffer->max_frames = max_frames;
625
- buffer->locations = locations;
626
642
  buffer->stack_buffer = ruby_xcalloc(max_frames, sizeof(frame_info));
627
643
  buffer->pending_sample = false;
628
644
  buffer->is_marking = false;
629
645
  buffer->pending_sample_result = 0;
630
646
  }
631
647
 
648
+ void sampling_buffer_reinitialize(sampling_buffer *buffer, uint16_t max_frames) {
649
+ sampling_buffer_free(buffer);
650
+ sampling_buffer_initialize(buffer, max_frames);
651
+ }
652
+
632
653
  void sampling_buffer_free(sampling_buffer *buffer) {
633
- if (buffer->max_frames == 0 || buffer->locations == NULL || buffer->stack_buffer == NULL) {
654
+ if (buffer->max_frames == 0 || buffer->stack_buffer == NULL) {
634
655
  raise_error(rb_eArgError, "sampling_buffer_free called with invalid buffer");
635
656
  }
636
657
 
637
658
  ruby_xfree(buffer->stack_buffer);
638
- // Note: buffer->locations are owned by whoever called sampling_buffer_initialize, not by the buffer itself
639
659
 
640
660
  buffer->max_frames = 0;
641
- buffer->locations = NULL;
642
661
  buffer->stack_buffer = NULL;
643
662
  buffer->pending_sample = false;
644
663
  buffer->is_marking = false;
@@ -11,16 +11,21 @@
11
11
  // Used as scratch space during sampling
12
12
  typedef struct {
13
13
  uint16_t max_frames;
14
- ddog_prof_Location *locations;
15
14
  frame_info *stack_buffer;
16
15
  bool pending_sample;
17
16
  bool is_marking; // Used to avoid recording a sample when marking
18
17
  int pending_sample_result;
19
18
  } sampling_buffer;
20
19
 
20
+ typedef struct {
21
+ ddog_prof_Location *ptr;
22
+ uint16_t len;
23
+ } sample_locations;
24
+
21
25
  void sample_thread(
22
26
  VALUE thread,
23
27
  sampling_buffer* buffer,
28
+ sample_locations locations,
24
29
  VALUE recorder_instance,
25
30
  sample_values values,
26
31
  sample_labels labels,
@@ -36,7 +41,8 @@ void record_placeholder_stack(
36
41
  bool prepare_sample_thread(VALUE thread, sampling_buffer *buffer);
37
42
 
38
43
  uint16_t sampling_buffer_check_max_frames(int max_frames);
39
- void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames, ddog_prof_Location *locations);
44
+ void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames);
45
+ void sampling_buffer_reinitialize(sampling_buffer *buffer, uint16_t max_frames);
40
46
  void sampling_buffer_free(sampling_buffer *buffer);
41
47
  void sampling_buffer_mark(sampling_buffer *buffer);
42
48
  static inline bool sampling_buffer_needs_marking(sampling_buffer *buffer) {