datadog 2.39.0 → 2.47.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 (296) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +269 -44
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  5. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +256 -193
  6. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -5
  7. data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +0 -2
  8. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +19 -20
  9. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
  10. data/ext/datadog_profiling_native_extension/collectors_stack.c +267 -171
  11. data/ext/datadog_profiling_native_extension/collectors_stack.h +17 -3
  12. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +307 -217
  13. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +10 -5
  14. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +5 -4
  15. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +19 -11
  16. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +7 -10
  17. data/ext/datadog_profiling_native_extension/extconf.rb +67 -99
  18. data/ext/datadog_profiling_native_extension/heap_recorder.c +353 -257
  19. data/ext/datadog_profiling_native_extension/heap_recorder.h +50 -31
  20. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +3 -27
  21. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +853 -496
  22. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +42 -15
  23. data/ext/datadog_profiling_native_extension/profiling.c +30 -23
  24. data/ext/datadog_profiling_native_extension/ruby_helpers.c +4 -80
  25. data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -7
  26. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
  27. data/ext/datadog_profiling_native_extension/stack_recorder.c +116 -89
  28. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -5
  29. data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
  30. data/ext/datadog_profiling_native_extension/time_helpers.h +15 -15
  31. data/ext/libdatadog_api/datadog_ruby_common.c +19 -11
  32. data/ext/libdatadog_api/datadog_ruby_common.h +7 -10
  33. data/ext/libdatadog_api/di.c +43 -3
  34. data/ext/libdatadog_api/extconf.rb +6 -1
  35. data/ext/libdatadog_api/feature_flags.c +8 -0
  36. data/ext/libdatadog_api/init.c +9 -1
  37. data/ext/libdatadog_api/otel_thread_context.c +274 -0
  38. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  39. data/ext/libdatadog_api/process_discovery.c +15 -1
  40. data/ext/libdatadog_api/trace_exporter.c +2078 -0
  41. data/ext/libdatadog_api/trace_exporter.h +5 -0
  42. data/ext/libdatadog_extconf_helpers.rb +14 -9
  43. data/lib/datadog/ai_guard/component.rb +17 -8
  44. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  45. data/lib/datadog/ai_guard/configuration.rb +13 -0
  46. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  47. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  48. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  49. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  50. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  51. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  52. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  53. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  54. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  55. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  56. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  57. data/lib/datadog/ai_guard/evaluation/request.rb +8 -64
  58. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  59. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  60. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  61. data/lib/datadog/ai_guard/evaluation.rb +36 -14
  62. data/lib/datadog/ai_guard/ext.rb +4 -1
  63. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +7 -5
  64. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  65. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  66. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  67. data/lib/datadog/ai_guard/redaction.rb +80 -0
  68. data/lib/datadog/ai_guard.rb +58 -57
  69. data/lib/datadog/appsec/actions_handler/serializable_backtrace.rb +21 -42
  70. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +1 -1
  71. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +1 -1
  72. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +1 -1
  73. data/lib/datadog/appsec/assets/blocked.html +1 -108
  74. data/lib/datadog/appsec/configuration.rb +11 -2
  75. data/lib/datadog/appsec/context.rb +1 -1
  76. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +1 -1
  77. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +2 -2
  78. data/lib/datadog/appsec/contrib/devise/configuration.rb +1 -1
  79. data/lib/datadog/appsec/contrib/devise/patcher.rb +1 -1
  80. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +2 -2
  81. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +2 -2
  82. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -1
  83. data/lib/datadog/appsec/contrib/rack/ext.rb +2 -2
  84. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +2 -2
  85. data/lib/datadog/appsec/contrib/rack/patcher.rb +1 -1
  86. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +6 -3
  87. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +30 -3
  88. data/lib/datadog/appsec/contrib/rails/patches/render_to_body_patch.rb +8 -7
  89. data/lib/datadog/appsec/contrib/rails/request_middleware.rb +2 -1
  90. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +2 -2
  91. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +15 -3
  92. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +8 -7
  93. data/lib/datadog/appsec/contrib/sinatra/request_middleware.rb +2 -1
  94. data/lib/datadog/appsec/processor/rule_loader.rb +12 -12
  95. data/lib/datadog/appsec/response.rb +1 -1
  96. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +3 -3
  97. data/lib/datadog/appsec/security_engine/engine.rb +5 -4
  98. data/lib/datadog/core/configuration/components.rb +50 -19
  99. data/lib/datadog/core/configuration/components_state.rb +8 -1
  100. data/lib/datadog/core/configuration/option_definition.rb +1 -1
  101. data/lib/datadog/core/configuration/options.rb +1 -1
  102. data/lib/datadog/core/configuration/settings.rb +38 -43
  103. data/lib/datadog/core/configuration/supported_configurations.rb +18 -2
  104. data/lib/datadog/core/crashtracking/component.rb +5 -1
  105. data/lib/datadog/core/diagnostics/environment_logger.rb +36 -1
  106. data/lib/datadog/core/environment/gc.rb +1 -1
  107. data/lib/datadog/core/environment/vm_cache.rb +1 -1
  108. data/lib/datadog/core/environment/yjit.rb +2 -2
  109. data/lib/datadog/core/metrics/helpers.rb +1 -1
  110. data/lib/datadog/core/metrics/options.rb +2 -2
  111. data/lib/datadog/core/process_discovery.rb +6 -2
  112. data/lib/datadog/core/remote/client/capabilities.rb +32 -21
  113. data/lib/datadog/core/remote/client.rb +45 -4
  114. data/lib/datadog/core/remote/component.rb +30 -7
  115. data/lib/datadog/core/remote/configuration/repository.rb +2 -2
  116. data/lib/datadog/core/remote/dispatcher.rb +47 -6
  117. data/lib/datadog/core/remote/worker.rb +8 -0
  118. data/lib/datadog/core/telemetry/configuration_value.rb +38 -0
  119. data/lib/datadog/core/telemetry/event/app_client_configuration_change.rb +3 -1
  120. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +1 -1
  121. data/lib/datadog/core/telemetry/event/app_started.rb +5 -24
  122. data/lib/datadog/core/telemetry/event/generate_metrics.rb +1 -1
  123. data/lib/datadog/core/telemetry/event/log.rb +2 -2
  124. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +1 -1
  125. data/lib/datadog/core/telemetry/metric.rb +2 -2
  126. data/lib/datadog/core/telemetry/request.rb +2 -2
  127. data/lib/datadog/core/transport/http.rb +1 -1
  128. data/lib/datadog/core/transport/transport.rb +1 -1
  129. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +170 -18
  130. data/lib/datadog/core/utils/sequence.rb +3 -6
  131. data/lib/datadog/core/utils/time.rb +16 -54
  132. data/lib/datadog/core/workers/runtime_metrics.rb +1 -1
  133. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  134. data/lib/datadog/data_streams/processor.rb +45 -14
  135. data/lib/datadog/di/boot.rb +1 -0
  136. data/lib/datadog/di/capture_expression_evaluator/result.rb +30 -0
  137. data/lib/datadog/di/capture_expression_evaluator.rb +39 -9
  138. data/lib/datadog/di/component.rb +108 -3
  139. data/lib/datadog/di/configuration.rb +53 -2
  140. data/lib/datadog/di/context.rb +7 -2
  141. data/lib/datadog/di/contrib/active_record.rb +8 -2
  142. data/lib/datadog/di/el/compiler.rb +88 -12
  143. data/lib/datadog/di/el/evaluator.rb +82 -2
  144. data/lib/datadog/di/el/expression.rb +13 -2
  145. data/lib/datadog/di/evaluation_failure.rb +28 -0
  146. data/lib/datadog/di/guardrails_telemetry/reason.rb +87 -0
  147. data/lib/datadog/di/guardrails_telemetry.rb +203 -0
  148. data/lib/datadog/di/instrumenter.rb +106 -15
  149. data/lib/datadog/di/logger.rb +32 -10
  150. data/lib/datadog/di/probe.rb +9 -1
  151. data/lib/datadog/di/probe_builder.rb +7 -6
  152. data/lib/datadog/di/probe_manager.rb +56 -8
  153. data/lib/datadog/di/probe_notification_builder.rb +31 -28
  154. data/lib/datadog/di/probe_notifier_worker.rb +56 -6
  155. data/lib/datadog/di/proc_responder.rb +1 -1
  156. data/lib/datadog/di/remote.rb +49 -5
  157. data/lib/datadog/di/serializer.rb +172 -55
  158. data/lib/datadog/di/snapshot_event.rb +38 -0
  159. data/lib/datadog/di/transport/http.rb +13 -3
  160. data/lib/datadog/di/transport/input.rb +32 -7
  161. data/lib/datadog/error_tracking/component.rb +1 -1
  162. data/lib/datadog/kit/appsec/events/v2.rb +2 -2
  163. data/lib/datadog/open_feature/activation.rb +268 -0
  164. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  165. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  166. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  167. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  168. data/lib/datadog/open_feature/component.rb +85 -13
  169. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  170. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  171. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  172. data/lib/datadog/open_feature/configuration.rb +124 -0
  173. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  174. data/lib/datadog/open_feature/exposures/batch_builder.rb +1 -1
  175. data/lib/datadog/open_feature/exposures/event.rb +15 -8
  176. data/lib/datadog/open_feature/ext.rb +19 -0
  177. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  178. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  179. data/lib/datadog/open_feature/hooks/evaluation_telemetry_opt_out_hook.rb +25 -0
  180. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  181. data/lib/datadog/open_feature/native_evaluator.rb +20 -6
  182. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  183. data/lib/datadog/open_feature/provider.rb +315 -10
  184. data/lib/datadog/open_feature/remote.rb +5 -5
  185. data/lib/datadog/open_feature/transport.rb +5 -0
  186. data/lib/datadog/open_feature.rb +31 -1
  187. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  188. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  189. data/lib/datadog/opentelemetry/trace.rb +3 -0
  190. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +10 -2
  191. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  192. data/lib/datadog/profiling/collectors/info.rb +40 -13
  193. data/lib/datadog/profiling/collectors/thread_context.rb +6 -17
  194. data/lib/datadog/profiling/component.rb +33 -39
  195. data/lib/datadog/profiling/exporter.rb +10 -10
  196. data/lib/datadog/profiling/tasks/exec.rb +1 -1
  197. data/lib/datadog/profiling.rb +4 -0
  198. data/lib/datadog/symbol_database/extractor.rb +89 -107
  199. data/lib/datadog/tracing/component.rb +44 -2
  200. data/lib/datadog/tracing/configuration/dynamic/option.rb +4 -0
  201. data/lib/datadog/tracing/configuration/dynamic.rb +5 -3
  202. data/lib/datadog/tracing/configuration/ext.rb +4 -0
  203. data/lib/datadog/tracing/configuration/settings.rb +36 -0
  204. data/lib/datadog/tracing/context.rb +24 -2
  205. data/lib/datadog/tracing/context_provider.rb +17 -5
  206. data/lib/datadog/tracing/contrib/action_cable/events.rb +1 -1
  207. data/lib/datadog/tracing/contrib/action_mailer/events.rb +1 -1
  208. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +2 -2
  209. data/lib/datadog/tracing/contrib/action_view/events.rb +1 -1
  210. data/lib/datadog/tracing/contrib/active_model_serializers/events.rb +1 -1
  211. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +1 -1
  212. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  213. data/lib/datadog/tracing/contrib/active_record/events.rb +1 -1
  214. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +2 -2
  215. data/lib/datadog/tracing/contrib/aws/ext.rb +1 -1
  216. data/lib/datadog/tracing/contrib/aws/services.rb +1 -1
  217. data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +1 -1
  218. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +10 -5
  219. data/lib/datadog/tracing/contrib/excon/middleware.rb +10 -3
  220. data/lib/datadog/tracing/contrib/ext.rb +3 -3
  221. data/lib/datadog/tracing/contrib/faraday/middleware.rb +10 -4
  222. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  223. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -1
  224. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +1 -1
  225. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +1 -1
  226. data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +2 -1
  227. data/lib/datadog/tracing/contrib/http/instrumentation.rb +16 -1
  228. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +8 -2
  229. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +10 -4
  230. data/lib/datadog/tracing/contrib/kafka/events/connection/request.rb +4 -0
  231. data/lib/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rb +7 -2
  232. data/lib/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rb +7 -2
  233. data/lib/datadog/tracing/contrib/kafka/events.rb +1 -1
  234. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +1 -1
  235. data/lib/datadog/tracing/contrib/mongodb/parsers.rb +1 -1
  236. data/lib/datadog/tracing/contrib/opensearch/ext.rb +1 -1
  237. data/lib/datadog/tracing/contrib/opensearch/quantize.rb +1 -1
  238. data/lib/datadog/tracing/contrib/patchable.rb +1 -1
  239. data/lib/datadog/tracing/contrib/patcher.rb +1 -1
  240. data/lib/datadog/tracing/contrib/que/tracer.rb +1 -1
  241. data/lib/datadog/tracing/contrib/racecar/events.rb +1 -1
  242. data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +1 -1
  243. data/lib/datadog/tracing/contrib/rack/header_tagging.rb +1 -1
  244. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  245. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +1 -1
  246. data/lib/datadog/tracing/contrib/redis/configuration/resolver.rb +1 -1
  247. data/lib/datadog/tracing/contrib/redis/quantize.rb +36 -2
  248. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +9 -3
  249. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +10 -3
  250. data/lib/datadog/tracing/contrib/sequel/database.rb +1 -5
  251. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  252. data/lib/datadog/tracing/contrib/sequel/utils.rb +50 -63
  253. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +1 -1
  254. data/lib/datadog/tracing/contrib/sinatra/configuration/settings.rb +1 -1
  255. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +1 -1
  256. data/lib/datadog/tracing/contrib/utils/quantization/hash_formatter.rb +1 -1
  257. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +108 -3
  258. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +1 -1
  259. data/lib/datadog/tracing/correlation.rb +2 -2
  260. data/lib/datadog/tracing/diagnostics/environment_logger.rb +3 -2
  261. data/lib/datadog/tracing/distributed/baggage.rb +1 -3
  262. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  263. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  264. data/lib/datadog/tracing/distributed/propagation_policy.rb +5 -0
  265. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  266. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  267. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  268. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  269. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  270. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  271. data/lib/datadog/tracing/metadata/tagging.rb +1 -1
  272. data/lib/datadog/tracing/otel_thread_context.rb +97 -0
  273. data/lib/datadog/tracing/remote.rb +236 -37
  274. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  275. data/lib/datadog/tracing/span.rb +1 -1
  276. data/lib/datadog/tracing/span_operation.rb +1 -1
  277. data/lib/datadog/tracing/sync_writer.rb +8 -3
  278. data/lib/datadog/tracing/trace_digest.rb +22 -4
  279. data/lib/datadog/tracing/trace_operation.rb +22 -10
  280. data/lib/datadog/tracing/tracer.rb +28 -7
  281. data/lib/datadog/tracing/transport/http/telemetry.rb +94 -0
  282. data/lib/datadog/tracing/transport/http/traces.rb +2 -0
  283. data/lib/datadog/tracing/transport/io/traces.rb +1 -1
  284. data/lib/datadog/tracing/transport/native/response.rb +71 -0
  285. data/lib/datadog/tracing/transport/native/telemetry.rb +17 -0
  286. data/lib/datadog/tracing/transport/native.rb +395 -0
  287. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  288. data/lib/datadog/tracing/transport/telemetry.rb +62 -0
  289. data/lib/datadog/tracing/transport/trace_formatter.rb +14 -4
  290. data/lib/datadog/tracing/transport/traces.rb +85 -38
  291. data/lib/datadog/tracing/writer.rb +17 -4
  292. data/lib/datadog/version.rb +1 -1
  293. metadata +51 -13
  294. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
  295. data/lib/datadog/appsec/utils/hash_coercion.rb +0 -23
  296. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -16,6 +16,7 @@
16
16
  #include "private_vm_api_access.h"
17
17
  #include "setup_signal_handler.h"
18
18
  #include "time_helpers.h"
19
+ #include "unsafe_api_calls_check.h"
19
20
 
20
21
  #ifdef __APPLE__
21
22
  #include "macos_sampler_thread.h"
@@ -68,14 +69,24 @@
68
69
  // 4. The Ruby VM calls our `sample_from_postponed_job` from a thread holding the global VM lock. A sample is recorded by
69
70
  // calling `thread_context_collector_sample`.
70
71
  //
71
- // ### TracePoints and Forking
72
+ //
73
+ // ### Hooks and TracePoints
74
+ //
75
+ // This class uses various hooks:
76
+ // * A RUBY_INTERNAL_EVENT_GC_ENTER & RUBY_INTERNAL_EVENT_GC_EXIT TracePoint
77
+ // * A RUBY_INTERNAL_EVENT_NEWOBJ "event hook"/internal tracepoint
78
+ // * A GVL thread event hook
79
+ //
80
+ // We refer to those collectively as "hooks".
81
+ //
82
+ // ### Hooks and Forking
72
83
  //
73
84
  // When the Ruby VM forks, the CPU/Wall-time profiling stops naturally because it's triggered by a background thread
74
85
  // that doesn't get automatically restarted by the VM on the child process. (The profiler does trigger its restart at
75
86
  // some point -- see `Profiling::Tasks::Setup` for details).
76
87
  //
77
- // But this doesn't apply to any `TracePoint`s this class may use, which will continue to be active. Thus, we need to
78
- // always remember consider this case of -- the worker thread may not be alive but the `TracePoint`s can continue to
88
+ // But this doesn't apply to any hooks this class may use, which will continue to be active. Thus, we need to
89
+ // always remember consider this case of -- the worker thread may not be alive but the hooks can continue to
79
90
  // trigger samples.
80
91
  //
81
92
  // ---
@@ -91,7 +102,7 @@ unsigned int MAX_ALLOC_WEIGHT = 10000;
91
102
  static rb_postponed_job_handle_t sample_from_postponed_job_handle;
92
103
  static rb_postponed_job_handle_t after_gc_from_postponed_job_handle;
93
104
  static rb_postponed_job_handle_t after_gvl_running_from_postponed_job_handle;
94
- static rb_postponed_job_handle_t after_allocation_from_postponed_job_handle;
105
+ static rb_postponed_job_handle_t commit_heap_recordings_from_postponed_job_may_lose_gvl_handle;
95
106
  #endif
96
107
 
97
108
  // Contains state for a single CpuAndWallTimeWorker instance
