datadog 2.30.0 → 2.43.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 (942) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +382 -1
  3. data/README.md +3 -0
  4. data/bin/ddprofrb +4 -4
  5. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  6. data/ext/datadog_profiling_native_extension/clock_id.h +9 -1
  7. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +66 -0
  8. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -1
  9. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +312 -99
  10. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  11. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +18 -4
  12. data/ext/datadog_profiling_native_extension/collectors_stack.c +256 -163
  13. data/ext/datadog_profiling_native_extension/collectors_stack.h +23 -4
  14. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +833 -513
  15. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +24 -9
  16. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +5 -4
  17. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +44 -19
  18. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +7 -12
  19. data/ext/datadog_profiling_native_extension/extconf.rb +83 -105
  20. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
  21. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
  22. data/ext/datadog_profiling_native_extension/heap_recorder.c +391 -277
  23. data/ext/datadog_profiling_native_extension/heap_recorder.h +50 -31
  24. data/ext/datadog_profiling_native_extension/http_transport.c +10 -5
  25. data/ext/datadog_profiling_native_extension/macos_sampler_thread.h +55 -0
  26. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +3 -27
  27. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +750 -482
  28. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +32 -12
  29. data/ext/datadog_profiling_native_extension/profiling.c +43 -4
  30. data/ext/datadog_profiling_native_extension/ruby_helpers.c +34 -106
  31. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  32. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +2 -0
  33. data/ext/datadog_profiling_native_extension/stack_recorder.c +139 -84
  34. data/ext/datadog_profiling_native_extension/stack_recorder.h +13 -4
  35. data/ext/datadog_profiling_native_extension/time_helpers.h +15 -8
  36. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
  37. data/ext/libdatadog_api/crashtracker.c +7 -8
  38. data/ext/libdatadog_api/datadog_ruby_common.c +44 -19
  39. data/ext/libdatadog_api/datadog_ruby_common.h +7 -12
  40. data/ext/libdatadog_api/di.c +213 -0
  41. data/ext/libdatadog_api/extconf.rb +40 -26
  42. data/ext/libdatadog_api/feature_flags.c +26 -0
  43. data/ext/libdatadog_api/init.c +11 -2
  44. data/ext/libdatadog_api/otel_thread_context.c +247 -0
  45. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  46. data/ext/libdatadog_api/trace_exporter.c +1932 -0
  47. data/ext/libdatadog_api/trace_exporter.h +5 -0
  48. data/ext/libdatadog_extconf_helpers.rb +50 -5
  49. data/lib/datadog/ai_guard/api_client.rb +9 -7
  50. data/lib/datadog/ai_guard/autoload.rb +10 -0
  51. data/lib/datadog/ai_guard/component.rb +11 -9
  52. data/lib/datadog/ai_guard/configuration.rb +106 -2
  53. data/lib/datadog/ai_guard/contrib/auto_instrument.rb +24 -0
  54. data/lib/datadog/ai_guard/contrib/rack/integration.rb +42 -0
  55. data/lib/datadog/ai_guard/contrib/rack/patcher.rb +26 -0
  56. data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +97 -0
  57. data/lib/datadog/ai_guard/contrib/rails/integration.rb +41 -0
  58. data/lib/datadog/ai_guard/contrib/rails/patcher.rb +97 -0
  59. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +41 -3
  60. data/lib/datadog/ai_guard/evaluation/content_builder.rb +31 -0
  61. data/lib/datadog/ai_guard/evaluation/content_part.rb +36 -0
  62. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +3 -1
  63. data/lib/datadog/ai_guard/evaluation/request.rb +22 -17
  64. data/lib/datadog/ai_guard/evaluation/result.rb +3 -1
  65. data/lib/datadog/ai_guard/evaluation.rb +51 -7
  66. data/lib/datadog/ai_guard/ext.rb +15 -0
  67. data/lib/datadog/ai_guard.rb +34 -8
  68. data/lib/datadog/appsec/actions_handler/serializable_backtrace.rb +22 -43
  69. data/lib/datadog/appsec/actions_handler.rb +6 -6
  70. data/lib/datadog/appsec/anonymizer.rb +1 -1
  71. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +2 -2
  72. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +6 -6
  73. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +1 -1
  74. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +2 -2
  75. data/lib/datadog/appsec/api_security/route_extractor.rb +26 -14
  76. data/lib/datadog/appsec/api_security/sampler.rb +5 -5
  77. data/lib/datadog/appsec/api_security.rb +1 -1
  78. data/lib/datadog/appsec/assets/blocked.html +1 -108
  79. data/lib/datadog/appsec/assets.rb +7 -7
  80. data/lib/datadog/appsec/autoload.rb +4 -4
  81. data/lib/datadog/appsec/component.rb +29 -22
  82. data/lib/datadog/appsec/compressed_json.rb +7 -7
  83. data/lib/datadog/appsec/configuration.rb +436 -1
  84. data/lib/datadog/appsec/context.rb +10 -8
  85. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +6 -6
  86. data/lib/datadog/appsec/contrib/active_record/integration.rb +4 -4
  87. data/lib/datadog/appsec/contrib/active_record/patcher.rb +4 -4
  88. data/lib/datadog/appsec/contrib/aws_lambda/gateway/watcher.rb +75 -0
  89. data/lib/datadog/appsec/contrib/aws_lambda/integration.rb +39 -0
  90. data/lib/datadog/appsec/contrib/aws_lambda/patcher.rb +30 -0
  91. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +146 -0
  92. data/lib/datadog/appsec/contrib/devise/configuration.rb +1 -1
  93. data/lib/datadog/appsec/contrib/devise/data_extractor.rb +3 -3
  94. data/lib/datadog/appsec/contrib/devise/ext.rb +20 -20
  95. data/lib/datadog/appsec/contrib/devise/integration.rb +4 -4
  96. data/lib/datadog/appsec/contrib/devise/patcher.rb +8 -8
  97. data/lib/datadog/appsec/contrib/devise/patches/signin_tracking_patch.rb +14 -13
  98. data/lib/datadog/appsec/contrib/devise/patches/signup_tracking_patch.rb +8 -8
  99. data/lib/datadog/appsec/contrib/devise/tracking_middleware.rb +24 -9
  100. data/lib/datadog/appsec/contrib/excon/integration.rb +4 -4
  101. data/lib/datadog/appsec/contrib/excon/patcher.rb +1 -1
  102. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +76 -21
  103. data/lib/datadog/appsec/contrib/faraday/integration.rb +4 -4
  104. data/lib/datadog/appsec/contrib/faraday/patcher.rb +4 -4
  105. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +72 -18
  106. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +4 -4
  107. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +66 -21
  108. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +7 -7
  109. data/lib/datadog/appsec/contrib/graphql/integration.rb +9 -8
  110. data/lib/datadog/appsec/contrib/graphql/patcher.rb +3 -3
  111. data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
  112. data/lib/datadog/appsec/contrib/rack/ext.rb +29 -29
  113. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +56 -17
  114. data/lib/datadog/appsec/contrib/rack/gateway/response.rb +1 -1
  115. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +46 -30
  116. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +79 -0
  117. data/lib/datadog/appsec/contrib/rack/integration.rb +6 -6
  118. data/lib/datadog/appsec/contrib/rack/patcher.rb +3 -3
  119. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +11 -8
  120. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +51 -61
  121. data/lib/datadog/appsec/contrib/rack/response_body.rb +1 -1
  122. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +43 -4
  123. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +25 -9
  124. data/lib/datadog/appsec/contrib/rails/integration.rb +5 -5
  125. data/lib/datadog/appsec/contrib/rails/patcher.rb +17 -17
  126. data/lib/datadog/appsec/contrib/rails/patches/process_action_patch.rb +2 -2
  127. data/lib/datadog/appsec/contrib/rails/patches/render_to_body_patch.rb +3 -3
  128. data/lib/datadog/appsec/contrib/rails/request.rb +3 -3
  129. data/lib/datadog/appsec/contrib/rails/request_middleware.rb +2 -1
  130. data/lib/datadog/appsec/contrib/rest_client/integration.rb +5 -5
  131. data/lib/datadog/appsec/contrib/rest_client/patcher.rb +1 -1
  132. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +74 -21
  133. data/lib/datadog/appsec/contrib/sinatra/gateway/request.rb +1 -1
  134. data/lib/datadog/appsec/contrib/sinatra/gateway/route_params.rb +1 -1
  135. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +34 -13
  136. data/lib/datadog/appsec/contrib/sinatra/integration.rb +5 -5
  137. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +11 -11
  138. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +3 -3
  139. data/lib/datadog/appsec/contrib/sinatra/request_middleware.rb +2 -1
  140. data/lib/datadog/appsec/counter_sampler.rb +1 -1
  141. data/lib/datadog/appsec/default_header_tags.rb +52 -0
  142. data/lib/datadog/appsec/event.rb +14 -14
  143. data/lib/datadog/appsec/ext.rb +11 -10
  144. data/lib/datadog/appsec/extensions.rb +2 -2
  145. data/lib/datadog/appsec/instrumentation/gateway.rb +1 -14
  146. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  147. data/lib/datadog/appsec/metrics/exporter.rb +17 -11
  148. data/lib/datadog/appsec/metrics/telemetry.rb +15 -3
  149. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +4 -4
  150. data/lib/datadog/appsec/metrics.rb +4 -4
  151. data/lib/datadog/appsec/monitor/gateway/watcher.rb +18 -12
  152. data/lib/datadog/appsec/monitor.rb +1 -1
  153. data/lib/datadog/appsec/processor/rule_loader.rb +32 -32
  154. data/lib/datadog/appsec/rate_limiter.rb +1 -1
  155. data/lib/datadog/appsec/remote.rb +15 -9
  156. data/lib/datadog/appsec/response.rb +22 -22
  157. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  158. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  159. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  160. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  161. data/lib/datadog/appsec/security_engine/engine.rb +40 -40
  162. data/lib/datadog/appsec/security_engine/runner.rb +3 -3
  163. data/lib/datadog/appsec/security_event.rb +2 -2
  164. data/lib/datadog/appsec/trace_keeper.rb +18 -6
  165. data/lib/datadog/appsec/utils/http/body.rb +11 -8
  166. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  167. data/lib/datadog/appsec/utils/http/media_range.rb +15 -15
  168. data/lib/datadog/appsec/utils/http/media_type.rb +5 -6
  169. data/lib/datadog/appsec/utils/http/url_encoded.rb +79 -24
  170. data/lib/datadog/appsec.rb +21 -24
  171. data/lib/datadog/auto_instrument.rb +2 -2
  172. data/lib/datadog/core/buffer/cruby.rb +1 -1
  173. data/lib/datadog/core/buffer/thread_safe.rb +1 -1
  174. data/lib/datadog/core/configuration/agent_settings.rb +1 -1
  175. data/lib/datadog/core/configuration/agent_settings_resolver.rb +15 -15
  176. data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
  177. data/lib/datadog/core/configuration/base.rb +19 -7
  178. data/lib/datadog/core/configuration/components.rb +142 -36
  179. data/lib/datadog/core/configuration/components_state.rb +6 -1
  180. data/lib/datadog/core/configuration/config_helper.rb +12 -3
  181. data/lib/datadog/core/configuration/deprecations.rb +6 -6
  182. data/lib/datadog/core/configuration/ext.rb +11 -11
  183. data/lib/datadog/core/configuration/option.rb +46 -21
  184. data/lib/datadog/core/configuration/option_definition.rb +43 -17
  185. data/lib/datadog/core/configuration/options.rb +42 -8
  186. data/lib/datadog/core/configuration/settings.rb +129 -104
  187. data/lib/datadog/core/configuration/stable_config.rb +1 -1
  188. data/lib/datadog/core/configuration/supported_configurations.rb +25 -1
  189. data/lib/datadog/core/configuration.rb +9 -9
  190. data/lib/datadog/core/contrib/rails/railtie.rb +32 -0
  191. data/lib/datadog/core/contrib/rails/utils.rb +7 -3
  192. data/lib/datadog/core/crashtracking/component.rb +17 -13
  193. data/lib/datadog/core/crashtracking/tag_builder.rb +5 -5
  194. data/lib/datadog/core/deprecations.rb +1 -1
  195. data/lib/datadog/core/diagnostics/environment_logger.rb +45 -8
  196. data/lib/datadog/core/diagnostics/health.rb +2 -2
  197. data/lib/datadog/core/encoding.rb +4 -4
  198. data/lib/datadog/core/environment/agent_info.rb +2 -2
  199. data/lib/datadog/core/environment/cgroup.rb +4 -4
  200. data/lib/datadog/core/environment/container.rb +11 -11
  201. data/lib/datadog/core/environment/execution.rb +3 -3
  202. data/lib/datadog/core/environment/ext.rb +21 -15
  203. data/lib/datadog/core/environment/gc.rb +1 -1
  204. data/lib/datadog/core/environment/git.rb +2 -2
  205. data/lib/datadog/core/environment/identity.rb +47 -11
  206. data/lib/datadog/core/environment/platform.rb +8 -8
  207. data/lib/datadog/core/environment/process.rb +54 -19
  208. data/lib/datadog/core/environment/socket.rb +15 -2
  209. data/lib/datadog/core/environment/variable_helpers.rb +1 -1
  210. data/lib/datadog/core/environment/vm_cache.rb +1 -1
  211. data/lib/datadog/core/environment/yjit.rb +2 -2
  212. data/lib/datadog/core/error.rb +5 -5
  213. data/lib/datadog/core/evp.rb +11 -0
  214. data/lib/datadog/core/extensions.rb +1 -1
  215. data/lib/datadog/core/feature_flags.rb +3 -3
  216. data/lib/datadog/core/git/ext.rb +4 -4
  217. data/lib/datadog/core/knuth_sampler.rb +3 -1
  218. data/lib/datadog/core/logger.rb +3 -3
  219. data/lib/datadog/core/logging/ext.rb +1 -1
  220. data/lib/datadog/core/metrics/client.rb +26 -26
  221. data/lib/datadog/core/metrics/ext.rb +5 -5
  222. data/lib/datadog/core/metrics/helpers.rb +1 -1
  223. data/lib/datadog/core/metrics/logging.rb +2 -2
  224. data/lib/datadog/core/metrics/options.rb +5 -5
  225. data/lib/datadog/core/pin.rb +1 -1
  226. data/lib/datadog/core/process_discovery.rb +8 -8
  227. data/lib/datadog/core/rate_limiter.rb +1 -1
  228. data/lib/datadog/core/remote/client/capabilities.rb +67 -15
  229. data/lib/datadog/core/remote/client.rb +18 -18
  230. data/lib/datadog/core/remote/component.rb +57 -32
  231. data/lib/datadog/core/remote/configuration/content.rb +3 -3
  232. data/lib/datadog/core/remote/configuration/digest.rb +1 -1
  233. data/lib/datadog/core/remote/configuration/path.rb +9 -9
  234. data/lib/datadog/core/remote/configuration/repository.rb +4 -4
  235. data/lib/datadog/core/remote/configuration/target.rb +9 -9
  236. data/lib/datadog/core/remote/configuration.rb +4 -4
  237. data/lib/datadog/core/remote/ext.rb +3 -3
  238. data/lib/datadog/core/remote/negotiation.rb +1 -1
  239. data/lib/datadog/core/remote/tie/tracing.rb +7 -7
  240. data/lib/datadog/core/remote/transport/config.rb +4 -4
  241. data/lib/datadog/core/remote/transport/http/config.rb +10 -10
  242. data/lib/datadog/core/remote/transport/http/negotiation.rb +4 -4
  243. data/lib/datadog/core/remote/transport/http.rb +10 -10
  244. data/lib/datadog/core/remote/transport/negotiation.rb +3 -3
  245. data/lib/datadog/core/remote/worker.rb +5 -5
  246. data/lib/datadog/core/runtime/ext.rb +22 -22
  247. data/lib/datadog/core/runtime/metrics.rb +10 -10
  248. data/lib/datadog/core/tag_builder.rb +18 -18
  249. data/lib/datadog/core/tag_normalizer.rb +7 -7
  250. data/lib/datadog/core/telemetry/component.rb +16 -13
  251. data/lib/datadog/core/telemetry/configuration_value.rb +38 -0
  252. data/lib/datadog/core/telemetry/emitter.rb +5 -5
  253. data/lib/datadog/core/telemetry/event/app_client_configuration_change.rb +10 -9
  254. data/lib/datadog/core/telemetry/event/app_closing.rb +2 -2
  255. data/lib/datadog/core/telemetry/event/app_dependencies_loaded.rb +2 -2
  256. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +3 -3
  257. data/lib/datadog/core/telemetry/event/app_extended_heartbeat.rb +32 -0
  258. data/lib/datadog/core/telemetry/event/app_heartbeat.rb +2 -2
  259. data/lib/datadog/core/telemetry/event/app_integrations_change.rb +2 -2
  260. data/lib/datadog/core/telemetry/event/app_started.rb +142 -193
  261. data/lib/datadog/core/telemetry/event/base.rb +1 -1
  262. data/lib/datadog/core/telemetry/event/distributions.rb +2 -2
  263. data/lib/datadog/core/telemetry/event/generate_metrics.rb +3 -3
  264. data/lib/datadog/core/telemetry/event/log.rb +6 -6
  265. data/lib/datadog/core/telemetry/event/message_batch.rb +1 -1
  266. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +3 -3
  267. data/lib/datadog/core/telemetry/event.rb +16 -22
  268. data/lib/datadog/core/telemetry/ext.rb +11 -10
  269. data/lib/datadog/core/telemetry/logger.rb +1 -1
  270. data/lib/datadog/core/telemetry/logging.rb +5 -5
  271. data/lib/datadog/core/telemetry/metric.rb +9 -9
  272. data/lib/datadog/core/telemetry/metrics_collection.rb +2 -2
  273. data/lib/datadog/core/telemetry/metrics_manager.rb +1 -1
  274. data/lib/datadog/core/telemetry/request.rb +6 -8
  275. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +16 -11
  276. data/lib/datadog/core/telemetry/transport/http.rb +7 -7
  277. data/lib/datadog/core/telemetry/transport/telemetry.rb +3 -3
  278. data/lib/datadog/core/telemetry/worker.rb +26 -6
  279. data/lib/datadog/core/transport/ext.rb +14 -14
  280. data/lib/datadog/core/transport/http/adapters/net.rb +4 -4
  281. data/lib/datadog/core/transport/http/adapters/test.rb +1 -1
  282. data/lib/datadog/core/transport/http/adapters/unix_socket.rb +4 -4
  283. data/lib/datadog/core/transport/http/api/endpoint.rb +1 -1
  284. data/lib/datadog/core/transport/http/api/map.rb +1 -1
  285. data/lib/datadog/core/transport/http/builder.rb +6 -6
  286. data/lib/datadog/core/transport/http/client.rb +2 -2
  287. data/lib/datadog/core/transport/http/response.rb +1 -1
  288. data/lib/datadog/core/transport/http.rb +9 -7
  289. data/lib/datadog/core/transport/response.rb +1 -1
  290. data/lib/datadog/core/transport/transport.rb +2 -2
  291. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +172 -20
  292. data/lib/datadog/core/utils/base64_codec.rb +23 -0
  293. data/lib/datadog/core/utils/duration.rb +1 -1
  294. data/lib/datadog/core/utils/{array.rb → enumerable_compat.rb} +2 -2
  295. data/lib/datadog/core/utils/forking.rb +3 -1
  296. data/lib/datadog/core/utils/hash.rb +0 -23
  297. data/lib/datadog/core/utils/lru_cache.rb +3 -3
  298. data/lib/datadog/core/utils/network.rb +11 -11
  299. data/lib/datadog/core/utils/only_once.rb +1 -1
  300. data/lib/datadog/core/utils/only_once_successful.rb +1 -1
  301. data/lib/datadog/core/utils/safe_dup.rb +2 -1
  302. data/lib/datadog/core/utils/sequence.rb +3 -6
  303. data/lib/datadog/core/utils/spawn_monkey_patch.rb +68 -0
  304. data/lib/datadog/core/utils/time.rb +18 -52
  305. data/lib/datadog/core/utils/url.rb +1 -1
  306. data/lib/datadog/core/utils.rb +5 -5
  307. data/lib/datadog/core/workers/async.rb +2 -2
  308. data/lib/datadog/core/workers/polling.rb +2 -2
  309. data/lib/datadog/core/workers/runtime_metrics.rb +4 -4
  310. data/lib/datadog/core.rb +7 -5
  311. data/lib/datadog/data_streams/configuration.rb +40 -1
  312. data/lib/datadog/data_streams/ext.rb +1 -1
  313. data/lib/datadog/data_streams/extensions.rb +2 -2
  314. data/lib/datadog/data_streams/pathway_context.rb +28 -30
  315. data/lib/datadog/data_streams/processor.rb +93 -62
  316. data/lib/datadog/data_streams/transport/http/stats.rb +5 -5
  317. data/lib/datadog/data_streams/transport/http.rb +5 -5
  318. data/lib/datadog/data_streams/transport/stats.rb +7 -7
  319. data/lib/datadog/data_streams.rb +5 -5
  320. data/lib/datadog/di/base.rb +16 -8
  321. data/lib/datadog/di/boot.rb +25 -24
  322. data/lib/datadog/di/capture_expression.rb +21 -0
  323. data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
  324. data/lib/datadog/di/capture_limits.rb +41 -0
  325. data/lib/datadog/di/code_tracker.rb +218 -16
  326. data/lib/datadog/di/component.rb +123 -37
  327. data/lib/datadog/di/configuration.rb +241 -2
  328. data/lib/datadog/di/context.rb +9 -3
  329. data/lib/datadog/di/contrib/active_record.rb +6 -2
  330. data/lib/datadog/di/contrib/railtie.rb +1 -1
  331. data/lib/datadog/di/contrib.rb +5 -5
  332. data/lib/datadog/di/el/compiler.rb +114 -38
  333. data/lib/datadog/di/el/evaluator.rb +82 -2
  334. data/lib/datadog/di/el/expression.rb +13 -2
  335. data/lib/datadog/di/el.rb +3 -3
  336. data/lib/datadog/di/error.rb +10 -0
  337. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  338. data/lib/datadog/di/instrumenter.rb +528 -184
  339. data/lib/datadog/di/logger.rb +3 -3
  340. data/lib/datadog/di/preload.rb +1 -1
  341. data/lib/datadog/di/probe.rb +69 -2
  342. data/lib/datadog/di/probe_builder.rb +98 -15
  343. data/lib/datadog/di/probe_file_loader/railtie.rb +1 -1
  344. data/lib/datadog/di/probe_file_loader.rb +14 -9
  345. data/lib/datadog/di/probe_manager.rb +73 -15
  346. data/lib/datadog/di/probe_notification_builder.rb +178 -25
  347. data/lib/datadog/di/probe_notifier_worker.rb +57 -37
  348. data/lib/datadog/di/probe_repository.rb +1 -1
  349. data/lib/datadog/di/redactor.rb +16 -1
  350. data/lib/datadog/di/remote.rb +187 -26
  351. data/lib/datadog/di/serializer.rb +79 -35
  352. data/lib/datadog/di/transport/diagnostics.rb +5 -5
  353. data/lib/datadog/di/transport/http/diagnostics.rb +3 -3
  354. data/lib/datadog/di/transport/http/input.rb +2 -2
  355. data/lib/datadog/di/transport/http.rb +10 -10
  356. data/lib/datadog/di/transport/input.rb +18 -14
  357. data/lib/datadog/di/utils.rb +43 -15
  358. data/lib/datadog/di.rb +147 -3
  359. data/lib/datadog/error_tracking/collector.rb +3 -2
  360. data/lib/datadog/error_tracking/component.rb +12 -12
  361. data/lib/datadog/error_tracking/configuration.rb +55 -2
  362. data/lib/datadog/error_tracking/ext.rb +7 -7
  363. data/lib/datadog/error_tracking/extensions.rb +2 -2
  364. data/lib/datadog/error_tracking/filters.rb +4 -4
  365. data/lib/datadog/error_tracking.rb +6 -6
  366. data/lib/datadog/kit/appsec/events/v2.rb +91 -33
  367. data/lib/datadog/kit/appsec/events.rb +25 -25
  368. data/lib/datadog/kit/enable_core_dumps.rb +3 -3
  369. data/lib/datadog/kit/identity.rb +18 -18
  370. data/lib/datadog/kit/tracing/method_tracer.rb +9 -6
  371. data/lib/datadog/kit.rb +1 -1
  372. data/lib/datadog/open_feature/component.rb +62 -13
  373. data/lib/datadog/open_feature/configuration.rb +22 -1
  374. data/lib/datadog/open_feature/evaluation_engine.rb +38 -12
  375. data/lib/datadog/open_feature/exposures/batch_builder.rb +1 -1
  376. data/lib/datadog/open_feature/exposures/buffer.rb +1 -1
  377. data/lib/datadog/open_feature/exposures/deduplicator.rb +1 -1
  378. data/lib/datadog/open_feature/exposures/event.rb +17 -10
  379. data/lib/datadog/open_feature/exposures/reporter.rb +3 -3
  380. data/lib/datadog/open_feature/exposures/worker.rb +6 -6
  381. data/lib/datadog/open_feature/ext.rb +32 -6
  382. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +442 -0
  383. data/lib/datadog/open_feature/flag_evaluation/writer.rb +544 -0
  384. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +105 -0
  385. data/lib/datadog/open_feature/hooks/flag_eval_metrics_hook.rb +51 -0
  386. data/lib/datadog/open_feature/hooks/span_enrichment_hook/codec.rb +58 -0
  387. data/lib/datadog/open_feature/hooks/span_enrichment_hook/span_enrichment_state.rb +96 -0
  388. data/lib/datadog/open_feature/hooks/span_enrichment_hook/span_enrichment_state_store.rb +49 -0
  389. data/lib/datadog/open_feature/hooks/span_enrichment_hook.rb +184 -0
  390. data/lib/datadog/open_feature/metrics/flag_eval_metrics.rb +149 -0
  391. data/lib/datadog/open_feature/native_evaluator.rb +56 -7
  392. data/lib/datadog/open_feature/noop_evaluator.rb +8 -3
  393. data/lib/datadog/open_feature/provider.rb +115 -21
  394. data/lib/datadog/open_feature/remote.rb +3 -3
  395. data/lib/datadog/open_feature/resolution_details.rb +4 -2
  396. data/lib/datadog/open_feature/transport.rb +71 -12
  397. data/lib/datadog/open_feature.rb +2 -2
  398. data/lib/datadog/opentelemetry/api/baggage.rb +3 -3
  399. data/lib/datadog/opentelemetry/api/context.rb +4 -4
  400. data/lib/datadog/opentelemetry/configuration/settings.rb +95 -28
  401. data/lib/datadog/opentelemetry/ext.rb +9 -0
  402. data/lib/datadog/opentelemetry/logs.rb +98 -0
  403. data/lib/datadog/opentelemetry/metrics.rb +17 -44
  404. data/lib/datadog/opentelemetry/sdk/configurator.rb +46 -6
  405. data/lib/datadog/opentelemetry/sdk/id_generator.rb +16 -10
  406. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +30 -0
  407. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +9 -16
  408. data/lib/datadog/opentelemetry/sdk/propagator.rb +17 -11
  409. data/lib/datadog/opentelemetry/sdk/span_processor.rb +18 -15
  410. data/lib/datadog/opentelemetry/sdk/trace/span.rb +32 -32
  411. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  412. data/lib/datadog/opentelemetry/signal_configuration.rb +53 -0
  413. data/lib/datadog/opentelemetry/trace.rb +4 -1
  414. data/lib/datadog/opentelemetry.rb +8 -7
  415. data/lib/datadog/profiling/collectors/code_provenance.rb +36 -11
  416. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +40 -3
  417. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +12 -4
  418. data/lib/datadog/profiling/collectors/info.rb +19 -10
  419. data/lib/datadog/profiling/collectors/thread_context.rb +16 -22
  420. data/lib/datadog/profiling/component.rb +41 -34
  421. data/lib/datadog/profiling/exporter.rb +38 -13
  422. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +6 -5
  423. data/lib/datadog/profiling/ext.rb +0 -2
  424. data/lib/datadog/profiling/flush.rb +21 -12
  425. data/lib/datadog/profiling/http_transport.rb +13 -2
  426. data/lib/datadog/profiling/load_native_extension.rb +2 -2
  427. data/lib/datadog/profiling/profiler.rb +13 -5
  428. data/lib/datadog/profiling/scheduler.rb +2 -2
  429. data/lib/datadog/profiling/sequence_tracker.rb +1 -1
  430. data/lib/datadog/profiling/stack_recorder.rb +0 -8
  431. data/lib/datadog/profiling/tag_builder.rb +19 -19
  432. data/lib/datadog/profiling/tasks/exec.rb +9 -4
  433. data/lib/datadog/profiling/tasks/help.rb +1 -0
  434. data/lib/datadog/profiling/tasks/setup.rb +2 -2
  435. data/lib/datadog/profiling.rb +32 -29
  436. data/lib/datadog/ruby_version.rb +25 -0
  437. data/lib/datadog/single_step_instrument.rb +2 -2
  438. data/lib/datadog/symbol_database/component.rb +734 -0
  439. data/lib/datadog/symbol_database/configuration.rb +73 -0
  440. data/lib/datadog/symbol_database/extensions.rb +19 -0
  441. data/lib/datadog/symbol_database/extractor.rb +1110 -0
  442. data/lib/datadog/symbol_database/file_hash.rb +48 -0
  443. data/lib/datadog/symbol_database/logger.rb +43 -0
  444. data/lib/datadog/symbol_database/remote.rb +195 -0
  445. data/lib/datadog/symbol_database/scope.rb +102 -0
  446. data/lib/datadog/symbol_database/scope_batcher.rb +292 -0
  447. data/lib/datadog/symbol_database/service_version.rb +66 -0
  448. data/lib/datadog/symbol_database/symbol.rb +69 -0
  449. data/lib/datadog/symbol_database/transport/http/endpoint.rb +28 -0
  450. data/lib/datadog/symbol_database/transport/http.rb +45 -0
  451. data/lib/datadog/symbol_database/transport.rb +54 -0
  452. data/lib/datadog/symbol_database/uploader.rb +230 -0
  453. data/lib/datadog/symbol_database.rb +71 -0
  454. data/lib/datadog/tracing/analytics.rb +1 -1
  455. data/lib/datadog/tracing/buffer.rb +8 -8
  456. data/lib/datadog/tracing/client_ip.rb +4 -4
  457. data/lib/datadog/tracing/component.rb +39 -7
  458. data/lib/datadog/tracing/configuration/dynamic/option.rb +4 -0
  459. data/lib/datadog/tracing/configuration/dynamic.rb +13 -11
  460. data/lib/datadog/tracing/configuration/ext.rb +52 -38
  461. data/lib/datadog/tracing/configuration/http.rb +2 -2
  462. data/lib/datadog/tracing/configuration/settings.rb +51 -17
  463. data/lib/datadog/tracing/context.rb +1 -1
  464. data/lib/datadog/tracing/context_provider.rb +2 -2
  465. data/lib/datadog/tracing/contrib/action_cable/configuration/settings.rb +2 -2
  466. data/lib/datadog/tracing/contrib/action_cable/event.rb +4 -4
  467. data/lib/datadog/tracing/contrib/action_cable/events/broadcast.rb +9 -6
  468. data/lib/datadog/tracing/contrib/action_cable/events/perform_action.rb +8 -5
  469. data/lib/datadog/tracing/contrib/action_cable/events/transmit.rb +9 -6
  470. data/lib/datadog/tracing/contrib/action_cable/events.rb +5 -5
  471. data/lib/datadog/tracing/contrib/action_cable/ext.rb +18 -18
  472. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -5
  473. data/lib/datadog/tracing/contrib/action_cable/integration.rb +7 -7
  474. data/lib/datadog/tracing/contrib/action_cable/patcher.rb +4 -4
  475. data/lib/datadog/tracing/contrib/action_mailer/configuration/settings.rb +2 -2
  476. data/lib/datadog/tracing/contrib/action_mailer/event.rb +7 -4
  477. data/lib/datadog/tracing/contrib/action_mailer/events/deliver.rb +8 -8
  478. data/lib/datadog/tracing/contrib/action_mailer/events/process.rb +4 -4
  479. data/lib/datadog/tracing/contrib/action_mailer/events.rb +3 -3
  480. data/lib/datadog/tracing/contrib/action_mailer/ext.rb +18 -18
  481. data/lib/datadog/tracing/contrib/action_mailer/integration.rb +7 -7
  482. data/lib/datadog/tracing/contrib/action_mailer/patcher.rb +3 -3
  483. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +15 -12
  484. data/lib/datadog/tracing/contrib/action_pack/action_controller/patcher.rb +2 -2
  485. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +31 -3
  486. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +5 -3
  487. data/lib/datadog/tracing/contrib/action_pack/configuration/settings.rb +3 -3
  488. data/lib/datadog/tracing/contrib/action_pack/ext.rb +10 -10
  489. data/lib/datadog/tracing/contrib/action_pack/integration.rb +7 -7
  490. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +3 -3
  491. data/lib/datadog/tracing/contrib/action_pack/utils.rb +1 -1
  492. data/lib/datadog/tracing/contrib/action_view/configuration/settings.rb +3 -3
  493. data/lib/datadog/tracing/contrib/action_view/event.rb +1 -1
  494. data/lib/datadog/tracing/contrib/action_view/events/render_partial.rb +10 -7
  495. data/lib/datadog/tracing/contrib/action_view/events/render_template.rb +11 -8
  496. data/lib/datadog/tracing/contrib/action_view/events.rb +3 -3
  497. data/lib/datadog/tracing/contrib/action_view/ext.rb +10 -10
  498. data/lib/datadog/tracing/contrib/action_view/integration.rb +11 -11
  499. data/lib/datadog/tracing/contrib/action_view/patcher.rb +5 -5
  500. data/lib/datadog/tracing/contrib/action_view/utils.rb +2 -2
  501. data/lib/datadog/tracing/contrib/active_job/configuration/settings.rb +2 -2
  502. data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
  503. data/lib/datadog/tracing/contrib/active_job/event.rb +3 -3
  504. data/lib/datadog/tracing/contrib/active_job/events/discard.rb +10 -7
  505. data/lib/datadog/tracing/contrib/active_job/events/enqueue.rb +10 -7
  506. data/lib/datadog/tracing/contrib/active_job/events/enqueue_at.rb +10 -7
  507. data/lib/datadog/tracing/contrib/active_job/events/enqueue_retry.rb +10 -7
  508. data/lib/datadog/tracing/contrib/active_job/events/perform.rb +10 -7
  509. data/lib/datadog/tracing/contrib/active_job/events/retry_stopped.rb +10 -7
  510. data/lib/datadog/tracing/contrib/active_job/events.rb +6 -6
  511. data/lib/datadog/tracing/contrib/active_job/ext.rb +23 -23
  512. data/lib/datadog/tracing/contrib/active_job/integration.rb +7 -7
  513. data/lib/datadog/tracing/contrib/active_job/patcher.rb +17 -5
  514. data/lib/datadog/tracing/contrib/active_model_serializers/configuration/settings.rb +2 -2
  515. data/lib/datadog/tracing/contrib/active_model_serializers/event.rb +4 -4
  516. data/lib/datadog/tracing/contrib/active_model_serializers/events/render.rb +7 -7
  517. data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +8 -9
  518. data/lib/datadog/tracing/contrib/active_model_serializers/events.rb +3 -3
  519. data/lib/datadog/tracing/contrib/active_model_serializers/ext.rb +10 -10
  520. data/lib/datadog/tracing/contrib/active_model_serializers/integration.rb +5 -5
  521. data/lib/datadog/tracing/contrib/active_model_serializers/patcher.rb +3 -3
  522. data/lib/datadog/tracing/contrib/active_record/configuration/makara_resolver.rb +1 -1
  523. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +9 -9
  524. data/lib/datadog/tracing/contrib/active_record/configuration/settings.rb +4 -4
  525. data/lib/datadog/tracing/contrib/active_record/event.rb +1 -1
  526. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +9 -9
  527. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +12 -14
  528. data/lib/datadog/tracing/contrib/active_record/events.rb +3 -3
  529. data/lib/datadog/tracing/contrib/active_record/ext.rb +15 -15
  530. data/lib/datadog/tracing/contrib/active_record/integration.rb +11 -11
  531. data/lib/datadog/tracing/contrib/active_record/patcher.rb +2 -2
  532. data/lib/datadog/tracing/contrib/active_record/utils.rb +4 -4
  533. data/lib/datadog/tracing/contrib/active_support/cache/event.rb +1 -1
  534. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +14 -17
  535. data/lib/datadog/tracing/contrib/active_support/cache/events.rb +1 -1
  536. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +5 -8
  537. data/lib/datadog/tracing/contrib/active_support/cache/patcher.rb +5 -5
  538. data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +4 -4
  539. data/lib/datadog/tracing/contrib/active_support/configuration/settings.rb +3 -3
  540. data/lib/datadog/tracing/contrib/active_support/ext.rb +16 -16
  541. data/lib/datadog/tracing/contrib/active_support/integration.rb +8 -8
  542. data/lib/datadog/tracing/contrib/active_support/notifications/event.rb +1 -1
  543. data/lib/datadog/tracing/contrib/active_support/notifications/subscriber.rb +2 -2
  544. data/lib/datadog/tracing/contrib/active_support/notifications/subscription.rb +3 -3
  545. data/lib/datadog/tracing/contrib/active_support/patcher.rb +2 -2
  546. data/lib/datadog/tracing/contrib/analytics.rb +1 -1
  547. data/lib/datadog/tracing/contrib/auto_instrument.rb +4 -4
  548. data/lib/datadog/tracing/contrib/aws/configuration/settings.rb +3 -3
  549. data/lib/datadog/tracing/contrib/aws/ext.rb +26 -26
  550. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +7 -11
  551. data/lib/datadog/tracing/contrib/aws/integration.rb +9 -9
  552. data/lib/datadog/tracing/contrib/aws/parsed_context.rb +1 -1
  553. data/lib/datadog/tracing/contrib/aws/patcher.rb +5 -5
  554. data/lib/datadog/tracing/contrib/aws/service/dynamodb.rb +2 -2
  555. data/lib/datadog/tracing/contrib/aws/service/eventbridge.rb +2 -2
  556. data/lib/datadog/tracing/contrib/aws/service/kinesis.rb +4 -4
  557. data/lib/datadog/tracing/contrib/aws/service/s3.rb +2 -2
  558. data/lib/datadog/tracing/contrib/aws/service/sns.rb +3 -3
  559. data/lib/datadog/tracing/contrib/aws/service/sqs.rb +3 -3
  560. data/lib/datadog/tracing/contrib/aws/service/states.rb +5 -5
  561. data/lib/datadog/tracing/contrib/aws/services.rb +14 -14
  562. data/lib/datadog/tracing/contrib/component.rb +1 -1
  563. data/lib/datadog/tracing/contrib/concurrent_ruby/async_patch.rb +1 -1
  564. data/lib/datadog/tracing/contrib/concurrent_ruby/configuration/settings.rb +2 -2
  565. data/lib/datadog/tracing/contrib/concurrent_ruby/context_composite_executor_service.rb +1 -1
  566. data/lib/datadog/tracing/contrib/concurrent_ruby/ext.rb +2 -2
  567. data/lib/datadog/tracing/contrib/concurrent_ruby/future_patch.rb +1 -1
  568. data/lib/datadog/tracing/contrib/concurrent_ruby/integration.rb +6 -6
  569. data/lib/datadog/tracing/contrib/concurrent_ruby/patcher.rb +4 -4
  570. data/lib/datadog/tracing/contrib/concurrent_ruby/promises_future_patch.rb +1 -1
  571. data/lib/datadog/tracing/contrib/configurable.rb +20 -5
  572. data/lib/datadog/tracing/contrib/configuration/resolver.rb +14 -4
  573. data/lib/datadog/tracing/contrib/configuration/resolvers/pattern_resolver.rb +1 -1
  574. data/lib/datadog/tracing/contrib/configuration/settings.rb +2 -2
  575. data/lib/datadog/tracing/contrib/dalli/configuration/settings.rb +2 -2
  576. data/lib/datadog/tracing/contrib/dalli/ext.rb +13 -13
  577. data/lib/datadog/tracing/contrib/dalli/instrumentation.rb +6 -10
  578. data/lib/datadog/tracing/contrib/dalli/integration.rb +7 -7
  579. data/lib/datadog/tracing/contrib/dalli/patcher.rb +3 -3
  580. data/lib/datadog/tracing/contrib/dalli/quantize.rb +3 -3
  581. data/lib/datadog/tracing/contrib/delayed_job/configuration/settings.rb +2 -2
  582. data/lib/datadog/tracing/contrib/delayed_job/ext.rb +14 -14
  583. data/lib/datadog/tracing/contrib/delayed_job/integration.rb +5 -5
  584. data/lib/datadog/tracing/contrib/delayed_job/patcher.rb +3 -3
  585. data/lib/datadog/tracing/contrib/delayed_job/plugin.rb +7 -5
  586. data/lib/datadog/tracing/contrib/delayed_job/server_internal_tracer/worker.rb +1 -0
  587. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +2 -2
  588. data/lib/datadog/tracing/contrib/elasticsearch/ext.rb +15 -15
  589. data/lib/datadog/tracing/contrib/elasticsearch/integration.rb +6 -6
  590. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +12 -16
  591. data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +4 -4
  592. data/lib/datadog/tracing/contrib/ethon/configuration/settings.rb +2 -2
  593. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +9 -13
  594. data/lib/datadog/tracing/contrib/ethon/ext.rb +13 -13
  595. data/lib/datadog/tracing/contrib/ethon/integration.rb +6 -6
  596. data/lib/datadog/tracing/contrib/ethon/multi_patch.rb +4 -11
  597. data/lib/datadog/tracing/contrib/ethon/patcher.rb +3 -3
  598. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +5 -5
  599. data/lib/datadog/tracing/contrib/excon/ext.rb +11 -11
  600. data/lib/datadog/tracing/contrib/excon/integration.rb +6 -6
  601. data/lib/datadog/tracing/contrib/excon/middleware.rb +10 -14
  602. data/lib/datadog/tracing/contrib/excon/patcher.rb +2 -2
  603. data/lib/datadog/tracing/contrib/ext.rb +20 -18
  604. data/lib/datadog/tracing/contrib/extensions.rb +11 -2
  605. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +4 -4
  606. data/lib/datadog/tracing/contrib/faraday/ext.rb +11 -11
  607. data/lib/datadog/tracing/contrib/faraday/integration.rb +6 -6
  608. data/lib/datadog/tracing/contrib/faraday/middleware.rb +10 -14
  609. data/lib/datadog/tracing/contrib/faraday/patcher.rb +6 -6
  610. data/lib/datadog/tracing/contrib/grape/configuration/settings.rb +4 -4
  611. data/lib/datadog/tracing/contrib/grape/endpoint.rb +28 -25
  612. data/lib/datadog/tracing/contrib/grape/ext.rb +15 -15
  613. data/lib/datadog/tracing/contrib/grape/instrumentation.rb +3 -3
  614. data/lib/datadog/tracing/contrib/grape/integration.rb +5 -5
  615. data/lib/datadog/tracing/contrib/grape/patcher.rb +5 -5
  616. data/lib/datadog/tracing/contrib/graphql/configuration/error_extension_env_parser.rb +1 -1
  617. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +3 -3
  618. data/lib/datadog/tracing/contrib/graphql/ext.rb +9 -9
  619. data/lib/datadog/tracing/contrib/graphql/integration.rb +9 -9
  620. data/lib/datadog/tracing/contrib/graphql/patcher.rb +7 -7
  621. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +38 -37
  622. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +2 -2
  623. data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +2 -2
  624. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +11 -15
  625. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +11 -15
  626. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor.rb +6 -6
  627. data/lib/datadog/tracing/contrib/grpc/distributed/fetcher.rb +1 -1
  628. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +11 -9
  629. data/lib/datadog/tracing/contrib/grpc/ext.rb +14 -14
  630. data/lib/datadog/tracing/contrib/grpc/formatting.rb +7 -7
  631. data/lib/datadog/tracing/contrib/grpc/integration.rb +5 -5
  632. data/lib/datadog/tracing/contrib/grpc/intercept_with_datadog.rb +1 -1
  633. data/lib/datadog/tracing/contrib/grpc/patcher.rb +3 -3
  634. data/lib/datadog/tracing/contrib/grpc.rb +7 -6
  635. data/lib/datadog/tracing/contrib/hanami/action_tracer.rb +4 -3
  636. data/lib/datadog/tracing/contrib/hanami/configuration/settings.rb +2 -2
  637. data/lib/datadog/tracing/contrib/hanami/ext.rb +8 -8
  638. data/lib/datadog/tracing/contrib/hanami/integration.rb +6 -6
  639. data/lib/datadog/tracing/contrib/hanami/patcher.rb +5 -5
  640. data/lib/datadog/tracing/contrib/hanami/renderer_policy_tracing.rb +4 -3
  641. data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +6 -4
  642. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +1 -1
  643. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +4 -4
  644. data/lib/datadog/tracing/contrib/http/distributed/fetcher.rb +3 -3
  645. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +10 -8
  646. data/lib/datadog/tracing/contrib/http/ext.rb +11 -11
  647. data/lib/datadog/tracing/contrib/http/instrumentation.rb +10 -14
  648. data/lib/datadog/tracing/contrib/http/integration.rb +7 -7
  649. data/lib/datadog/tracing/contrib/http/patcher.rb +3 -3
  650. data/lib/datadog/tracing/contrib/http.rb +7 -6
  651. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +4 -4
  652. data/lib/datadog/tracing/contrib/httpclient/ext.rb +11 -11
  653. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +12 -12
  654. data/lib/datadog/tracing/contrib/httpclient/integration.rb +6 -6
  655. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +3 -3
  656. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +4 -4
  657. data/lib/datadog/tracing/contrib/httprb/ext.rb +11 -11
  658. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +10 -14
  659. data/lib/datadog/tracing/contrib/httprb/integration.rb +7 -7
  660. data/lib/datadog/tracing/contrib/httprb/patcher.rb +3 -3
  661. data/lib/datadog/tracing/contrib/integration.rb +3 -3
  662. data/lib/datadog/tracing/contrib/kafka/configuration/settings.rb +2 -2
  663. data/lib/datadog/tracing/contrib/kafka/event.rb +4 -3
  664. data/lib/datadog/tracing/contrib/kafka/events/connection/request.rb +7 -3
  665. data/lib/datadog/tracing/contrib/kafka/events/consumer/process_batch.rb +4 -4
  666. data/lib/datadog/tracing/contrib/kafka/events/consumer/process_message.rb +4 -4
  667. data/lib/datadog/tracing/contrib/kafka/events/consumer_group/heartbeat.rb +5 -5
  668. data/lib/datadog/tracing/contrib/kafka/events/consumer_group/join_group.rb +5 -5
  669. data/lib/datadog/tracing/contrib/kafka/events/consumer_group/leave_group.rb +5 -5
  670. data/lib/datadog/tracing/contrib/kafka/events/consumer_group/sync_group.rb +5 -5
  671. data/lib/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rb +10 -5
  672. data/lib/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rb +10 -5
  673. data/lib/datadog/tracing/contrib/kafka/events.rb +10 -10
  674. data/lib/datadog/tracing/contrib/kafka/ext.rb +40 -40
  675. data/lib/datadog/tracing/contrib/kafka/instrumentation/consumer.rb +4 -4
  676. data/lib/datadog/tracing/contrib/kafka/instrumentation/producer.rb +6 -6
  677. data/lib/datadog/tracing/contrib/kafka/integration.rb +6 -6
  678. data/lib/datadog/tracing/contrib/kafka/patcher.rb +5 -5
  679. data/lib/datadog/tracing/contrib/karafka/configuration/settings.rb +2 -2
  680. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +11 -9
  681. data/lib/datadog/tracing/contrib/karafka/ext.rb +13 -13
  682. data/lib/datadog/tracing/contrib/karafka/integration.rb +5 -5
  683. data/lib/datadog/tracing/contrib/karafka/monitor.rb +14 -14
  684. data/lib/datadog/tracing/contrib/karafka/patcher.rb +9 -9
  685. data/lib/datadog/tracing/contrib/karafka.rb +7 -6
  686. data/lib/datadog/tracing/contrib/lograge/configuration/settings.rb +2 -2
  687. data/lib/datadog/tracing/contrib/lograge/ext.rb +1 -1
  688. data/lib/datadog/tracing/contrib/lograge/integration.rb +5 -5
  689. data/lib/datadog/tracing/contrib/lograge/patcher.rb +9 -9
  690. data/lib/datadog/tracing/contrib/mongodb/configuration/settings.rb +2 -2
  691. data/lib/datadog/tracing/contrib/mongodb/ext.rb +18 -18
  692. data/lib/datadog/tracing/contrib/mongodb/instrumentation.rb +3 -3
  693. data/lib/datadog/tracing/contrib/mongodb/integration.rb +7 -7
  694. data/lib/datadog/tracing/contrib/mongodb/parsers.rb +9 -9
  695. data/lib/datadog/tracing/contrib/mongodb/patcher.rb +3 -3
  696. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +18 -21
  697. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +3 -3
  698. data/lib/datadog/tracing/contrib/mysql2/ext.rb +11 -11
  699. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +7 -11
  700. data/lib/datadog/tracing/contrib/mysql2/integration.rb +5 -5
  701. data/lib/datadog/tracing/contrib/mysql2/patcher.rb +2 -2
  702. data/lib/datadog/tracing/contrib/opensearch/configuration/settings.rb +2 -2
  703. data/lib/datadog/tracing/contrib/opensearch/ext.rb +23 -23
  704. data/lib/datadog/tracing/contrib/opensearch/integration.rb +6 -6
  705. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +15 -19
  706. data/lib/datadog/tracing/contrib/opensearch/quantize.rb +5 -5
  707. data/lib/datadog/tracing/contrib/patchable.rb +1 -1
  708. data/lib/datadog/tracing/contrib/patcher.rb +3 -3
  709. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +3 -3
  710. data/lib/datadog/tracing/contrib/pg/ext.rb +19 -19
  711. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +19 -23
  712. data/lib/datadog/tracing/contrib/pg/integration.rb +5 -5
  713. data/lib/datadog/tracing/contrib/pg/patcher.rb +2 -2
  714. data/lib/datadog/tracing/contrib/presto/configuration/settings.rb +2 -2
  715. data/lib/datadog/tracing/contrib/presto/ext.rb +21 -21
  716. data/lib/datadog/tracing/contrib/presto/instrumentation.rb +8 -10
  717. data/lib/datadog/tracing/contrib/presto/integration.rb +6 -6
  718. data/lib/datadog/tracing/contrib/presto/patcher.rb +4 -4
  719. data/lib/datadog/tracing/contrib/propagation/sql_comment/comment.rb +2 -2
  720. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +19 -16
  721. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +3 -3
  722. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +5 -5
  723. data/lib/datadog/tracing/contrib/que/configuration/settings.rb +2 -2
  724. data/lib/datadog/tracing/contrib/que/ext.rb +19 -19
  725. data/lib/datadog/tracing/contrib/que/integration.rb +6 -6
  726. data/lib/datadog/tracing/contrib/que/patcher.rb +1 -1
  727. data/lib/datadog/tracing/contrib/que/tracer.rb +3 -2
  728. data/lib/datadog/tracing/contrib/racecar/configuration/settings.rb +3 -3
  729. data/lib/datadog/tracing/contrib/racecar/event.rb +5 -9
  730. data/lib/datadog/tracing/contrib/racecar/events/batch.rb +3 -3
  731. data/lib/datadog/tracing/contrib/racecar/events/consume.rb +3 -3
  732. data/lib/datadog/tracing/contrib/racecar/events/message.rb +3 -3
  733. data/lib/datadog/tracing/contrib/racecar/events.rb +4 -4
  734. data/lib/datadog/tracing/contrib/racecar/ext.rb +18 -18
  735. data/lib/datadog/tracing/contrib/racecar/integration.rb +5 -5
  736. data/lib/datadog/tracing/contrib/racecar/patcher.rb +3 -3
  737. data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +9 -3
  738. data/lib/datadog/tracing/contrib/rack/ext.rb +43 -16
  739. data/lib/datadog/tracing/contrib/rack/header_collection.rb +3 -3
  740. data/lib/datadog/tracing/contrib/rack/header_tagging.rb +24 -1
  741. data/lib/datadog/tracing/contrib/rack/integration.rb +6 -6
  742. data/lib/datadog/tracing/contrib/rack/middlewares.rb +28 -25
  743. data/lib/datadog/tracing/contrib/rack/patcher.rb +5 -5
  744. data/lib/datadog/tracing/contrib/rack/request_queue.rb +4 -4
  745. data/lib/datadog/tracing/contrib/rack/route_inference.rb +12 -10
  746. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +124 -6
  747. data/lib/datadog/tracing/contrib/rails/auto_instrument_railtie.rb +1 -1
  748. data/lib/datadog/tracing/contrib/rails/configuration/settings.rb +3 -3
  749. data/lib/datadog/tracing/contrib/rails/ext.rb +15 -15
  750. data/lib/datadog/tracing/contrib/rails/framework.rb +11 -11
  751. data/lib/datadog/tracing/contrib/rails/integration.rb +5 -5
  752. data/lib/datadog/tracing/contrib/rails/log_injection.rb +1 -1
  753. data/lib/datadog/tracing/contrib/rails/middlewares.rb +1 -1
  754. data/lib/datadog/tracing/contrib/rails/patcher.rb +8 -9
  755. data/lib/datadog/tracing/contrib/rails/railtie.rb +4 -4
  756. data/lib/datadog/tracing/contrib/rails/runner.rb +4 -2
  757. data/lib/datadog/tracing/contrib/rake/configuration/settings.rb +3 -3
  758. data/lib/datadog/tracing/contrib/rake/ext.rb +12 -12
  759. data/lib/datadog/tracing/contrib/rake/instrumentation.rb +9 -7
  760. data/lib/datadog/tracing/contrib/rake/integration.rb +5 -5
  761. data/lib/datadog/tracing/contrib/rake/patcher.rb +3 -3
  762. data/lib/datadog/tracing/contrib/redis/configuration/resolver.rb +3 -3
  763. data/lib/datadog/tracing/contrib/redis/configuration/settings.rb +2 -2
  764. data/lib/datadog/tracing/contrib/redis/ext.rb +16 -16
  765. data/lib/datadog/tracing/contrib/redis/instrumentation.rb +6 -6
  766. data/lib/datadog/tracing/contrib/redis/integration.rb +7 -7
  767. data/lib/datadog/tracing/contrib/redis/patcher.rb +10 -10
  768. data/lib/datadog/tracing/contrib/redis/quantize.rb +40 -6
  769. data/lib/datadog/tracing/contrib/redis/tags.rb +6 -11
  770. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +16 -8
  771. data/lib/datadog/tracing/contrib/resque/configuration/settings.rb +2 -2
  772. data/lib/datadog/tracing/contrib/resque/ext.rb +7 -7
  773. data/lib/datadog/tracing/contrib/resque/integration.rb +5 -5
  774. data/lib/datadog/tracing/contrib/resque/patcher.rb +3 -3
  775. data/lib/datadog/tracing/contrib/resque/resque_job.rb +6 -5
  776. data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +2 -2
  777. data/lib/datadog/tracing/contrib/rest_client/ext.rb +10 -10
  778. data/lib/datadog/tracing/contrib/rest_client/integration.rb +6 -6
  779. data/lib/datadog/tracing/contrib/rest_client/patcher.rb +2 -2
  780. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +6 -10
  781. data/lib/datadog/tracing/contrib/roda/configuration/settings.rb +2 -2
  782. data/lib/datadog/tracing/contrib/roda/ext.rb +6 -5
  783. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +8 -5
  784. data/lib/datadog/tracing/contrib/roda/integration.rb +6 -6
  785. data/lib/datadog/tracing/contrib/roda/patcher.rb +3 -3
  786. data/lib/datadog/tracing/contrib/semantic_logger/configuration/settings.rb +2 -2
  787. data/lib/datadog/tracing/contrib/semantic_logger/ext.rb +1 -1
  788. data/lib/datadog/tracing/contrib/semantic_logger/integration.rb +5 -5
  789. data/lib/datadog/tracing/contrib/semantic_logger/patcher.rb +2 -2
  790. data/lib/datadog/tracing/contrib/sequel/configuration/settings.rb +2 -2
  791. data/lib/datadog/tracing/contrib/sequel/database.rb +7 -10
  792. data/lib/datadog/tracing/contrib/sequel/dataset.rb +6 -5
  793. data/lib/datadog/tracing/contrib/sequel/ext.rb +12 -8
  794. data/lib/datadog/tracing/contrib/sequel/integration.rb +5 -5
  795. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  796. data/lib/datadog/tracing/contrib/sequel/patcher.rb +3 -3
  797. data/lib/datadog/tracing/contrib/sequel/utils.rb +103 -16
  798. data/lib/datadog/tracing/contrib/shoryuken/configuration/settings.rb +2 -2
  799. data/lib/datadog/tracing/contrib/shoryuken/ext.rb +12 -12
  800. data/lib/datadog/tracing/contrib/shoryuken/integration.rb +6 -6
  801. data/lib/datadog/tracing/contrib/shoryuken/patcher.rb +1 -1
  802. data/lib/datadog/tracing/contrib/shoryuken/tracer.rb +3 -2
  803. data/lib/datadog/tracing/contrib/sidekiq/client_tracer.rb +9 -8
  804. data/lib/datadog/tracing/contrib/sidekiq/configuration/settings.rb +2 -2
  805. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +12 -10
  806. data/lib/datadog/tracing/contrib/sidekiq/ext.rb +30 -30
  807. data/lib/datadog/tracing/contrib/sidekiq/integration.rb +7 -7
  808. data/lib/datadog/tracing/contrib/sidekiq/patcher.rb +8 -8
  809. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/heartbeat.rb +2 -0
  810. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/job_fetch.rb +1 -0
  811. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/redis_info.rb +1 -0
  812. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/scheduled_poller.rb +2 -0
  813. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/stop.rb +1 -0
  814. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +15 -14
  815. data/lib/datadog/tracing/contrib/sidekiq/utils.rb +10 -10
  816. data/lib/datadog/tracing/contrib/sidekiq.rb +7 -6
  817. data/lib/datadog/tracing/contrib/sinatra/configuration/settings.rb +3 -3
  818. data/lib/datadog/tracing/contrib/sinatra/env.rb +6 -6
  819. data/lib/datadog/tracing/contrib/sinatra/ext.rb +17 -17
  820. data/lib/datadog/tracing/contrib/sinatra/framework.rb +2 -2
  821. data/lib/datadog/tracing/contrib/sinatra/integration.rb +5 -5
  822. data/lib/datadog/tracing/contrib/sinatra/patcher.rb +5 -5
  823. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +11 -10
  824. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +9 -8
  825. data/lib/datadog/tracing/contrib/sneakers/configuration/settings.rb +1 -1
  826. data/lib/datadog/tracing/contrib/sneakers/ext.rb +12 -12
  827. data/lib/datadog/tracing/contrib/sneakers/integration.rb +7 -7
  828. data/lib/datadog/tracing/contrib/sneakers/patcher.rb +2 -2
  829. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +5 -4
  830. data/lib/datadog/tracing/contrib/span_attribute_schema.rb +2 -2
  831. data/lib/datadog/tracing/contrib/status_range_env_parser.rb +2 -2
  832. data/lib/datadog/tracing/contrib/status_range_matcher.rb +4 -0
  833. data/lib/datadog/tracing/contrib/stripe/configuration/settings.rb +2 -2
  834. data/lib/datadog/tracing/contrib/stripe/ext.rb +12 -12
  835. data/lib/datadog/tracing/contrib/stripe/integration.rb +5 -5
  836. data/lib/datadog/tracing/contrib/stripe/patcher.rb +2 -2
  837. data/lib/datadog/tracing/contrib/stripe/request.rb +4 -4
  838. data/lib/datadog/tracing/contrib/sucker_punch/configuration/settings.rb +2 -2
  839. data/lib/datadog/tracing/contrib/sucker_punch/exception_handler.rb +1 -1
  840. data/lib/datadog/tracing/contrib/sucker_punch/ext.rb +13 -13
  841. data/lib/datadog/tracing/contrib/sucker_punch/instrumentation.rb +5 -4
  842. data/lib/datadog/tracing/contrib/sucker_punch/integration.rb +5 -5
  843. data/lib/datadog/tracing/contrib/sucker_punch/patcher.rb +4 -4
  844. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +3 -3
  845. data/lib/datadog/tracing/contrib/trilogy/ext.rb +11 -11
  846. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +7 -11
  847. data/lib/datadog/tracing/contrib/trilogy/integration.rb +5 -5
  848. data/lib/datadog/tracing/contrib/trilogy/patcher.rb +2 -2
  849. data/lib/datadog/tracing/contrib/utils/database.rb +5 -5
  850. data/lib/datadog/tracing/contrib/utils/quantization/{hash.rb → hash_formatter.rb} +3 -3
  851. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +10 -10
  852. data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +2 -2
  853. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +11 -9
  854. data/lib/datadog/tracing/contrib/waterdrop/ext.rb +4 -4
  855. data/lib/datadog/tracing/contrib/waterdrop/integration.rb +5 -5
  856. data/lib/datadog/tracing/contrib/waterdrop/middleware.rb +2 -2
  857. data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +7 -7
  858. data/lib/datadog/tracing/contrib/waterdrop/producer.rb +3 -3
  859. data/lib/datadog/tracing/contrib/waterdrop.rb +7 -6
  860. data/lib/datadog/tracing/contrib.rb +62 -54
  861. data/lib/datadog/tracing/correlation.rb +12 -12
  862. data/lib/datadog/tracing/diagnostics/environment_logger.rb +14 -12
  863. data/lib/datadog/tracing/diagnostics/ext.rb +19 -19
  864. data/lib/datadog/tracing/diagnostics/health.rb +1 -1
  865. data/lib/datadog/tracing/distributed/b3_multi.rb +8 -8
  866. data/lib/datadog/tracing/distributed/b3_single.rb +4 -4
  867. data/lib/datadog/tracing/distributed/baggage.rb +81 -28
  868. data/lib/datadog/tracing/distributed/datadog.rb +34 -32
  869. data/lib/datadog/tracing/distributed/fetcher.rb +1 -1
  870. data/lib/datadog/tracing/distributed/helpers.rb +3 -3
  871. data/lib/datadog/tracing/distributed/propagation.rb +46 -11
  872. data/lib/datadog/tracing/distributed/propagation_policy.rb +5 -0
  873. data/lib/datadog/tracing/distributed/trace_context.rb +29 -263
  874. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  875. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  876. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  877. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  878. data/lib/datadog/tracing/event.rb +2 -2
  879. data/lib/datadog/tracing/ext.rb +9 -0
  880. data/lib/datadog/tracing/metadata/analytics.rb +2 -2
  881. data/lib/datadog/tracing/metadata/errors.rb +4 -4
  882. data/lib/datadog/tracing/metadata/ext.rb +76 -66
  883. data/lib/datadog/tracing/metadata/metastruct.rb +1 -1
  884. data/lib/datadog/tracing/metadata/metastruct_tagging.rb +1 -1
  885. data/lib/datadog/tracing/metadata/tagging.rb +5 -5
  886. data/lib/datadog/tracing/metadata.rb +4 -4
  887. data/lib/datadog/tracing/otel_thread_context.rb +34 -0
  888. data/lib/datadog/tracing/pipeline/span_filter.rb +2 -2
  889. data/lib/datadog/tracing/pipeline.rb +3 -3
  890. data/lib/datadog/tracing/remote.rb +227 -21
  891. data/lib/datadog/tracing/sampling/all_sampler.rb +1 -1
  892. data/lib/datadog/tracing/sampling/ext.rb +9 -7
  893. data/lib/datadog/tracing/sampling/matcher.rb +4 -4
  894. data/lib/datadog/tracing/sampling/priority_sampler.rb +17 -4
  895. data/lib/datadog/tracing/sampling/rate_by_key_sampler.rb +3 -3
  896. data/lib/datadog/tracing/sampling/rate_by_service_sampler.rb +2 -2
  897. data/lib/datadog/tracing/sampling/rate_sampler.rb +2 -2
  898. data/lib/datadog/tracing/sampling/rule.rb +5 -5
  899. data/lib/datadog/tracing/sampling/rule_sampler.rb +68 -37
  900. data/lib/datadog/tracing/sampling/sampler.rb +2 -2
  901. data/lib/datadog/tracing/sampling/span/ext.rb +3 -3
  902. data/lib/datadog/tracing/sampling/span/matcher.rb +2 -2
  903. data/lib/datadog/tracing/sampling/span/rule.rb +1 -1
  904. data/lib/datadog/tracing/sampling/span/rule_parser.rb +10 -10
  905. data/lib/datadog/tracing/span.rb +9 -9
  906. data/lib/datadog/tracing/span_event.rb +5 -5
  907. data/lib/datadog/tracing/span_operation.rb +39 -26
  908. data/lib/datadog/tracing/sync_writer.rb +12 -7
  909. data/lib/datadog/tracing/trace_digest.rb +29 -4
  910. data/lib/datadog/tracing/trace_operation.rb +112 -31
  911. data/lib/datadog/tracing/trace_segment.rb +10 -10
  912. data/lib/datadog/tracing/tracer.rb +68 -27
  913. data/lib/datadog/tracing/transport/http/client.rb +4 -4
  914. data/lib/datadog/tracing/transport/http/statistics.rb +2 -2
  915. data/lib/datadog/tracing/transport/http/traces.rb +8 -8
  916. data/lib/datadog/tracing/transport/http.rb +10 -10
  917. data/lib/datadog/tracing/transport/io/client.rb +3 -3
  918. data/lib/datadog/tracing/transport/io/response.rb +1 -1
  919. data/lib/datadog/tracing/transport/io/traces.rb +5 -5
  920. data/lib/datadog/tracing/transport/io.rb +3 -3
  921. data/lib/datadog/tracing/transport/native/response.rb +71 -0
  922. data/lib/datadog/tracing/transport/native.rb +379 -0
  923. data/lib/datadog/tracing/transport/serializable_trace.rb +19 -19
  924. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  925. data/lib/datadog/tracing/transport/statistics.rb +2 -2
  926. data/lib/datadog/tracing/transport/trace_formatter.rb +10 -10
  927. data/lib/datadog/tracing/transport/traces.rb +11 -31
  928. data/lib/datadog/tracing/utils.rb +2 -2
  929. data/lib/datadog/tracing/workers.rb +4 -3
  930. data/lib/datadog/tracing/writer.rb +22 -9
  931. data/lib/datadog/tracing.rb +3 -3
  932. data/lib/datadog/version.rb +4 -4
  933. data/lib/datadog.rb +15 -11
  934. metadata +92 -22
  935. data/ext/datadog_profiling_native_extension/clock_id_noop.c +0 -21
  936. data/lib/datadog/ai_guard/configuration/settings.rb +0 -113
  937. data/lib/datadog/appsec/configuration/settings.rb +0 -423
  938. data/lib/datadog/core/utils/base64.rb +0 -22
  939. data/lib/datadog/data_streams/configuration/settings.rb +0 -49
  940. data/lib/datadog/di/configuration/settings.rb +0 -243
  941. data/lib/datadog/error_tracking/configuration/settings.rb +0 -63
  942. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -82
@@ -1,4 +1,5 @@
1
1
  #include <ruby.h>
2
+ #include <ruby/debug.h>
2
3
 
3
4
  #include "datadog_ruby_common.h"
4
5
  #include "collectors_thread_context.h"
@@ -42,11 +43,8 @@
42
43
  // When `thread_context_collector_on_gc_start` gets called, the current cpu and wall-time get recorded to the thread
43
44
  // context: `cpu_time_at_gc_start_ns` and `wall_time_at_gc_start_ns`.
44
45
  //
45
- // While `cpu_time_at_gc_start_ns` is set, regular samples (if any) do not account for cpu-time any time that passes
46
- // after this timestamp. The idea is that this cpu-time will be blamed separately on GC, and not on the user thread.
47
- // Wall-time accounting is not affected by this (e.g. we still record 60 seconds every 60 seconds).
48
- //
49
- // (Regular samples can still account for the cpu-time between the previous sample and the start of GC.)
46
+ // While `cpu_time_at_gc_start_ns` is set, we don't expect the thread to be sampled: the VM is doing GC
47
+ // on the thread holding the GVL so no other samples can/will be triggered until GC finishes.
50
48
  //
51
49
  // When `thread_context_collector_on_gc_finish` gets called, the cpu-time and wall-time spent during GC gets recorded
