datadog 2.35.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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -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 +71 -31
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +18 -4
  7. data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
  8. data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +596 -341
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +11 -7
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
  12. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
  13. data/ext/datadog_profiling_native_extension/extconf.rb +13 -2
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
  15. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
  16. data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
  17. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
  18. data/ext/datadog_profiling_native_extension/profiling.c +41 -4
  19. data/ext/datadog_profiling_native_extension/ruby_helpers.c +40 -34
  20. data/ext/datadog_profiling_native_extension/stack_recorder.c +24 -3
  21. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  22. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
  23. data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
  24. data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
  25. data/ext/libdatadog_api/di.c +76 -0
  26. data/ext/libdatadog_api/extconf.rb +6 -0
  27. data/ext/libdatadog_extconf_helpers.rb +1 -1
  28. data/lib/datadog/ai_guard/api_client.rb +6 -4
  29. data/lib/datadog/appsec/api_security/route_extractor.rb +13 -4
  30. data/lib/datadog/appsec/component.rb +1 -1
  31. data/lib/datadog/appsec/configuration.rb +13 -0
  32. data/lib/datadog/appsec/context.rb +4 -2
  33. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
  34. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  35. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  36. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +64 -19
  37. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
  38. data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
  39. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
  40. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +24 -7
  41. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +79 -0
  42. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  43. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +35 -1
  44. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
  45. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  46. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +24 -3
  47. data/lib/datadog/appsec/default_header_tags.rb +10 -6
  48. data/lib/datadog/appsec/ext.rb +1 -0
  49. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  50. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  51. data/lib/datadog/appsec/remote.rb +9 -3
  52. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  53. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  54. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  55. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  56. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  57. data/lib/datadog/core/configuration/components.rb +83 -2
  58. data/lib/datadog/core/configuration/components_state.rb +6 -1
  59. data/lib/datadog/core/configuration/settings.rb +1 -5
  60. data/lib/datadog/core/configuration/supported_configurations.rb +4 -0
  61. data/lib/datadog/core/evp.rb +11 -0
  62. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  63. data/lib/datadog/core/remote/component.rb +1 -1
  64. data/lib/datadog/core/telemetry/event/app_started.rb +8 -22
  65. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  66. data/lib/datadog/core/utils/forking.rb +3 -1
  67. data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
  68. data/lib/datadog/core.rb +3 -0
  69. data/lib/datadog/di/base.rb +7 -2
  70. data/lib/datadog/di/code_tracker.rb +5 -2
  71. data/lib/datadog/di/component.rb +110 -36
  72. data/lib/datadog/di/error.rb +10 -0
  73. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  74. data/lib/datadog/di/instrumenter.rb +381 -165
  75. data/lib/datadog/di/probe.rb +14 -0
  76. data/lib/datadog/di/probe_file_loader.rb +8 -3
  77. data/lib/datadog/di/probe_manager.rb +67 -9
  78. data/lib/datadog/di/probe_notification_builder.rb +4 -1
  79. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  80. data/lib/datadog/di/redactor.rb +16 -1
  81. data/lib/datadog/di/remote.rb +175 -14
  82. data/lib/datadog/di/serializer.rb +12 -5
  83. data/lib/datadog/di/transport/input.rb +8 -4
  84. data/lib/datadog/di.rb +63 -0
  85. data/lib/datadog/error_tracking/collector.rb +2 -1
  86. data/lib/datadog/error_tracking/component.rb +2 -2
  87. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  88. data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
  89. data/lib/datadog/open_feature/component.rb +29 -6
  90. data/lib/datadog/open_feature/configuration.rb +8 -0
  91. data/lib/datadog/open_feature/ext.rb +2 -0
  92. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  93. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  94. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  95. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  96. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  97. data/lib/datadog/open_feature/provider.rb +59 -27
  98. data/lib/datadog/open_feature/transport.rb +55 -1
  99. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  100. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  101. data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
  102. data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
  103. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  104. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  105. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  106. data/lib/datadog/profiling/collectors/thread_context.rb +1 -0
  107. data/lib/datadog/profiling/component.rb +13 -15
  108. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
  109. data/lib/datadog/ruby_version.rb +25 -0
  110. data/lib/datadog/symbol_database/component.rb +453 -128
  111. data/lib/datadog/symbol_database/configuration.rb +12 -4
  112. data/lib/datadog/symbol_database/extensions.rb +19 -0
  113. data/lib/datadog/symbol_database/extractor.rb +326 -150
  114. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  115. data/lib/datadog/symbol_database/remote.rb +8 -3
  116. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  117. data/lib/datadog/symbol_database/uploader.rb +9 -2
  118. data/lib/datadog/symbol_database.rb +22 -0
  119. data/lib/datadog/tracing/configuration/ext.rb +13 -0
  120. data/lib/datadog/tracing/configuration/settings.rb +17 -0
  121. data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
  122. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
  123. data/lib/datadog/tracing/contrib/grpc.rb +1 -0
  124. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
  125. data/lib/datadog/tracing/contrib/http.rb +1 -0
  126. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
  127. data/lib/datadog/tracing/contrib/karafka.rb +1 -0
  128. data/lib/datadog/tracing/contrib/rack/middlewares.rb +3 -1
  129. data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
  130. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
  131. data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
  132. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
  133. data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
  134. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  135. data/lib/datadog/tracing/distributed/propagation.rb +33 -1
  136. data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
  137. data/lib/datadog/tracing/ext.rb +9 -0
  138. data/lib/datadog/tracing/remote.rb +34 -3
  139. data/lib/datadog/tracing/trace_digest.rb +7 -0
  140. data/lib/datadog/tracing/trace_operation.rb +4 -1
  141. data/lib/datadog/tracing/tracer.rb +1 -0
  142. data/lib/datadog/version.rb +1 -1
  143. data/lib/datadog.rb +5 -1
  144. metadata +22 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4029cb3d97dee5a685f9f34027d3f46c960d452eb82339f8a167227f597881aa
4
- data.tar.gz: 3a108d7cb90cb00326d2f9ec563e0370855161ea21c270e31cfe8bbd16b9e154
3
+ metadata.gz: 5435fe8fe48f8cd33010ad82ee5fe6f969077a86cdcca6a96cc9d8633c89eb0e
4
+ data.tar.gz: 3681864939e4601b11bb94fa4b6647d1505efc621eae82ac9664fcc3819b34a3
5
5
  SHA512:
6
- metadata.gz: c10991420b5026c39d0ca76529be13c276479236ebf01ab8e5050c110f687266620bfa04127f76cdb55c87b54e84c3cee5ffa39f1de46492f2dd90a58f8ed484
7
- data.tar.gz: f7508958b880c20197ab687ff2eb98c29af51aadbc1835f8ae88a88b8858a520518aefb0d73797ae0728018fe871056c4fa4baccc86aa77b9ec0e1d20a8df298
6
+ metadata.gz: de74ddeb7271f1ced1c21dde221b2bdd10fec9b7c9f8663cdf01f146c6a242d93dcde9ac7c63828e41045565f4870359469124a55ef999847f1d02416e36b919
7
+ data.tar.gz: 2d7d44bb00879fa869c5ec630654632632fd74719c7cb276a9a5310606a952cf3eece0bd52c70b25a2a75cb6e69949a1f3ab9597badfb89e17175905b5e08f3e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.37.0] - 2026-07-06
6
+
7
+ ### Added
8
+
9
+ * AppSec: Add sign-up event tracking to `Datadog::Kit::AppSec::Events::V2` ([#5973][])
10
+ * AppSec: Integrations: Add normalized HTTP route for API Security Testing ([#5834][])
11
+ * Dynamic Instrumentation: DI can now be enabled from the Datadog UI without setting `DD_DYNAMIC_INSTRUMENTATION_ENABLED` or restarting the application. Setting `DD_DYNAMIC_INSTRUMENTATION_ENABLED=false` explicitly still disables it and blocks UI-driven enablement ([#5525][])
12
+
13
+ ### Changed
14
+
15
+ * Profiling: Reduce profiler overhead by sampling profiler-internal threads only once per minute ([#5955][])
16
+ * Dynamic Instrumentation: Reject method probes that target `Kernel#lambda`, which cannot be safely instrumented ([#5954][])
17
+ * Dynamic Instrumentation: Reject method probes that target classes or modules in the Datadog namespace ([#5907][])
18
+ * Dynamic Instrumentation: Enable Symbol Database by default when Dynamic Instrumentation is enabled ([#5828][])
19
+
20
+ ### Fixed
21
+
22
+ * AppSec: AI Guard: Fix endpoint configuration via `DD_SITE` environment variable ([#5940][])
23
+ * AppSec: Fix API Security schema extraction timeout and telemetry ([#5975][])
24
+ * AppSec: Fix remote configuration errors when the ruleset name changes ([#5970][])
25
+ * AppSec: Integrations: Add configurable size limit for downstream HTTP request and response body analysis ([#5990][])
26
+ * AppSec: Integrations: Fix WAF analysis for requests with cached or parsed bodies in Rack and Sinatra ([#5991][])
27
+ * AppSec: Integrations: Fix an exception for unauthorized requests when using Devise with Rails. ([#5987][])
28
+ * Tracing: Fix a `SIGSEGV` crash that could happen with experimental heap profiling enabled on Ruby 4.0 ([#5938][]) ([@navidemad][])
29
+ * Dynamic Instrumentation: Normalize Rack CGI header prefix (`HTTP_<HEADER>`) in redactor so snapshot captures redact sensitive headers like their bare names ([#5911][])
30
+ * Dynamic Instrumentation: Symbol Database and DI background threads no longer stop when application code raises a non-StandardError during introspection ([#5945][])
31
+
32
+ ## [2.36.0] - 2026-06-24
33
+
34
+ ### Added
35
+
36
+ * Tracing: Add `DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT` to control trace extraction behavior with `continue`, `restart`, and `ignore` modes ([#5844][])
37
+ * AppSec: Add `DD_APPSEC_BODY_PARSING_SIZE_LIMIT` to control processing of request and response body size; set to 0 to disable ([#5877][])
38
+ * AppSec: Detect attacks from inline fragments in GraphQL queries ([#5916][])
39
+ * Dynamic Instrumentation: Show lazily loaded classes in UI ([#5697][])
40
+
41
+ ### Changed
42
+
43
+ * Dynamic Instrumentation: Reduce peak memory usage during Symbol Database extraction ([#5883][])
44
+ * Profiling: Reduce profiler overhead by up to 50% by skipping redundant samples for threads without the GVL ([#5777][])
45
+ * Profiling: Remove overhead when cleaning up dead threads ([#5816][])
46
+
47
+ ### Fixed
48
+
49
+ * Tracing: Workaround Ruby VM bug causing segmentation faults inside CachingResolver ([#5719][], [#5890][])
50
+ * Dynamic Instrumentation: Prevent uploading stale class definitions for apps using `remove_const`-then-redefine patterns ([#5872][])
51
+ * Profiling: Fix GC profiling being incorrectly disabled on Ruby 3.2.10 and 3.2.11 ([#5894][])
52
+ * Profiling: Fix over-counting of the first allocation sample at profiler startup ([#5881][])
53
+ * Profiling: Fix rare profiler crash during shutdown in heap profiling cleanup ([#5920][])
54
+ * Core: Fix exception message formatting from native extensions ([#5857][])
55
+
5
56
  ## [2.35.0] - 2026-06-03
6
57
 
7
58
  ### Added
@@ -3634,7 +3685,9 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3634
3685
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3635
3686
 
3636
3687
 
3637
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.35.0...master
3688
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.37.0...master
3689
+ [2.37.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.36.0...v2.37.0
3690
+ [2.36.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.35.0...v2.36.0
3638
3691
  [2.35.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.34.0...v2.35.0
3639
3692
  [2.34.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.33.0...v2.34.0
3640
3693
  [2.33.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.32.0...v2.33.0
@@ -5360,6 +5413,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5360
5413
  [#5499]: https://github.com/DataDog/dd-trace-rb/issues/5499
5361
5414
  [#5501]: https://github.com/DataDog/dd-trace-rb/issues/5501
5362
5415
  [#5509]: https://github.com/DataDog/dd-trace-rb/issues/5509
5416
+ [#5525]: https://github.com/DataDog/dd-trace-rb/issues/5525
5363
5417
  [#5531]: https://github.com/DataDog/dd-trace-rb/issues/5531
5364
5418
  [#5564]: https://github.com/DataDog/dd-trace-rb/issues/5564
5365
5419
  [#5569]: https://github.com/DataDog/dd-trace-rb/issues/5569
@@ -5379,8 +5433,10 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5379
5433
  [#5681]: https://github.com/DataDog/dd-trace-rb/issues/5681
5380
5434
  [#5687]: https://github.com/DataDog/dd-trace-rb/issues/5687
5381
5435
  [#5689]: https://github.com/DataDog/dd-trace-rb/issues/5689
5436
+ [#5697]: https://github.com/DataDog/dd-trace-rb/issues/5697
5382
5437
  [#5705]: https://github.com/DataDog/dd-trace-rb/issues/5705
5383
5438
  [#5717]: https://github.com/DataDog/dd-trace-rb/issues/5717
5439
+ [#5719]: https://github.com/DataDog/dd-trace-rb/issues/5719
5384
5440
  [#5723]: https://github.com/DataDog/dd-trace-rb/issues/5723
5385
5441
  [#5724]: https://github.com/DataDog/dd-trace-rb/issues/5724
5386
5442
  [#5750]: https://github.com/DataDog/dd-trace-rb/issues/5750
@@ -5389,10 +5445,37 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5389
5445
  [#5762]: https://github.com/DataDog/dd-trace-rb/issues/5762
5390
5446
  [#5768]: https://github.com/DataDog/dd-trace-rb/issues/5768
5391
5447
  [#5773]: https://github.com/DataDog/dd-trace-rb/issues/5773
5448
+ [#5777]: https://github.com/DataDog/dd-trace-rb/issues/5777
5392
5449
  [#5811]: https://github.com/DataDog/dd-trace-rb/issues/5811
5393
5450
  [#5812]: https://github.com/DataDog/dd-trace-rb/issues/5812
5451
+ [#5816]: https://github.com/DataDog/dd-trace-rb/issues/5816
5452
+ [#5828]: https://github.com/DataDog/dd-trace-rb/issues/5828
5394
5453
  [#5830]: https://github.com/DataDog/dd-trace-rb/issues/5830
5454
+ [#5834]: https://github.com/DataDog/dd-trace-rb/issues/5834
5395
5455
  [#5836]: https://github.com/DataDog/dd-trace-rb/issues/5836
5456
+ [#5844]: https://github.com/DataDog/dd-trace-rb/issues/5844
5457
+ [#5857]: https://github.com/DataDog/dd-trace-rb/issues/5857
5458
+ [#5872]: https://github.com/DataDog/dd-trace-rb/issues/5872
5459
+ [#5877]: https://github.com/DataDog/dd-trace-rb/issues/5877
5460
+ [#5881]: https://github.com/DataDog/dd-trace-rb/issues/5881
5461
+ [#5883]: https://github.com/DataDog/dd-trace-rb/issues/5883
5462
+ [#5890]: https://github.com/DataDog/dd-trace-rb/issues/5890
5463
+ [#5894]: https://github.com/DataDog/dd-trace-rb/issues/5894
5464
+ [#5907]: https://github.com/DataDog/dd-trace-rb/issues/5907
5465
+ [#5911]: https://github.com/DataDog/dd-trace-rb/issues/5911
5466
+ [#5916]: https://github.com/DataDog/dd-trace-rb/issues/5916
5467
+ [#5920]: https://github.com/DataDog/dd-trace-rb/issues/5920
5468
+ [#5938]: https://github.com/DataDog/dd-trace-rb/issues/5938
5469
+ [#5940]: https://github.com/DataDog/dd-trace-rb/issues/5940
5470
+ [#5945]: https://github.com/DataDog/dd-trace-rb/issues/5945
5471
+ [#5954]: https://github.com/DataDog/dd-trace-rb/issues/5954
5472
+ [#5955]: https://github.com/DataDog/dd-trace-rb/issues/5955
5473
+ [#5970]: https://github.com/DataDog/dd-trace-rb/issues/5970
5474
+ [#5973]: https://github.com/DataDog/dd-trace-rb/issues/5973
5475
+ [#5975]: https://github.com/DataDog/dd-trace-rb/issues/5975
5476
+ [#5987]: https://github.com/DataDog/dd-trace-rb/issues/5987
5477
+ [#5990]: https://github.com/DataDog/dd-trace-rb/issues/5990
5478
+ [#5991]: https://github.com/DataDog/dd-trace-rb/issues/5991
5396
5479
  [@AdrianLC]: https://github.com/AdrianLC
5397
5480
  [@Azure7111]: https://github.com/Azure7111
5398
5481
  [@BabyGroot]: https://github.com/BabyGroot
@@ -5507,6 +5590,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5507
5590
  [@mscrivo]: https://github.com/mscrivo
5508
5591
  [@mstruve]: https://github.com/mstruve
5509
5592
  [@mustela]: https://github.com/mustela
5593
+ [@navidemad]: https://github.com/navidemad
5510
5594
  [@nic-lan]: https://github.com/nic-lan
5511
5595
  [@noma4i]: https://github.com/noma4i
5512
5596
  [@norbertnytko]: https://github.com/norbertnytko
@@ -5,7 +5,6 @@
5
5
  #ifdef HAVE_MACH_THREAD_INFO
6
6
 
7
7
  #include <pthread.h>
8
- #include <time.h>
9
8
  #include <mach/mach.h>
10
9
  #include <mach/thread_info.h>
11
10
 
@@ -47,17 +46,11 @@ thread_cpu_time thread_cpu_time_for(thread_cpu_time_id time_id) {
47
46
 
48
47
  if (!time_id.valid) return error;
49
48
 
50
- // Fast path: clock_gettime(CLOCK_THREAD_CPUTIME_ID) is ~5x cheaper than thread_info()
51
- // and gives sub-microsecond precision (vs microsecond), but only measures the calling
52
- // thread on macOS (there is no pthread_getcpuclockid() equivalent).
53
- if (time_id.clock_id == pthread_mach_thread_np(pthread_self())) {
54
- struct timespec ts;
55
- if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) == 0) {
56
- return (thread_cpu_time) {.valid = true, .result_ns = SECONDS_AS_NS(ts.tv_sec) + ts.tv_nsec};
57
- }
58
- // Fall through to thread_info on the unlikely failure case.
59
- }
60
-
49
+ // We intentionally use thread_info() for all threads, including the current one, even though
50
+ // clock_gettime(CLOCK_THREAD_CPUTIME_ID) is ~5x faster for the current thread. Mixing the two
51
+ // sources causes CPU time to appear to go backwards: thread_info() has microsecond precision
52
+ // while clock_gettime() has nanosecond precision, and comparing values across sources on
53
+ // successive samples produces small negative deltas.
61
54
  struct thread_basic_info info;
62
55
  mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
63
56
  kern_return_t kr = thread_info(time_id.clock_id, THREAD_BASIC_INFO, (thread_info_t)&info, &count);
@@ -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
@@ -525,6 +530,8 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
525
530
  // situation we stop immediately and never even start the sampling trigger loop.
526
531
  if (state->stop_thread == rb_thread_current()) return Qnil;
527
532
 
533
+ thread_context_collector_profiler_internal_thread_started();
534
+
528
535
  // Reset the dynamic sampling rate state, if any (reminder: the monotonic clock reference may change after a fork)
529
536
  dynamic_sampling_rate_reset(&state->cpu_dynamic_sampling_rate);
530
537
  long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
@@ -659,7 +666,7 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
659
666
  if (sample_from_signal_handler) {
660
667
  // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
661
668
  // until the postponed job below gets run.
662
- bool prepared = thread_context_collector_prepare_sample_inside_signal_handler(state->thread_context_collector_instance);
669
+ bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
663
670
 
664
671
  if (prepared) state->stats.signal_handler_prepared_sample++;
665
672
  }
@@ -808,8 +815,7 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
808
815
 
809
816
  state->stats.cpu_sampled++;
810
817
 
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);
818
+ thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
813
819
 
814
820
  long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
815
821
  long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
@@ -865,15 +871,12 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
865
871
 
866
872
  if (state->gvl_profiling_enabled) {
867
873
  #ifndef NO_GVL_INSTRUMENTATION
868
- #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS
869
- gvl_profiling_state_thread_tracking_workaround();
870
- #endif
871
-
872
874
  state->gvl_profiling_hook = rb_internal_thread_add_event_hook(
873
875
  on_gvl_event,
874
876
  (
875
877
  // For now we're only asking for these events, even though there's more
876
878
  // (e.g. check docs or gvl-tracing gem)
879
+ RUBY_INTERNAL_THREAD_EVENT_SUSPENDED | /* released gvl */
877
880
  RUBY_INTERNAL_THREAD_EVENT_READY /* waiting for gvl */ |
878
881
  RUBY_INTERNAL_THREAD_EVENT_RESUMED /* running/runnable */
879
882
  ),
@@ -892,6 +895,8 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
892
895
  // If we stopped sampling due to an exception, re-raise it (now in the worker thread)
893
896
  if (state->failure_exception != Qnil) rb_exc_raise(state->failure_exception);
894
897
 
898
+ thread_context_collector_profiler_internal_thread_done(state->thread_context_collector_instance);
899
+
895
900
  return Qnil;
896
901
  }
897
902
 
@@ -1055,6 +1060,9 @@ static VALUE _native_simulate_sample_from_postponed_job(DDTRACE_UNUSED VALUE sel
1055
1060
  //
1056
1061
  // In the future, if we add more other components with tracepoints, we will need to coordinate stopping all such
1057
1062
  // tracepoints before doing the other cleaning steps.
1063
+ //
1064
+ // Note that tests call this method directly in the same process without forking,
1065
+ // and in such a case non-current Threads keep running.
1058
1066
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE instance) {
1059
1067
  cpu_and_wall_time_worker_state *state;
1060
1068
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -1129,6 +1137,9 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
1129
1137
  ID2SYM(rb_intern("gvl_waiting_time_ns_total")), /* => */ state->gvl_profiling_enabled ? ULL2NUM(state->stats.vm_metrics.gvl_waiting_time_ns_total) : Qnil,
1130
1138
  };
1131
1139
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(stats_as_hash, arguments[i], arguments[i+1]);
1140
+
1141
+ thread_context_collector_stats(state->thread_context_collector_instance, stats_as_hash);
1142
+
1132
1143
  return stats_as_hash;
1133
1144
  }
1134
1145
 
@@ -1136,6 +1147,7 @@ static VALUE _native_stats_reset_not_thread_safe(DDTRACE_UNUSED VALUE self, VALU
1136
1147
  cpu_and_wall_time_worker_state *state;
1137
1148
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1138
1149
  reset_stats_not_thread_safe(state);
1150
+ thread_context_collector_stats_reset_not_thread_safe(state->thread_context_collector_instance);
1139
1151
  return Qnil;
1140
1152
  }
1141
1153
 
@@ -1245,13 +1257,22 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1245
1257
  return;
1246
1258
  }
1247
1259
 
1260
+ VALUE current_thread = rb_thread_current();
1261
+
1248
1262
  // 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
1263
  // trigger an exception inside the profiler code, bad things will happen (specifically, Ruby will try to kill off the
1250
1264
  // thread even though we may try to catch the exception).
1251
1265
  //
1252
1266
  // Note that "in the middle of raising an exception" means the exception itself has already been allocated.
1253
1267
  // 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())) {
1268
+ if (is_raised_flag_set(current_thread)) {
1269
+ return;
1270
+ }
1271
+
1272
+ per_thread_context *thread_context = get_per_thread_context(current_thread);
1273
+ if (!thread_context) {
1274
+ // Context is created eagerly via on_thread_begin_event, so this should not normally be NULL.
1275
+ // We keep the guard since we can't allocate here (inside on_newobj_event).
1255
1276
  return;
1256
1277
  }
1257
1278
 
@@ -1286,7 +1307,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1286
1307
  // Rescue against any exceptions that happen during sampling
1287
1308
  safely_call(
1288
1309
  rescued_sample_allocation,
1289
- Qnil,
1310
+ (VALUE) thread_context,
1290
1311
  state->self_instance,
1291
1312
  handle_sampling_failure_rescued_sample_allocation
1292
1313
  );
@@ -1338,7 +1359,8 @@ static VALUE _native_with_blocked_sigprof(DDTRACE_UNUSED VALUE self) {
1338
1359
  }
1339
1360
  }
1340
1361
 
1341
- static VALUE rescued_sample_allocation(DDTRACE_UNUSED VALUE unused) {
1362
+ static VALUE rescued_sample_allocation(VALUE arg) {
1363
+ per_thread_context *thread_context = (per_thread_context*) arg;
1342
1364
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1343
1365
 
1344
1366
  // This should not happen in a normal situation because on_newobj_event already checked for this, but just in case...
@@ -1357,7 +1379,7 @@ static VALUE rescued_sample_allocation(DDTRACE_UNUSED VALUE unused) {
1357
1379
  // To control bias from sampling, we clamp the maximum weight attributed to a single allocation sample. This avoids
1358
1380
  // assigning a very large number to a sample, if for instance the dynamic sampling mechanism chose a really big interval.
1359
1381
  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);
1382
+ bool needs_after_allocation = thread_context_collector_sample_allocation(state->thread_context_collector_instance, thread_context, weight, new_object);
1361
1383
  // ...but we still represent the skipped samples in the profile, thus the data will account for all allocations.
1362
1384
  if (weight < allocations_since_last_sample) {
1363
1385
  uint32_t skipped_samples = (uint32_t) uint64_min_of(allocations_since_last_sample - weight, UINT32_MAX);
@@ -1414,30 +1436,49 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1414
1436
  #ifndef NO_GVL_INSTRUMENTATION
1415
1437
  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
1438
  // 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);
1439
+ // This function gets called without the GVL, and potentially from non-main Ractors!
1422
1440
 
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.
1441
+ // The thread that this event is about may not be the current thread
1442
+ // (as documented on rb_internal_thread_add_event_hook(), and this is notably the case for READY on Ruby 4.0),
1443
+ // so be careful with native thread locals that are not directly tied to the thread object and the like.
1432
1444
 
1433
- #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS
1434
- target_thread = gvl_profiling_state_maybe_initialize();
1435
- #endif
1445
+ // On Ruby 3.2 the event does not carry the thread, but all events always fire on the event thread on Ruby 3.2.
1446
+ // However, during early thread startup rb_thread_current() can crash because the execution context (Fiber) isn't
1447
+ // stored in TLS yet; ruby_native_thread_p() guards against this.
1448
+ #ifdef HAVE_RUBY_THREAD_STORAGE_API
1449
+ VALUE target_thread = event_data->thread;
1450
+ #else
1451
+ if (!ruby_native_thread_p()) return;
1452
+ VALUE target_thread = rb_thread_current();
1453
+ #endif
1436
1454
 
1455
+ per_thread_context* thread_context = get_per_thread_context(target_thread);
1456
+ if (!thread_context) return;
1457
+ // If non-NULL the thread is profiled and from the main Ractor
1458
+
1459
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED) { /* released gvl */
1460
+ thread_context_collector_on_gvl_released(thread_context);
1461
+ } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_READY) { /* waiting for gvl */
1462
+ thread_context_collector_on_gvl_waiting(thread_context);
1463
+ } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) { /* running/runnable */
1464
+ // Interesting note: A RUBY_INTERNAL_THREAD_EVENT_RESUMED is guaranteed to be called with the GVL being acquired
1465
+ // and on the event thread.
1466
+ // However, on_gvl_event() is called while holding the scheduler lock, so we do as little work as possible here,
1467
+ // and perform the sample in a postponed_job.
1437
1468
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1438
1469
  if (state == NULL) return; // This should not happen, but just in case...
1439
1470
 
1440
- on_gvl_running_result result = thread_context_collector_on_gvl_running(target_thread);
1471
+ // on_gvl_running prepares a stack sample so we need to gate it with `during_sample_enter` to avoid a
1472
+ // SIGPROF signal coming in during that function and potentially causing a corrupted final state.
1473
+ //
1474
+ // TODO: Currently, the sample prepared in on_gvl_running can still be clobbered if the signal handler runs
1475
+ // after `during_sample_exit` but before `after_gvl_running_from_postponed_job` gets to run. We'll need to fix
1476
+ // that next.
1477
+ during_sample_enter(state);
1478
+
1479
+ on_gvl_running_result result = thread_context_collector_on_gvl_running(state->thread_context_collector_instance, target_thread, thread_context);
1480
+
1481
+ during_sample_exit(state);
1441
1482
 
1442
1483
  if (result.waiting_for_gvl_duration_ns > 0) {
1443
1484
  state->stats.vm_metrics.gvl_waiting_time_ns_total += (uint64_t) result.waiting_for_gvl_duration_ns;
@@ -1453,8 +1494,7 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1453
1494
  state->stats.gvl_dont_sample++;
1454
1495
  }
1455
1496
  } 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);
1497
+ rb_bug("[ddtrace] Unexpected value in on_gvl_event (%d)\n", event_id);
1458
1498
  }
1459
1499
  }
1460
1500
 
@@ -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
@@ -6,6 +6,7 @@
6
6
  #include "helpers.h"
7
7
  #include "ruby_helpers.h"
8
8
  #include "collectors_idle_sampling_helper.h"
9
+ #include "collectors_thread_context.h"
9
10
 
10
11
  // Used by the Collectors::CpuAndWallTimeWorker to gather samples when the Ruby process is idle.
11
12
  //
@@ -30,7 +31,9 @@ typedef struct {
30
31
 
31
32
  static VALUE _native_new(VALUE klass);
32
33
  static void reset_state(idle_sampling_loop_state *state);
33
- static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
34
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE thread_context_collector_instance);
35
+ static VALUE idle_sampling_loop_body(VALUE self_instance);
36
+ static VALUE idle_sampling_loop_ensure(VALUE thread_context_collector_instance);
34
37
  static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
35
38
  static void *run_idle_sampling_loop(void *state_ptr);
36
39
  static void interrupt_idle_sampling_loop(void *state_ptr);
@@ -56,7 +59,7 @@ void collectors_idle_sampling_helper_init(VALUE profiling_module) {
56
59
  // https://bugs.ruby-lang.org/issues/18007 for a discussion around this.
57
60
  rb_define_alloc_func(collectors_idle_sampling_helper_class, _native_new);
58
61
 
59
- rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_idle_sampling_loop", _native_idle_sampling_loop, 1);
62
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_idle_sampling_loop", _native_idle_sampling_loop, 2);
60
63
  rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_stop", _native_stop, 1);
61
64
  rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_reset", _native_reset, 1);
62
65
  rb_define_singleton_method(testing_module, "_native_idle_sampling_helper_request_action", _native_idle_sampling_helper_request_action, 1);
@@ -109,16 +112,27 @@ static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
109
112
  return Qtrue;
110
113
  }
111
114
 
112
- static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
115
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance, VALUE thread_context_collector_instance) {
116
+ return rb_ensure(idle_sampling_loop_body, self_instance, idle_sampling_loop_ensure, thread_context_collector_instance);
117
+ }
118
+
119
+ static VALUE idle_sampling_loop_body(VALUE self_instance) {
113
120
  idle_sampling_loop_state *state;
114
121
  TypedData_Get_Struct(self_instance, idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
115
122
 
123
+ thread_context_collector_profiler_internal_thread_started();
124
+
116
125
  // Release GVL and run the loop waiting for requests
117
126
  rb_thread_call_without_gvl(run_idle_sampling_loop, state, interrupt_idle_sampling_loop, state);
118
127
 
119
128
  return Qtrue;
120
129
  }
121
130
 
131
+ static VALUE idle_sampling_loop_ensure(VALUE thread_context_collector_instance) {
132
+ thread_context_collector_profiler_internal_thread_done(thread_context_collector_instance);
133
+ return Qnil;
134
+ }
135
+
122
136
  static void *run_idle_sampling_loop(void *state_ptr) {
123
137
  idle_sampling_loop_state *state = (idle_sampling_loop_state *) state_ptr;
124
138
  int error = 0;
@@ -208,7 +222,7 @@ void idle_sampling_helper_request_action(VALUE self_instance, void (*run_action_
208
222
  if (!rb_typeddata_is_kind_of(self_instance, &idle_sampling_helper_typed_data)) {
209
223
  grab_gvl_and_raise(rb_eTypeError, "Wrong argument for idle_sampling_helper_request_action");
210
224
  }
211
- // This should never fail the the above check passes
225
+ // This should never fail when the above check passes
212
226
  TypedData_Get_Struct(self_instance, idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
213
227
 
214
228
  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;