@@ -107,9 +118,12 @@ typedef struct {
107
118
  bool skip_idle_samples_for_testing;
108
119
  bool sighandler_sampling_enabled;
109
120
  uint32_t cpu_sampling_interval_ms;
121
+ // Minimum duration of a "Waiting for GVL" period to trigger a sample
122
+ uint32_t waiting_for_gvl_threshold_ns;
110
123
  VALUE self_instance;
111
124
  VALUE thread_context_collector_instance;
112
125
  VALUE idle_sampling_helper_instance;
126
+ idle_sampling_loop_state *idle_sampling_helper_state;
113
127
  VALUE owner_thread;
114
128
  dynamic_sampling_rate_state cpu_dynamic_sampling_rate;
115
129
  discrete_dynamic_sampler allocation_sampler;
@@ -127,13 +141,29 @@ typedef struct {
127
141
 
128
142
  // Others
129
143
 
130
- // Used to detect/avoid nested sampling, e.g. when on_newobj_event gets triggered by a memory allocation
131
- // that happens during another sample, or when the signal handler gets triggered while we're already in the middle of
132
- // sampling.
144
+ // Used to detect/avoid nested sampling, and intended to behave as a lock to ensure the profiler doesn't recurse on itself,
145
+ // e.g. when on_newobj_event gets triggered by a memory allocation that happens during another sample, or when the
146
+ // signal handler gets triggered while we're already in the middle of sampling.
147
+ //
148
+ // It's not an actual lock because we rely on the GVL for correct synchronization
149
+ // (and thus this flag is only valid when we know we have the GVL).
150
+ // Exception: `on_gvl_event` sets it for RUBY_INTERNAL_THREAD_EVENT_RESUMED without the GVL -- see the notes there.
151
+ //
152
+ // Similar to a lock, it should not be held across long-running operations,
153
+ // **in particular it MUST NEVER be held during operations where we might lose the GVL**
154
+ // because effectively that would stop the profiler from working until control
155
+ // goes back to that special thread, which on a contended Ruby app, can take hundreds of ms (or more).
156
+ //
157
+ // Because what we want is "profiler doesn't recurse on itself" we want this to behave as a non-reentrant lock
158
+ // (FIXME: We should have checks for this)
133
159
  //
134
160
  // @ivoanjo: Right now we always sample inside `safely_call`; if that ever changes, this flag may need to become
135
161
  // volatile/atomic/have some barriers to ensure it's visible during e.g. signal handlers.
136
162
  bool during_sample;
163
+ // Used from the signal handler to ask for a sample at the end of GC.
164
+ // The GC_EXIT event is emitted by the thread that had the GVL + was doing GC, e.g. Ruby won't switch threads between
165
+ // this flag getting set in the signal handler and the GC_EXIT event.
166
+ bool prepare_sample_on_gc_finish;
137
167
 
138
168
  #ifndef NO_GVL_INSTRUMENTATION
139
169
  // Only set when sampling is active (gets created at start and cleaned on stop)
@@ -205,6 +235,8 @@ static VALUE _native_new(VALUE klass);
205
235
  static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
206
236
  static void cpu_and_wall_time_worker_typed_data_mark(void *state_ptr);
207
237
  static VALUE _native_sampling_loop(VALUE self, VALUE instance);
238
+ static VALUE _native_profiler_internal_thread_done(VALUE self_instance);
239
+ static VALUE _native_prepare_serialize(DDTRACE_UNUSED VALUE self, VALUE self_instance);
208
240
  static VALUE _native_stop(DDTRACE_UNUSED VALUE _self, VALUE self_instance, VALUE worker_thread);
209
241
  static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *optional_exception_during_operation);
210
242
  static void stop_state(cpu_and_wall_time_worker_state *state, VALUE optional_exception, const char *optional_operation_name);
@@ -220,10 +252,12 @@ static VALUE _native_failure_exception_during_operation(DDTRACE_UNUSED VALUE sel
220
252
  static void testing_signal_handler(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, DDTRACE_UNUSED void *_ucontext);
221
253
  static VALUE _native_install_testing_signal_handler(DDTRACE_UNUSED VALUE self);
222
254
  static VALUE _native_remove_testing_signal_handler(DDTRACE_UNUSED VALUE self);
223
- static VALUE _native_install_sigprof_handler_on_altstack(DDTRACE_UNUSED VALUE self);
224
255
  static VALUE _native_trigger_sample(DDTRACE_UNUSED VALUE self);
225
256
  static VALUE _native_gc_tracepoint(DDTRACE_UNUSED VALUE self, VALUE instance);
226
257
  static void on_gc_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused);
258
+ static void handle_gc_event(cpu_and_wall_time_worker_state *state, rb_event_flag_t event);
259
+ static VALUE _native_on_gc_event(VALUE self, VALUE instance, VALUE event, VALUE during_sample);
260
+ static VALUE _native_request_prepare_on_gc_finish(VALUE self, VALUE instance);
227
261
  static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused);
228
262
  static VALUE safely_call(
229
263
  VALUE (*function_to_call_safely)(VALUE),
@@ -243,12 +277,11 @@ static void reset_stats_not_thread_safe(cpu_and_wall_time_worker_state *state);
243
277
  static void sleep_for(uint64_t time_ns);
244
278
  static VALUE _native_allocation_count(DDTRACE_UNUSED VALUE self);
245
279
  static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *unused2);
246
- static void disable_tracepoints(cpu_and_wall_time_worker_state *state);
280
+ static void disable_hooks(cpu_and_wall_time_worker_state *state);
247
281
  static VALUE _native_with_blocked_sigprof(DDTRACE_UNUSED VALUE self);
248
282
  static VALUE rescued_sample_allocation(VALUE tracepoint_data);
249
- static VALUE rescued_after_allocation(VALUE self_instance);
283
+ static VALUE rescued_commit_heap_recordings_may_lose_gvl(VALUE self_instance);
250
284
  static void delayed_error(cpu_and_wall_time_worker_state *state, const char *error);
251
- static void delayed_error_clock_failure(cpu_and_wall_time_worker_state *state);
252
285
  static VALUE _native_delayed_error(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE error_msg);
253
286
  static VALUE _native_hold_signals(DDTRACE_UNUSED VALUE self);
254
287
  static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self);
@@ -261,11 +294,12 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self);
261
294
  static VALUE _native_gvl_profiling_hook_active(DDTRACE_UNUSED VALUE self, VALUE instance);
262
295
  static VALUE handle_sampling_failure_rescued_sample_from_postponed_job(VALUE self_instance, VALUE exception);
263
296
  static VALUE handle_sampling_failure_thread_context_collector_sample_after_gc(VALUE self_instance, VALUE exception);
297
+ static VALUE handle_sampling_failure_thread_context_collector_heap_update(VALUE self_instance, VALUE exception);
264
298
  static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instance, VALUE exception);
265
- static VALUE handle_sampling_failure_rescued_after_allocation(VALUE self_instance, VALUE exception);
299
+ static VALUE handle_sampling_failure_rescued_commit_heap_recordings(VALUE self_instance, VALUE exception);
266
300
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state);
267
301
  static inline void during_sample_exit(cpu_and_wall_time_worker_state* state);
268
- static void after_allocation_from_postponed_job(DDTRACE_UNUSED void *_unused);
302
+ static void commit_heap_recordings_from_postponed_job_may_lose_gvl(DDTRACE_UNUSED void *_unused);
269
303
 
270
304
  // We're using `on_newobj_event` function with `rb_add_event_hook2`, which requires in its public signature a function
271
305
  // with signature `rb_event_hook_func_t` which doesn't match `on_newobj_event`.
@@ -291,16 +325,6 @@ static void after_allocation_from_postponed_job(DDTRACE_UNUSED void *_unused);
291
325
  // (e.g. signal handler) where it's impossible or just awkward to pass it as an argument.
292
326
  static VALUE active_sampler_instance = Qnil;
293
327
  static cpu_and_wall_time_worker_state *active_sampler_instance_state = NULL;
294
- static VALUE clock_failure_exception_class = Qnil;
295
-
296
- // Stats that live outside of any particular `cpu_and_wall_time_worker_state`, so that they can always be safely
297
- // touched, even when we're not sure it's safe to touch the state (e.g. signal handler, no GVL, etc).
298
- typedef struct {
299
- // How many times we skipped sampling because we were running on the alternate signal stack (e.g. nested inside
300
- // another signal handler, presumably GC compaction).
301
- unsigned int signal_handler_skipped_sample_on_altstack;
302
- } global_stats_t;
303
- static global_stats_t global_stats;
304
328
 
305
329
  // See handle_sampling_signal for details on what this does
306
330
  #ifdef NO_POSTPONED_TRIGGER
@@ -322,13 +346,14 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
322
346
  sample_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, sample_from_postponed_job, NULL);
323
347
  after_gc_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_gc_from_postponed_job, NULL);
324
348
  after_gvl_running_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_gvl_running_from_postponed_job, NULL);
325
- after_allocation_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_allocation_from_postponed_job, NULL);
349
+ commit_heap_recordings_from_postponed_job_may_lose_gvl_handle =
350
+ rb_postponed_job_preregister(unused_flags, commit_heap_recordings_from_postponed_job_may_lose_gvl, NULL);
326
351
 
327
352
  if (
328
353
  sample_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
329
354
  after_gc_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
330
355
  after_gvl_running_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
331
- after_allocation_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID
356
+ commit_heap_recordings_from_postponed_job_may_lose_gvl_handle == POSTPONED_JOB_HANDLE_INVALID
332
357
  ) {
333
358
  raise_error(rb_eRuntimeError, "Failed to register profiler postponed jobs (got POSTPONED_JOB_HANDLE_INVALID)");
334
359
  }
@@ -340,8 +365,6 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
340
365
  VALUE collectors_cpu_and_wall_time_worker_class = rb_define_class_under(collectors_module, "CpuAndWallTimeWorker", rb_cObject);
341
366
  // Hosts methods used for testing the native code using RSpec
342
367
  VALUE testing_module = rb_define_module_under(collectors_cpu_and_wall_time_worker_class, "Testing");
343
- clock_failure_exception_class = rb_define_class_under(collectors_cpu_and_wall_time_worker_class, "ClockFailure", rb_eRuntimeError);
344
- rb_gc_register_mark_object(clock_failure_exception_class);
345
368
 
346
369
  // Instances of the CpuAndWallTimeWorker class are "TypedData" objects.
347
370
  // "TypedData" objects are special objects in the Ruby VM that can wrap C structs.
@@ -355,6 +378,8 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
355
378
 
