Skip to content

perf(ddtrace/tracer): snapshot config in StartSpan to avoid RWMutex contention#4713

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 4 commits into
mainfrom
mtoff/startspan-config-snapshot
May 8, 2026
Merged

perf(ddtrace/tracer): snapshot config in StartSpan to avoid RWMutex contention#4713
gh-worker-dd-mergequeue-cf854d[bot] merged 4 commits into
mainfrom
mtoff/startspan-config-snapshot

Conversation

@mtoffl01

@mtoffl01 mtoffl01 commented May 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds Config.SpanStartSnapshot() to internal/config, which returns all the config fields read by tracer.StartSpan under a single RLock. StartSpan and applyPPROFLabels are updated to read from the snapshot instead of calling individual getters. Lock acquisitions per StartSpan drop from 13 → 2 (snapshot + ServiceMapping(span.service), which can't be folded in because the lookup key isn't known until after the snapshot is read).

Motivation

Each per-field getter on internal/config.Config acquires the same sync.RWMutex for read. These don't block each other, but every RLock/RUnlock does an atomic op on the mutex's internal reader counter — when StartSpan is invoked at high concurrency -- which it very well may be in many environments -- the counter cache line bounces across cores, so the read path scales negatively with parallelism.

Benchmarks (Apple M3 Max, 5 runs, -benchtime=3s, via benchstat):

                      │ baseline   │  after      vs base
StartSpan             │  802.5 ns  │  773.9 ns       ~ (p=0.222)
StartSpan-8           │  643.6 ns  │  628.6 ns   -2.33% (p=0.016)
StartSpanConcurrent   │  817.2 ns  │  781.4 ns   -4.38% (p=0.008)
StartSpanConcurrent-8 │  937.6 ns  │  366.9 ns  -60.87% (p=0.008)
geomean               │  793.1 ns  │  611.1 ns  -22.95%

Allocation count and bytes/op are unchanged (15 allocs/op, ~1.5 KiB/op) across all four benchmarks. The single-goroutine cases barely move because uncontended atomics are cheap; the win is concentrated in the multi-core concurrent case (~2.5× speedup), which is exactly what we were targeting.

Reviewer's Checklist

  • New code doesn't break existing tests (./internal/config/... and ./ddtrace/tracer/... pass under -race).
  • There is a benchmark covering the change (existing BenchmarkStartSpan/BenchmarkStartSpanConcurrent).
  • Changed code has unit tests for its functionality at or near 100% coverage.
  • New code is free of linting errors.

…ontention

StartSpan was acquiring internal/config's RWMutex 13 times per call (once
per field getter). Under concurrent load, the atomic ops on the mutex's
reader counter cause cache-line bouncing across cores, making the read
path scale negatively with concurrency.

Add Config.SpanStartSnapshot() which returns the StartSpan-relevant
fields under a single RLock, and use it from tracer.StartSpan and
applyPPROFLabels. Lock acquisitions per StartSpan drop from 13 to 2
(snapshot + the parameterized ServiceMapping lookup).

BenchmarkStartSpanConcurrent-8: 938 ns/op -> 367 ns/op (-60.9%)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 59.37500% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.12%. Comparing base (09bfbae) to head (c1b98a0).

Files with missing lines Patch % Lines
internal/config/snapshots.go 0.00% 13 Missing ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
ddtrace/tracer/tracer.go 88.42% <100.00%> (-4.22%) ⬇️
internal/config/snapshots.go 0.00% <0.00%> (ø)

... and 280 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented May 1, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 60.00%
Overall Coverage: 61.46% (-0.08%)

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

mtoffl01 and others added 2 commits May 1, 2026 14:37
Separate file for the snapshot pattern. config.go is dominated by
per-field getter/setter pairs; snapshots are conceptually distinct and
likely to grow as more hot paths adopt the pattern. Top-of-file comment
establishes the per-caller convention so future snapshots have an
obvious home.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ry surfaces

Add a one-liner to the Config struct doc comment and a subsection in the
package README so an agent or developer exploring this package finds the
snapshot pattern from the entry points they're likely to hit, rather than
needing to open snapshots.go from the top.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@pr-commenter

pr-commenter Bot commented May 1, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-05-01 21:35:31

Comparing candidate commit c25d80c in PR branch mtoff/startspan-config-snapshot with baseline commit 09bfbae in branch main.

Found 7 performance improvements and 0 performance regressions! Performance is the same for 194 metrics, 0 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 ----------------------------------'

scenario:BenchmarkOTelApiWithCustomTags/datadog_otel_api-25

  • 🟩 execution_time [-242.748ns; -198.452ns] or [-6.166%; -5.041%]

scenario:BenchmarkOTelApiWithCustomTags/otel_api-25

  • 🟩 execution_time [-235.798ns; -189.602ns] or [-4.275%; -3.437%]

scenario:BenchmarkSingleSpanRetention/no-rules-25

  • 🟩 execution_time [-18.026µs; -17.069µs] or [-7.340%; -6.951%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-all-25

  • 🟩 execution_time [-18.104µs; -16.903µs] or [-7.229%; -6.749%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-half-25

  • 🟩 execution_time [-17.781µs; -16.675µs] or [-7.081%; -6.640%]

scenario:BenchmarkStartSpan-25

  • 🟩 execution_time [-164.868ns; -151.932ns] or [-10.404%; -9.588%]

scenario:BenchmarkTracerAddSpans-25

  • 🟩 execution_time [-192.148ns; -165.252ns] or [-4.862%; -4.181%]

@mtoffl01
mtoffl01 marked this pull request as ready for review May 1, 2026 20:44
@mtoffl01
mtoffl01 requested review from a team as code owners May 1, 2026 20:44

@genesor genesor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I like the approach a lot

@mtoffl01

mtoffl01 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented May 8, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-05-08 20:12:43 UTC ℹ️ Start processing command /merge


2026-05-08 20:12:47 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in main is approximately 15m (p90).


2026-05-08 20:26:07 UTC ℹ️ MergeQueue: This merge request was merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants