Skip to content

DI: coordinated sampling for Live Debugger snapshots (runtime_id, trace_id_source) - #6118

Draft
p-datadog wants to merge 30 commits into
masterfrom
casual-correlation
Draft

DI: coordinated sampling for Live Debugger snapshots (runtime_id, trace_id_source)#6118
p-datadog wants to merge 30 commits into
masterfrom
casual-correlation

Conversation

@p-datadog

@p-datadog p-datadog commented Jul 28, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Adds coordinated sampling for Live Debugger snapshots, plus a runtimeId field in the snapshot envelope.

  • Live Debugger probes firing within the same APM trace now share one sampling decision, so related snapshots arrive together instead of being independently sampled. A per-probe cap prevents one probe in a high-iteration loop from crowding out sibling probes in the same trace. Probes firing outside an active trace keep independent per-probe sampling.
  • runtimeId distinguishes snapshots emitted before and after a process restart inside the same container.

Motivation:

Independent per-probe sampling fragments chains of related snapshots: the system can emit one probe's snapshot while dropping a sibling, parent, or child probe that would explain it, with no signal to the user.

Change log entry

Yes. Live Debugger snapshots from probes in the same trace now share one sampling decision so related snapshots arrive together instead of fragmenting, and each snapshot carries the process runtime id.

Additional Notes:

Probes firing outside an active trace are not correlated.

How to test the change?

Unit and integration tests added

p-ddsign added 3 commits July 28, 2026 16:15
…ing gate

Coordinated sampling for Live Debugger snapshots: one emit/drop decision per
execution unit (active APM trace, or a task-scoped correlation id read from
fiber-local storage), shared across every probe in the unit so related
snapshots stop fragmenting under independent per-probe sampling. Within an
emitting unit a per-probe-per-span cap bounds each probe to one snapshot.

The decision reuses the probe's existing rate limiter (first probe in a unit
decides, siblings inherit), introducing no new sampling-rate wire or config
contract. Decision and cap state live in bounded LRU maps.

Includes RBS signatures and unit tests.
…apshots

Gap 1 (process identity): emit the per-process runtimeId in the snapshot
envelope, distinguishing snapshots from before and after a restart inside the
same container. Same value already sent in probe status diagnostics.

envelope-source: emit trace_id_source ("apm" | "task" | "none") so a consumer
knows when the trace id in the envelope is a valid join key to APM. Mirrors the
correlation gate's tier ordering using in-process reads only.
Wire Correlation into Component and Instrumenter. Replace the two per-probe
rate-limiter checks (method-probe and line-probe paths) with a single emit?
gate that delegates to Correlation#gate, so probes in one execution unit share
the emit/drop decision and a per-probe-per-span cap bounds each probe.

emit? fails open: when correlation is absent or the gate raises (outside
propagate_all_exceptions), it falls back to the probe's own rate limiter so a
correlation bug cannot silence all snapshots.

Adds an integration test covering tier-1 coordination, per-span cap, tier-2
task units, runtime id on the wire, and fail-open.
@p-datadog p-datadog added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Jul 28, 2026
@dd-octo-sts dd-octo-sts Bot added the debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database) label Jul 28, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Typing analysis

Ignored files

This PR clears 4 ignored files. It increases the percentage of typed files from 49.84% to 50.57% (+0.73%).

Ignored files (+0-4)Cleared:
lib/datadog/tracing/contrib/kafka/events/connection/request.rb
lib/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rb
lib/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rb
lib/datadog/tracing/distributed/datadog_tags_codec.rb

Note: Ignored files are excluded from the next sections.

steep:ignore comments

This PR introduces 14 steep:ignore comments, and clears 22 steep:ignore comments.