356
379
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_initialize", _native_initialize, -1);
357
380
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_sampling_loop", _native_sampling_loop, 1);
381
+ rb_define_method(collectors_cpu_and_wall_time_worker_class, "_native_profiler_internal_thread_done", _native_profiler_internal_thread_done, 0);
382
+ rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_prepare_serialize", _native_prepare_serialize, 1);
358
383
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_stop", _native_stop, 2);
359
384
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
360
385
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_stats", _native_stats, 1);
@@ -367,9 +392,10 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
367
392
  rb_define_singleton_method(collectors_cpu_and_wall_time_worker_class, "_native_resume_signals", _native_resume_signals, 0);
368
393
  rb_define_singleton_method(testing_module, "_native_install_testing_signal_handler", _native_install_testing_signal_handler, 0);
369
394
  rb_define_singleton_method(testing_module, "_native_remove_testing_signal_handler", _native_remove_testing_signal_handler, 0);
370
- rb_define_singleton_method(testing_module, "_native_install_sigprof_handler_on_altstack", _native_install_sigprof_handler_on_altstack, 0);
371
395
  rb_define_singleton_method(testing_module, "_native_trigger_sample", _native_trigger_sample, 0);
372
396
  rb_define_singleton_method(testing_module, "_native_gc_tracepoint", _native_gc_tracepoint, 1);
397
+ rb_define_singleton_method(testing_module, "_native_on_gc_event", _native_on_gc_event, 3);
398
+ rb_define_singleton_method(testing_module, "_native_request_prepare_on_gc_finish", _native_request_prepare_on_gc_finish, 1);
373
399
  rb_define_singleton_method(testing_module, "_native_simulate_handle_sampling_signal", _native_simulate_handle_sampling_signal, 0);
374
400
  rb_define_singleton_method(testing_module, "_native_simulate_sample_from_postponed_job", _native_simulate_sample_from_postponed_job, 0);
375
401
  rb_define_singleton_method(testing_module, "_native_is_sigprof_blocked_in_current_thread", _native_is_sigprof_blocked_in_current_thread, 0);
@@ -392,7 +418,7 @@ static const rb_data_type_t cpu_and_wall_time_worker_typed_data = {
392
418
  };
393
419
 
394
420
  static VALUE _native_new(VALUE klass) {
395
- long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
421
+ long now = monotonic_wall_time_now_ns();
396
422
 
397
423
  cpu_and_wall_time_worker_state *state = ruby_xcalloc(1, sizeof(cpu_and_wall_time_worker_state));
398
424
 
@@ -408,8 +434,10 @@ static VALUE _native_new(VALUE klass) {
408
434
  state->skip_idle_samples_for_testing = false;
409
435
  state->sighandler_sampling_enabled = false;
410
436
  state->cpu_sampling_interval_ms = 10;
437
+ state->waiting_for_gvl_threshold_ns = 10 * 1000 * 1000;
411
438
  state->thread_context_collector_instance = Qnil;
412
439
  state->idle_sampling_helper_instance = Qnil;
440
+ state->idle_sampling_helper_state = NULL;
413
441
  state->owner_thread = Qnil;
414
442
  dynamic_sampling_rate_init(&state->cpu_dynamic_sampling_rate);
415
443
  state->gc_tracepoint = Qnil;
@@ -453,6 +481,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
453
481
  VALUE skip_idle_samples_for_testing = rb_hash_fetch(options, ID2SYM(rb_intern("skip_idle_samples_for_testing")));
454
482
  VALUE sighandler_sampling_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("sighandler_sampling_enabled")));
455
483
  VALUE cpu_sampling_interval_ms = rb_hash_fetch(options, ID2SYM(rb_intern("cpu_sampling_interval_ms")));
484
+ VALUE waiting_for_gvl_threshold_ns = rb_hash_fetch(options, ID2SYM(rb_intern("waiting_for_gvl_threshold_ns")));
456
485
 
457
486
  ENFORCE_BOOLEAN(gc_profiling_enabled);
458
487
  ENFORCE_BOOLEAN(no_signals_workaround_enabled);
@@ -464,6 +493,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
464
493
  ENFORCE_BOOLEAN(skip_idle_samples_for_testing)
465
494
  ENFORCE_BOOLEAN(sighandler_sampling_enabled)
466
495
  ENFORCE_TYPE(cpu_sampling_interval_ms, T_FIXNUM);
496
+ ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
467
497
 
468
498
  cpu_and_wall_time_worker_state *state;
469
499
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -477,6 +507,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
477
507
  state->skip_idle_samples_for_testing = (skip_idle_samples_for_testing == Qtrue);
478
508
  state->sighandler_sampling_enabled = (sighandler_sampling_enabled == Qtrue);
479
509
  state->cpu_sampling_interval_ms = NUM2INT(cpu_sampling_interval_ms);
510
+ state->waiting_for_gvl_threshold_ns = NUM2UINT(waiting_for_gvl_threshold_ns);
480
511
 
481
512
  double total_overhead_target_percentage = NUM2DBL(dynamic_sampling_rate_overhead_target_percentage);
482
513
  if (!state->allocation_profiling_enabled) {
@@ -485,12 +516,14 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
485
516
  // TODO: May be nice to offer customization here? Distribute available "overhead" margin with a bias towards one or the other
486
517
  // sampler.
487
518
  dynamic_sampling_rate_set_overhead_target_percentage(&state->cpu_dynamic_sampling_rate, total_overhead_target_percentage / 2);
488
- long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
489
- discrete_dynamic_sampler_set_overhead_target_percentage(&state->allocation_sampler, total_overhead_target_percentage / 2, now);
519
+ discrete_dynamic_sampler_set_overhead_target_percentage(
520
+ &state->allocation_sampler, total_overhead_target_percentage / 2, monotonic_wall_time_now_ns()
521
+ );
490
522
  }
491
523
 
492
524
  state->thread_context_collector_instance = enforce_thread_context_collector_instance(thread_context_collector_instance);
493
525
  state->idle_sampling_helper_instance = idle_sampling_helper_instance;
526
+ state->idle_sampling_helper_state = idle_sampling_helper_get_state(idle_sampling_helper_instance);
494
527
  state->gc_tracepoint = rb_tracepoint_new(Qnil, RUBY_INTERNAL_EVENT_GC_ENTER | RUBY_INTERNAL_EVENT_GC_EXIT, on_gc_event, NULL /* unused */);
495
528
 
496
529
  return Qtrue;
@@ -515,7 +548,7 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
515
548
 
516
549
  // If we already got a delayed exception registered even before starting, raise before starting
517
550
  if (state->failure_exception != Qnil) {
518
- disable_tracepoints(state);
551
+ disable_hooks(state);
519
552
  rb_exc_raise(state->failure_exception);
520
553
  }
521
554
 
@@ -525,13 +558,13 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
525
558
  raise_error(rb_eRuntimeError, "Could not start CpuAndWallTimeWorker: There's already another instance of CpuAndWallTimeWorker active in a different thread");
526
559
  } else {
527
560
  // The previously active thread seems to have died without cleaning up after itself.
528
- // In this case, we can still go ahead and start the profiler BUT we make sure to disable any existing tracepoint
561
+ // In this case, we can still go ahead and start the profiler BUT we make sure to disable any existing hooks
529
562
  // first as:
530
- // a) If this is a new instance of the CpuAndWallTimeWorker, we don't want the tracepoint from the old instance
563
+ // a) If this is a new instance of the CpuAndWallTimeWorker, we don't want the hooks from the old instance
531
564
  // being kept around
532
565
  // b) If this is the same instance of the CpuAndWallTimeWorker if we call enable on a tracepoint that is already
533
566
  // enabled, it will start firing more than once, see https://bugs.ruby-lang.org/issues/19114 for details.
534
- disable_tracepoints(old_state);
567
+ disable_hooks(old_state);
535
568
  }
536
569
  }
537
570
 
@@ -543,13 +576,12 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
543
576
 
544
577
  // Reset the dynamic sampling rate state, if any (reminder: the monotonic clock reference may change after a fork)
545
578
  dynamic_sampling_rate_reset(&state->cpu_dynamic_sampling_rate);
546
- long now = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
547
- discrete_dynamic_sampler_reset(&state->allocation_sampler, now);
579
+ discrete_dynamic_sampler_reset(&state->allocation_sampler, monotonic_wall_time_now_ns());
548
580
 
549
581
  // Reset per-thread state, if any. This ensures there's no leftover state from a previous profiler run that would
550
582
  // affect or be included in samples taken by this profiler about to run.
551
583
  //
552
- // NOTE: This needs to be called before we enable any tracepoints or anything that could trigger samples (e.g.
584
+ // NOTE: This needs to be called before we enable any hooks or anything that could trigger samples (e.g.
553
585
  // reset cannot be concurrent with any sampling activity)
554
586
  thread_context_collector_reset_all_per_thread_contexts(state->thread_context_collector_instance);
555
587
 
@@ -576,7 +608,7 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
576
608
 
577
609
  // The sample trigger loop finished (either cleanly or with an error); let's clean up
578
610
 
579
- disable_tracepoints(state);
611
+ disable_hooks(state);
580
612
 
581
613
  active_sampler_instance_state = NULL;
582
614
  active_sampler_instance = Qnil;
@@ -621,6 +653,38 @@ static VALUE _native_sampling_loop(DDTRACE_UNUSED VALUE _self, VALUE instance) {
621
653
  return Qnil;
622
654
  }
623
655
 