52
50
  // into the global gc_tracking structure, and further samples are not affected. (The `cpu_time_at_previous_sample_ns`
@@ -75,15 +73,19 @@
75
73
 
76
74
  #define THREAD_ID_LIMIT_CHARS 44 // Why 44? "#{2**64} (#{2**64})".size + 1 for \0
77
75
  #define THREAD_INVOKE_LOCATION_LIMIT_CHARS 512
78
- #define IS_WALL_TIME true
79
- #define IS_NOT_WALL_TIME false
80
76
  #define MISSING_TRACER_CONTEXT_KEY 0
77
+
78
+ // Used as markers in the code
79
+ #define OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED INT2FIX(1)
80
+ #define OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED INT2FIX(2)
81
+
81
82
  #define TIME_BETWEEN_GC_EVENTS_NS MILLIS_AS_NS(10)
83
+ #define GVL_SUSPENDED ((uint64_t)1)
84
+ #define GVL_RUNNING ((uint64_t)0)
82
85
 
83
- // This is used as a placeholder to mark threads that are allowed to be profiled (enabled)
84
- // (e.g. to avoid trying to gvl profile threads that are not from the main Ractor)
85
- // and for which there's no data yet
86
- #define GVL_WAITING_ENABLED_EMPTY RUBY_FIXNUM_MAX
86
+ #define MAX(a, b) ((a) < (b) ? (b) : (a))
87
+
88
+ static ID dd_per_thread_context_id; // Hidden ivar (no @ prefix, inaccessible from Ruby)
87
89
 
88
90
  static ID at_active_span_id; // id of :@active_span in Ruby
89
91
  static ID at_active_trace_id; // id of :@active_trace in Ruby
@@ -107,12 +109,20 @@ static ID server_id; // id of :server in Ruby
107
109
  static ID otel_context_storage_id; // id of :__opentelemetry_context_storage__ in Ruby
108
110
  static ID otel_fiber_context_storage_id; // id of :@opentelemetry_context in Ruby
109
111
 
110
- // This is used by `thread_context_collector_on_gvl_running`. Because when that method gets called we're not sure if
111
- // it's safe to access the state of the thread context collector, we store this setting as a global value. This does
112
- // mean this setting is shared among all thread context collectors, and thus it's "last writer wins".
113
- // In production this should not be a problem: there should only be one profiler, which is the last one created,
114
- // and that'll be the one that last wrote this setting.
115
- static uint32_t global_waiting_for_gvl_threshold_ns = MILLIS_AS_NS(10);
112
+ // This is mutable and gets set last-writer-wins style by
113
+ // `thread_context_collector_reset_all_per_thread_contexts`, which is called whenever
114
+ // profiling is starting or restarting.
115
+ //
116
+ // Note: We must be careful to not change this value while the profiler is still running,
117
+ // otherwise new threads can get the new value and cause profiling to stop with an
118
+ // exception due to the mismatched size.
119
+ //
120
+ // The initial value should be kept in sync with the default for DD_PROFILING_MAX_FRAMES
121
+ // in settings.rb. See `initialize_context` for details on why this is needed/used.
122
+ static uint16_t latest_max_frames = 400;
123
+
124
+ // Global tracepoint for RUBY_EVENT_THREAD_BEGIN. Created and enabled once when the first ThreadContext collector is initialized.
125
+ static VALUE thread_begin_tracepoint = Qnil;
116
126
 
117
127
  typedef enum { OTEL_CONTEXT_ENABLED_FALSE, OTEL_CONTEXT_ENABLED_ONLY, OTEL_CONTEXT_ENABLED_BOTH } otel_context_enabled;
118
128
  typedef enum { OTEL_CONTEXT_SOURCE_UNKNOWN, OTEL_CONTEXT_SOURCE_FIBER_IVAR, OTEL_CONTEXT_SOURCE_FIBER_LOCAL } otel_context_source;