steep:ignore comments (+14-22)Introduced:
lib/datadog/core/telemetry/event/app_started.rb:146
lib/datadog/core/telemetry/event/app_started.rb:152
lib/datadog/di/el/compiler.rb:60
lib/datadog/di/el/compiler.rb:72
lib/datadog/di/el/compiler.rb:82
lib/datadog/di/el/compiler.rb:85
lib/datadog/di/instrumenter.rb:209
lib/datadog/di/instrumenter.rb:298
lib/datadog/di/instrumenter.rb:300
lib/datadog/di/instrumenter.rb:928
lib/datadog/di/probe_notification_builder.rb:419
lib/datadog/di/serializer.rb:264
lib/datadog/di/serializer.rb:404
lib/datadog/di/serializer.rb:529
Cleared:
lib/datadog/core/telemetry/event/app_started.rb:145
lib/datadog/core/telemetry/event/app_started.rb:151
lib/datadog/core/transport/transport.rb:72
lib/datadog/di/el/compiler.rb:37
lib/datadog/di/el/compiler.rb:49
lib/datadog/di/el/compiler.rb:59
lib/datadog/di/el/compiler.rb:62
lib/datadog/di/el/expression.rb:34
lib/datadog/di/instrumenter.rb:196
lib/datadog/di/instrumenter.rb:201
lib/datadog/di/instrumenter.rb:210
lib/datadog/di/instrumenter.rb:233
lib/datadog/di/instrumenter.rb:294
lib/datadog/di/instrumenter.rb:296
lib/datadog/di/instrumenter.rb:903
lib/datadog/di/probe_notification_builder.rb:416
lib/datadog/di/serializer.rb:257
lib/datadog/di/serializer.rb:389
lib/datadog/di/serializer.rb:504
lib/datadog/symbol_database/extractor.rb:650
lib/datadog/symbol_database/extractor.rb:804
lib/datadog/tracing/distributed/baggage.rb:235

Untyped methods

This PR introduces 9 untyped methods and 21 partially typed methods, and clears 7 untyped methods and 21 partially typed methods. It increases the percentage of typed methods from 69.55% to 70.22% (+0.67%).