656
+ static VALUE during_sample_exit_ensure(VALUE state_ptr) {
657
+ during_sample_exit((cpu_and_wall_time_worker_state *) state_ptr);
658
+ return Qnil;
659
+ }
660
+
661
+ static VALUE _native_profiler_internal_thread_done(VALUE self_instance) {
662
+ cpu_and_wall_time_worker_state *state;
663
+ TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
664
+
665
+ during_sample_enter(state);
666
+ return rb_ensure(
667
+ thread_context_collector_profiler_internal_thread_done, state->thread_context_collector_instance,
668
+ during_sample_exit_ensure, (VALUE) state
669
+ );
670
+ }
671
+
672
+ // This method exists so that we run `thread_context_prepare_serialize` protected with
673
+ // `during_sample_enter`/`during_sample_exit` before we actually serialize.
674
+ //
675
+ // That's why in normal operation we go through the `CpuAndWallTimeWorker` and not directly to the `ThreadContext` to
676
+ // serialize.
677
+ static VALUE _native_prepare_serialize(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
678
+ cpu_and_wall_time_worker_state *state;
679
+ TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
680
+
681
+ during_sample_enter(state);
682
+ return rb_ensure(
683
+ thread_context_prepare_serialize, state->thread_context_collector_instance,
684
+ during_sample_exit_ensure, (VALUE) state
685
+ );
686
+ }
687
+
624
688
  static VALUE _native_stop(DDTRACE_UNUSED VALUE _self, VALUE self_instance, VALUE worker_thread) {
625
689
  cpu_and_wall_time_worker_state *state;
626
690
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -636,8 +700,8 @@ static void stop_state(cpu_and_wall_time_worker_state *state, VALUE optional_exc
636
700
  state->failure_exception = optional_exception;
637
701
  state->failure_exception_during_operation = optional_exception_during_operation;
638
702
 
639
- // Disable the tracepoints as soon as possible, so the VM doesn't keep on calling them
640
- disable_tracepoints(state);
703
+ // Disable the hooks as soon as possible, so the VM doesn't keep on calling them
704
+ disable_hooks(state);
641
705
  }
642
706
 
643
707
  static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *optional_exception_during_operation) {
@@ -649,57 +713,19 @@ static VALUE stop(VALUE self_instance, VALUE optional_exception, const char *opt
649
713
  return Qtrue;
650
714
  }
651
715
 
652
- // Our SIGPROF handler is not installed with SA_ONSTACK (see `install_sigprof_signal_handler`).
653
- // If we detect our signal handler is running on the alt stack, it means we interrupted another signal handler
654
- // that IS running on the alt stack -- in practice, Ruby's GC compaction read-barrier handler.
655
- #ifdef __APPLE__
656
- // On macOS, `ucontext->uc_stack.ss_sp` reports the (live) stack pointer near the top of the alt stack rather than the
657
- // alt-stack base like Linux does, and `ss_size` is the full alt-stack size. Thus the linux version of the check
658
- // didn't work for macOS.
659
- // As an alternative, we query `sigaltstack()`; we save/restore errno since `sigaltstack()` may clobber it.
660
- static bool is_running_on_alternate_signal_stack(DDTRACE_UNUSED void *ucontext) {
661
- int old_errno = errno;
662
- stack_t current_alt_stack;
663
- bool on_alt_stack = sigaltstack(NULL, &current_alt_stack) == 0 && (current_alt_stack.ss_flags & SS_ONSTACK) != 0;
664
- errno = old_errno;
665
- return on_alt_stack;
666
- }
667
- #else
668
- // NOTE: On Linux we use the alt-stack bounds from the `ucontext` rather than `sigaltstack()`, allowing us to avoid one syscall.
669
- static bool is_running_on_alternate_signal_stack(void *ucontext) {
670
- if (ucontext == NULL) return false;
671
-
672
- stack_t alt_stack = ((ucontext_t *) ucontext)->uc_stack;
673
- if (alt_stack.ss_size == 0) return false; // No alternate signal stack registered on this thread
674
-
675
- char stack_local;
676
- uintptr_t current_sp = (uintptr_t) &stack_local;
677
- uintptr_t alt_stack_start = (uintptr_t) alt_stack.ss_sp;
678
-
679
- return current_sp >= alt_stack_start && current_sp < alt_stack_start + alt_stack.ss_size;
680
- }
681
- #endif
682
-
683
716
  // NOTE: Remember that this will run in the thread and within the scope of user code, including user C code.
684
717
  // We need to be careful not to change any state that may be observed OR to restore it if we do. For instance, if anything
685
718
  // we do here can set `errno`, then we must be careful to restore the old `errno` after the fact.
686
- static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, void *ucontext) {
687
- // If we're running on the alternate signal stack, we've interrupted another signal handler that's running
688
- // there -- in practice, Ruby's GC compaction read-barrier handler.
689
- // During GC compaction, Ruby protects pages containing Ruby objects, so many of the checks we do below are unsafe
690
- // since they can touch those pages, and thus we just bail out here in this situation to avoid crashes.
691
- if (is_running_on_alternate_signal_stack(ucontext)) {
692
- global_stats.signal_handler_skipped_sample_on_altstack++;
693
- return;
694
- }
695
-
719
+ //
720
+ // Neither this handler nor its callees may dereference Ruby objects before confirming GVL ownership or during GC.
721
+ // Holding the GVL is not enough: we may have interrupted GC itself, with Ruby heap pages protected by compaction.
722
+ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED siginfo_t *_info, DDTRACE_UNUSED void *_ucontext) {
696
723
  // We must first check that we landed on the correct thread and can proceed.
697
724
  // We must never touch the state before we confirm that we have landed on the thread that is holding the GVL on the main
698
725
  // ractor as otherwise we may be concurrent with the profiler shutting down and removing its state.
699
726
  if (
700
727
  !ruby_native_thread_p() || // Not a Ruby thread
701
- !is_current_thread_holding_the_gvl() || // Not safe to enqueue a sample from this thread
702
- !ddtrace_rb_ractor_main_p() // We're not on the main Ractor; we currently don't support profiling non-main Ractors
728
+ !is_current_thread_in_main_ractor_and_holding_the_gvl() // Not safe to enqueue a sample from this thread
703
729
  ) return;
704
730
 
705
731
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
@@ -711,6 +737,8 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
711
737
  // a) we get triggered using SIGPROF, and the docs state a second SIGPROF will not interrupt an existing one (see sigaction docs on sa_mask)
712
738
  // b) we validate we are in the thread that has the global VM lock; if a different thread gets a signal, it will return early
713
739
  // because it will not have the global VM lock
740
+ // c) `simulate_sampling_signal_delivery` calls us directly instead of via a real SIGPROF, and blocks SIGPROF delivery on its
741
+ // thread for the duration of that call, so it can't be nested into by a real SIGPROF either
714
742
 
715
743
  state->stats.signal_handler_enqueued_sample++;
716
744
 
@@ -720,11 +748,20 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
720
748
  !state->during_sample;
721
749
 
722
750
  if (sample_from_signal_handler) {
723
- // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
724
- // until the postponed job below gets run.
725
- bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
751
+ if (rb_during_gc()) {
752
+ // During GC Ruby might be marking our buffer or compacting (moving) where iseqs live so we skip preparing the
753
+ // sample immediately, and instead defer preparation until GC_EXIT.
754
+ //
755
+ // When GC profiling is disabled, we just fall back to sampling the stack after GC finishes, e.g. it's the
756
+ // equivalent of disabling `sample_from_signal_handler` for samples that happen during GC.
757
+ state->prepare_sample_on_gc_finish = true;
758
+ } else {
759
+ // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
760
+ // until the postponed job below gets run.
761
+ bool prepared = thread_context_collector_prepare_sample_inside_signal_handler();
726
762
 
727
- if (prepared) state->stats.signal_handler_prepared_sample++;
763
+ if (prepared) state->stats.signal_handler_prepared_sample++;
764
+ }
728
765
  }
729
766
 
730
767
  #ifndef NO_POSTPONED_TRIGGER // Ruby 3.3+
@@ -758,7 +795,8 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
758
795
  #endif
759
796
  }
760
797
 
761
- // The actual sampling trigger loop always runs **without** the global vm lock.
798
+ // Runs without the GVL: neither this loop nor its callees may dereference Ruby objects until they reacquire it.
799
+ // GC compaction can protect Ruby heap pages while this loop runs, so even reading an object's header is unsafe.
762
800
  static void *run_sampling_trigger_loop(void *state_ptr) {
763
801
  cpu_and_wall_time_worker_state *state = (cpu_and_wall_time_worker_state *) state_ptr;
764
802
 
@@ -778,7 +816,7 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
778
816
  state->stats.trigger_simulated_signal_delivery_attempts++;
779
817
  grab_gvl_and_sample(); // Note: Can raise exceptions
780
818
  } else {
781
- current_gvl_owner owner = gvl_owner();
819
+ current_gvl_owner owner = main_ractor_gvl_owner();
782
820
  if (owner.valid) {
783
821
  // Note that reading the GVL owner and sending them a signal is a race -- the Ruby VM keeps on executing while
784
822
  // we're doing this, so we may still not signal the correct thread from time to time, but our signal handler
@@ -802,7 +840,7 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
802
840
  // for an uncontrolled amount of time. (This can still happen to the IdleSamplingHelper, but the
803
841
  // CpuAndWallTimeWorker will still be free to interrupt the Ruby VM and keep sampling for the entire blocking period).
804
842
  state->stats.trigger_simulated_signal_delivery_attempts++;
805
- idle_sampling_helper_request_action(state->idle_sampling_helper_instance, grab_gvl_and_sample);
843
+ idle_sampling_helper_request_action(state->idle_sampling_helper_state, grab_gvl_and_sample);
806
844
  }
807
845
  }
808
846
  }
@@ -814,7 +852,7 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
814
852
  // Note that we deliberately should NOT combine this sleep_for with the one above because the result of
815
853
  // `dynamic_sampling_rate_get_sleep` may have changed while the above sleep was ongoing.
816
854
  uint64_t extra_sleep =
817
- dynamic_sampling_rate_get_sleep(&state->cpu_dynamic_sampling_rate, monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE));
855
+ dynamic_sampling_rate_get_sleep(&state->cpu_dynamic_sampling_rate, monotonic_wall_time_now_ns());
818
856
  if (state->dynamic_sampling_rate_enabled && extra_sleep > 0) {
819
857
  state->stats.trigger_sample_extra_sleep++;
820
858
  sleep_for(extra_sleep);
@@ -848,7 +886,7 @@ static void sample_from_postponed_job(DDTRACE_UNUSED void *_unused) {
848
886
  during_sample_enter(state);
849
887
 
850
888
  // Rescue against any exceptions that happen during sampling
851
- safely_call(
889
+ VALUE needs_otel_span_key = safely_call(
852
890
  rescued_sample_from_postponed_job,
853
891
  state->self_instance,
854
892
  state->self_instance,
@@ -856,28 +894,37 @@ static void sample_from_postponed_job(DDTRACE_UNUSED void *_unused) {
856
894
  );
857
895
 
858
896
  during_sample_exit(state);
897
+
898
+ // Extracting the otel span key can lose the GVL, so we move it outside `during_sample`
899
+ // (It can't raise: it rescues its own exceptions)
900
+ if (needs_otel_span_key == Qtrue) {
901
+ thread_context_collector_resolve_otel_span_key_may_lose_gvl(state->thread_context_collector_instance);
902
+ }
859
903
  }
860
904
 
861
905
  static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
862
906
  cpu_and_wall_time_worker_state *state;
863
907
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
864
908
 
865
- long wall_time_ns_before_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
909
+ long wall_time_ns_before_sample = monotonic_wall_time_now_ns();
866
910
 
867
911
  if (state->dynamic_sampling_rate_enabled && !dynamic_sampling_rate_should_sample(&state->cpu_dynamic_sampling_rate, wall_time_ns_before_sample)) {
868
912
  state->stats.cpu_skipped++;
869
- return Qnil;
913
+ return Qfalse;
870
914
  }
871
915
 
872
916
  state->stats.cpu_sampled++;
873
917
 
874
- thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
918
+ bool needs_otel_span_key =
919
+ thread_context_collector_sample(state->thread_context_collector_instance, wall_time_ns_before_sample);
920
+
921
+ long wall_time_ns_after_sample = monotonic_wall_time_now_ns();
875
922
 
876
- long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
877
- long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
923
+ if (wall_time_ns_after_sample < wall_time_ns_before_sample) {
924
+ raise_error(rb_eRuntimeError, "BUG: Unexpected wall time going backwards when tracking overhead");
925
+ }
878
926
 
879
- // Guard against wall-time going backwards, see https://github.com/DataDog/dd-trace-rb/pull/2336 for discussion.
880
- uint64_t sampling_time_ns = delta_ns < 0 ? 0 : delta_ns;
927
+ uint64_t sampling_time_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
881
928
 
882
929
  state->stats.cpu_sampling_time_ns_min = uint64_min_of(sampling_time_ns, state->stats.cpu_sampling_time_ns_min);
883
930
  state->stats.cpu_sampling_time_ns_max = uint64_max_of(sampling_time_ns, state->stats.cpu_sampling_time_ns_max);
@@ -885,8 +932,7 @@ static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
885
932
 
886
933
  dynamic_sampling_rate_after_sample(&state->cpu_dynamic_sampling_rate, wall_time_ns_after_sample, sampling_time_ns);
887
934
 
888
- // Return a dummy VALUE because we're called from rb_rescue2 which requires it
889
- return Qnil;
935
+ return needs_otel_span_key ? Qtrue : Qfalse;
890
936
  }
891
937
 
892
938
  // This method exists only to enable testing Datadog::Profiling::Collectors::CpuAndWallTimeWorker behavior using RSpec.
@@ -912,7 +958,7 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
912
958
  cpu_and_wall_time_worker_state *state;
913
959
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
914
960
 
915
- // Final preparations: Setup signal handler and enable tracepoints. We run these here and not in `_native_sampling_loop`
961
+ // Final preparations: Setup signal handler and enable hooks. We run these here and not in `_native_sampling_loop`
916
962
  // because they may raise exceptions.
917
963
  install_sigprof_signal_handler(handle_sampling_signal, "handle_sampling_signal");
918
964
  if (state->gc_profiling_enabled) rb_tracepoint_enable(state->gc_tracepoint);
@@ -951,7 +997,7 @@ static VALUE release_gvl_and_run_sampling_trigger_loop(VALUE instance) {
951
997
  // If we stopped sampling due to an exception, re-raise it (now in the worker thread)
952
998
  if (state->failure_exception != Qnil) rb_exc_raise(state->failure_exception);
953
999
 
954
- thread_context_collector_profiler_internal_thread_done(state->thread_context_collector_instance);
1000
+ _native_profiler_internal_thread_done(instance);
955
1001
 
956
1002
  return Qnil;
957
1003
  }
@@ -991,28 +1037,6 @@ static VALUE _native_remove_testing_signal_handler(DDTRACE_UNUSED VALUE self) {
991
1037
  return Qtrue;
992
1038
  }
993
1039
 
994
- // Reproduces a profiler sample being taken in the middle of GC compaction: during compaction a SIGPROF can be
995
- // delivered while nested inside Ruby's read-barrier signal handler, which runs on the alternate signal stack. We
996
- // simulate that here by re-installing the production `handle_sampling_signal` with the `SA_ONSTACK` flag, so it runs
997
- // on the alternate signal stack just like it would in that scenario.
998
- //
999
- // The `SA_ONSTACK` will be undone by any next `replace_sigprof_signal_handler_with_empty_handler` /
1000
- // `install_sigprof_signal_handler_internal` / `remove_sigprof_signal_handle` so this doesn't need a specific, symmetric
1001
- // `uninstall` to undo this.
1002
- static VALUE _native_install_sigprof_handler_on_altstack(DDTRACE_UNUSED VALUE self) {
1003
- struct sigaction signal_handler_config = {
1004
- .sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK,
1005
- .sa_sigaction = handle_sampling_signal,
1006
- };
1007
- sigemptyset(&signal_handler_config.sa_mask);
1008
-
1009
- if (sigaction(SIGPROF, &signal_handler_config, NULL) != 0) {
1010
- rb_sys_fail("Failed to install SA_ONSTACK SIGPROF signal handler for testing");
1011
- }
1012
-
1013
- return Qtrue;
1014
- }
1015
-
1016
1040
  // This method exists only to enable testing Datadog::Profiling::Collectors::CpuAndWallTimeWorker behavior using RSpec.
1017
1041
  // It SHOULD NOT be used for other purposes.
1018
1042
  static VALUE _native_trigger_sample(DDTRACE_UNUSED VALUE self) {
@@ -1056,9 +1080,20 @@ static void on_gc_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused) {
1056
1080
  // and disabled before it is cleared, but just in case...
1057
1081
  if (state == NULL) return;
1058
1082
 
1083
+ handle_gc_event(state, event);
1084
+ }
1085
+
1086
+ static void handle_gc_event(cpu_and_wall_time_worker_state *state, rb_event_flag_t event) {
1059
1087
  if (event == RUBY_INTERNAL_EVENT_GC_ENTER) {
1088
+ // A signal can arrive after GC_EXIT while `rb_during_gc` is still true; discard requests from the previous GC step.
1089
+ state->prepare_sample_on_gc_finish = false;
1060
1090
  thread_context_collector_on_gc_start(state->thread_context_collector_instance);
1061
1091
  } else if (event == RUBY_INTERNAL_EVENT_GC_EXIT) {
1092
+ if (state->prepare_sample_on_gc_finish && !state->during_sample) {
1093
+ during_sample_enter(state);
1094
+ thread_context_collector_prepare_sample_on_gc_finish();
1095
+ during_sample_exit(state);
1096
+ }
1062
1097
  bool should_flush = thread_context_collector_on_gc_finish(state->thread_context_collector_instance);
1063
1098
 
1064
1099
  // We use rb_postponed_job_register_one to ask Ruby to run thread_context_collector_sample_after_gc when the
@@ -1073,6 +1108,31 @@ static void on_gc_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused) {
1073
1108
  }
1074
1109
  }
1075
1110
 
1111
+ static VALUE _native_request_prepare_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE instance) {
1112
+ cpu_and_wall_time_worker_state *state;
1113
+ TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1114
+ state->prepare_sample_on_gc_finish = true;
1115
+ return Qnil;
1116
+ }
1117
+
1118
+ static VALUE _native_on_gc_event(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE event, VALUE during_sample) {
1119
+ cpu_and_wall_time_worker_state *state;
1120
+ TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1121
+ ENFORCE_BOOLEAN(during_sample);
1122
+ rb_event_flag_t event_flag;
1123
+ if (event == ID2SYM(rb_intern("enter"))) event_flag = RUBY_INTERNAL_EVENT_GC_ENTER;
1124
+ else if (event == ID2SYM(rb_intern("exit"))) event_flag = RUBY_INTERNAL_EVENT_GC_EXIT;
1125
+ else rb_raise(rb_eArgError, "Expected :enter or :exit");
1126
+
1127
+ bool was_during_sample = state->during_sample;
1128
+ if (during_sample == Qtrue) during_sample_enter(state);
1129
+ debug_enter_unsafe_context();
1130
+ handle_gc_event(state, event_flag);
1131
+ debug_leave_unsafe_context();
1132
+ if (!was_during_sample) during_sample_exit(state);
1133
+ return Qnil;
1134
+ }
1135
+
1076
1136
  static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1077
1137
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1078
1138
 
@@ -1095,6 +1155,15 @@ static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1095
1155
  );
1096
1156
 
1097
1157
  during_sample_exit(state);
1158
+
1159
+ // This part runs separately from above because it may lose the GVL and we don't want `during_sample` to be set in
1160
+ // such a situation
1161
+ safely_call(
1162
+ thread_context_collector_heap_update_may_lose_gvl,
1163
+ state->thread_context_collector_instance,
1164
+ state->self_instance,
1165
+ handle_sampling_failure_thread_context_collector_heap_update
1166
+ );
1098
1167
  }
1099
1168
 
1100
1169
  // Equivalent to Ruby begin/rescue call, where we call a C function and jump to the exception handler if an
@@ -1132,12 +1201,12 @@ static VALUE _native_simulate_sample_from_postponed_job(DDTRACE_UNUSED VALUE sel
1132
1201
 
1133
1202
  // After the Ruby VM forks, this method gets called in the child process to clean up any leftover state from the parent.
1134
1203
  //
1135
- // Assumption: This method gets called BEFORE restarting profiling. Note that profiling-related tracepoints may still
1204
+ // Assumption: This method gets called BEFORE restarting profiling. Note that profiling-related hooks may still
1136
1205
  // be active, so we make sure to disable them before calling into anything else, so that there are no components
1137
1206
  // attempting to trigger samples at the same time as the reset is done.
1138
1207
  //
1139
- // In the future, if we add more other components with tracepoints, we will need to coordinate stopping all such
1140
- // tracepoints before doing the other cleaning steps.
1208
+ // In the future, if we add more other components with hooks, we will need to coordinate stopping all such
1209
+ // hooks before doing the other cleaning steps.
1141
1210
  //
1142
1211
  // Note that tests call this method directly in the same process without forking,
1143
1212
  // and in such a case non-current Threads keep running.
@@ -1145,8 +1214,10 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE instance)
1145
1214
  cpu_and_wall_time_worker_state *state;
1146
1215
  TypedData_Get_Struct(instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1147
1216
 
1148
- // Disable all tracepoints, so that there are no more attempts to mutate the profile
1149
- disable_tracepoints(state);
1217
+ // Disable all hooks, so that there are no more attempts to mutate the profile
1218
+ disable_hooks(state);
1219
+
1220
+ private_vm_api_access_self_test();
1150
1221
 
1151
1222
  reset_stats_not_thread_safe(state);
1152
1223
 
@@ -1182,7 +1253,6 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
1182
1253
  ID2SYM(rb_intern("simulated_signal_delivery")), /* => */ UINT2NUM(state->stats.simulated_signal_delivery),
1183
1254
  ID2SYM(rb_intern("signal_handler_enqueued_sample")), /* => */ UINT2NUM(state->stats.signal_handler_enqueued_sample),
1184
1255
  ID2SYM(rb_intern("signal_handler_prepared_sample")), /* => */ UINT2NUM(state->stats.signal_handler_prepared_sample),
1185
- ID2SYM(rb_intern("signal_handler_skipped_sample_on_altstack")), /* => */ UINT2NUM(global_stats.signal_handler_skipped_sample_on_altstack),
1186
1256
  ID2SYM(rb_intern("interrupt_thread_attempts")), /* => */ UINT2NUM(state->stats.interrupt_thread_attempts),
1187
1257
 
1188
1258
  // CPU Stats
@@ -1235,11 +1305,19 @@ void *simulate_sampling_signal_delivery(DDTRACE_UNUSED void *_unused) {
1235
1305
  // This can potentially happen if the CpuAndWallTimeWorker was stopped while the IdleSamplingHelper was trying to execute this action
1236
1306
  if (state == NULL) return NULL;
1237
1307
 
1308
+ // Since this is not a real signal firing, we need to block SIGPROF delivery on this thread to avoid an actual SIGPROF
1309
+ // signal coming in nested and interrupting us on this thread. Thus we respect the invariant of "no nesting" for `handle_sampling_signal`.
1310
+ //
1311
+ // Not needed when `no_signals_workaround_enabled` is set: no SIGPROFs are ever sent in that mode, so there's nothing to mask.
1312
+ if (!state->no_signals_workaround_enabled) block_sigprof_signal_handler_from_running_in_current_thread();
1313
+
1238
1314
  state->stats.simulated_signal_delivery++;
1239
1315
 
1240
1316
  // `handle_sampling_signal` does a few things extra on top of `sample_from_postponed_job` so that's why we don't shortcut here
1241
1317
  handle_sampling_signal(0, NULL, NULL);
1242
1318
 
1319
+ if (!state->no_signals_workaround_enabled) unblock_sigprof_signal_handler_from_running_in_current_thread();
1320
+
1243
1321
  return NULL; // Unused
1244
1322
  }
1245
1323
 
@@ -1260,7 +1338,6 @@ static void reset_stats_not_thread_safe(cpu_and_wall_time_worker_state *state) {
1260
1338
  .gvl_sampling_time_ns_min = UINT64_MAX,
1261
1339
  // Other fields are reset to 0
1262
1340
  };
1263
- global_stats = (global_stats_t) {0};
1264
1341
  }
1265
1342
 
1266
1343
  static void sleep_for(uint64_t time_ns) {
@@ -1289,18 +1366,12 @@ static VALUE _native_allocation_count(DDTRACE_UNUSED VALUE self) {
1289
1366
  return are_allocations_being_tracked ? ULL2NUM(allocation_count) : Qnil;
1290
1367
  }
1291
1368
 
1292
- #define HANDLE_CLOCK_FAILURE(call) ({ \
1293
- long _result = (call); \
1294
- if (_result == 0) { \
1295
- delayed_error_clock_failure(state); \
1296
- return; \
1297
- } \
1298
- _result; \
1299
- })
1300
-
1301
1369
  // Implements memory-related profiling events. This function is called by Ruby via the `rb_add_event_hook2`
1302
1370
  // when the RUBY_INTERNAL_EVENT_NEWOBJ event is triggered.
1303
1371
  //
1372
+ // This function is called from the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1373
+ // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1374
+ //
1304
1375
  // When allocation sampling is enabled, this function gets called for almost all* objects allocated by the Ruby VM.
1305
1376
  // (*In some weird cases the VM may skip this tracepoint.)
1306
1377
  //
@@ -1360,16 +1431,13 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1360
1431
  if (RB_LIKELY(state->dynamic_sampling_rate_enabled && !discrete_dynamic_sampler_should_sample(&state->allocation_sampler))) {
1361
1432
  state->stats.allocation_skipped++;
1362
1433
 
1363
- coarse_instant now = monotonic_coarse_wall_time_now_ns();
1364
- HANDLE_CLOCK_FAILURE(now.timestamp_ns);
1365
-
1366
- bool needs_readjust = discrete_dynamic_sampler_skipped_sample(&state->allocation_sampler, now);
1434
+ bool needs_readjust = discrete_dynamic_sampler_skipped_sample(&state->allocation_sampler, monotonic_coarse_wall_time_now_ns());
1367
1435
  if (RB_UNLIKELY(needs_readjust)) {
1368
1436
  // We rarely readjust, so this is a cold path
1369
1437
  // Also, while above we used the cheaper monotonic_coarse, for this call we want the regular monotonic call,
1370
1438
  // which is why we end up getting time "again".
1371
1439
  discrete_dynamic_sampler_readjust(
1372
- &state->allocation_sampler, HANDLE_CLOCK_FAILURE(monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE))
1440
+ &state->allocation_sampler, monotonic_wall_time_now_ns()
1373
1441
  );
1374
1442
  }
1375
1443
 
@@ -1379,7 +1447,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1379
1447
  // From here on, we've decided to go ahead with the sample, which is way less common than skipping it
1380
1448
 
1381
1449
  discrete_dynamic_sampler_before_sample(
1382
- &state->allocation_sampler, HANDLE_CLOCK_FAILURE(monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE))
1450
+ &state->allocation_sampler, monotonic_wall_time_now_ns()
1383
1451
  );
1384
1452
 
1385
1453
  during_sample_enter(state);
@@ -1393,12 +1461,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1393
1461
  );
1394
1462
 
1395
1463
  if (state->dynamic_sampling_rate_enabled) {
1396
- long now = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
1397
- if (now == 0) {
1398
- delayed_error_clock_failure(state);
1399
- // NOTE: Not short-circuiting here to make sure cleanup happens
1400
- }
1401
- uint64_t sampling_time_ns = discrete_dynamic_sampler_after_sample(&state->allocation_sampler, now);
1464
+ uint64_t sampling_time_ns = discrete_dynamic_sampler_after_sample(&state->allocation_sampler, monotonic_wall_time_now_ns());
1402
1465
  // NOTE: To keep things lean when dynamic sampling rate is disabled we skip clock interactions which is
1403
1466
  // why we're fine with having this inside this conditional.
1404
1467
  state->stats.allocation_sampling_time_ns_min = uint64_min_of(sampling_time_ns, state->stats.allocation_sampling_time_ns_min);
@@ -1411,7 +1474,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1411
1474
  during_sample_exit(state);
1412
1475
  }
1413
1476
 
1414
- static void disable_tracepoints(cpu_and_wall_time_worker_state *state) {
1477
+ static void disable_hooks(cpu_and_wall_time_worker_state *state) {
1415
1478
  if (state->gc_tracepoint != Qnil) {
1416
1479
  rb_tracepoint_disable(state->gc_tracepoint);
1417
1480
  }
@@ -1459,18 +1522,18 @@ static VALUE rescued_sample_allocation(VALUE arg) {
1459
1522
  // To control bias from sampling, we clamp the maximum weight attributed to a single allocation sample. This avoids
1460
1523
  // assigning a very large number to a sample, if for instance the dynamic sampling mechanism chose a really big interval.
1461
1524
  unsigned int weight = allocations_since_last_sample > MAX_ALLOC_WEIGHT ? MAX_ALLOC_WEIGHT : (unsigned int) allocations_since_last_sample;
1462
- bool needs_after_allocation = thread_context_collector_sample_allocation(state->thread_context_collector_instance, thread_context, weight, new_object);
1525
+ bool needs_commit = thread_context_collector_sample_allocation(state->thread_context_collector_instance, thread_context, weight, new_object);
1463
1526
  // ...but we still represent the skipped samples in the profile, thus the data will account for all allocations.
1464
1527
  if (weight < allocations_since_last_sample) {
1465
1528
  uint32_t skipped_samples = (uint32_t) uint64_min_of(allocations_since_last_sample - weight, UINT32_MAX);
1466
1529
  thread_context_collector_sample_skipped_allocation_samples(state->thread_context_collector_instance, skipped_samples);
1467
1530
  }
1468
1531
 
1469
- if (needs_after_allocation) {
1532
+ if (needs_commit) {
1470
1533
  #ifndef NO_POSTPONED_TRIGGER
1471
- rb_postponed_job_trigger(after_allocation_from_postponed_job_handle);
1534
+ rb_postponed_job_trigger(commit_heap_recordings_from_postponed_job_may_lose_gvl_handle);
1472
1535
  #else
1473
- // Not needed on legacy rubies
1536
+ rb_postponed_job_register_one(0, commit_heap_recordings_from_postponed_job_may_lose_gvl, NULL);
1474
1537
  #endif
1475
1538
  }
1476
1539
 
@@ -1483,11 +1546,6 @@ static void delayed_error(cpu_and_wall_time_worker_state *state, const char *err
1483
1546
  stop_state(state, rb_exc_new_cstr(rb_eRuntimeError, error), "delayed_error");
1484
1547
  }
1485
1548
 
1486
- static void delayed_error_clock_failure(cpu_and_wall_time_worker_state *state) {
1487
- // If we can't raise an immediate exception at the calling site, use the asynchronous flow through the main worker loop.
1488
- stop_state(state, rb_exc_new_cstr(clock_failure_exception_class, "failed to get clock time"), "delayed_error_clock_failure");
1489
- }
1490
-
1491
1549
  static VALUE _native_delayed_error(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE error_msg) {
1492
1550
  ENFORCE_TYPE(error_msg, T_STRING);
1493
1551
 
@@ -1536,6 +1594,8 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1536
1594
  VALUE target_thread = rb_thread_current();
1537
1595
  #endif
1538
1596
 
1597
+ // TODO: `rb_internal_thread_specific_get` is documented as "async and native thread safe.",
1598
+ // unclear if that holds during GC compaction.
1539
1599
  per_thread_context* thread_context = get_per_thread_context(target_thread);
1540
1600
  if (!thread_context) return;
1541
1601
  // If non-NULL the thread is profiled and from the main Ractor
@@ -1545,10 +1605,11 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1545
1605
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_READY) { /* waiting for gvl */
1546
1606
  thread_context_collector_on_gvl_waiting(thread_context);
1547
1607
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) { /* running/runnable */
1548
- // Interesting note: A RUBY_INTERNAL_THREAD_EVENT_RESUMED is guaranteed to be called with the GVL being acquired
1549
- // and on the event thread.
1550
- // However, on_gvl_event() is called while holding the scheduler lock, so we do as little work as possible here,
1551
- // and perform the sample in a postponed_job.
1608
+ // We must only use async-signal-safe functions here and not call arbitrary Ruby APIs and not allocate!
1609
+ // One might assume RUBY_INTERNAL_THREAD_EVENT_RESUMED means having the GVL and running that thread.
1610
+ // However, the reality is more complicated (https://bugs.ruby-lang.org/issues/22098),
1611
+ // it only "sort of" has the GVL but not fully, and it's called while holding the scheduler lock,
1612
+ // so we do as little work as possible here, and perform the sample in a postponed_job.
1552
1613
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state; // Read from global variable, see "sampler global state safety" note above
1553
1614
  if (state == NULL) return; // This should not happen, but just in case...
1554
1615
 
@@ -1560,7 +1621,8 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1560
1621
  // that next.
1561
1622
  during_sample_enter(state);
1562
1623
 
1563
- on_gvl_running_result result = thread_context_collector_on_gvl_running(state->thread_context_collector_instance, target_thread, thread_context);
1624
+ on_gvl_running_result result =
1625
+ thread_context_collector_on_gvl_running(target_thread, thread_context, state->waiting_for_gvl_threshold_ns);
1564
1626
 
1565
1627
  during_sample_exit(state);
1566
1628
 
@@ -1605,14 +1667,15 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1605
1667
  cpu_and_wall_time_worker_state *state;
1606
1668
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1607
1669
 
1608
- long wall_time_ns_before_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1670
+ long wall_time_ns_before_sample = monotonic_wall_time_now_ns();
1609
1671
  thread_context_collector_sample_after_gvl_running(state->thread_context_collector_instance, rb_thread_current(), wall_time_ns_before_sample);
1610
- long wall_time_ns_after_sample = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1672
+ long wall_time_ns_after_sample = monotonic_wall_time_now_ns();
1611
1673
 
1612
- long delta_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
1674
+ if (wall_time_ns_after_sample < wall_time_ns_before_sample) {
1675
+ raise_error(rb_eRuntimeError, "BUG: Unexpected wall time going backwards when tracking overhead");
1676
+ }
1613
1677
 
1614
- // Guard against wall-time going backwards, see https://github.com/DataDog/dd-trace-rb/pull/2336 for discussion.
1615
- uint64_t sampling_time_ns = delta_ns < 0 ? 0 : delta_ns;
1678
+ uint64_t sampling_time_ns = wall_time_ns_after_sample - wall_time_ns_before_sample;
1616
1679
 
1617
1680
  state->stats.gvl_sampling_time_ns_min = uint64_min_of(sampling_time_ns, state->stats.gvl_sampling_time_ns_min);
1618
1681
  state->stats.gvl_sampling_time_ns_max = uint64_max_of(sampling_time_ns, state->stats.gvl_sampling_time_ns_max);
@@ -1650,54 +1713,54 @@ static VALUE handle_sampling_failure_thread_context_collector_sample_after_gc(VA
1650
1713
  return Qnil;
1651
1714
  }
1652
1715
 
1716
+ static VALUE handle_sampling_failure_thread_context_collector_heap_update(VALUE self_instance, VALUE exception) {
1717
+ stop(self_instance, exception, "thread_context_collector_heap_update_may_lose_gvl");
1718
+ return Qnil;
1719
+ }
1720
+
1653
1721
  static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instance, VALUE exception) {
1654
1722
  stop(self_instance, exception, "rescued_sample_allocation");
1655
1723
  return Qnil;
1656
1724
  }
1657
1725
 
1658
- static VALUE handle_sampling_failure_rescued_after_allocation(VALUE self_instance, VALUE exception) {
1659
- stop(self_instance, exception, "rescued_after_allocation");
1726
+ static VALUE handle_sampling_failure_rescued_commit_heap_recordings(VALUE self_instance, VALUE exception) {
1727
+ stop(self_instance, exception, "rescued_commit_heap_recordings_may_lose_gvl");
1660
1728
  return Qnil;
1661
1729
  }
1662
1730
 
1663
- static VALUE rescued_after_allocation(VALUE self_instance) {
1731
+ static VALUE rescued_commit_heap_recordings_may_lose_gvl(VALUE self_instance) {
1664
1732
  cpu_and_wall_time_worker_state *state;
1665
1733
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1666
1734
 
1667
- thread_context_collector_after_allocation(state->thread_context_collector_instance);
1735
+ thread_context_collector_commit_heap_recordings_may_lose_gvl(state->thread_context_collector_instance);
1668
1736
 
1669
1737
  // Return a dummy VALUE because we're called from rb_rescue2 which requires it
1670
1738
  return Qnil;
1671
1739
  }
1672
1740
 
1673
- // This postponed job callback is used to finalize heap allocation recordings on Ruby 4+.
1674
- // During on_newobj_event, calling rb_obj_id() is unsafe because it mutates the object.
1675
- // So we defer getting the object_id until after the event completes.
1676
- #pragma GCC diagnostic push
1677
- #pragma GCC diagnostic ignored "-Wunused-function" // This is only used for some Rubies, but we want to build on all to make it easier to dev
1678
- static void after_allocation_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1741
+ // This postponed job callback is used to commit heap allocation recordings.
1742
+ // During on_newobj_event, we can't take the weak reference the heap recorder needs to track the object, so we defer
1743
+ // that until after the event completes.
1744
+ static void commit_heap_recordings_from_postponed_job_may_lose_gvl(DDTRACE_UNUSED void *_unused) {
1679
1745
  cpu_and_wall_time_worker_state *state = active_sampler_instance_state;
1680
1746
 
1681
1747
  if (state == NULL || !ddtrace_rb_ractor_main_p()) return;
1682
1748
 
1683
- // Protect against nested operations
1684
- if (state->during_sample) return;
1685
-
1686
- during_sample_enter(state);
1749
+ if (state->during_sample) {
1750
+ delayed_error(state, "commit_heap_recordings_from_postponed_job_may_lose_gvl called during_sample");
1751
+ return;
1752
+ }
1687
1753
 
1688
1754
  // NOTE: We're not updating the allocation_sampler here.
1689
1755
  // This means work done in this function isn't accounted for as profiler overhead.
1690
1756
  // This is acceptable as the amount of work done here is expected to be small.
1691
1757
  safely_call(
1692
- rescued_after_allocation,
1758
+ rescued_commit_heap_recordings_may_lose_gvl,
1693
1759
  state->self_instance,
1694
1760
  state->self_instance,
1695
- handle_sampling_failure_rescued_after_allocation
1761
+ handle_sampling_failure_rescued_commit_heap_recordings
1696
1762
  );
1697
-
1698
- during_sample_exit(state);
1699
1763
  }
1700
- #pragma GCC diagnostic pop
1701
1764
 
1702
1765
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state) {
1703
1766
  // Tell the compiler it's not allowed to reorder the `during_sample` flag with anything that happens after.