@@ -122,28 +132,18 @@ typedef struct {
122
132
  // Note: Places in this file that usually need to be changed when this struct is changed are tagged with
123
133
  // "Update this when modifying state struct"
124
134
 
125
- // Required by Datadog::Profiling::Collectors::Stack as a scratch buffer during sampling
126
- ddog_prof_Location *locations;
127
- uint16_t max_frames;
128
- // Hashmap <Thread Object, per_thread_context>
129
- // Note: Be very careful when mutating this map, as it gets read e.g. in the middle of GC and signal handlers.
130
- st_table *hash_map_per_thread_context;
135
+ // Output buffer for stack traces, passed to sample_thread()
136
+ sample_locations locations;
131
137
  // Datadog::Profiling::StackRecorder instance
132
138
  VALUE recorder_instance;
133
139
  // If the tracer is available and enabled, this will be the fiber-local symbol for accessing its running context,
134
140
  // to enable code hotspots and endpoint aggregation.
135
141
  // When not available, this is set to MISSING_TRACER_CONTEXT_KEY.
136
142
  ID tracer_context_key;
137
- // Track how many regular samples we've taken. Does not include garbage collection samples.
138
- // Currently **outside** of stats struct because we also use it to decide when to clean the contexts, and thus this
139
- // is not (just) a stat.
140
- unsigned int sample_count;
141
143
  // Reusable array to get list of threads
142
144
  VALUE thread_list_buffer;
143
145
  // Used to omit endpoint names (retrieved from tracer) from collected data
144
146
  bool endpoint_collection_enabled;
145
- // Used to omit timestamps / timeline events from collected data
146
- bool timeline_enabled;
147
147
  // Used to control context collection
148
148
  otel_context_enabled otel_context_enabled;
149
149
  // Used to remember where otel context is being stored after we observe it the first time
@@ -153,19 +153,32 @@ typedef struct {
153
153
  // Used to identify the main thread, to give it a fallback name
154
154
  VALUE main_thread;
155
155
  // Used when extracting trace identifiers from otel spans. Lazily initialized.
156
- // Qtrue serves as a marker we've not yet extracted it; when we try to extract it, we set it to an object if
157
- // successful and Qnil if not.
156
+ // * `OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED`: we've not needed it yet
157
+ // * `OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED`: a sample needed it, and it's waiting to be extracted
158
+ // * anything else: the extracted value, or Qnil if we tried to extract it and failed
158
159
  VALUE otel_current_span_key;
159
160
  // Used to enable native filenames in stack traces
160
161
  bool native_filenames_enabled;
162
+ // Used to include class/module names in method names (e.g. "Foo#bar" instead of "bar")
163
+ bool show_classes;
161
164
  // Used to cache native filename lookup results (Map[void *function_pointer, char *filename])
162
165
  st_table *native_filenames_cache;
166
+ // Used to attribute overhead during sampling to this component
167
+ VALUE overhead_filename;
163
168
 
164
169
  struct stats {
170
+ // Track how many regular samples we've taken. Does not include garbage collection samples.
171
+ unsigned int sample_count;
165
172
  // Track how many garbage collection samples we've taken.
166
173
  unsigned int gc_samples;
167
174
  // See thread_context_collector_on_gc_start for details
168
175
  unsigned int gc_samples_missed_due_to_missing_context;
176
+ // See thread_context_collector_sample_after_gc for details
177
+ unsigned int gc_samples_skipped_nothing_to_flush;
178
+ // How many per-thread samples were skipped because the thread has been continuously suspended
179
+ // (no GVL) since its previous sample, so its Ruby stack cannot have changed.
180
+ unsigned int inactive_thread_samples_skipped;
181
+ unsigned int profiler_thread_samples_skipped;
169
182
  } stats;
170
183
 
171
184
  struct {
@@ -177,16 +190,90 @@ typedef struct {
177
190
  } gc_tracking;
178
191
  } thread_context_collector_state;
179
192
 
180
- // Tracks per-thread state
181
- typedef struct {
193
+ // Tracks per-thread state.
194
+ // This state is global and lives forever on the Ruby Thread (until the Thread is GC'd).
195
+ // The state is created early on for all threads on the main Ractor
196
+ // (enabling a TracePoint only enables it for the current Ractor).
197
+ // The state is either created when the Thread starts running (via the RUBY_EVENT_THREAD_BEGIN TracePoint),
198
+ // or via `thread_context_collector_reset_all_per_thread_contexts` when the profiler starts.
199
+ //
200
+ // Unfortunately that RUBY_EVENT_THREAD_BEGIN TracePoint still fires after some other events:
201
+ // * RUBY_INTERNAL_THREAD_EVENT_RESUMED for the Thread acquiring the GVL for the first time
202
+ // * an early SIGPROF calling handle_sampling_signal()
203
+ // * Ruby might check for interrupts and run postponed jobs (e.g. thread_context_collector_sample)
204
+ // * another RUBY_EVENT_THREAD_BEGIN TracePoint which might run before ours
205
+ // * etc
206
+ // For those cases we have to ignore those events and we cannot assume the state is always set,
207
+ // however this only matters for a very short period when a thread starts.
208
+ struct per_thread_context {
182
209
  sampling_buffer sampling_buffer;
183
210
  char thread_id[THREAD_ID_LIMIT_CHARS];
184
211
  ddog_CharSlice thread_id_char_slice;
185
212
  char thread_invoke_location[THREAD_INVOKE_LOCATION_LIMIT_CHARS];
186
213
  ddog_CharSlice thread_invoke_location_char_slice;
187
214
  thread_cpu_time_id thread_cpu_time_id;
188
- long cpu_time_at_previous_sample_ns; // Can be INVALID_TIME until initialized or if getting it fails for another reason
189
- long wall_time_at_previous_sample_ns; // Can be INVALID_TIME until initialized
215
+ long cpu_time_at_previous_sample_ns;
216
+ long wall_time_at_previous_sample_ns;
217
+
218
+ // There are 3 possible states for the GVL (per thread), and 3 transitions for which we receive GVL events:
219
+ // Thread holds the GVL
220
+ // on_gvl_released() the thread releases the GVL (RUBY_INTERNAL_THREAD_EVENT_SUSPENDED)
221
+ // Thread runs without the GVL
222
+ // on_gvl_waiting() the thread wants the GVL (RUBY_INTERNAL_THREAD_EVENT_READY)
223
+ // Thread is "Waiting for GVL"
224
+ // on_gvl_running() the thread now got the GVL (RUBY_INTERNAL_THREAD_EVENT_RESUMED)
225
+ // ... and the cycle restarts
226
+
227
+
228
+ // --- GVL waiting tracking state machine ---
229
+ //
230
+ // gvl_waiting_at tracks the GVL wait state for each profiled thread:
231
+ //
232
+ // ┌───────────────────────────────────┐
233
+ // │ on_gvl_waiting │
234
+ // │ ▼
235
+ // Not Waiting (0) ◀────────────────── Waiting (> 0)
236
+ // ▲ on_gvl_running │
237
+ // │ (below threshold) │ on_gvl_running (above threshold)
238
+ // │ ▼
239
+ // └─────────────────────────── Sample Pending (< 0)
240
+ // sample / sample_after_gvl_running
241
+ //
242
+ // Not Waiting (0): thread is running or not waiting for the GVL
243
+ // Waiting (> 0): monotonic wall time (ns) when the thread started waiting
244
+ // Sample Pending (< 0): negated timestamp; the wait ended and a sample is pending
245
+ //
246
+ // The field is accessed under the GVL for most functions EXCEPT on_gvl_waiting() which writes to it without the GVL.
247
+ // So we need to pack the above state in a single long to ensure atomicity.
248
+ long gvl_waiting_at;
249
+
250
+ // Per-thread "state + version" word, updated on every GVL state transition. The encoding is:
251
+ // - low bit: current state (1 = currently suspended, 0 = currently running)
252
+ // - bits 1+: monotonic event counter (incremented on every RESUMED)
253
+ // The hooks set the state bit explicitly rather than relying on parity, so the encoding stays
254
+ // correct even when events are not paired properly (as in tests).
255
+ //
256
+ // Note that SUSPENDED can happen multiple times in a row on Ruby 3.2,
257
+ // see https://github.com/DataDog/dd-trace-rb/pull/5777#discussion_r3388560254,
258
+ // the encoding is designed to naturally not change the field in such a case.
259
+ uint64_t gvl_state_change_count;
260
+ // Snapshot of the thread's gvl_state_change_count at the moment we last sampled it.
261
+ // Equality with this snapshot means no GVL transition since the last sample.
262
+ uint64_t gvl_state_change_count_at_previous_sample;
263
+ // True when the previous per-tick sample was skipped by the SUSPENDED-skip optimization, so the
264
+ // flush-before-serialize pass knows it needs to report this thread.
265
+ // As a result, we will accumulate all wall & CPU time as a single batch per reporting period,
266
+ // but this is deemed worth it for this optimization. In any case we don't know exactly
267
+ // at what time a thread was doing CPU work (unless it's on CPU 100% of the time).
268
+ bool was_skipped_at_last_sample;
269
+ // Set as true for CpuAndWallTimeWorker and IdleSamplingHelper threads.
270
+ // When true, per-tick samples are skipped entirely; the thread is sampled only once per
271
+ // reporting period during the on_serialize flush.
272
+ //
273
+ // These threads are always in native code so their stacks aren't interesting;
274
+ // the Profiling::Scheduler thread on the other hand does a lot of different
275
+ // things using a mix of Ruby and native code, so that one isn't considered internal.
276
+ bool is_profiler_internal_thread;
190
277
 
191
278
  struct {
192
279
  // Both of these fields are set by on_gc_start and kept until on_gc_finish is called.
@@ -194,7 +281,7 @@ typedef struct {
194
281
  long cpu_time_at_start_ns;
195
282
  long wall_time_at_start_ns;
196
283
  } gc_tracking;
197
- } per_thread_context;
284
+ };
198
285
 
199
286
  // Used to correlate profiles with traces
200
287
  typedef struct {
@@ -212,50 +299,43 @@ typedef struct {
212
299
 
213
300
  static void thread_context_collector_typed_data_mark(void *state_ptr);
214
301
  static void thread_context_collector_typed_data_free(void *state_ptr);
215
- static int hash_map_per_thread_context_mark(st_data_t key_thread, st_data_t value_thread_context, DDTRACE_UNUSED st_data_t _argument);
216
- static int hash_map_per_thread_context_free_values(st_data_t _thread, st_data_t value_per_thread_context, st_data_t _argument);
302
+ static void per_thread_context_typed_data_mark(void *ctx_ptr);
303
+ static void per_thread_context_typed_data_free(void *ctx_ptr);
217
304
  static VALUE _native_new(VALUE klass);
218
305
  static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
219
- static VALUE _native_sample(VALUE self, VALUE collector_instance, VALUE profiler_overhead_stack_thread, VALUE allow_exception);
306
+ static VALUE _native_sample(VALUE self, VALUE collector_instance, VALUE allow_exception);
220
307
  static VALUE _native_on_gc_start(VALUE self, VALUE collector_instance);
221
308
  static VALUE _native_on_gc_finish(VALUE self, VALUE collector_instance);
222
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE allow_exception);
309
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
223
310
  static void update_metrics_and_sample(
224
311
  thread_context_collector_state *state,
225
312
  VALUE thread_being_sampled,
226
- VALUE stack_from_thread,
227
313
  per_thread_context *thread_context,
228
- sampling_buffer* sampling_buffer,
229
314
  long current_cpu_time_ns,
230
- long current_monotonic_wall_time_ns
315
+ long current_monotonic_wall_time_ns,
316
+ bool force_sample
231
317
  );
232
318
  static void trigger_sample_for_thread(
233
319
  thread_context_collector_state *state,
234
- VALUE thread,
235
- VALUE stack_from_thread,
320
+ VALUE thread_being_sampled,
236
321
  per_thread_context *thread_context,
237
- sampling_buffer* sampling_buffer,
238
322
  sample_values values,
239
323
  long current_monotonic_wall_time_ns,
240
324
  ddog_CharSlice *ruby_vm_type,
241
325
  ddog_CharSlice *class_name,
242
- bool is_gvl_waiting_state,
243
- bool is_safe_to_allocate_objects
326
+ bool is_gvl_waiting_state
244
327
  );
245
328
  static VALUE _native_thread_list(VALUE self);
246
- static per_thread_context *get_or_create_context_for(VALUE thread, thread_context_collector_state *state);
247
- static per_thread_context *get_context_for(VALUE thread, thread_context_collector_state *state);
248
- static void initialize_context(VALUE thread, per_thread_context *thread_context, thread_context_collector_state *state);
249
- static void free_context(per_thread_context* thread_context);
329
+ static void check_frozen_thread(VALUE thread);
330
+ static per_thread_context *get_or_create_context_for(VALUE thread);
331
+ static void initialize_context(VALUE thread, per_thread_context *thread_context);
250
332
  static VALUE _native_inspect(VALUE self, VALUE collector_instance);
251
- static VALUE per_thread_context_st_table_as_ruby_hash(thread_context_collector_state *state);
252
- static int per_thread_context_as_ruby_hash(st_data_t key_thread, st_data_t value_context, st_data_t result_hash);
253
- static VALUE stats_as_ruby_hash(thread_context_collector_state *state);
333
+ static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context);
334
+ static VALUE stats_to_ruby_hash(thread_context_collector_state *state, VALUE hash);
254
335
  static VALUE gc_tracking_as_ruby_hash(thread_context_collector_state *state);
255
- static void remove_context_for_dead_threads(thread_context_collector_state *state);
256
- static int remove_if_dead_thread(st_data_t key_thread, st_data_t value_context, st_data_t _argument);
257
336
  static VALUE _native_per_thread_context(VALUE self, VALUE collector_instance);
258
- static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, long gc_start_time_ns, bool is_wall_time);
337
+ static long update_cpu_time_since_previous_sample(per_thread_context *thread_context, long current_cpu_time_ns);
338
+ static long update_wall_time_since_previous_sample(per_thread_context *thread_context, long current_wall_time_ns);
259
339
  static long cpu_time_now_ns(per_thread_context *thread_context);
260
340
  static long thread_id_for(VALUE thread);
261
341
  static VALUE _native_stats(VALUE self, VALUE collector_instance);
@@ -263,8 +343,7 @@ static VALUE _native_gc_tracking(VALUE self, VALUE collector_instance);
263
343
  static void trace_identifiers_for(
264
344
  thread_context_collector_state *state,
265
345
  VALUE thread,
266
- trace_identifiers *trace_identifiers_result,
267
- bool is_safe_to_allocate_objects
346
+ trace_identifiers *trace_identifiers_result
268
347
  );
269
348
  static bool should_collect_resource(VALUE root_span);
270
349
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
@@ -278,40 +357,44 @@ static void ddtrace_otel_trace_identifiers_for(
278
357
  VALUE *root_span,
279
358
  VALUE *numeric_span_id,
280
359
  VALUE active_span,
281
- VALUE otel_values,
282
- bool is_safe_to_allocate_objects
360
+ VALUE otel_values
283
361
  );
284
362
  static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE skipped_samples);
285
363
  static bool handle_gvl_waiting(
286
364
  thread_context_collector_state *state,
287
365
  VALUE thread_being_sampled,
288
- VALUE stack_from_thread,
289
366
  per_thread_context *thread_context,
290
- sampling_buffer* sampling_buffer,
291
367
  long current_cpu_time_ns
292
368
  );
369
+ static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
370
+ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread);
293
371
  #ifndef NO_GVL_INSTRUMENTATION
294
- static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread);
295
372
  static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread);
296
- static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread);
297
- static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception);
298
- static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns);
373
+ static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE waiting_for_gvl_threshold_ns);
374
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
299
375
  #endif
376
+ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns);
300
377
  static void otel_without_ddtrace_trace_identifiers_for(
301
378
  thread_context_collector_state *state,
302
379
  VALUE thread,
303
- trace_identifiers *trace_identifiers_result,
304
- bool is_safe_to_allocate_objects
380
+ trace_identifiers *trace_identifiers_result
305
381
  );
306
382
  static otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key);
383
+ static bool is_otel_current_span_key_needed(thread_context_collector_state *state);
307
384
  static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
308
385
  static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key);
309
386
  static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
310
- static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
387
+ static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self);
388
+ static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread);
389
+ static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread);
390
+ static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
391
+ static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample);
392
+ static void on_thread_begin_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused);
311
393
 
312
394
  void collectors_thread_context_init(VALUE profiling_module) {
313
395
  VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
314
396
  VALUE collectors_thread_context_class = rb_define_class_under(collectors_module, "ThreadContext", rb_cObject);
397
+
315
398
  // Hosts methods used for testing the native code using RSpec
316
399
  VALUE testing_module = rb_define_module_under(collectors_thread_context_class, "Testing");
317
400
 
@@ -328,11 +411,11 @@ void collectors_thread_context_init(VALUE profiling_module) {
328
411
  rb_define_singleton_method(collectors_thread_context_class, "_native_initialize", _native_initialize, -1);
329
412
  rb_define_singleton_method(collectors_thread_context_class, "_native_inspect", _native_inspect, 1);
330
413
  rb_define_singleton_method(collectors_thread_context_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
331
- rb_define_singleton_method(testing_module, "_native_sample", _native_sample, 3);
414
+ rb_define_singleton_method(testing_module, "_native_sample", _native_sample, 2);
332
415
  rb_define_singleton_method(testing_module, "_native_sample_allocation", _native_sample_allocation, 3);
333
416
  rb_define_singleton_method(testing_module, "_native_on_gc_start", _native_on_gc_start, 1);
334
417
  rb_define_singleton_method(testing_module, "_native_on_gc_finish", _native_on_gc_finish, 1);
335
- rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 2);
418
+ rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 1);
336
419
  rb_define_singleton_method(testing_module, "_native_thread_list", _native_thread_list, 0);
337
420
  rb_define_singleton_method(testing_module, "_native_per_thread_context", _native_per_thread_context, 1);
338
421
  rb_define_singleton_method(testing_module, "_native_stats", _native_stats, 1);
@@ -340,14 +423,18 @@ void collectors_thread_context_init(VALUE profiling_module) {
340
423
  rb_define_singleton_method(testing_module, "_native_new_empty_thread", _native_new_empty_thread, 0);
341
424
  rb_define_singleton_method(testing_module, "_native_sample_skipped_allocation_samples", _native_sample_skipped_allocation_samples, 2);
342
425
  rb_define_singleton_method(testing_module, "_native_system_epoch_time_now_ns", _native_system_epoch_time_now_ns, 1);
343
- rb_define_singleton_method(testing_module, "_native_prepare_sample_inside_signal_handler", _native_prepare_sample_inside_signal_handler, 1);
426
+ rb_define_singleton_method(testing_module, "_native_prepare_sample_inside_signal_handler", _native_prepare_sample_inside_signal_handler, 0);
427
+ rb_define_singleton_method(testing_module, "_native_remove_per_thread_context_for", _native_remove_per_thread_context_for, 1);
428
+ rb_define_singleton_method(testing_module, "_native_global_reset_per_thread_context", _native_global_reset_per_thread_context, 1);
429
+ rb_define_singleton_method(testing_module, "_native_mark_thread_as_profiler_internal", _native_mark_thread_as_profiler_internal, 1);
430
+ rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
431
+ rb_define_singleton_method(testing_module, "_native_on_gvl_released", _native_on_gvl_released, 1);
344
432
  #ifndef NO_GVL_INSTRUMENTATION
345
- rb_define_singleton_method(testing_module, "_native_on_gvl_waiting", _native_on_gvl_waiting, 1);
346
433
  rb_define_singleton_method(testing_module, "_native_gvl_waiting_at_for", _native_gvl_waiting_at_for, 1);
347
- rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 1);
348
- rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 3);
349
- rb_define_singleton_method(testing_module, "_native_apply_delta_to_cpu_time_at_previous_sample_ns", _native_apply_delta_to_cpu_time_at_previous_sample_ns, 3);
434
+ rb_define_singleton_method(testing_module, "_native_on_gvl_running", _native_on_gvl_running, 2);
435
+ rb_define_singleton_method(testing_module, "_native_sample_after_gvl_running", _native_sample_after_gvl_running, 2);
350
436
  #endif
437
+ rb_define_singleton_method(testing_module, "_native_apply_delta_to_cpu_time_at_previous_sample_ns", _native_apply_delta_to_cpu_time_at_previous_sample_ns, 2);
351
438
 
352
439
  at_active_span_id = rb_intern_const("@active_span");
353
440
  at_active_trace_id = rb_intern_const("@active_trace");
@@ -368,10 +455,12 @@ void collectors_thread_context_init(VALUE profiling_module) {
368
455
  otel_context_storage_id = rb_intern_const("__opentelemetry_context_storage__");
369
456
  otel_fiber_context_storage_id = rb_intern_const("@opentelemetry_context");
370
457
 
371
- #ifndef NO_GVL_INSTRUMENTATION
372
- // This will raise if Ruby already ran out of thread-local keys
373
- gvl_profiling_init();
374
- #endif
458
+ dd_per_thread_context_id = rb_intern_const("dd_per_thread_context");
459
+
460
+ // This will raise if Ruby already ran out of thread-local keys
461
+ per_thread_context_tls_init();
462
+
463
+ rb_global_variable(&thread_begin_tracepoint);
375
464
 
376
465
  gc_profiling_init();
377
466
  }
@@ -396,10 +485,10 @@ static void thread_context_collector_typed_data_mark(void *state_ptr) {
396
485
 
397
486
  // Update this when modifying state struct
398
487
  rb_gc_mark(state->recorder_instance);
399
- st_foreach(state->hash_map_per_thread_context, hash_map_per_thread_context_mark, 0 /* unused */);
400
488
  rb_gc_mark(state->thread_list_buffer);
401
489
  rb_gc_mark(state->main_thread);
402
490
  rb_gc_mark(state->otel_current_span_key);
491
+ rb_gc_mark(state->overhead_filename);
403
492
  }
404
493
 
405
494
  static void thread_context_collector_typed_data_free(void *state_ptr) {
@@ -409,36 +498,54 @@ static void thread_context_collector_typed_data_free(void *state_ptr) {
409
498
 
410
499
  // Important: Remember that we're only guaranteed to see here what's been set in _native_new, aka
411
500
  // pointers that have been set NULL there may still be NULL here.
412
- if (state->locations != NULL) ruby_xfree(state->locations);
413
-
414
- // Free each entry in the map
415
- st_foreach(state->hash_map_per_thread_context, hash_map_per_thread_context_free_values, 0 /* unused */);
416
- // ...and then the map
417
- st_free_table(state->hash_map_per_thread_context);
501
+ sample_locations_free(&state->locations);
418
502
 
419
503
  st_free_table(state->native_filenames_cache);
420
504
 
421
505
  ruby_xfree(state);
422
506
  }
423
507
 
424
- // Mark Ruby thread references we keep as keys in hash_map_per_thread_context
425
- static int hash_map_per_thread_context_mark(st_data_t key_thread, st_data_t value_thread_context, DDTRACE_UNUSED st_data_t _argument) {
426
- VALUE thread = (VALUE) key_thread;
427
- per_thread_context *thread_context = (per_thread_context *) value_thread_context;
508
+ // per_thread_context is wrapped in a TypedData Ruby object stored as an ivar on each Ruby Thread.
509
+ // This gives us automatic GC marking (for sampling_buffer iseq VALUEs) and lifecycle management.
510
+ static const rb_data_type_t per_thread_context_typed_data = {
511
+ .wrap_struct_name = "Datadog::Profiling::PerThreadContext",
512
+ .function = {
513
+ .dmark = per_thread_context_typed_data_mark,
514
+ .dfree = per_thread_context_typed_data_free,
515
+ .dsize = NULL,
516
+ },
517
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
518
+ };
428
519
 
429
- rb_gc_mark(thread);
430
- if (sampling_buffer_needs_marking(&thread_context->sampling_buffer)) {
431
- sampling_buffer_mark(&thread_context->sampling_buffer);
520
+ static void per_thread_context_typed_data_mark(void *ctx_ptr) {
521
+ per_thread_context *ctx = (per_thread_context *) ctx_ptr;
522
+ if (sampling_buffer_needs_marking(&ctx->sampling_buffer)) {
523
+ sampling_buffer_mark(&ctx->sampling_buffer);
432
524
  }
525
+ }
433
526
 
434
- return ST_CONTINUE;
527
+ static void per_thread_context_typed_data_free(void *ctx_ptr) {
528
+ per_thread_context *ctx = (per_thread_context *) ctx_ptr;
529
+ sampling_buffer_free(&ctx->sampling_buffer);
530
+ free(ctx);
435
531
  }
436
532
 
437
- // Used to clear each of the per_thread_contexts inside the hash_map_per_thread_context
438
- static int hash_map_per_thread_context_free_values(DDTRACE_UNUSED st_data_t _thread, st_data_t value_per_thread_context, DDTRACE_UNUSED st_data_t _argument) {
439
- per_thread_context *thread_context = (per_thread_context*) value_per_thread_context;
440
- free_context(thread_context);
441
- return ST_CONTINUE;
533
+ // Only for testing: removes the per-thread context without recreating it, so the thread has no context.
534
+ // This simulates situations where we observe a thread running before our RUBY_EVENT_THREAD_BEGIN tracepoint fires,
535
+ // so that we can test our `get_per_thread_context(...) => null` code paths.
536
+ static VALUE _native_remove_per_thread_context_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
537
+ check_frozen_thread(thread);
538
+ per_thread_context *ctx = get_per_thread_context(thread);
539
+ if (ctx != NULL) {
540
+ set_per_thread_context(thread, NULL);
541
+ rb_ivar_set(thread, dd_per_thread_context_id, Qnil);
542
+ }
543
+ return Qnil;
544
+ }
545
+
546
+ static VALUE _native_global_reset_per_thread_context(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
547
+ thread_context_collector_reset_all_per_thread_contexts(collector_instance);
548
+ return Qnil;
442
549
  }
443
550
 
444
551
  static VALUE _native_new(VALUE klass) {
@@ -448,25 +555,20 @@ static VALUE _native_new(VALUE klass) {
448
555
  // being leaked.
449
556
 
450
557
  // Update this when modifying state struct
451
- state->locations = NULL;
452
- state->max_frames = 0;
453
- state->hash_map_per_thread_context =
454
- // "numtable" is an awful name, but TL;DR it's what should be used when keys are `VALUE`s.
455
- st_init_numtable();
456
558
  state->recorder_instance = Qnil;
457
559
  state->tracer_context_key = MISSING_TRACER_CONTEXT_KEY;
458
560
  VALUE thread_list_buffer = rb_ary_new();
459
561
  state->thread_list_buffer = thread_list_buffer;
460
562
  state->endpoint_collection_enabled = true;
461
- state->timeline_enabled = true;
462
563
  state->native_filenames_enabled = false;
564
+ state->show_classes = false;
463
565
  state->native_filenames_cache = st_init_numtable();
464
566
  state->otel_context_enabled = OTEL_CONTEXT_ENABLED_FALSE;
465
567
  state->otel_context_source = OTEL_CONTEXT_SOURCE_UNKNOWN;
466
568
  state->time_converter_state = (monotonic_to_system_epoch_state) MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER;
467
569
  VALUE main_thread = rb_thread_main();
468
570
  state->main_thread = main_thread;
469
- state->otel_current_span_key = Qtrue;
571
+ state->otel_current_span_key = OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED;
470
572
  state->gc_tracking.wall_time_at_previous_gc_ns = INVALID_TIME;
471
573
  state->gc_tracking.wall_time_at_last_flushed_gc_event_ns = 0;
472
574
 
@@ -492,28 +594,30 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
492
594
  VALUE max_frames = rb_hash_fetch(options, ID2SYM(rb_intern("max_frames")));
493
595
  VALUE tracer_context_key = rb_hash_fetch(options, ID2SYM(rb_intern("tracer_context_key")));
494
596
  VALUE endpoint_collection_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("endpoint_collection_enabled")));
495
- VALUE timeline_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("timeline_enabled")));
496
- VALUE waiting_for_gvl_threshold_ns = rb_hash_fetch(options, ID2SYM(rb_intern("waiting_for_gvl_threshold_ns")));
497
597
  VALUE otel_context_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("otel_context_enabled")));
498
598
  VALUE native_filenames_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("native_filenames_enabled")));
599
+ VALUE show_classes = rb_hash_fetch(options, ID2SYM(rb_intern("show_classes")));
600
+ VALUE overhead_filename = rb_hash_fetch(options, ID2SYM(rb_intern("overhead_filename")));
499
601
 
500
602
  ENFORCE_TYPE(max_frames, T_FIXNUM);
501
603
  ENFORCE_BOOLEAN(endpoint_collection_enabled);
502
- ENFORCE_BOOLEAN(timeline_enabled);
503
- ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
504
604
  ENFORCE_BOOLEAN(native_filenames_enabled);
605
+ ENFORCE_BOOLEAN(show_classes);
606
+ ENFORCE_TYPE(overhead_filename, T_STRING);
607
+
608
+ uint16_t max_frame_int = sampling_buffer_check_max_frames(NUM2INT(max_frames));
505
609
 
506
610
  thread_context_collector_state *state;
507
611
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
508
612
 
509
613
  // Update this when modifying state struct
510
- state->max_frames = sampling_buffer_check_max_frames(NUM2INT(max_frames));
511
- state->locations = ruby_xcalloc(state->max_frames, sizeof(ddog_prof_Location));
512
- // hash_map_per_thread_context is already initialized, nothing to do here
614
+ sample_locations_initialize(&state->locations, max_frame_int, show_classes == Qtrue);
513
615
  state->recorder_instance = enforce_recorder_instance(recorder_instance);
616
+ recorder_install_on_serialize(recorder_instance, self_instance);
514
617
  state->endpoint_collection_enabled = (endpoint_collection_enabled == Qtrue);
515
- state->timeline_enabled = (timeline_enabled == Qtrue);
516
618
  state->native_filenames_enabled = (native_filenames_enabled == Qtrue);
619
+ state->show_classes = (show_classes == Qtrue);
620
+ state->overhead_filename = overhead_filename;
517
621
  if (otel_context_enabled == Qfalse || otel_context_enabled == Qnil) {
518
622
  state->otel_context_enabled = OTEL_CONTEXT_ENABLED_FALSE;
519
623
  } else if (otel_context_enabled == ID2SYM(rb_intern("only"))) {
@@ -524,8 +628,6 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
524
628
  raise_error(rb_eArgError, "Unexpected value for otel_context_enabled: %+" PRIsVALUE, otel_context_enabled);
525
629
  }
526
630
 
527
- global_waiting_for_gvl_threshold_ns = NUM2UINT(waiting_for_gvl_threshold_ns);
528
-
529
631
  if (RTEST(tracer_context_key)) {
530
632
  ENFORCE_TYPE(tracer_context_key, T_SYMBOL);
531
633
  // Note about rb_to_id and dynamic symbols: calling `rb_to_id` prevents symbols from ever being garbage collected.
@@ -534,22 +636,29 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
534
636
  state->tracer_context_key = rb_to_id(tracer_context_key);
535
637
  }
536
638
 
639
+ if (thread_begin_tracepoint == Qnil) {
640
+ thread_begin_tracepoint = rb_tracepoint_new(Qnil, RUBY_EVENT_THREAD_BEGIN, on_thread_begin_event, NULL);
641
+ rb_tracepoint_enable(thread_begin_tracepoint);
642
+ }
643
+
537
644
  return Qtrue;
538
645
  }
539
646
 
540
647
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
541
648
  // It SHOULD NOT be used for other purposes.
542
- static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance, VALUE profiler_overhead_stack_thread, VALUE allow_exception) {
649
+ static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance, VALUE allow_exception) {
543
650
  ENFORCE_BOOLEAN(allow_exception);
544
651
 
545
- if (!is_thread_alive(profiler_overhead_stack_thread)) raise_error(rb_eArgError, "Unexpected: profiler_overhead_stack_thread is not alive");
546
-
547
652
  if (allow_exception == Qfalse) debug_enter_unsafe_context();
548
653
 
549
- thread_context_collector_sample(collector_instance, monotonic_wall_time_now_ns(RAISE_ON_FAILURE), profiler_overhead_stack_thread);
654
+ bool needs_otel_span_key =
655
+ thread_context_collector_sample(collector_instance, monotonic_wall_time_now_ns(RAISE_ON_FAILURE));
550
656
 
551
657
  if (allow_exception == Qfalse) debug_leave_unsafe_context();
552
658
 
659
+ // Same as the CpuAndWallTimeWorker does: this can't happen during a sample (and thus not inside the unsafe context)
660
+ if (needs_otel_span_key) thread_context_collector_resolve_otel_span_key_may_lose_gvl(collector_instance);
661
+
553
662
  return Qtrue;
554
663
  }
555
664
 
@@ -577,18 +686,54 @@ static VALUE _native_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE collector_ins
577
686
  return Qtrue;
578
687
  }
579
688
 
580
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE allow_exception) {
581
- ENFORCE_BOOLEAN(allow_exception);
582
-
583
- if (allow_exception == Qfalse) debug_enter_unsafe_context();
689
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
690
+ debug_enter_unsafe_context();
584
691
 
585
692
  thread_context_collector_sample_after_gc(collector_instance);
586
693
 
587
- if (allow_exception == Qfalse) debug_leave_unsafe_context();
694
+ debug_leave_unsafe_context();
588
695
 
589
696
  return Qtrue;
590
697
  }
591
698
 
699
+ // Record profiler sampling overhead as a placeholder stack
700
+ static void record_sampling_overhead(thread_context_collector_state *state, per_thread_context *current_thread_context) {
701
+ long wall_time_after_sampling = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
702
+ long cpu_time_after_sampling = cpu_time_now_ns(current_thread_context);
703
+
704
+ long overhead_cpu_time_ns = update_cpu_time_since_previous_sample(current_thread_context, cpu_time_after_sampling);
705
+ long overhead_wall_time_ns = update_wall_time_since_previous_sample(current_thread_context, wall_time_after_sampling);
706
+
707
+ ddog_prof_Label overhead_labels[] = {
708
+ {.key = DDOG_CHARSLICE_C("thread id"), .str = DDOG_CHARSLICE_C("0"), .num = 0},
709
+ {.key = DDOG_CHARSLICE_C("thread name"), .str = DDOG_CHARSLICE_C("Datadog::Profiling::Sampling"), .num = 0},
710
+ {.key = DDOG_CHARSLICE_C("state"), .str = DDOG_CHARSLICE_C("had cpu"), .num = 0},
711
+ {.key = DDOG_CHARSLICE_C("profiler overhead"), .num = 1},
712
+ };
713
+
714
+ int64_t end_timestamp_ns = monotonic_to_system_epoch_ns(&state->time_converter_state, wall_time_after_sampling);
715
+
716
+ ddog_prof_Location overhead_location = {
717
+ .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C(""), .build_id_id = {}},
718
+ .function = {
719
+ .name = DDOG_CHARSLICE_C("sampling"),
720
+ .filename = char_slice_from_ruby_string(state->overhead_filename),
721
+ },
722
+ .line = 0,
723
+ };
724
+
725
+ record_sample(
726
+ state->recorder_instance,
727
+ (ddog_prof_Slice_Location) {.ptr = &overhead_location, .len = 1},
728
+ (sample_values) {.cpu_time_ns = overhead_cpu_time_ns, .cpu_or_wall_samples = 1, .wall_time_ns = overhead_wall_time_ns},
729
+ (sample_labels) {
730
+ .labels = (ddog_prof_Slice_Label) {.ptr = overhead_labels, .len = sizeof(overhead_labels) / sizeof(overhead_labels[0])},
731
+ .state_label = NULL,
732
+ .end_timestamp_ns = end_timestamp_ns,
733
+ }
734
+ );
735
+ }
736
+
592
737
  // This function gets called from the Collectors::CpuAndWallTimeWorker to trigger the actual sampling.
593
738
  //
594
739
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
@@ -597,14 +742,12 @@ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_
597
742
  // Assumption 4: This function IS NOT called in a reentrant way.
598
743
  // Assumption 5: This function is called from the main Ractor (if Ruby has support for Ractors).
599
744
  //
600
- // The `profiler_overhead_stack_thread` is used to attribute the profiler overhead to a stack borrowed from a different thread
601
- // (belonging to ddtrace), so that the overhead is visible in the profile rather than blamed on user code.
602
- void thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns, VALUE profiler_overhead_stack_thread) {
745
+ bool thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns) {
603
746
  thread_context_collector_state *state;
604
747
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
605
748
 
606
749
  VALUE current_thread = rb_thread_current();
607
- per_thread_context *current_thread_context = get_or_create_context_for(current_thread, state);
750
+ per_thread_context *current_thread_context = get_or_create_context_for(current_thread);
608
751
  long cpu_time_at_sample_start_for_current_thread = cpu_time_now_ns(current_thread_context);
609
752
 
610
753
  VALUE threads = thread_list(state);
@@ -612,76 +755,59 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
612
755
  const long thread_count = RARRAY_LEN(threads);
613
756
  for (long i = 0; i < thread_count; i++) {
614
757
  VALUE thread = RARRAY_AREF(threads, i);
615
- per_thread_context *thread_context = get_or_create_context_for(thread, state);
758
+ per_thread_context *thread_context = get_or_create_context_for(thread);
616
759
 
617
- // We account for cpu-time for the current thread in a different way -- we use the cpu-time at sampling start, to avoid
618
- // blaming the time the profiler took on whatever's running on the thread right now
619
- long current_cpu_time_ns = thread != current_thread ? cpu_time_now_ns(thread_context) : cpu_time_at_sample_start_for_current_thread;
760
+ // We account for cpu-time for the current thread in a different way: we use the cpu-time at sampling start,
761
+ // to avoid blaming the time the profiler took on whatever is currently running on the thread,
762
+ // and instead we report that time the profiler took as sampling overhead below.
763
+ long current_cpu_time_ns = (thread == current_thread) ? cpu_time_at_sample_start_for_current_thread : cpu_time_now_ns(thread_context);
620
764
 
621
765
  update_metrics_and_sample(
622
766
  state,
623
- /* thread_being_sampled: */ thread,
624
- /* stack_from_thread: */ thread,
767
+ thread,
625
768
  thread_context,
626
- &thread_context->sampling_buffer,
627
769
  current_cpu_time_ns,
628
- current_monotonic_wall_time_ns
629
- );
770
+ current_monotonic_wall_time_ns,
771
+ false);
630
772
  }
631
773
 
632
- state->sample_count++;
774
+ state->stats.sample_count++;
633
775
 
634
- // TODO: This seems somewhat overkill and inefficient to do often; right now we just do it every few samples
635
- // but there's probably a better way to do this if we actually track when threads finish
636
- if (state->sample_count % 100 == 0) remove_context_for_dead_threads(state);
776
+ // If the current thread is a profiler-internal thread, we don't use record_sampling_overhead()
777
+ // and accept the sampling overhead is attributed to the profiler-internal thread
778
+ // (both are under the same datadog group in the flamegraph).
779
+ // The reason we need to do this is profiler-internal threads are skipped during per-tick sampling
780
+ // so their timestamps are not updated. record_sampling_overhead() would see the stale previous-sample timestamps and
781
+ // attribute the entire sleep interval incorrectly as overhead instead of just the sampling work.
782
+ if (!current_thread_context->is_profiler_internal_thread) {
783
+ record_sampling_overhead(state, current_thread_context);
784
+ }
637
785
 
638
- update_metrics_and_sample(
639
- state,
640
- /* thread_being_sampled: */ current_thread,
641
- /* stack_from_thread: */ profiler_overhead_stack_thread,
642
- current_thread_context,
643
- // Here we use the overhead thread's sampling buffer so as to not invalidate the cache in the buffer of the thread being sampled
644
- &get_or_create_context_for(profiler_overhead_stack_thread, state)->sampling_buffer,
645
- cpu_time_now_ns(current_thread_context),
646
- monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
647
- );
786
+ return is_otel_current_span_key_needed(state);
648
787
  }
649
788
 
650
789
  static void update_metrics_and_sample(
651
790
  thread_context_collector_state *state,
652
791
  VALUE thread_being_sampled,
653
- VALUE stack_from_thread, // This can be different when attributing profiler overhead using a different stack
654
792
  per_thread_context *thread_context,
655
- sampling_buffer* sampling_buffer,
656
793
  long current_cpu_time_ns,
657
- long current_monotonic_wall_time_ns
794
+ long current_monotonic_wall_time_ns,
795
+ bool force_sample
658
796
  ) {
659
797
  bool is_gvl_waiting_state =
660
- handle_gvl_waiting(state, thread_being_sampled, stack_from_thread, thread_context, sampling_buffer, current_cpu_time_ns);
798
+ handle_gvl_waiting(state, thread_being_sampled, thread_context, current_cpu_time_ns);
799
+
800
+ if (skip_sample(state, thread_context, is_gvl_waiting_state, force_sample)) return;
661
801
 
662
802
  // Don't assign/update cpu during "Waiting for GVL"
663
- long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_time_since_previous_sample(
664
- &thread_context->cpu_time_at_previous_sample_ns,
665
- current_cpu_time_ns,
666
- thread_context->gc_tracking.cpu_time_at_start_ns,
667
- IS_NOT_WALL_TIME
668
- );
803
+ long cpu_time_elapsed_ns = is_gvl_waiting_state ? 0 : update_cpu_time_since_previous_sample(thread_context, current_cpu_time_ns);
669
804
 
670
- long wall_time_elapsed_ns = update_time_since_previous_sample(
671
- &thread_context->wall_time_at_previous_sample_ns,
672
- current_monotonic_wall_time_ns,
673
- // We explicitly pass in `INVALID_TIME` as an argument for `gc_start_time_ns` here because we don't want wall-time
674
- // accounting to change during GC.
675
- // E.g. if 60 seconds pass in the real world, 60 seconds of wall-time are recorded, regardless of the thread doing
676
- // GC or not.
677
- INVALID_TIME,
678
- IS_WALL_TIME
679
- );
805
+ long wall_time_elapsed_ns = update_wall_time_since_previous_sample(thread_context, current_monotonic_wall_time_ns);
680
806
 
681
807
  // A thread enters "Waiting for GVL", well, as the name implies, without the GVL.
682
808
  //
683
809
  // As a consequence, it's possible that a thread enters "Waiting for GVL" in parallel with the current thread working
684
- // on sampling, and thus for the `current_monotonic_wall_time_ns` (which is recorded at the start of sampling)
810
+ // on sampling, and thus for the `current_monotonic_wall_time_ns` (which is recorded at the start of sampling)
685
811
  // to be < the time at which we started Waiting for GVL.
686
812
  //
687
813
  // All together, this means that when `handle_gvl_waiting` creates an extra sample (see comments on that function for
@@ -696,18 +822,46 @@ static void update_metrics_and_sample(
696
822
  trigger_sample_for_thread(
697
823
  state,
698
824
  thread_being_sampled,
699
- stack_from_thread,
700
825
  thread_context,
701
- sampling_buffer,
702
826
  (sample_values) {.cpu_time_ns = cpu_time_elapsed_ns, .cpu_or_wall_samples = 1, .wall_time_ns = wall_time_elapsed_ns},
703
827
  current_monotonic_wall_time_ns,
704
828
  NULL,
705
829
  NULL,
706
- is_gvl_waiting_state,
707
- /* is_safe_to_allocate_objects: */ true // We called from a context that's safe to run any regular code, including allocations
830
+ is_gvl_waiting_state
708
831
  );
709
832
  }
710
833
 
834
+ static bool skip_sample(thread_context_collector_state *state, per_thread_context *thread_context, bool is_gvl_waiting_state, bool force_sample) {
835
+ if (!force_sample && thread_context->is_profiler_internal_thread) {
836
+ state->stats.profiler_thread_samples_skipped++;
837
+ return true;
838
+ }
839
+
840
+ // Racy read but harmless, can only cause an extra sample
841
+ uint64_t gvl_state_change_count = thread_context->gvl_state_change_count;
842
+
843
+ // Skip this per-tick sample entirely when the thread does not have the GVL and did not acquire
844
+ // it since the previous sample: its Ruby-level stack has not changed. The skipped wall-time will
845
+ // be picked up by either by an extra sample when the thread acquires the GVL, or by
846
+ // the on-serialize flush in the stack recorder (using was_skipped_at_last_sample).
847
+ // The check is gated by `!is_gvl_waiting_state` so the existing "Waiting for GVL" machinery
848
+ // in handle_gvl_waiting (situation 1 extra sample, situation 2 regular sample) keeps running.
849
+ // TODO: we could probably also skip while "Waiting for GVL"
850
+ if (!is_gvl_waiting_state &&
851
+ !force_sample &&
852
+ (gvl_state_change_count & GVL_SUSPENDED) &&
853
+ gvl_state_change_count == thread_context->gvl_state_change_count_at_previous_sample) {
854
+ state->stats.inactive_thread_samples_skipped++;
855
+ thread_context->was_skipped_at_last_sample = true;
856
+ return true; // Do NOT update wall_time_at_previous_sample_ns or cpu_time_at_previous_sample_ns
857
+ } else {
858
+ // We are going to sample, update the state accordingly:
859
+ thread_context->gvl_state_change_count_at_previous_sample = gvl_state_change_count;
860
+ thread_context->was_skipped_at_last_sample = false;
861
+ return false;
862
+ }
863
+ }
864
+
711
865
  // This function gets called when Ruby is about to start running the Garbage Collector on the current thread.
712
866
  // It updates the per_thread_context of the current thread to include the current cpu/wall times, to be used to later
713
867
  // create an event including the cpu/wall time spent in garbage collector work.
@@ -721,22 +875,25 @@ static void update_metrics_and_sample(
721
875
  void thread_context_collector_on_gc_start(VALUE self_instance) {
722
876
  thread_context_collector_state *state;
723
877
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return;
724
- // This should never fail the the above check passes
878
+ // This should never fail when the above check passes
725
879
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
726
880
 
727
- per_thread_context *thread_context = get_context_for(rb_thread_current(), state);
881
+ per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
728
882
 
729
- // If there was no previously-existing context for this thread, we won't allocate one (see safety). For now we just drop
730
- // the GC sample, under the assumption that "a thread that is so new that we never sampled it even once before it triggers
731
- // GC" is a rare enough case that we can just ignore it.
732
- // We can always improve this later if we find that this happens often (and we have the counter to help us figure that out)!
883
+ // Context is created eagerly via on_thread_begin_event, so this should not normally be NULL.
884
+ // We can't get_or_create_context_for() here since we can't allocate (GC context).
733
885
  if (thread_context == NULL) {
734
886
  state->stats.gc_samples_missed_due_to_missing_context++;
735
887
  return;
736
888
  }
737
889
 
738
890
  // Here we record the wall-time first and in on_gc_finish we record it second to try to avoid having wall-time be slightly < cpu-time
739
- thread_context->gc_tracking.wall_time_at_start_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
891
+ long wall_time_at_start_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
892
+
893
+ // If our start timestamp is not OK, we skip tracking this GC as well
894
+ if (wall_time_at_start_ns == 0) return;
895
+
896
+ thread_context->gc_tracking.wall_time_at_start_ns = wall_time_at_start_ns;
740
897
  thread_context->gc_tracking.cpu_time_at_start_ns = cpu_time_now_ns(thread_context);
741
898
  }
742
899
 
@@ -754,19 +911,19 @@ __attribute__((warn_unused_result))
754
911
  bool thread_context_collector_on_gc_finish(VALUE self_instance) {
755
912
  thread_context_collector_state *state;
756
913
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
757
- // This should never fail the the above check passes
914
+ // This should never fail when the above check passes
758
915
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
759
916
 
760
- per_thread_context *thread_context = get_context_for(rb_thread_current(), state);
917
+ per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
761
918
 
762
- // If there was no previously-existing context for this thread, we won't allocate one (see safety). We keep a metric for
763
- // how often this happens -- see on_gc_start.
919
+ // Context is created eagerly, so this should not normally be NULL (see on_gc_start).
764
920
  if (thread_context == NULL) return false;
765
921
 
766
922
  long cpu_time_at_start_ns = thread_context->gc_tracking.cpu_time_at_start_ns;
767
923
  long wall_time_at_start_ns = thread_context->gc_tracking.wall_time_at_start_ns;
768
924
 
769
- if (cpu_time_at_start_ns == INVALID_TIME && wall_time_at_start_ns == INVALID_TIME) {
925
+ // Both fields are always set and cleared together, so checking one is enough
926
+ if (cpu_time_at_start_ns == INVALID_TIME) {
770
927
  // If this happened, it means that on_gc_start was either never called for the thread OR it was called but no thread
771
928
  // context existed at the time. The former can be the result of a bug, but since we can't distinguish them, we just
772
929
  // do nothing.
@@ -801,9 +958,9 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
801
958
  state->gc_tracking.accumulated_wall_time_ns += gc_wall_time_elapsed_ns;
802
959
  state->gc_tracking.wall_time_at_previous_gc_ns = wall_time_at_finish_ns;
803
960
 
804
- // Update cpu-time accounting so it doesn't include the cpu-time spent in GC during the next sample
805
- // We don't update the wall-time because we don't subtract the wall-time spent in GC (see call to
806
- // `update_time_since_previous_sample` for wall-time in `update_metrics_and_sample`).
961
+ // Update cpu-time accounting so it doesn't include the cpu-time spent in GC during the next sample.
962
+ // We don't do the same for wall-time, because GC is just like any other reason a thread didn't make
963
+ // progress -- time always goes forward regardless of the thread making progress on what it wanted.
807
964
  if (thread_context->cpu_time_at_previous_sample_ns != INVALID_TIME) {
808
965
  thread_context->cpu_time_at_previous_sample_ns += gc_cpu_time_elapsed_ns;
809
966
  }
@@ -834,7 +991,14 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
834
991
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
835
992
 
836
993
  if (state->gc_tracking.wall_time_at_previous_gc_ns == INVALID_TIME) {
837
- raise_error(rb_eRuntimeError, "BUG: Unexpected call to sample_after_gc without valid GC information available");
994
+ // Rarely, we might be called with nothing to do, as an earlier call already flushed the needed info.
995
+ // This is because Ruby clears the "pending" flag for an entire batch of postponed jobs BEFORE running any of them
996
+ // (see `rb_postponed_job_flush`). Thus, if another GC happens while an earlier job in the postponed batch is
997
+ // running (and other parts of the profiler such as the heap profiler can trigger this)
998
+ // then `on_gc_finish` records that data and asks for a second flush, even though Ruby has not yet run the
999
+ // first one. Because of this, Ruby can call us once more with nothing left to do.
1000
+ state->stats.gc_samples_skipped_nothing_to_flush++;
1001
+ return Qnil;
838
1002
  }
839
1003
 
840
1004
  int max_labels_needed_for_gc = 7; // Magic number gets validated inside gc_profiling_set_metadata
@@ -844,11 +1008,7 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
844
1008
  ddog_prof_Slice_Label slice_labels = {.ptr = labels, .len = label_pos};
845
1009
 
846
1010
  // The end_timestamp_ns is treated specially by libdatadog and that's why it's not added as a ddog_prof_Label
847
- int64_t end_timestamp_ns = 0;
848
-
849
- if (state->timeline_enabled) {
850
- end_timestamp_ns = monotonic_to_system_epoch_ns(&state->time_converter_state, state->gc_tracking.wall_time_at_previous_gc_ns);
851
- }
1011
+ int64_t end_timestamp_ns = monotonic_to_system_epoch_ns(&state->time_converter_state, state->gc_tracking.wall_time_at_previous_gc_ns);
852
1012
 
853
1013
  record_placeholder_stack(
854
1014
  state->recorder_instance,
@@ -872,33 +1032,37 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
872
1032
 
873
1033
  state->stats.gc_samples++;
874
1034
 
875
- // Let recorder do any cleanup/updates it requires after a GC step.
876
- recorder_after_gc_step(state->recorder_instance);
877
-
878
1035
  // Return a VALUE to make it easier to call this function from Ruby APIs that expect a return value (such as rb_rescue2)
879
1036
  return Qnil;
880
1037
  }
881
1038
 
1039
+ // Let the recorder do any cleanup/updates it requires after a GC step.
1040
+ //
1041
+ // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
1042
+ // Assumption 2: This function is allowed to raise exceptions. Caller is responsible for handling them, if needed.
1043
+ VALUE thread_context_collector_heap_update_may_lose_gvl(VALUE self_instance) {
1044
+ thread_context_collector_state *state;
1045
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1046
+
1047
+ recorder_heap_update_may_lose_gvl(state->recorder_instance);
1048
+
1049
+ return Qnil; // for rb_rescue2
1050
+ }
1051
+
882
1052
  static void trigger_sample_for_thread(
883
1053
  thread_context_collector_state *state,
884
- VALUE thread,
885
- VALUE stack_from_thread, // This can be different when attributing profiler overhead using a different stack
1054
+ VALUE thread_being_sampled,
886
1055
  per_thread_context *thread_context,
887
- sampling_buffer* sampling_buffer,
888
1056
  sample_values values,
889
1057
  long current_monotonic_wall_time_ns,
890
1058
  // These two labels are only used for allocation profiling; @ivoanjo: may want to refactor this at some point?
891
1059
  ddog_CharSlice *ruby_vm_type,
892
1060
  ddog_CharSlice *class_name,
893
- bool is_gvl_waiting_state,
894
- // If the Ruby VM is at a state that can allocate objects safely, or not. Added for allocation profiling: we're not
895
- // allowed to allocate objects (or raise exceptions) when inside the NEWOBJ tracepoint.
896
- bool is_safe_to_allocate_objects
1061
+ bool is_gvl_waiting_state
897
1062
  ) {
898
1063
  int max_label_count =
899
1064
  1 + // thread id
900
1065
  1 + // thread name
901
- 1 + // profiler overhead
902
1066
  2 + // ruby vm type and allocation class
903
1067
  1 + // state (only set for cpu/wall-time samples)
904
1068
  2; // local root span id and span id
@@ -910,13 +1074,13 @@ static void trigger_sample_for_thread(
910
1074
  .str = thread_context->thread_id_char_slice
911
1075
  };
912
1076
 
913
- VALUE thread_name = thread_name_for(thread);
1077
+ VALUE thread_name = thread_name_for(thread_being_sampled);
914
1078
  if (thread_name != Qnil) {
915
1079
  labels[label_pos++] = (ddog_prof_Label) {
916
1080
  .key = DDOG_CHARSLICE_C("thread name"),
917
1081
  .str = char_slice_from_ruby_string(thread_name)
918
1082
  };
919
- } else if (thread == state->main_thread) { // Threads are often not named, but we can have a nice fallback for this special thread
1083
+ } else if (thread_being_sampled == state->main_thread) { // Threads are often not named, but we can have a nice fallback for this special thread
920
1084
  ddog_CharSlice main_thread_name = DDOG_CHARSLICE_C("main");
921
1085
  labels[label_pos++] = (ddog_prof_Label) {
922
1086
  .key = DDOG_CHARSLICE_C("thread name"),
@@ -932,11 +1096,11 @@ static void trigger_sample_for_thread(
932
1096
  }
933
1097
 
934
1098
  trace_identifiers trace_identifiers_result = {.valid = false, .trace_endpoint = Qnil};
935
- trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
1099
+ trace_identifiers_for(state, thread_being_sampled, &trace_identifiers_result);
936
1100
 
937
1101
  if (!trace_identifiers_result.valid && state->otel_context_enabled != OTEL_CONTEXT_ENABLED_FALSE) {
938
1102
  // If we couldn't get something with ddtrace, let's see if we can get some trace identifiers from opentelemetry directly
939
- otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
1103
+ otel_without_ddtrace_trace_identifiers_for(state, thread_being_sampled, &trace_identifiers_result);
940
1104
  }
941
1105
 
942
1106
  if (trace_identifiers_result.valid) {
@@ -961,13 +1125,6 @@ static void trigger_sample_for_thread(
961
1125
  }
962
1126
  }
963
1127
 
964
- if (thread != stack_from_thread) {
965
- labels[label_pos++] = (ddog_prof_Label) {
966
- .key = DDOG_CHARSLICE_C("profiler overhead"),
967
- .num = 1
968
- };
969
- }
970
-
971
1128
  if (ruby_vm_type != NULL) {
972
1129
  labels[label_pos++] = (ddog_prof_Label) {
973
1130
  .key = DDOG_CHARSLICE_C("ruby vm type"),
@@ -1008,13 +1165,14 @@ static void trigger_sample_for_thread(
1008
1165
 
1009
1166
  // The end_timestamp_ns is treated specially by libdatadog and that's why it's not added as a ddog_prof_Label
1010
1167
  int64_t end_timestamp_ns = 0;
1011
- if (state->timeline_enabled && current_monotonic_wall_time_ns != INVALID_TIME) {
1168
+ if (current_monotonic_wall_time_ns != INVALID_TIME) {
1012
1169
  end_timestamp_ns = monotonic_to_system_epoch_ns(&state->time_converter_state, current_monotonic_wall_time_ns);
1013
1170
  }
1014
1171
 
1015
1172
  sample_thread(
1016
- stack_from_thread,
1017
- sampling_buffer,
1173
+ thread_being_sampled,
1174
+ &thread_context->sampling_buffer,
1175
+ state->locations,
1018
1176
  state->recorder_instance,
1019
1177
  values,
1020
1178
  (sample_labels) {
@@ -1024,7 +1182,8 @@ static void trigger_sample_for_thread(
1024
1182
  .is_gvl_waiting_state = is_gvl_waiting_state,
1025
1183
  },
1026
1184
  state->native_filenames_enabled,
1027
- state->native_filenames_cache
1185
+ state->native_filenames_cache,
1186
+ state->show_classes
1028
1187
  );
1029
1188
  }
1030
1189
 
@@ -1042,32 +1201,38 @@ static VALUE _native_thread_list(DDTRACE_UNUSED VALUE _self) {
1042
1201
  return result;
1043
1202
  }
1044
1203
 
1045
- static per_thread_context *get_or_create_context_for(VALUE thread, thread_context_collector_state *state) {
1046
- per_thread_context* thread_context = NULL;
1047
- st_data_t value_context = 0;
1048
-
1049
- if (st_lookup(state->hash_map_per_thread_context, (st_data_t) thread, &value_context)) {
1050
- thread_context = (per_thread_context*) value_context;
1051
- } else {
1052
- thread_context = calloc(1, sizeof(per_thread_context)); // See "note on calloc vs ruby_xcalloc use" in heap_recorder.c
1053
- initialize_context(thread, thread_context, state);
1054
- st_insert(state->hash_map_per_thread_context, (st_data_t) thread, (st_data_t) thread_context);
1204
+ static void check_frozen_thread(VALUE thread) {
1205
+ if (RB_OBJ_FROZEN(thread)) {
1206
+ raise_error(rb_eFrozenError, "Cannot setup profiler state for Thread %"PRIsVALUE" because it is frozen. Please avoid freezing Thread instances and/or report the issue to dd-trace-rb", thread);
1055
1207
  }
1056
-
1057
- return thread_context;
1058
1208
  }
1059
1209
 
1060
- static per_thread_context *get_context_for(VALUE thread, thread_context_collector_state *state) {
1061
- per_thread_context* thread_context = NULL;
1062
- st_data_t value_context = 0;
1210
+ // See the docs on struct per_thread_context.
1211
+ // This allocates a Ruby object and therefore needs the GVL and is not safe to call from RUBY_INTERNAL_EVENT_* hooks.
1212
+ static per_thread_context *get_or_create_context_for(VALUE thread) {
1213
+ per_thread_context *thread_context = get_per_thread_context(thread);
1214
+ if (thread_context != NULL) return thread_context;
1063
1215
 
1064
- if (st_lookup(state->hash_map_per_thread_context, (st_data_t) thread, &value_context)) {
1065
- thread_context = (per_thread_context*) value_context;
1066
- }
1216
+ check_frozen_thread(thread);
1067
1217
 
1218
+ thread_context = calloc(1, sizeof(per_thread_context)); // See "note on calloc vs ruby_xcalloc use" in heap_recorder.c
1219
+ initialize_context(thread, thread_context);
1220
+
1221
+ VALUE wrapper = TypedData_Wrap_Struct(rb_cObject, &per_thread_context_typed_data, thread_context);
1222
+ rb_ivar_set(thread, dd_per_thread_context_id, wrapper);
1223
+
1224
+ set_per_thread_context(thread, thread_context);
1068
1225
  return thread_context;
1069
1226
  }
1070
1227
 
1228
+ static void on_thread_begin_event(VALUE tracepoint_data, DDTRACE_UNUSED void *unused) {
1229
+ if (!ddtrace_rb_ractor_main_p()) return;
1230
+
1231
+ VALUE thread = rb_tracearg_self(rb_tracearg_from_tracepoint(tracepoint_data));
1232
+ ENFORCE_THREAD(thread);
1233
+ get_or_create_context_for(thread);
1234
+ }
1235
+
1071
1236
  #define LOGGING_GEM_PATH "/lib/logging/diagnostic_context.rb"
1072
1237
 
1073
1238
  // The `logging` gem monkey patches thread creation, which makes the `invoke_location_for` useless, since every thread
@@ -1089,8 +1254,11 @@ static bool is_logging_gem_monkey_patch(VALUE invoke_file_location) {
1089
1254
  return strncmp(invoke_file + invoke_file_len - logging_gem_path_len, LOGGING_GEM_PATH, logging_gem_path_len) == 0;
1090
1255
  }
1091
1256
 
1092
- static void initialize_context(VALUE thread, per_thread_context *thread_context, thread_context_collector_state *state) {
1093
- sampling_buffer_initialize(&thread_context->sampling_buffer, state->max_frames, state->locations);
1257
+ static void initialize_context(VALUE thread, per_thread_context *thread_context) {
1258
+ // We always create per_thread_context's with latest_max_frames; that value is kept in sync with the
1259
+ // active profiler's max_frames by the global reset that runs when profiling starts
1260
+ // so we expect to always see here the latest correct value to be used.
1261
+ sampling_buffer_initialize(&thread_context->sampling_buffer, latest_max_frames);
1094
1262
 
1095
1263
  snprintf(thread_context->thread_id, THREAD_ID_LIMIT_CHARS, "%"PRIu64" (%lu)", native_thread_id_for(thread), (unsigned long) thread_id_for(thread));
1096
1264
  thread_context->thread_id_char_slice = (ddog_CharSlice) {.ptr = thread_context->thread_id, .len = strlen(thread_context->thread_id)};
@@ -1109,7 +1277,7 @@ static void initialize_context(VALUE thread, per_thread_context *thread_context,
1109
1277
  } else {
1110
1278
  snprintf(thread_context->thread_invoke_location, THREAD_INVOKE_LOCATION_LIMIT_CHARS, "%s", "(Unnamed thread)");
1111
1279
  }
1112
- } else if (thread != state->main_thread) {
1280
+ } else if (thread != rb_thread_main()) {
1113
1281
  // If the first function of a thread is native code, there won't be an invoke location, so we use this fallback.
1114
1282
  // NOTE: In the future, I wonder if we could take the pointer to the native function, and try to see if there's a native
1115
1283
  // symbol attached to it.
@@ -1123,32 +1291,54 @@ static void initialize_context(VALUE thread, per_thread_context *thread_context,
1123
1291
 
1124
1292
  thread_context->thread_cpu_time_id = thread_cpu_time_id_for(thread);
1125
1293
 
1126
- // These will get initialized during actual sampling
1127
- thread_context->cpu_time_at_previous_sample_ns = INVALID_TIME;
1128
- thread_context->wall_time_at_previous_sample_ns = INVALID_TIME;
1294
+ thread_context->wall_time_at_previous_sample_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
1295
+ thread_context->cpu_time_at_previous_sample_ns = cpu_time_now_ns(thread_context);
1129
1296
 
1130
1297
  // These will only be used during a GC operation
1131
1298
  thread_context->gc_tracking.cpu_time_at_start_ns = INVALID_TIME;
1132
1299
  thread_context->gc_tracking.wall_time_at_start_ns = INVALID_TIME;
1133
1300
 
1134
- #ifndef NO_GVL_INSTRUMENTATION
1135
- // We use this special location to store data that can be accessed without any
1136
- // kind of synchronization (e.g. by threads without the GVL).
1137
- //
1138
- // We set this marker here for two purposes:
1139
- // * To make sure there's no stale data from a previous execution of the profiler.
1140
- // * To mark threads that are actually being profiled
1141
- //
1142
- // (Setting this is potentially a race, but what we want is to avoid _stale_ data, so
1143
- // if this gets set concurrently with context initialization, then such a value will belong
1144
- // to the current profiler instance, so that's OK)
1145
- gvl_profiling_state_thread_object_set(thread, GVL_WAITING_ENABLED_EMPTY);
1146
- #endif
1301
+ thread_context->gvl_waiting_at = 0;
1302
+ thread_context->gvl_state_change_count = 0;
1147
1303
  }
1148
1304
 
1149
- static void free_context(per_thread_context* thread_context) {
1150
- sampling_buffer_free(&thread_context->sampling_buffer);
1151
- free(thread_context); // See "note on calloc vs ruby_xcalloc use" in heap_recorder.c
1305
+ // This MUST be called before profiling starts, so that a new profiler session starts from a fresh state and never
1306
+ // observes or includes any leftover stale state from a previous session.
1307
+ // Such a call MUST happen while the CpuAndWallTimeWorker is stopped (e.g. no tracepoints active, no signals
1308
+ // triggering samples, no gvl hooks, etc).
1309
+ //
1310
+ // It updates the global `latest_max_frames` from the given (latest) ThreadContext and (re)creates every per-thread
1311
+ // context's sampling buffer sized accordingly, so the buffers always match the collector that's about to start
1312
+ // sampling -- even if a previous session used a different max_frames.
1313
+ void thread_context_collector_reset_all_per_thread_contexts(VALUE self_instance) {
1314
+ thread_context_collector_state *state;
1315
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1316
+
1317
+ // Update global max frames to be used when allocating sampling buffers
1318
+ if (state->locations.len == 0) {
1319
+ raise_error(rb_eRuntimeError, "BUG: Unexpected locations.len == 0. Is this ThreadContext not initialized?");
1320
+ }
1321
+
1322
+ latest_max_frames = state->locations.len;
1323
+
1324
+ VALUE threads = thread_list(state);
1325
+ const long thread_count = RARRAY_LEN(threads);
1326
+ for (long i = 0; i < thread_count; i++) {
1327
+ VALUE thread = rb_ary_entry(threads, i);
1328
+ per_thread_context *thread_context = get_per_thread_context(thread);
1329
+ if (thread_context != NULL) {
1330
+ bool is_profiler_internal_thread = thread_context->is_profiler_internal_thread;
1331
+
1332
+ sampling_buffer_free(&thread_context->sampling_buffer);
1333
+ memset(thread_context, 0, sizeof(per_thread_context));
1334
+ initialize_context(thread, thread_context);
1335
+
1336
+ thread_context->is_profiler_internal_thread = is_profiler_internal_thread;
1337
+ } else {
1338
+ // If thread didn't have a context, let's trigger its creation
1339
+ get_or_create_context_for(thread);
1340
+ }
1341
+ }
1152
1342
  }
1153
1343
 
1154
1344
  static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
@@ -1158,16 +1348,14 @@ static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instanc
1158
1348
  VALUE result = rb_str_new2(" (native state)");
1159
1349
 
1160
1350
  // Update this when modifying state struct
1161
- rb_str_concat(result, rb_sprintf(" max_frames=%d", state->max_frames));
1162
- rb_str_concat(result, rb_sprintf(" hash_map_per_thread_context=%"PRIsVALUE, per_thread_context_st_table_as_ruby_hash(state)));
1351
+ rb_str_concat(result, rb_sprintf(" max_frames=%d", state->locations.len));
1163
1352
  rb_str_concat(result, rb_sprintf(" recorder_instance=%"PRIsVALUE, state->recorder_instance));
1164
1353
  VALUE tracer_context_key = state->tracer_context_key == MISSING_TRACER_CONTEXT_KEY ? Qnil : ID2SYM(state->tracer_context_key);
1165
1354
  rb_str_concat(result, rb_sprintf(" tracer_context_key=%+"PRIsVALUE, tracer_context_key));
1166
- rb_str_concat(result, rb_sprintf(" sample_count=%u", state->sample_count));
1167
- rb_str_concat(result, rb_sprintf(" stats=%"PRIsVALUE, stats_as_ruby_hash(state)));
1355
+ rb_str_concat(result, rb_sprintf(" stats=%"PRIsVALUE, stats_to_ruby_hash(state, rb_hash_new())));
1168
1356
  rb_str_concat(result, rb_sprintf(" endpoint_collection_enabled=%"PRIsVALUE, state->endpoint_collection_enabled ? Qtrue : Qfalse));
1169
- rb_str_concat(result, rb_sprintf(" timeline_enabled=%"PRIsVALUE, state->timeline_enabled ? Qtrue : Qfalse));
1170
1357
  rb_str_concat(result, rb_sprintf(" native_filenames_enabled=%"PRIsVALUE, state->native_filenames_enabled ? Qtrue : Qfalse));
1358
+ rb_str_concat(result, rb_sprintf(" show_classes=%"PRIsVALUE, state->show_classes ? Qtrue : Qfalse));
1171
1359
  // Note: `st_table_size()` is available from Ruby 3.2+ but not before
1172
1360
  rb_str_concat(result, rb_sprintf(" native_filenames_cache_size=%zu", state->native_filenames_cache->num_entries));
1173
1361
  rb_str_concat(result, rb_sprintf(" otel_context_enabled=%d", state->otel_context_enabled));
@@ -1179,53 +1367,51 @@ static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instanc
1179
1367
  rb_str_concat(result, rb_sprintf(" main_thread=%"PRIsVALUE, state->main_thread));
1180
1368
  rb_str_concat(result, rb_sprintf(" gc_tracking=%"PRIsVALUE, gc_tracking_as_ruby_hash(state)));
1181
1369
  rb_str_concat(result, rb_sprintf(" otel_current_span_key=%"PRIsVALUE, state->otel_current_span_key));
1182
- rb_str_concat(result, rb_sprintf(" global_waiting_for_gvl_threshold_ns=%u", global_waiting_for_gvl_threshold_ns));
1183
-
1184
- return result;
1185
- }
1186
1370
 
1187
- static VALUE per_thread_context_st_table_as_ruby_hash(thread_context_collector_state *state) {
1188
- VALUE result = rb_hash_new();
1189
- st_foreach(state->hash_map_per_thread_context, per_thread_context_as_ruby_hash, result);
1190
1371
  return result;
1191
1372
  }
1192
1373
 
1193
- static int per_thread_context_as_ruby_hash(st_data_t key_thread, st_data_t value_context, st_data_t result_hash) {
1194
- VALUE thread = (VALUE) key_thread;
1195
- per_thread_context *thread_context = (per_thread_context*) value_context;
1196
- VALUE result = (VALUE) result_hash;
1374
+ static VALUE per_thread_context_to_ruby_hash(per_thread_context *thread_context) {
1197
1375
  VALUE context_as_hash = rb_hash_new();
1198
- rb_hash_aset(result, thread, context_as_hash);
1199
1376
 
1200
1377
  VALUE arguments[] = {
1201
1378
  ID2SYM(rb_intern("thread_id")), /* => */ rb_str_new2(thread_context->thread_id),
1202
1379
  ID2SYM(rb_intern("thread_invoke_location")), /* => */ rb_str_new2(thread_context->thread_invoke_location),
1203
1380
  ID2SYM(rb_intern("thread_cpu_time_id_valid?")), /* => */ thread_context->thread_cpu_time_id.valid ? Qtrue : Qfalse,
1204
- ID2SYM(rb_intern("thread_cpu_time_id")), /* => */ CLOCKID2NUM(thread_context->thread_cpu_time_id.clock_id),
1381
+ #ifdef __APPLE__
1382
+ ID2SYM(rb_intern("thread_cpu_time_id")), /* => */ ULL2NUM(thread_context->thread_cpu_time_id.clock_id),
1383
+ #else
1384
+ ID2SYM(rb_intern("thread_cpu_time_id")), /* => */ CLOCKID2NUM(thread_context->thread_cpu_time_id.clock_id),
1385
+ #endif
1205
1386
  ID2SYM(rb_intern("cpu_time_at_previous_sample_ns")), /* => */ LONG2NUM(thread_context->cpu_time_at_previous_sample_ns),
1206
1387
  ID2SYM(rb_intern("wall_time_at_previous_sample_ns")), /* => */ LONG2NUM(thread_context->wall_time_at_previous_sample_ns),
1207
1388
 
1208
1389
  ID2SYM(rb_intern("gc_tracking.cpu_time_at_start_ns")), /* => */ LONG2NUM(thread_context->gc_tracking.cpu_time_at_start_ns),
1209
1390
  ID2SYM(rb_intern("gc_tracking.wall_time_at_start_ns")), /* => */ LONG2NUM(thread_context->gc_tracking.wall_time_at_start_ns),
1210
1391
 
1211
- #ifndef NO_GVL_INSTRUMENTATION
1212
- ID2SYM(rb_intern("gvl_waiting_at")), /* => */ LONG2NUM(gvl_profiling_state_thread_object_get(thread)),
1213
- #endif
1392
+ ID2SYM(rb_intern("gvl_waiting_at")), /* => */ LONG2NUM(thread_context->gvl_waiting_at),
1393
+ ID2SYM(rb_intern("gvl_state_change_count")), /* => */ ULL2NUM(thread_context->gvl_state_change_count),
1394
+ ID2SYM(rb_intern("gvl_state_change_count_at_previous_sample")), /* => */ ULL2NUM(thread_context->gvl_state_change_count_at_previous_sample),
1395
+ ID2SYM(rb_intern("was_skipped_at_last_sample")), /* => */ thread_context->was_skipped_at_last_sample ? Qtrue : Qfalse,
1396
+ ID2SYM(rb_intern("is_profiler_internal_thread")), /* => */ thread_context->is_profiler_internal_thread ? Qtrue : Qfalse,
1214
1397
  };
1215
1398
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(context_as_hash, arguments[i], arguments[i+1]);
1216
1399
 
1217
- return ST_CONTINUE;
1400
+ return context_as_hash;
1218
1401
  }
1219
1402
 
1220
- static VALUE stats_as_ruby_hash(thread_context_collector_state *state) {
1403
+ static VALUE stats_to_ruby_hash(thread_context_collector_state *state, VALUE hash) {
1221
1404
  // Update this when modifying state struct (stats inner struct)
1222
- VALUE stats_as_hash = rb_hash_new();
1223
1405
  VALUE arguments[] = {
1406
+ ID2SYM(rb_intern("sample_count")), /* => */ UINT2NUM(state->stats.sample_count),
1224
1407
  ID2SYM(rb_intern("gc_samples")), /* => */ UINT2NUM(state->stats.gc_samples),
1225
1408
  ID2SYM(rb_intern("gc_samples_missed_due_to_missing_context")), /* => */ UINT2NUM(state->stats.gc_samples_missed_due_to_missing_context),
1409
+ ID2SYM(rb_intern("gc_samples_skipped_nothing_to_flush")), /* => */ UINT2NUM(state->stats.gc_samples_skipped_nothing_to_flush),
1410
+ ID2SYM(rb_intern("inactive_thread_samples_skipped")), /* => */ UINT2NUM(state->stats.inactive_thread_samples_skipped),
1411
+ ID2SYM(rb_intern("profiler_thread_samples_skipped")), /* => */ UINT2NUM(state->stats.profiler_thread_samples_skipped),
1226
1412
  };
1227
- for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(stats_as_hash, arguments[i], arguments[i+1]);
1228
- return stats_as_hash;
1413
+ for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(hash, arguments[i], arguments[i+1]);
1414
+ return hash;
1229
1415
  }
1230
1416
 
1231
1417
  static VALUE gc_tracking_as_ruby_hash(thread_context_collector_state *state) {
@@ -1241,70 +1427,84 @@ static VALUE gc_tracking_as_ruby_hash(thread_context_collector_state *state) {
1241
1427
  return result;
1242
1428
  }
1243
1429
 
1244
- static void remove_context_for_dead_threads(thread_context_collector_state *state) {
1245
- st_foreach(state->hash_map_per_thread_context, remove_if_dead_thread, 0 /* unused */);
1246
- }
1247
-
1248
- static int remove_if_dead_thread(st_data_t key_thread, st_data_t value_context, DDTRACE_UNUSED st_data_t _argument) {
1249
- VALUE thread = (VALUE) key_thread;
1250
- per_thread_context* thread_context = (per_thread_context*) value_context;
1251
-
1252
- if (is_thread_alive(thread)) return ST_CONTINUE;
1253
-
1254
- free_context(thread_context);
1255
- return ST_DELETE;
1256
- }
1257
-
1258
1430
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1259
1431
  // It SHOULD NOT be used for other purposes.
1260
1432
  //
1261
- // Returns the whole contents of the per_thread_context structs being tracked.
1433
+ // Returns the whole contents of the per_thread_context structs being tracked, by iterating all live threads.
1262
1434
  static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
1263
1435
  thread_context_collector_state *state;
1264
1436
  TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1265
1437
 
1266
- return per_thread_context_st_table_as_ruby_hash(state);
1438
+ VALUE result = rb_hash_new();
1439
+ VALUE threads = thread_list(state);
1440
+ const long thread_count = RARRAY_LEN(threads);
1441
+ for (long i = 0; i < thread_count; i++) {
1442
+ VALUE thread = RARRAY_AREF(threads, i);
1443
+ per_thread_context *thread_context = get_per_thread_context(thread);
1444
+ if (thread_context != NULL) {
1445
+ rb_hash_aset(result, thread, per_thread_context_to_ruby_hash(thread_context));
1446
+ }
1447
+ }
1448
+ return result;
1267
1449
  }
1268
1450
 
1269
- static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, long gc_start_time_ns, bool is_wall_time) {
1270
- // If we didn't have a time for the previous sample, we use the current one
1271
- if (*time_at_previous_sample_ns == INVALID_TIME) *time_at_previous_sample_ns = current_time_ns;
1272
-
1273
- bool is_thread_doing_gc = gc_start_time_ns != INVALID_TIME;
1274
- long elapsed_time_ns = -1;
1451
+ static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, per_thread_context *thread_context) {
1452
+ // We don't expect to be sampling a thread (and thus updating these counters) while Ruby is doing GC (between
1453
+ // `thread_context_collector_on_gc_start` and `thread_context_collector_on_gc_finish`)
1454
+ if (thread_context->gc_tracking.cpu_time_at_start_ns != INVALID_TIME) {
1455
+ raise_error(
1456
+ rb_eRuntimeError,
1457
+ "BUG: Unexpected sample during GC (thread_id=%s, gc_tracking.cpu_time_at_start_ns=%ld, "
1458
+ "gc_tracking.wall_time_at_start_ns=%ld, monotonic_wall_time_now_ns=%ld)",
1459
+ thread_context->thread_id,
1460
+ thread_context->gc_tracking.cpu_time_at_start_ns,
1461
+ thread_context->gc_tracking.wall_time_at_start_ns,
1462
+ monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
1463
+ );
1464
+ }
1275
1465
 
1276
- if (is_thread_doing_gc) {
1277
- bool previous_sample_was_during_gc = gc_start_time_ns <= *time_at_previous_sample_ns;
1466
+ long elapsed_time_ns = current_time_ns - *time_at_previous_sample_ns; // Capture all time since previous sample
1467
+ *time_at_previous_sample_ns = current_time_ns;
1278
1468
 
1279
- if (previous_sample_was_during_gc) {
1280
- elapsed_time_ns = 0; // No time to account for -- any time since the last sample is going to get assigned to GC separately
1281
- } else {
1282
- elapsed_time_ns = gc_start_time_ns - *time_at_previous_sample_ns; // Capture time between previous sample and start of GC only
1283
- }
1469
+ return elapsed_time_ns;
1470
+ }
1284
1471
 
1285
- // Remaining time (from gc_start_time to current_time_ns) will be accounted for inside `sample_after_gc`
1286
- *time_at_previous_sample_ns = gc_start_time_ns;
1287
- } else {
1288
- elapsed_time_ns = current_time_ns - *time_at_previous_sample_ns; // Capture all time since previous sample
1289
- *time_at_previous_sample_ns = current_time_ns;
1472
+ static long update_cpu_time_since_previous_sample(per_thread_context *thread_context, long current_cpu_time_ns) {
1473
+ // A previous `cpu_time_now_ns` may have failed to read the clock and thus invalidated our baseline; in that case we
1474
+ // use the current time, so this sample gets no cpu-time and the next one gets an accurate delta.
1475
+ // Note wall-time never needs this: it is always seeded with a real value in `initialize_context`.
1476
+ if (thread_context->cpu_time_at_previous_sample_ns == INVALID_TIME) {
1477
+ thread_context->cpu_time_at_previous_sample_ns = current_cpu_time_ns;
1290
1478
  }
1291
1479
 
1480
+ long elapsed_time_ns = update_time_since_previous_sample(
1481
+ &thread_context->cpu_time_at_previous_sample_ns,
1482
+ current_cpu_time_ns,
1483
+ thread_context
1484
+ );
1485
+
1486
+ // We don't expect cpu-time to go backwards, so let's flag this as a bug
1292
1487
  if (elapsed_time_ns < 0) {
1293
- if (is_wall_time) {
1294
- // Wall-time can actually go backwards (e.g. when the system clock gets set) so we can't assume time going backwards
1295
- // was a bug.
1296
- // @ivoanjo: I've also observed time going backwards spuriously on macOS, see discussion on
1297
- // https://github.com/DataDog/dd-trace-rb/pull/2336.
1298
- elapsed_time_ns = 0;
1299
- } else {
1300
- // We don't expect non-wall time to go backwards, so let's flag this as a bug
1301
- raise_error(rb_eRuntimeError, "BUG: Unexpected negative elapsed_time_ns between samples");
1302
- }
1488
+ raise_error(rb_eRuntimeError, "BUG: Unexpected CPU time going backwards between samples");
1303
1489
  }
1304
1490
 
1305
1491
  return elapsed_time_ns;
1306
1492
  }
1307
1493
 
1494
+ static long update_wall_time_since_previous_sample(per_thread_context *thread_context, long current_wall_time_ns) {
1495
+ long elapsed_time_ns = update_time_since_previous_sample(
1496
+ &thread_context->wall_time_at_previous_sample_ns,
1497
+ current_wall_time_ns,
1498
+ thread_context
1499
+ );
1500
+
1501
+ // Wall-time can actually go backwards (e.g. when the system clock gets set) so we can't assume time going backwards
1502
+ // was a bug.
1503
+ // @ivoanjo: I've also observed time going backwards spuriously on macOS, see discussion on
1504
+ // https://github.com/DataDog/dd-trace-rb/pull/2336.
1505
+ return long_max_of(elapsed_time_ns, 0);
1506
+ }
1507
+
1308
1508
  // Safety: This function is assumed never to raise exceptions by callers
1309
1509
  static long cpu_time_now_ns(per_thread_context *thread_context) {
1310
1510
  thread_cpu_time cpu_time = thread_cpu_time_for(thread_context->thread_cpu_time_id);
@@ -1341,14 +1541,14 @@ VALUE enforce_thread_context_collector_instance(VALUE object) {
1341
1541
  return object;
1342
1542
  }
1343
1543
 
1344
- // Finalize any pending heap allocation recordings.
1345
- // On Ruby 4+, heap allocations are recorded in two phases: during on_newobj_event we capture
1346
- // the object reference, then later we safely call rb_obj_id() to get the object ID.
1347
- void thread_context_collector_after_allocation(VALUE self_instance) {
1544
+ // Commit any pending heap allocation recordings.
1545
+ // Recording an allocation only puts it on a pending list, because taking the weak reference the heap profiler needs
1546
+ // can't be done from inside the NEWOBJ tracepoint -- see `heap_recorder_commit_recordings_may_lose_gvl`.
1547
+ void thread_context_collector_commit_heap_recordings_may_lose_gvl(VALUE self_instance) {
1348
1548
  thread_context_collector_state *state;
1349
1549
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1350
1550
 
1351
- recorder_after_sample(state->recorder_instance);
1551
+ recorder_commit_heap_recordings_may_lose_gvl(state->recorder_instance);
1352
1552
  }
1353
1553
 
1354
1554
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
@@ -1357,7 +1557,7 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE _self, VALUE collector_instance)
1357
1557
  thread_context_collector_state *state;
1358
1558
  TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1359
1559
 
1360
- return stats_as_ruby_hash(state);
1560
+ return stats_to_ruby_hash(state, rb_hash_new());
1361
1561
  }
1362
1562
 
1363
1563
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
@@ -1373,8 +1573,7 @@ static VALUE _native_gc_tracking(DDTRACE_UNUSED VALUE _self, VALUE collector_ins
1373
1573
  static void trace_identifiers_for(
1374
1574
  thread_context_collector_state *state,
1375
1575
  VALUE thread,
1376
- trace_identifiers *trace_identifiers_result,
1377
- bool is_safe_to_allocate_objects
1576
+ trace_identifiers *trace_identifiers_result
1378
1577
  ) {
1379
1578
  if (state->otel_context_enabled == OTEL_CONTEXT_ENABLED_ONLY) return;
1380
1579
  if (state->tracer_context_key == MISSING_TRACER_CONTEXT_KEY) return;
@@ -1395,7 +1594,7 @@ static void trace_identifiers_for(
1395
1594
  VALUE numeric_span_id = Qnil;
1396
1595
 
1397
1596
  if (otel_values != Qnil) {
1398
- ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values, is_safe_to_allocate_objects);
1597
+ ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values);
1399
1598
  }
1400
1599
 
1401
1600
  if (root_span == Qnil || (active_span == Qnil && numeric_span_id == Qnil)) return;
@@ -1452,17 +1651,18 @@ static bool should_collect_resource(VALUE root_span) {
1452
1651
  //
1453
1652
  // Assumption: This method gets called BEFORE restarting profiling -- e.g. there are no components attempting to
1454
1653
  // trigger samples at the same time.
1654
+ //
1655
+ // Note that tests call this method directly in the same process without forking,
1656
+ // and in such a case non-current Threads keep running.
1455
1657
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
1456
1658
  thread_context_collector_state *state;
1457
1659
  TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1458
1660
 
1459
- // Release all context memory before clearing the existing context
1460
- st_foreach(state->hash_map_per_thread_context, hash_map_per_thread_context_free_values, 0 /* unused */);
1461
-
1462
- st_clear(state->hash_map_per_thread_context);
1463
-
1464
1661
  state->stats = (struct stats) {}; // Resets all stats back to zero
1465
1662
 
1663
+ // No need to clean-up the per-thread context because the CpuAndWallTimeWorker always cleans
1664
+ // it up unconditionally on every start/restart and that includes after a fork.
1665
+
1466
1666
  rb_funcall(state->recorder_instance, rb_intern("reset_after_fork"), 0);
1467
1667
 
1468
1668
  return Qtrue;
@@ -1482,30 +1682,25 @@ static VALUE thread_list(thread_context_collector_state *state) {
1482
1682
  // expected to be called from a signal handler and to be async-signal-safe.
1483
1683
  //
1484
1684
  // Also, no allocation (Ruby or malloc) can happen.
1485
- bool thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_instance) {
1486
- thread_context_collector_state *state;
1487
- if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
1488
- // This should never fail if the above check passes
1489
- TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1490
-
1685
+ bool thread_context_collector_prepare_sample_inside_signal_handler(void) {
1491
1686
  VALUE current_thread = rb_thread_current();
1492
- per_thread_context *thread_context = get_context_for(current_thread, state);
1687
+ per_thread_context *thread_context = get_per_thread_context(current_thread);
1493
1688
  if (thread_context == NULL) return false;
1494
1689
 
1495
1690
  return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1496
1691
  }
1497
1692
 
1498
- // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should never allocate in the
1499
- // Ruby heap.
1693
+ // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
1694
+ // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
1695
+ //
1696
+ // Returns true if `thread_context_collector_commit_heap_recordings_may_lose_gvl` needs to be called (to do work
1697
+ // that can't be done from inside the tracepoint, such as allocate new objects), and false if it doesn't
1500
1698
  //
1501
- // Returns true if the after_allocation needs to be called (to do work that can't be done from inside the
1502
- // tracepoint, such as allocate new objects), and false if it doesn't
1503
- bool thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object) {
1699
+ // The callers must ensure thread_context is non-NULL.
1700
+ bool thread_context_collector_sample_allocation(VALUE self_instance, per_thread_context *thread_context, unsigned int sample_weight, VALUE new_object) {
1504
1701
  thread_context_collector_state *state;
1505
1702
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1506
1703
 
1507
- VALUE current_thread = rb_thread_current();
1508
-
1509
1704
  enum ruby_value_type type = rb_type(new_object);
1510
1705
 
1511
1706
  // Tag samples with the VM internal types
@@ -1544,13 +1739,10 @@ bool thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1544
1739
  // Thus, we need to make sure there's actually a class before getting its name.
1545
1740
 
1546
1741
  if (klass != 0) {
1547
- const char *name = rb_class2name(klass);
1548
- size_t name_length = name != NULL ? strlen(name) : 0;
1549
-
1550
- if (name_length > 0) {
1551
- class_name = (ddog_CharSlice) {.ptr = name, .len = name_length};
1742
+ VALUE name = ddtrace_alloc_free_rb_mod_name(klass);
1743
+ if (name != Qnil) {
1744
+ class_name = char_slice_from_ruby_string(name);
1552
1745
  } else {
1553
- // @ivoanjo: I'm not sure this can ever happen, but just-in-case
1554
1746
  class_name = ruby_value_type_to_class_name(type);
1555
1747
  }
1556
1748
  } else {
@@ -1570,42 +1762,47 @@ bool thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1570
1762
  class_name = ruby_vm_type; // For other weird internal things we just use the VM type
1571
1763
  }
1572
1764
 
1573
- bool needs_after_allocation = track_object(state->recorder_instance, new_object, sample_weight, class_name);
1765
+ VALUE current_thread = rb_thread_current();
1574
1766
 
1575
- per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1767
+ heap_sample_values heap_sample = {.new_object = new_object, .alloc_class = class_name};
1576
1768
 
1577
1769
  trigger_sample_for_thread(
1578
1770
  state,
1579
- /* thread: */ current_thread,
1580
- /* stack_from_thread: */ current_thread,
1771
+ current_thread,
1581
1772
  thread_context,
1582
- &thread_context->sampling_buffer,
1583
- (sample_values) {.alloc_samples = sample_weight, .alloc_samples_unscaled = 1, .heap_sample = true},
1773
+ (sample_values) {.alloc_samples = sample_weight, .alloc_samples_unscaled = 1, .heap_sample = &heap_sample},
1584
1774
  INVALID_TIME, // For now we're not collecting timestamps for allocation events, as per profiling team internal discussions
1585
1775
  &ruby_vm_type,
1586
- &class_name,
1587
- /* is_gvl_waiting_state: */ false,
1588
- /* is_safe_to_allocate_objects: */ false // Not safe to allocate further inside the NEWOBJ tracepoint
1776
+ &class_name,
1777
+ /* is_gvl_waiting_state: */ false
1589
1778
  );
1590
1779
 
1591
- return needs_after_allocation;
1780
+ return heap_sample.out_needs_commit;
1592
1781
  }
1593
1782
 
1594
1783
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1595
1784
  // It SHOULD NOT be used for other purposes.
1596
1785
  static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object) {
1786
+ per_thread_context *thread_context = get_per_thread_context(rb_thread_current());
1787
+ if (thread_context == NULL) {
1788
+ rb_raise(rb_eRuntimeError, "Missing per_thread_context for current thread in _native_sample_allocation");
1789
+ }
1790
+
1597
1791
  debug_enter_unsafe_context();
1598
1792
 
1599
- bool needs_after_allocation = thread_context_collector_sample_allocation(collector_instance, NUM2UINT(sample_weight), new_object);
1793
+ bool needs_commit = thread_context_collector_sample_allocation(collector_instance, thread_context, NUM2UINT(sample_weight), new_object);
1600
1794
 
1601
1795
  debug_leave_unsafe_context();
1602
1796
 
1603
1797
  // We could instead choose to automatically trigger the after allocation here; yet, it seems kinda nice to keep it manual for
1604
1798
  // the tests so we can pull on each lever separately and observe "the sausage being made" in steps
1605
- return needs_after_allocation ? Qtrue : Qfalse;
1799
+ return needs_commit ? Qtrue : Qfalse;
1606
1800
  }
1607
1801
 
1608
- static VALUE new_empty_thread_inner(DDTRACE_UNUSED void *arg) { return Qnil; }
1802
+ static VALUE new_empty_thread_inner(DDTRACE_UNUSED void *arg) {
1803
+ rb_thread_sleep(INT_MAX);
1804
+ return Qnil;
1805
+ }
1609
1806
 
1610
1807
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1611
1808
  // It SHOULD NOT be used for other purposes.
@@ -1649,26 +1846,57 @@ static VALUE read_otel_current_span_key_const(DDTRACE_UNUSED VALUE _unused) {
1649
1846
  return rb_const_get(trace_module, rb_intern("CURRENT_SPAN_KEY"));
1650
1847
  }
1651
1848
 
1652
- static VALUE get_otel_current_span_key(thread_context_collector_state *state, bool is_safe_to_allocate_objects) {
1653
- if (state->otel_current_span_key == Qtrue) { // Qtrue means we haven't tried to extract it yet
1654
- if (!is_safe_to_allocate_objects) {
1655
- // Calling read_otel_current_span_key_const below can trigger exceptions and arbitrary Ruby code running (e.g.
1656
- // `const_missing`, etc). Not safe to call in this situation, so we just skip otel info for this sample.
1657
- return Qnil;
1658
- }
1659
-
1660
- // If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
1661
- // gets refactored, we should not fall on our face)
1662
- VALUE span_key = rb_protect(read_otel_current_span_key_const, Qnil, NULL);
1663
- rb_set_errinfo(Qnil); // **Clear any pending exception after ignoring it**
1849
+ static VALUE otel_current_span_key_not_found(DDTRACE_UNUSED VALUE _unused, DDTRACE_UNUSED VALUE _exception) {
1850
+ return Qnil;
1851
+ }
1664
1852
 
1665
- // Note that this gets set to Qnil if we failed to extract the correct value, and thus we won't try to extract it again
1666
- state->otel_current_span_key = span_key;
1853
+ static VALUE get_otel_current_span_key(thread_context_collector_state *state) {
1854
+ if (state->otel_current_span_key == OTEL_CURRENT_SPAN_KEY_NOT_EXTRACTED) {
1855
+ // Extracting this needs calling into Ruby code, which we don't want to do in the middle of a sample.
1856
+ // So instead we signal that we want this, and let it be resolved separately.
1857
+ state->otel_current_span_key = OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED;
1858
+ return Qnil;
1667
1859
  }
1668
1860
 
1861
+ // We asked for it, but it hasn't been extracted yet
1862
+ if (is_otel_current_span_key_needed(state)) return Qnil;
1863
+
1669
1864
  return state->otel_current_span_key;
1670
1865
  }
1671
1866
 
1867
+ static bool is_otel_current_span_key_needed(thread_context_collector_state *state) {
1868
+ return state->otel_current_span_key == OTEL_CURRENT_SPAN_KEY_EXTRACTION_NEEDED;
1869
+ }
1870
+
1871
+ // Extracts `otel_current_span_key`, if a sample asked for it (see `get_otel_current_span_key`).
1872
+ // This is deliberately not done during sampling because we can lose the GVL.
1873
+ //
1874
+ // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
1875
+ void thread_context_collector_resolve_otel_span_key_may_lose_gvl(VALUE self_instance) {
1876
+ thread_context_collector_state *state;
1877
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1878
+
1879
+ if (!is_otel_current_span_key_needed(state)) return;
1880
+
1881
+ // If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
1882
+ // gets refactored, we should not fall on our face).
1883
+ //
1884
+ // Note that we use `rb_rescue2` and not `rb_protect` so that we only swallow exceptions: anything else that can
1885
+ // unwind through here (e.g. a `Thread#kill`) is not ours to swallow. `rb_rescue2` also takes care of restoring
1886
+ // any previously-pending exception for us.
1887
+ VALUE span_key = rb_rescue2(
1888
+ read_otel_current_span_key_const,
1889
+ Qnil,
1890
+ otel_current_span_key_not_found,
1891
+ Qnil,
1892
+ rb_eException, // rb_eException is the base class of all Ruby exceptions
1893
+ 0 // Required by API to be the last argument
1894
+ );
1895
+
1896
+ // Note that this gets set to Qnil if we failed to extract the correct value, and thus we won't try to extract it again
1897
+ state->otel_current_span_key = span_key;
1898
+ }
1899
+
1672
1900
  // This method gets used when ddtrace is being used indirectly via the opentelemetry APIs. Information gets stored slightly
1673
1901
  // differently, and this codepath handles it.
1674
1902
  static void ddtrace_otel_trace_identifiers_for(
@@ -1677,8 +1905,7 @@ static void ddtrace_otel_trace_identifiers_for(
1677
1905
  VALUE *root_span,
1678
1906
  VALUE *numeric_span_id,
1679
1907
  VALUE active_span,
1680
- VALUE otel_values,
1681
- bool is_safe_to_allocate_objects
1908
+ VALUE otel_values
1682
1909
  ) {
1683
1910
  VALUE resolved_numeric_span_id =
1684
1911
  active_span == Qnil ?
@@ -1689,7 +1916,7 @@ static void ddtrace_otel_trace_identifiers_for(
1689
1916
 
1690
1917
  if (resolved_numeric_span_id == Qnil) return;
1691
1918
 
1692
- VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
1919
+ VALUE otel_current_span_key = get_otel_current_span_key(state);
1693
1920
  if (otel_current_span_key == Qnil) return;
1694
1921
  VALUE current_trace = *active_trace;
1695
1922
 
@@ -1809,15 +2036,14 @@ static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self
1809
2036
  static void otel_without_ddtrace_trace_identifiers_for(
1810
2037
  thread_context_collector_state *state,
1811
2038
  VALUE thread,
1812
- trace_identifiers *trace_identifiers_result,
1813
- bool is_safe_to_allocate_objects
2039
+ trace_identifiers *trace_identifiers_result
1814
2040
  ) {
1815
2041
  VALUE context_storage = otel_context_storage_for(state, thread);
1816
2042
 
1817
2043
  // If it exists, context_storage is expected to be an Array[OpenTelemetry::Context]
1818
2044
  if (context_storage == Qnil || !RB_TYPE_P(context_storage, T_ARRAY)) return;
1819
2045
 
1820
- VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
2046
+ VALUE otel_current_span_key = get_otel_current_span_key(state);
1821
2047
  if (otel_current_span_key == Qnil) return;
1822
2048
 
1823
2049
  long active_context_index = RARRAY_LEN(context_storage) - 1;
@@ -1898,36 +2124,143 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1898
2124
  ((uint64_t)span_bytes[7]);
1899
2125
  }
1900
2126
 
1901
- #ifndef NO_GVL_INSTRUMENTATION
1902
- // This function can get called from outside the GVL and even on non-main Ractors
1903
- void thread_context_collector_on_gvl_waiting(gvl_profiling_thread thread) {
1904
- // Because this function gets called from a thread that is NOT holding the GVL, we avoid touching the
1905
- // per-thread context directly.
1906
- //
1907
- // Instead, we ask Ruby to hold the data we need in Ruby's own special per-thread context area
1908
- // that's thread-safe and built for this kind of use
1909
- //
1910
- // Also, this function can get called on the non-main Ractor. We deal with this by checking if the value in the context
1911
- // is non-zero, since only `initialize_context` ever sets the value from 0 to non-zero for threads it sees.
1912
- intptr_t thread_being_profiled = gvl_profiling_state_get(thread);
1913
- if (!thread_being_profiled) return;
2127
+ void thread_context_collector_stats(VALUE self_instance, VALUE stats_hash) {
2128
+ thread_context_collector_state *state;
2129
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2130
+ stats_to_ruby_hash(state, stats_hash);
2131
+ }
2132
+
2133
+ void thread_context_collector_stats_reset_not_thread_safe(VALUE self_instance) {
2134
+ thread_context_collector_state *state;
2135
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2136
+ state->stats = (struct stats) {};
2137
+ }
2138
+
2139
+ static void mark_thread_as_profiler_internal(per_thread_context *ctx) {
2140
+ ctx->is_profiler_internal_thread = true;
2141
+ }
2142
+
2143
+ void thread_context_collector_profiler_internal_thread_started(void) {
2144
+ per_thread_context *ctx = get_or_create_context_for(rb_thread_current());
2145
+ mark_thread_as_profiler_internal(ctx);
2146
+ }
2147
+
2148
+ static VALUE _native_mark_thread_as_profiler_internal(DDTRACE_UNUSED VALUE self, VALUE thread) {
2149
+ per_thread_context *ctx = get_or_create_context_for(thread);
2150
+ mark_thread_as_profiler_internal(ctx);
2151
+ return Qnil;
2152
+ }
1914
2153
 
1915
- long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
1916
- if (current_monotonic_wall_time_ns <= 0 || current_monotonic_wall_time_ns > GVL_WAITING_ENABLED_EMPTY) return;
2154
+ // Called via rb_ensure when a profiler-internal thread (worker or idle helper) is about to exit.
2155
+ // Records a final sample so the thread's accumulated cpu/wall time since the last on_serialize
2156
+ // flush is not lost. on_serialize (below) also flushes profiler-internal threads during periodic
2157
+ // serialization, but it can't help at shutdown: by the time the final serialize runs, these
2158
+ // threads are already dead and absent from thread_list.
2159
+ void thread_context_collector_profiler_internal_thread_done(VALUE self_instance) {
2160
+ thread_context_collector_state *state;
2161
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1917
2162
 
1918
- gvl_profiling_state_set(thread, current_monotonic_wall_time_ns);
2163
+ VALUE current_thread = rb_thread_current();
2164
+ per_thread_context *thread_context = get_or_create_context_for(current_thread);
2165
+ if (!thread_context->is_profiler_internal_thread) {
2166
+ rb_raise(rb_eRuntimeError, "current thread %"PRIsVALUE" is not profiler-internal thread", current_thread);
1919
2167
  }
1920
2168
 
1921
- // This function can get called from outside the GVL and even on non-main Ractors
1922
- __attribute__((warn_unused_result))
1923
- on_gvl_running_result thread_context_collector_on_gvl_running_with_threshold(gvl_profiling_thread thread, uint32_t waiting_for_gvl_threshold_ns) {
1924
- intptr_t gvl_waiting_at = gvl_profiling_state_get(thread);
2169
+ long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2170
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2171
+
2172
+ update_metrics_and_sample(
2173
+ state,
2174
+ current_thread,
2175
+ thread_context,
2176
+ current_cpu_time_ns,
2177
+ current_monotonic_wall_time_ns,
2178
+ true);
2179
+ }
2180
+
2181
+ // Flushes threads whose last per-tick sample was skipped (either by the SUSPENDED-skip
2182
+ // optimization, or by is_profiler_internal_thread) so their accumulated time is recorded.
2183
+ // Called by the stack recorder at the start of _native_serialize (regular periodic flush).
2184
+ void thread_context_collector_on_serialize(VALUE self_instance) {
2185
+ thread_context_collector_state *state;
2186
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2187
+
2188
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(RAISE_ON_FAILURE);
2189
+ VALUE threads = thread_list(state);
2190
+ const long thread_count = RARRAY_LEN(threads);
2191
+
2192
+ for (long i = 0; i < thread_count; i++) {
2193
+ VALUE thread = RARRAY_AREF(threads, i);
2194
+ per_thread_context *thread_context = get_per_thread_context(thread);
2195
+
2196
+ if (thread_context != NULL && (thread_context->was_skipped_at_last_sample || thread_context->is_profiler_internal_thread)) {
2197
+ long current_cpu_time_ns = cpu_time_now_ns(thread_context);
2198
+ // We need to force_sample=true otherwise this sample would be skipped too
2199
+ update_metrics_and_sample(
2200
+ state,
2201
+ thread,
2202
+ thread_context,
2203
+ current_cpu_time_ns,
2204
+ current_monotonic_wall_time_ns,
2205
+ true);
2206
+ }
2207
+ }
2208
+ }
2209
+
2210
+ void thread_context_collector_on_gvl_released(per_thread_context *thread_context) {
2211
+ thread_context->gvl_state_change_count |= GVL_SUSPENDED;
2212
+ }
2213
+
2214
+ void thread_context_collector_on_gvl_waiting(per_thread_context *thread_context) {
2215
+ long current_monotonic_wall_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
2216
+ if (current_monotonic_wall_time_ns <= 0) return;
2217
+
2218
+ thread_context->gvl_waiting_at = current_monotonic_wall_time_ns;
2219
+ }
1925
2220
 
1926
- // Thread was not being profiled / not waiting on gvl
1927
- if (gvl_waiting_at == 0 || gvl_waiting_at == GVL_WAITING_ENABLED_EMPTY) return ON_GVL_RUNNING_UNKNOWN;
2221
+ static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
2222
+ ENFORCE_THREAD(thread);
2223
+
2224
+ debug_enter_unsafe_context();
2225
+
2226
+ per_thread_context *thread_context = get_per_thread_context(thread);
2227
+ if (thread_context) thread_context_collector_on_gvl_waiting(thread_context);
2228
+
2229
+ debug_leave_unsafe_context();
1928
2230
 
2231
+ return Qnil;
2232
+ }
2233
+
2234
+ static VALUE _native_on_gvl_released(DDTRACE_UNUSED VALUE self, VALUE thread) {
2235
+ ENFORCE_THREAD(thread);
2236
+
2237
+ debug_enter_unsafe_context();
2238
+
2239
+ per_thread_context *thread_context = get_per_thread_context(thread);
2240
+ if (thread_context) thread_context_collector_on_gvl_released(thread_context);
2241
+
2242
+ debug_leave_unsafe_context();
2243
+
2244
+ return Qnil;
2245
+ }
2246
+
2247
+ #ifndef NO_GVL_INSTRUMENTATION
2248
+ // We must only use async-signal-safe functions here, see notes in the caller
2249
+ __attribute__((warn_unused_result))
2250
+ on_gvl_running_result thread_context_collector_on_gvl_running(VALUE thread, per_thread_context *thread_context, uint32_t waiting_for_gvl_threshold_ns) {
2251
+ // Bump the event counter and clears the state bit to "running"
2252
+ uint64_t counter_portion = thread_context->gvl_state_change_count >> 1;
2253
+ thread_context->gvl_state_change_count = ((counter_portion + 1) << 1) | GVL_RUNNING;
2254
+
2255
+ long gvl_waiting_at = thread_context->gvl_waiting_at;
2256
+ // Thread was not waiting on gvl
2257
+ if (gvl_waiting_at == 0) {
2258
+ return (on_gvl_running_result) {.action = ON_GVL_RUNNING_UNKNOWN, .waiting_for_gvl_duration_ns = 0};
2259
+ }
1929
2260
  // @ivoanjo: I'm not sure if this can happen -- It means we should've sampled already but haven't gotten the chance yet?
1930
- if (gvl_waiting_at < 0) return ON_GVL_RUNNING_SAMPLE;
2261
+ if (gvl_waiting_at < 0) {
2262
+ return (on_gvl_running_result) {.action = ON_GVL_RUNNING_SAMPLE, .waiting_for_gvl_duration_ns = 0};
2263
+ }
1931
2264
 
1932
2265
  long waiting_for_gvl_duration_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - gvl_waiting_at;
1933
2266
 
@@ -1935,20 +2268,32 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1935
2268
 
1936
2269
  if (should_sample) {
1937
2270
  // We flip the gvl_waiting_at to negative to mark that the thread is now running and no longer waiting
1938
- intptr_t gvl_waiting_at_is_now_running = -gvl_waiting_at;
2271
+ long gvl_waiting_at_is_now_running = -gvl_waiting_at;
1939
2272
 
1940
- gvl_profiling_state_set(thread, gvl_waiting_at_is_now_running);
2273
+ thread_context->gvl_waiting_at = gvl_waiting_at_is_now_running;
1941
2274
  } else {
1942
- // We decided not to sample. Let's mark the thread back to the initial "enabled but empty" state
1943
- gvl_profiling_state_set(thread, GVL_WAITING_ENABLED_EMPTY);
2275
+ thread_context->gvl_waiting_at = 0;
2276
+
2277
+ // Even though the GVL wait itself was below threshold, if the thread had skipped samples
2278
+ // (was suspended for a long time without the GVL), we still need to force a sample now.
2279
+ // Otherwise, the accumulated idle wall-time would be reported against whatever stack the
2280
+ // thread runs next, misrepresenting the time spent idle.
2281
+ if (thread_context->was_skipped_at_last_sample) {
2282
+ should_sample = true;
2283
+ }
1944
2284
  }
1945
2285
 
1946
- return should_sample ? ON_GVL_RUNNING_SAMPLE : ON_GVL_RUNNING_DONT_SAMPLE;
1947
- }
2286
+ if (should_sample) {
2287
+ // We prepare the sample here because the postponed job might be called some time later,
2288
+ // possibly after some Ruby calls which change the Ruby stack,
2289
+ // and we want to attribute the time acquiring or without the GVL to the correct Ruby stack.
2290
+ prepare_sample_thread(thread, &thread_context->sampling_buffer);
2291
+ }
1948
2292
 
1949
- __attribute__((warn_unused_result))
1950
- on_gvl_running_result thread_context_collector_on_gvl_running(gvl_profiling_thread thread) {
1951
- return thread_context_collector_on_gvl_running_with_threshold(thread, global_waiting_for_gvl_threshold_ns);
2293
+ return (on_gvl_running_result) {
2294
+ .action = should_sample ? ON_GVL_RUNNING_SAMPLE : ON_GVL_RUNNING_DONT_SAMPLE,
2295
+ .waiting_for_gvl_duration_ns = waiting_for_gvl_duration_ns,
2296
+ };
1952
2297
  }
1953
2298
 
1954
2299
  // Why does this method need to exist?
@@ -1968,7 +2313,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1968
2313
  //
1969
2314
  // Stack:
1970
2315
  // If the thread starts working without the end of the Waiting for GVL sample, then by the time the thread is sampled
1971
- // via the regular cpu/wall-time samples mechanism, the stack can be be inaccurate (e.g. does not correctly pinpoint
2316
+ // via the regular cpu/wall-time samples mechanism, the stack can be inaccurate (e.g. does not correctly pinpoint
1972
2317
  // where the waiting happened).
1973
2318
  //
1974
2319
  // Arguably, the last sample after Waiting for GVL ended (when gvl_waiting_at < 0) should always come from this method
@@ -1977,17 +2322,19 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1977
2322
  //
1978
2323
  // ---
1979
2324
  //
2325
+ // Always called with the GVL, either from a postponed_job or from tests.
2326
+ //
1980
2327
  // NOTE: In normal use, current_thread is expected to be == rb_thread_current(); the `current_thread` parameter only
1981
2328
  // exists to enable testing.
1982
2329
  VALUE thread_context_collector_sample_after_gvl_running(VALUE self_instance, VALUE current_thread, long current_monotonic_wall_time_ns) {
1983
2330
  thread_context_collector_state *state;
1984
2331
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1985
2332
 
1986
- if (!state->timeline_enabled) raise_error(rb_eRuntimeError, "GVL profiling requires timeline to be enabled");
2333
+ per_thread_context *thread_context = get_or_create_context_for(current_thread);
1987
2334
 
1988
- intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(current_thread);
2335
+ long gvl_waiting_at = thread_context->gvl_waiting_at;
1989
2336
 
1990
- if (gvl_waiting_at >= 0) {
2337
+ if (gvl_waiting_at >= 0 && !thread_context->was_skipped_at_last_sample) {
1991
2338
  // @ivoanjo: I'm not sure if this can ever happen. This means that we're not on the same thread
1992
2339
  // that ran `thread_context_collector_on_gvl_running` and made the decision to sample OR a regular sample was
1993
2340
  // triggered ahead of us.
@@ -1995,9 +2342,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1995
2342
  return Qfalse;
1996
2343
  }
1997
2344
 
1998
- per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1999
-
2000
- // We don't actually account for cpu-time during Waiting for GVL. BUT, we may chose to push an
2345
+ // We don't actually account for cpu-time during Waiting for GVL. BUT, we may choose to push an
2001
2346
  // extra sample to represent the period prior to Waiting for GVL. To support that, we retrieve the current
2002
2347
  // cpu-time of the thread and let `update_metrics_and_sample` decide what to do with it.
2003
2348
  long cpu_time_for_thread = cpu_time_now_ns(thread_context);
@@ -2006,13 +2351,11 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2006
2351
 
2007
2352
  update_metrics_and_sample(
2008
2353
  state,
2009
- /* thread_being_sampled: */ current_thread,
2010
- /* stack_from_thread: */ current_thread,
2354
+ current_thread,
2011
2355
  thread_context,
2012
- &thread_context->sampling_buffer,
2013
2356
  cpu_time_for_thread,
2014
- current_monotonic_wall_time_ns
2015
- );
2357
+ current_monotonic_wall_time_ns,
2358
+ false);
2016
2359
 
2017
2360
  return Qtrue;
2018
2361
  }
@@ -2023,14 +2366,12 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2023
2366
  static bool handle_gvl_waiting(
2024
2367
  thread_context_collector_state *state,
2025
2368
  VALUE thread_being_sampled,
2026
- VALUE stack_from_thread,
2027
2369
  per_thread_context *thread_context,
2028
- sampling_buffer* sampling_buffer,
2029
2370
  long current_cpu_time_ns
2030
2371
  ) {
2031
- intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(thread_being_sampled);
2372
+ long gvl_waiting_at = thread_context->gvl_waiting_at;
2032
2373
 
2033
- bool is_gvl_waiting_state = gvl_waiting_at != 0 && gvl_waiting_at != GVL_WAITING_ENABLED_EMPTY;
2374
+ bool is_gvl_waiting_state = gvl_waiting_at != 0;
2034
2375
 
2035
2376
  if (!is_gvl_waiting_state) return false;
2036
2377
 
@@ -2043,17 +2384,17 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2043
2384
  // ...──────────────┬───────────────────...
2044
2385
  // Other state │ Waiting for GVL
2045
2386
  // ...──────────────┴───────────────────...
2046
- // ▲ ▲
2387
+ // ▲ ▲
2047
2388
  // └─ Previous sample └─ Regular sample (caller)
2048
2389
  //
2049
2390
  // In this case, we'll want to push two samples: a) one for the current time (handled by the caller), b) an extra sample
2050
- // to represent the remaining cpu/wall time before the "Waiting for GVL" started:
2391
+ // to represent the remaining cpu/wall time before the "Waiting for GVL" started (for timeline purposes):
2051
2392
  //
2052
2393
  // time ─────►
2053
2394
  // ...──────────────┬───────────────────...
2054
2395
  // Other state │ Waiting for GVL
2055
2396
  // ...──────────────┴───────────────────...
2056
- // ▲ ▲ ▲
2397
+ // ▲ ▲ ▲
2057
2398
  // └─ Prev... └─ Extra sample └─ Regular sample (caller)
2058
2399
  //
2059
2400
  // 2. The current sample is the n-th one after we entered the "Waiting for GVL" state
@@ -2063,7 +2404,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2063
2404
  // ...──────────────┬───────────────────────────────────────────────...
2064
2405
  // Other state │ Waiting for GVL
2065
2406
  // ...──────────────┴───────────────────────────────────────────────...
2066
- // ▲ ▲ ▲
2407
+ // ▲ ▲ ▲
2067
2408
  // └─ Previous sample └─ Previous sample └─ Regular sample (caller)
2068
2409
  //
2069
2410
  // In this case, we just report back to the caller that the thread is in the "Waiting for GVL" state.
@@ -2078,88 +2419,71 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2078
2419
 
2079
2420
  if (gvl_waiting_at < 0) {
2080
2421
  // Negative means the waiting for GVL just ended, so we clear the state, so next samples no longer represent waiting
2081
- gvl_profiling_state_thread_object_set(thread_being_sampled, GVL_WAITING_ENABLED_EMPTY);
2422
+ thread_context->gvl_waiting_at = 0;
2082
2423
  }
2083
2424
 
2084
2425
  long gvl_waiting_started_wall_time_ns = labs(gvl_waiting_at);
2085
2426
 
2086
2427
  if (thread_context->wall_time_at_previous_sample_ns < gvl_waiting_started_wall_time_ns) { // situation 1 above
2087
- long cpu_time_elapsed_ns = update_time_since_previous_sample(
2088
- &thread_context->cpu_time_at_previous_sample_ns,
2089
- current_cpu_time_ns,
2090
- thread_context->gc_tracking.cpu_time_at_start_ns,
2091
- IS_NOT_WALL_TIME
2092
- );
2428
+ long cpu_time_elapsed_ns = update_cpu_time_since_previous_sample(thread_context, current_cpu_time_ns);
2093
2429
 
2094
- long duration_until_start_of_gvl_waiting_ns = update_time_since_previous_sample(
2095
- &thread_context->wall_time_at_previous_sample_ns,
2096
- gvl_waiting_started_wall_time_ns,
2097
- INVALID_TIME,
2098
- IS_WALL_TIME
2099
- );
2430
+ long duration_until_start_of_gvl_waiting_ns =
2431
+ update_wall_time_since_previous_sample(thread_context, gvl_waiting_started_wall_time_ns);
2100
2432
 
2101
2433
  // Push extra sample
2102
2434
  trigger_sample_for_thread(
2103
2435
  state,
2104
2436
  thread_being_sampled,
2105
- stack_from_thread,
2106
2437
  thread_context,
2107
- sampling_buffer,
2108
2438
  (sample_values) {.cpu_time_ns = cpu_time_elapsed_ns, .cpu_or_wall_samples = 1, .wall_time_ns = duration_until_start_of_gvl_waiting_ns},
2109
2439
  gvl_waiting_started_wall_time_ns,
2110
2440
  NULL,
2111
2441
  NULL,
2112
- /* is_gvl_waiting_state: */ false, // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
2113
- /* is_safe_to_allocate_objects: */ true // This is similar to a regular cpu/wall sample, so it's also safe
2442
+ /* is_gvl_waiting_state: */ false // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
2114
2443
  );
2115
2444
  }
2116
2445
 
2117
2446
  return true;
2118
2447
  }
2119
2448
 
2120
- static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
2121
- ENFORCE_THREAD(thread);
2122
-
2123
- debug_enter_unsafe_context();
2124
-
2125
- thread_context_collector_on_gvl_waiting(thread_from_thread_object(thread));
2126
-
2127
- debug_leave_unsafe_context();
2128
-
2129
- return Qnil;
2130
- }
2131
-
2132
2449
  static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
2133
2450
  ENFORCE_THREAD(thread);
2134
2451
 
2135
2452
  debug_enter_unsafe_context();
2136
2453
 
2137
- intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(thread);
2454
+ per_thread_context *thread_context = get_per_thread_context(thread);
2455
+ VALUE result = thread_context ? LONG2NUM(thread_context->gvl_waiting_at) : Qnil;
2138
2456
 
2139
2457
  debug_leave_unsafe_context();
2140
2458
 
2141
- return LONG2NUM(gvl_waiting_at);
2459
+ return result;
2142
2460
  }
2143
2461
 
2144
- static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread) {
2462
+ static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE waiting_for_gvl_threshold_ns) {
2145
2463
  ENFORCE_THREAD(thread);
2464
+ ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
2146
2465
 
2147
2466
  debug_enter_unsafe_context();
2148
2467
 
2149
- VALUE result = thread_context_collector_on_gvl_running(thread_from_thread_object(thread)) == ON_GVL_RUNNING_SAMPLE ? Qtrue : Qfalse;
2468
+ per_thread_context *thread_context = get_per_thread_context(thread);
2469
+ VALUE result;
2470
+ if (thread_context) {
2471
+ on_gvl_running_result gvl_running_result =
2472
+ thread_context_collector_on_gvl_running(thread, thread_context, NUM2UINT(waiting_for_gvl_threshold_ns));
2473
+ result = gvl_running_result.action == ON_GVL_RUNNING_SAMPLE ? Qtrue : Qfalse;
2474
+ } else {
2475
+ result = Qfalse;
2476
+ }
2150
2477
 
2151
2478
  debug_leave_unsafe_context();
2152
2479
 
2153
2480
  return result;
2154
2481
  }
2155
2482
 
2156
- static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE allow_exception) {
2483
+ static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread) {
2157
2484
  ENFORCE_THREAD(thread);
2158
- ENFORCE_BOOLEAN(allow_exception);
2159
-
2160
-
2161
2485
 
2162
- if (allow_exception == Qfalse) debug_enter_unsafe_context();
2486
+ debug_enter_unsafe_context();
2163
2487
 
2164
2488
  VALUE result = thread_context_collector_sample_after_gvl_running(
2165
2489
  collector_instance,
@@ -2167,36 +2491,32 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
2167
2491
  monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
2168
2492
  );
2169
2493
 
2170
- if (allow_exception == Qfalse) debug_leave_unsafe_context();
2494
+ debug_leave_unsafe_context();
2171
2495
 
2172
2496
  return result;
2173
2497
  }
2174
2498
 
2175
- static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns) {
2176
- ENFORCE_THREAD(thread);
2177
-
2178
- thread_context_collector_state *state;
2179
- TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
2180
-
2181
- per_thread_context *thread_context = get_context_for(thread, state);
2182
- if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2183
-
2184
- thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
2185
-
2186
- return Qtrue;
2187
- }
2188
-
2189
2499
  #else
2190
2500
  static bool handle_gvl_waiting(
2191
2501
  DDTRACE_UNUSED thread_context_collector_state *state,
2192
2502
  DDTRACE_UNUSED VALUE thread_being_sampled,
2193
- DDTRACE_UNUSED VALUE stack_from_thread,
2194
2503
  DDTRACE_UNUSED per_thread_context *thread_context,
2195
- DDTRACE_UNUSED sampling_buffer* sampling_buffer,
2196
2504
  DDTRACE_UNUSED long current_cpu_time_ns
2197
2505
  ) { return false; }
2506
+
2198
2507
  #endif // NO_GVL_INSTRUMENTATION
2199
2508
 
2509
+ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE thread, VALUE delta_ns) {
2510
+ ENFORCE_THREAD(thread);
2511
+
2512
+ per_thread_context *thread_context = get_per_thread_context(thread);
2513
+ if (thread_context == NULL) raise_error(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
2514
+
2515
+ thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
2516
+
2517
+ return Qtrue;
2518
+ }
2519
+
2200
2520
  #define MAX_SAFE_LOOKUP_SIZE 16
2201
2521
 
2202
2522
  typedef struct { VALUE lookup_key; VALUE result; } safe_lookup_hash_state;
@@ -2241,6 +2561,6 @@ static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE c
2241
2561
  return LONG2NUM(system_epoch_time_ns);
2242
2562
  }
2243
2563
 
2244
- static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
2245
- return thread_context_collector_prepare_sample_inside_signal_handler(collector_instance) ? Qtrue : Qfalse;
2564
+ static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self) {
2565
+ return thread_context_collector_prepare_sample_inside_signal_handler() ? Qtrue : Qfalse;
2246
2566
  }