Untyped methods (+9-7)Introduced:
sig/datadog/core/diagnostics/environment_logger.rbs:41
└── def self.hash_serializer: (untyped h) -> untyped
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:23
└── def ==: (untyped other) -> untyped
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:27
└── def hash: () -> untyped
sig/datadog/tracing/contrib/kafka/events/connection/request.rbs:16
└── def self?.span_name: () -> untyped
sig/datadog/tracing/contrib/kafka/events/connection/request.rbs:18
└── def self?.span_options: () -> untyped
sig/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rbs:16
└── def self?.span_name: () -> untyped
sig/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rbs:18
└── def self?.span_options: () -> untyped
sig/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rbs:16
└── def self?.span_name: () -> untyped
sig/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rbs:18
└── def self?.span_options: () -> untyped
Cleared:
sig/datadog/core/diagnostics/environment_logger.rbs:20
└── def self.collect_config!: () -> { date: untyped, os_name: untyped, version: untyped, lang: untyped, lang_version: untyped, env: untyped, service: untyped, dd_version: untyped, debug: untyped, tags: untyped, runtime_metrics_enabled: untyped, vm: untyped, health_metrics_enabled: untyped }
sig/datadog/core/diagnostics/environment_logger.rbs:37
└── def self.hash_serializer: (untyped h) -> untyped
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:18
└── def payload: () -> { configuration: untyped }
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:22
└── def ==: (untyped other) -> untyped
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:26
└── def hash: () -> untyped
sig/datadog/tracing/configuration/dynamic.rbs:11
└── def call: (untyped tracing_header_tags) -> untyped
sig/datadog/tracing/configuration/dynamic.rbs:15
└── def call: (untyped tracing_sampling_rate) -> untyped
Partially typed methods (+21-21)Introduced:
sig/datadog/core/diagnostics/environment_logger.rbs:20
└── def self.collect_config!: () -> { date: untyped, os_name: untyped, version: untyped, lang: untyped, lang_version: untyped, env: untyped, service: untyped, dd_version: untyped, debug: untyped, tags: untyped, runtime_metrics_enabled: untyped, vm: untyped, health_metrics_enabled: untyped, otlp_traces_export_enabled: bool, otlp_metrics_export_enabled: bool, otlp_logs_export_enabled: bool }
sig/datadog/di/component.rbs:74
└── def parse_probe_spec_and_notify: (Hash[String, untyped] probe_spec) -> Probe
sig/datadog/di/instrumenter.rbs:48
└── def hook_method: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:51
└── def hook_line: (Probe probe, untyped responder) -> bool?
sig/datadog/di/instrumenter.rbs:55
└── def hook: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:59
└── def self.get_local_variables: (TracePoint trace_point) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:60
└── def self.get_instance_variables: (Object self) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:67
└── def run_method_probe: (::Array[untyped] args, ::Hash[::Symbol, untyped] kwargs, ::Proc? target_block, any target_self, Probe probe, untyped responder, [::String, ::Integer]? loc, ::String method_name) { () -> untyped } -> untyped
sig/datadog/di/instrumenter.rbs:71
└── def kwargs_from_splat: (::Array[untyped] args) -> [::Array[untyped], ::Hash[untyped, untyped]]
sig/datadog/di/instrumenter.rbs:79
└── def line_trace_point_callback: (Probe probe, RubyVM::InstructionSequence? iseq, untyped responder, TracePoint tp) -> void
sig/datadog/di/instrumenter.rbs:83
└── def check_and_disable_if_exceeded: (Probe probe, untyped responder, Float di_start_time, ?Float accumulated_duration) -> void
sig/datadog/di/serializer.rbs:25
└── def combine_args: (Array[untyped] args, Hash[Symbol, untyped] kwargs, Object target_self) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:26
└── def serialize_args: (Array[untyped] args, Hash[Symbol, untyped] kwargs, untyped instance_vars, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:27
└── def serialize_vars: (Hash[Symbol, untyped] vars, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:28
└── def serialize_value: (any value, ?name: (Symbol | String)?, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?, ?type: Class?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:31
└── def self.register: (?condition: Proc?) {
        (Serializer serializer, any value, name: ::Symbol?, depth: ::Integer, ?attribute_count: ::Integer?) -> untyped } -> void
sig/datadog/profiling/collectors/thread_context.rbs:29
└── def self.for_testing: (
          recorder: Datadog::Profiling::StackRecorder,
          ?max_frames: ::Integer,
          ?tracer: Datadog::Tracing::Tracer?,
          ?endpoint_collection_enabled: bool,
          ?waiting_for_gvl_threshold_ns: ::Integer,
          ?otel_context_enabled: (::Symbol? | bool),
          ?native_filenames_enabled: bool,
          ?show_classes: bool,
          ?trigger_global_reset: bool,
          **untyped
        ) -> Datadog::Profiling::Collectors::ThreadContext
sig/datadog/profiling/collectors/thread_context.rbs:56
└── def safely_extract_context_key_from: (untyped tracer) -> ::Symbol?
sig/datadog/profiling.rbs:20
└── def self.try_reading_skipped_reason_file: (?untyped file_api) -> ::String?
sig/datadog/tracing/configuration/dynamic.rbs:22
└── def call: (Array[Hash[String, untyped]]? tracing_sampling_rules) -> Datadog::Core::Telemetry::Event::telemetry_value
sig/datadog/tracing/trace_digest.rbs:30
└── def initialize: (?span_id: untyped?, ?span_name: untyped?, ?span_resource: untyped?, ?span_service: untyped?, ?span_type: untyped?, ?span_links: ::Array[SpanLink]?, ?trace_distributed_tags: untyped?, ?trace_hostname: untyped?, ?trace_id: untyped?, ?trace_name: untyped?, ?trace_origin: untyped?, ?trace_process_id: untyped?, ?trace_resource: untyped?, ?trace_runtime_id: untyped?, ?trace_sampling_priority: untyped?, ?trace_service: untyped?, ?trace_distributed_id: untyped?, ?trace_flags: untyped?, ?trace_state: untyped?, ?trace_state_unknown_fields: untyped?, ?trace_otel_random_value: ::String?, ?trace_otel_threshold: ::String?, ?trace_otel_unknown_fields: ::String?, ?span_remote: untyped?, ?baggage: untyped?) -> void
Cleared:
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:20
└── def configuration: () -> Array[Hash[Symbol, untyped]]
sig/datadog/di/component.rbs:70
└── def parse_probe_spec_and_notify: (Hash[String, untyped] probe_spec) -> Probe
sig/datadog/di/instrumenter.rbs:44
└── def hook_method: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:47
└── def hook_line: (Probe probe, untyped responder) -> bool?
sig/datadog/di/instrumenter.rbs:51
└── def hook: (Probe probe, untyped responder) -> void
sig/datadog/di/instrumenter.rbs:55
└── def self.get_local_variables: (TracePoint trace_point) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:56
└── def self.get_instance_variables: (Object self) -> Hash[Symbol, untyped]
sig/datadog/di/instrumenter.rbs:63
└── def run_method_probe: (::Array[untyped] args, ::Hash[::Symbol, untyped] kwargs, ::Proc? target_block, Object target_self, Probe probe, untyped responder, [::String, ::Integer]? loc, ::String method_name) { () -> untyped } -> untyped
sig/datadog/di/instrumenter.rbs:67
└── def kwargs_from_splat: (::Array[untyped] args) -> [::Array[untyped], ::Hash[untyped, untyped]]
sig/datadog/di/instrumenter.rbs:73
└── def line_trace_point_callback: (Probe probe, RubyVM::InstructionSequence? iseq, untyped responder, TracePoint tp) -> void
sig/datadog/di/instrumenter.rbs:77
└── def check_and_disable_if_exceeded: (Probe probe, untyped responder, Float di_start_time, ?Float accumulated_duration) -> void
sig/datadog/di/serializer.rbs:24
└── def combine_args: (Array[untyped] args, Hash[Symbol, untyped] kwargs, Object target_self) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:25
└── def serialize_args: (Array[untyped] args, Hash[Symbol, untyped] kwargs, untyped instance_vars, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:26
└── def serialize_vars: (Hash[Symbol, untyped] vars, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:27
└── def serialize_value: (any value, ?name: (Symbol | String)?, ?depth: Integer, ?attribute_count: Integer?, ?length: Integer?, ?collection_size: Integer?, ?type: Class?) -> Hash[Symbol, untyped]
sig/datadog/di/serializer.rbs:30
└── def self.register: (?condition: Proc?) {
        (Serializer serializer, any value, name: ::Symbol?, depth: ::Integer, ?attribute_count: ::Integer?) -> untyped } -> void
sig/datadog/profiling/collectors/thread_context.rbs:27
└── def self.for_testing: (
          recorder: Datadog::Profiling::StackRecorder,
          ?max_frames: ::Integer,
          ?tracer: Datadog::Tracing::Tracer?,
          ?endpoint_collection_enabled: bool,
          ?waiting_for_gvl_threshold_ns: ::Integer,
          ?otel_context_enabled: (::Symbol? | bool),
          ?native_filenames_enabled: bool,
          ?trigger_global_reset: bool,
          **untyped
        ) -> Datadog::Profiling::Collectors::ThreadContext
sig/datadog/profiling/collectors/thread_context.rbs:53
└── def safely_extract_context_key_from: (untyped tracer) -> ::Symbol?
sig/datadog/profiling.rbs:18
└── def self.try_reading_skipped_reason_file: (?untyped file_api) -> ::String?
sig/datadog/tracing/configuration/dynamic.rbs:22
└── def call: (Array[Hash[String, untyped]] tracing_sampling_rules) -> void
sig/datadog/tracing/trace_digest.rbs:27
└── def initialize: (?span_id: untyped?, ?span_name: untyped?, ?span_resource: untyped?, ?span_service: untyped?, ?span_type: untyped?, ?span_links: ::Array[SpanLink]?, ?trace_distributed_tags: untyped?, ?trace_hostname: untyped?, ?trace_id: untyped?, ?trace_name: untyped?, ?trace_origin: untyped?, ?trace_process_id: untyped?, ?trace_resource: untyped?, ?trace_runtime_id: untyped?, ?trace_sampling_priority: untyped?, ?trace_service: untyped?, ?trace_distributed_id: untyped?, ?trace_flags: untyped?, ?trace_state: untyped?, ?trace_state_unknown_fields: untyped?, ?span_remote: untyped?, ?baggage: untyped?) -> void

Untyped other declarations

This PR introduces 2 untyped other declarations and 1 partially typed other declaration, and clears 4 untyped other declarations and 1 partially typed other declaration. It increases the percentage of typed other declarations from 84.88% to 85.53% (+0.65%).

Untyped other declarations (+2-4)Introduced:
sig/datadog/tracing/trace_digest.rbs:27
└── attr_reader span_remote: untyped
sig/datadog/tracing/trace_digest.rbs:28
└── attr_reader baggage: untyped
Cleared:
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:6
└── @changes: untyped
sig/datadog/core/telemetry/event/app_client_configuration_change.rbs:8
└── @origin: untyped
sig/datadog/tracing/trace_digest.rbs:24
└── attr_reader span_remote: untyped
sig/datadog/tracing/trace_digest.rbs:25
└── attr_reader baggage: untyped
Partially typed other declarations (+1-1)Introduced:
sig/datadog/di/serializer.rbs:9
└── @@flat_registry: ::Array[{condition: ::Proc?, proc: ^(Serializer serializer, any value, name: ::Symbol?, depth: ::Integer, ?attribute_count: ::Integer?) -> untyped}]
Cleared:
sig/datadog/di/serializer.rbs:8
└── @@flat_registry: ::Array[{condition: ::Proc?, proc: ^(Serializer serializer, any value, name: ::Symbol?, depth: ::Integer, ?attribute_count: ::Integer?) -> untyped}]

If you believe a method or an attribute is rightfully untyped or partially typed, you can add # untyped:accept on the line before the definition to remove it from the stats.

@datadog-prod-us1-6

datadog-prod-us1-6 Bot commented Jul 28, 2026

Copy link
Copy Markdown

Pipelines  Tests

⚠️ Warnings

🚦 9 Pipeline jobs failed

Unit Tests | Ruby 2.6 / build & test (standard) [0]   View in Datadog   GitHub Actions

🔧 Fix in code. This looks caused by changes in this PR. 5 failed tests in DI integration due to expression evaluation errors and mismatches in expected payloads.

Unit Tests | Ruby 2.7 / build & test (standard) [0]   View in Datadog   GitHub Actions

🔧 Fix in code. This looks caused by changes in this PR. 5 failed tests in everything_from_remote_config_spec.rb due to Datadog::DI::Error::ExpressionEvaluationError: Invalid arguments for contains: false, baz.

Unit Tests | Ruby 3.0 / build & test (standard) [0]   View in Datadog   GitHub Actions

🔧 Fix in code. This looks caused by changes in this PR. 5 failed tests related to incorrect condition evaluations and runtime errors during integration testing in the DI module.

View all 9 failed jobs.

📋 Copy prompt for your agent
CI on my pull request is failing. Help me find and fix the root cause of each failing job below — they were flagged as caused by changes in this PR, so focus on the diff. For each job, explain the failure and propose a fix.

Branch: casual-correlation

Unit Tests | Ruby 2.6 / build & test (standard) [0]
Commit: dde7e1f2232c0fa77f565d1d824ba9fa39d2e1ce
Error (code / test):
5 failed tests in DI integration due to expression evaluation errors and mismatches in expected payloads.
CI job: https://github.com/DataDog/dd-trace-rb/actions/runs/32068222091/job/95505431289

Unit Tests | Ruby 2.7 / build & test (standard) [0]
Commit: dde7e1f2232c0fa77f565d1d824ba9fa39d2e1ce
Error (code / test):
5 failed tests in everything_from_remote_config_spec.rb due to Datadog::DI::Error::ExpressionEvaluationError: Invalid arguments for contains: false, baz.
CI job: https://github.com/DataDog/dd-trace-rb/actions/runs/32068222091/job/95505491745

Unit Tests | Ruby 3.0 / build & test (standard) [0]
Commit: dde7e1f2232c0fa77f565d1d824ba9fa39d2e1ce
Error (code / test):
5 failed tests related to incorrect condition evaluations and runtime errors during integration testing in the DI module.
CI job: https://github.com/DataDog/dd-trace-rb/actions/runs/32068222091/job/95505507352

Plus 5 more failing jobs not shown here.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 91c7867 | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jul 28, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-17 14:13:51

Comparing candidate commit caa8beb in PR branch casual-correlation with baseline commit e1e1961 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 48 metrics, 1 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:tracing - trace.to_digest - Continue

  • unstable throughput [-1588.479op/s; +1538.751op/s] or [-5.417%; +5.248%]

p-ddsign added 16 commits July 28, 2026 21:25
Extract execution-unit identity (APM trace / task boundary / individual hit)
into Datadog::DI::ExecutionUnit, the single source of truth for tiering used
by both the sampler and the snapshot envelope. Correlation becomes a sampler
with one responsibility: emit?(probe, unit) decides once per unit, shares the
decision across sibling probes, and caps each probe once per scope.

Drops the unused decisions_made/last_decision_at counters.
Correlation used none of settings/logger/telemetry; remove them and their
attr_readers rather than document dead surface. Constructor now takes only
max_entries. Add brief describing docstrings to the constructors.
The requirements decision permits only existing tracer context and prohibits new
context mechanisms; with no active context the hit is not correlated. Remove the
task tier from ExecutionUnit: TASK_KEY, the task branch in .current, and the
.bracket/.open/.close boundary API (a new fiber-local context mechanism). Units
now resolve to :apm (active trace) or :none.

Correlation and the notification builder need no change: Correlation is
unit-agnostic (nil key -> per-probe, i.e. not correlated), and trace_id_source
serializes the unit source, which now yields only "apm" or "none". Drop the
tier-2 tests and RBS entries.
Replace untyped in the ExecutionUnit and Correlation signatures with concrete
types: unit key/scope are Integer? (trace/span ids); the decision and cap maps
are Hash[Integer, bool] and Hash[Integer, Set[String]]; probe ids are String;
evict is generic over [K, V]. Bind unit.key/unit.scope to locals in emit? so the
nil-guard narrows them to Integer for the map operations.

Steep clean, DI correlation specs green.
Rename the unit that groups related probe hits from 'execution unit' to
'sampling unit': class ExecutionUnit -> SamplingUnit (file, sig, spec), the
Correlation param unit -> sampling_unit, the @unit_decisions map and
unit_decision method -> @sampling_unit_decisions / sampling_unit_decision, and
the prose throughout. Also drop a stale 'task' mention left in the
trace_id_source envelope comment.

Behavior unchanged. Steep clean, DI correlation specs green.
Non-capturing (log-only) probes bypass the correlation gate and keep
their own per-probe rate limit; only snapshot/expression-capturing
probes share a sampling-unit decision and the per-span cap. Matches the
current RFC scope ("every capturing/snapshot probe").

Adds Probe#capturing? and gates Instrumenter#emit? on it.
Drops the author-facing trace_id_source field ("apm"|"none") and its
builder method; runtimeId and dd.trace_id/dd.span_id are unchanged. The
require of sampling_unit is dropped from the builder (SamplingUnit is
still used by Instrumenter).
A token bucket that permits consumption below zero and refills the deficit
over time at its configured rate. Coordinated snapshot sampling uses it as the
process-wide GLOBAL budget: a trace that has started emitting keeps emitting
after the per-second budget is spent, and new traces are held off until the
balance recovers. rate and max_tokens are constructor parameters.
Replace the inherited-decision + per-span cap model with the mechanism in the
Casual Correlation requirements:

- SamplingUnit carries only the trace key (drop span scope and source).
- Correlation gates the first capturing probe in a trace (the top probe) on a
  process-wide TOP rate limit (10/s, non-borrowing) and GLOBAL rate limit
  (20/s, borrowing); on pass it emits and seeds per-trace counters.
- Correlated probes are bounded by per-trace per-probe (5) and all (20)
  emission counters and consume GLOBAL on emit.
- A top probe that fails GLOBAL or TOP marks the trace starved so every
  correlated probe in it drops.
- No active trace falls back to the probe's own rate limiter.
- Rates and budgets are constructor-overridable for tests.
Cover the wired behaviors under production limits: a nested capturing chain
emits together sharing the trace id; one probe is bounded to the per-probe
counter (5) within a trace; non-capturing probes bypass coordination; a
capturing probe with no active trace keeps its own rate limit; runtimeId rides
the snapshot; the gate fails open. TOP/GLOBAL limits and starvation are covered
in the unit spec where the limits are constructed small.
BorrowingTokenBucket#rate multiplies by elapsed time, so its rate (and the
GLOBAL rate Correlation forwards to it) must be Float | Integer, matching
TokenBucket; Numeric has no multiplication in the core RBS. Clears the
rake typecheck error.
@dd-octo-sts dd-octo-sts Bot added the core Involves Datadog core libraries label Aug 17, 2026
Drop the private attr_reader comments that restated the RBS and names, the
editorial phrasing in the class and constant docstrings, and the usage
narrative on BorrowingTokenBucket.
The class decides whether a capturing Live Debugger probe hit emits a
snapshot (a coordinated sampler). "Correlation" named the domain concept,
not the responsibility, and collided with the pre-existing
Datadog::Tracing::Correlation (trace-log correlation identifiers), which
would lead a reader to expect the same meaning here. The DI sibling
classes are agent nouns (Redactor, Serializer, Instrumenter); this one now
follows that pattern.

Renames the class, its file, sig, and unit spec, and the wiring
accessor/keyword/ivar `correlation` to `correlation_sampler` for accessor
consistency with the new class name.

Verified: rspec (correlation_sampler_spec, sampling_unit_spec) green;
rubocop, standard, rbs:stale, rbs:missing, steep:check clean under
Ruby 4.0.6. The correlation_integration_spec was not run locally: the
libdatadog_api native extension fails to compile against the vendored
libdatadog-38.0.0 headers (trace_exporter.c), a pre-existing build issue
unrelated to this rename; the two edited identifiers there are covered by
steep and rubocop.
The three private helpers dispatched from #emit? had names that did not
convey purpose from their signatures: `per_probe`, `top`, and `correlated`
each returned a Boolean emit decision but read as nouns/adjectives. Rename
them to predicates naming the case they decide:

  per_probe   -> emit_uncorrelated?   (hit with no sampling unit)
  top         -> emit_first_in_unit?  (first capturing probe in the unit)
  correlated  -> emit_correlated?     (subsequent probe in an established unit)

Updates call sites in #emit? and the sig file.

Verified: rspec (correlation_sampler_spec, sampling_unit_spec) green;
rubocop, rbs:stale, steep:check clean under Ruby 4.0.6.
The initializer parameter `all` and the reader `all` were bare
determiners that did not convey their roles from the signature: the
parameter is a starting budget, the reader is the live remainder. Rename
the parameter to `all_budget`, the ivar and reader to `all_remaining`, so
each name states what it holds. Updates the sig file and unit spec
readouts.

Verified: rspec (correlation_sampler_spec) green; rubocop, rbs:stale,
steep:check clean under Ruby 4.0.6.
Fixes:
1. Add @return [Boolean] YARD tag to Probe#capturing?
2. Replace double() with instance_double() in test specs for
   interface verification (Probe, RateLimiter, transports, trace, span)
3. Convert BorrowingTokenBucket defaulted positional args to keyword
   args (max_tokens: rate, consume(size: 1))
4. Add :: prefix to all core types in correlation_sampler.rbs and
   sampling_unit.rbs RBS signatures
5. Add explicit nil return to CorrelationSampler#store (void method)
6. Add frozen NONE constant to SamplingUnit to avoid allocation on
   the no-trace hot path
7. Trim redundant comment in probe_notification_builder.rb

Not verified: tests not run (no bundler environment in this context).
Proposed fix in: changes address all review findings.
Reverts the frozen NONE constant added to avoid allocation on the
no-trace path. The constant name is too terse for the diff.
NONE = new(nil) sat at the top of the SamplingUnit class body, before
def initialize(key). During class-body evaluation the custom initialize
is not yet defined, so new(nil) dispatched to the inherited zero-arg
initialize and raised:

  ArgumentError: wrong number of arguments (given 1, expected 0)
  lib/datadog/di/sampling_unit.rb:12

This ran whenever datadog is required (datadog.rb -> di/boot.rb ->
instrumenter.rb -> sampling_unit.rb), so spec_helper failed to load and
every test batch aborted on every Ruby that loads DI (2.6+). Ruby 2.5
was spared because DI does not load there. The placement was restored by
the revert in 3928129 (Revert "Remove NONE constant from
SamplingUnit").

Move NONE = new(nil) to the end of the class body, after initialize is
defined. self.current references NONE at call time, so relocation keeps
it resolvable.

Verified on Ruby 3.2.3: require "datadog" loads, sampling_unit_spec,
correlation_sampler_spec, probe_notification_builder_spec, and
integration/correlation_integration_spec pass.
The BorrowingTokenBucket subject called described_class.new(rate,
max_tokens) with max_tokens positional, but the constructor takes it as
a keyword: def initialize(rate, max_tokens: rate). This matches the RBS
signature (?max_tokens: ::Numeric) and the production call site
CorrelationSampler#initialize -> Core::BorrowingTokenBucket.new(global_rate).

Passing two positional arguments raised:

  ArgumentError: wrong number of arguments (given 2, expected 1)

failing all 10 BorrowingTokenBucket examples. On Ruby 2.6+ this surfaced
only after the SamplingUnit load-time crash was fixed; on Ruby 2.5 it was
already the sole failing batch.

Pass max_tokens: max_tokens. Verified on Ruby 3.2.3: rate_limiter_spec
passes (both TokenBucket and BorrowingTokenBucket).
sig/datadog/di/sampling_unit.rbs did not declare the NONE constant, so
steep reported Ruby::UnknownConstant twice in sampling_unit.rb: the
reference returned by self.current and the NONE = new(nil) assignment.
This failed the steep/typecheck CI job.

Declare NONE: SamplingUnit, matching the singleton instance returned on
the no-trace path.

Verified on Ruby 4.0.6 with gemfiles/ruby-4.0.gemfile: rake rbs:stale,
rbs:missing, and steep:check all pass (No type error detected).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos core Involves Datadog core libraries debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants