Skip to content

Add span-creation JMH benchmarks (front-half allocation) - #11915

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 8 commits into
masterfrom
dougqh/span-creation-bench
Jul 22, 2026
Merged

Add span-creation JMH benchmarks (front-half allocation)#11915
gh-worker-dd-mergequeue-cf854d[bot] merged 8 commits into
masterfrom
dougqh/span-creation-bench

Conversation

@dougqh

@dougqhdougqh commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Adds application-thread ("front-half") allocation benchmarks for the full span-creation lifecycle (create → tag → finish), to ground the TagMap 2.0 / SpanPrototype allocation work against the real span path.

  • SpanCreationBenchmark — single span. Bare baseline (startSpan vs buildSpan), two known-tag shapes set after start — web-server (7 tags) and JDBC/DB client (9 tags) — plus a builder-tag-path arm (withTag before start(), the OTel-bridge shape).
  • TraceAssemblyBenchmark — web-shape root + N children (childCount 1/5/20), exercising the per-span baseline-tag copy that map-to-map copy (TagMap 1.0) and the trace/span tag split (level-split) target.
  • SpanCreationVirtualThreadBenchmarkstartSpan on a virtual thread (JDK 21+ via reflection), the regime where SpanBuilder reuse is disabled.
  • DropWriter — shared no-op Writer so finish() excludes serialization / agent I/O, isolating front-half allocation for -prof gc.

Motivation

gc.alloc.rate.norm (B/op) is deterministic and is the primary signal; throughput is directional. The benchmarks are deliberately drift-stable (only API byte-identical v1.53→master), so the same file can be grafted onto past release tags to reconstruct a historical allocation curve.

Additional Notes

  • Measurement infrastructure only — no product change.
  • Run with -prof gc, and force tracer logging to WARN (unsuppressed DEBUG logging allocates and corrupts the numbers).

🤖 Generated with Claude Code


Validation — rigorous 8-thread version sweep (1.53 → 1.64)

These benchmarks were run across every release 1.53→1.64 (-f3 -wi5 -i5 -t8 -prof gc) to validate the allocation trend. Allocation is the trustworthy axis; throughput is directional (build-to-build inlining-bimodal on a laptop — read trends, not points).

Allocation (B/op) — trustworthy axis

benchmark1.531.541.551.561.571.581.591.601.611.621.631.64
bareStartSpan13311424130913221322131311751181959948927923
bareBuildSpan13311424142213931363131011661192952949927958
webServerSpan205921312098213120632018186918591639167516271637
webServerSpanViaBuilder243524912477246424112442198719631774178717381751
jdbcClientSpan182120382029201720742065186618181619161414301421
webServerTrace[1]279228752817282727472640250425192240225622392241
webServerTrace[5]570157735752576353015643537652644635465148694721
webServerTrace[20]166751672816733167281505915072153711498714259136891499214592

Cumulative 1.53 → 1.64:

benchmark1.531.64Δ
bareStartSpan1331923-31%
bareBuildSpan1331958-28%
webServerSpan20591637-21%
webServerSpanViaBuilder24351751-28%
jdbcClientSpan18211421-22%
webServerTrace[1]27922241-20%
webServerTrace[5]57014721-17%
webServerTrace[20]1667514592-12%

Two structural steps carry the curve: 1.59 (TagMap 1.0 default → Entry-sharing) and 1.61 (interceptor/links elimination cluster), plus a 1.63 jdbc-specific drop. Single-span −28-31% (triple-confirmed: this sweep, the 4-thread archaeology, and the PetClinic macro stress test at −29%). Trace path smaller (−12-20%) — the per-child fresh-tag insertion cost that TagMap 2.0 (dense + level-split) targets next.

Throughput (ops/us) — directional only

benchmark1.531.541.551.561.571.581.591.601.611.621.631.64
bareStartSpan5.694.263.873.924.214.164.205.484.175.536.035.37
bareBuildSpan5.494.254.134.034.104.194.665.664.315.725.975.26
webServerSpan4.003.473.373.783.313.343.433.413.464.194.664.62
webServerSpanViaBuilder3.993.553.523.323.493.523.523.543.584.375.135.29
jdbcClientSpan5.333.153.143.153.373.273.453.783.464.494.495.06
webServerTrace[1]2.432.322.352.522.302.482.292.302.372.342.912.42
webServerTrace[5]0.780.840.930.900.951.051.020.980.881.141.040.94
webServerTrace[20]0.250.260.250.280.310.300.280.300.260.300.250.25

Caveat: throughput on this harness is build-to-build inlining-bimodal (see the bareStart column alternating ~4.2 / ~5.5) — reproducible across clean re-runs, not thermal or contamination, and not stabilized by @Fork(3) (which averages within a build, not across builds). Read trends, not points. The one real-ish signal is the 1.62→1.64 climb on the tagged arms (web 3.4→4.6, webBuilder 3.5→5.3), corroborating the 1.62 isOutbound/span.kind CPU win (#11116) + client-stats changes. Allocation is the axis; throughput corroborates large effects only. A CPU win should be sized from a profile (cycles), not this axis.

Measured a real change (#11701)

TraceAssemblyBenchmark was used to size #11701 (local children share the parent's PropagationTags). Isolating just that one-line toggle (share vs empty()-per-child, all else identical), the shared path drops gc.alloc.rate.norm by ~96 B per child (JOL-confirmed shallow size of one empty()PTags), scaling with fan-out: trace[20] −2031 B/op (≈14.5%), trace[5] −442 B/op, single-span control flat. A concrete consumer validating the per-child slope this harness exists to expose.

dougqhand others added 2 commits July 10, 2026 14:27
Adds application-thread (front-half) allocation benchmarks that ground the
TagMap 2.0 / SpanPrototype work against the real span lifecycle:
- SpanCreationBenchmark (front-A): single span create -> tag -> finish. Bare
baseline (startSpan vs buildSpan), two known-tag shapes set after start —
web-server (7 tags) and JDBC/DB client (9 tags) — plus a builder-tag-path arm
(withTag before start, the OTel-bridge shape) to track how the startSpan /
buildSpan lineages diverge across releases.
- TraceAssemblyBenchmark (front-B): web-shape root + N children (childCount
1/5/20), exercising the per-span baseline-tag copy that map-to-map copy
(TagMap 1.0) and the trace/span tag split (level-split) target.
- DropWriter: shared no-op Writer so finish() excludes serialization / agent
I/O, isolating front-half allocation for -prof gc.
Deliberately drift-stable (v1.53->master API) so the same file can be grafted
onto old release tags for a historical allocation curve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runs startSpan create+finish on a virtual thread — the regime the platform-thread
SpanCreationBenchmark is blind to. startSpan's thread-local SpanBuilder reuse
(#9537) is disabled on virtual threads, so a builder is allocated per span there;
the builder bypass (#9998) removes it. This bench is where that difference shows.
Starts the virtual thread via reflection (Thread.startVirtualThread) so the jmh
source set still compiles on pre-21 toolchains; requires a 21+ JDK at run time.
The per-op vthread spawn/join cost is constant across tracer versions, so it
cancels in the version-over-version delta.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqhdougqh added comp: core Tracer core type: refactoring tag: no release note tag: ai generated Largely based on code generated by an AI or LLM labels Jul 10, 2026
@dd-octo-sts

dd-octo-stsBot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

SuiteStatus
Startup🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
ScenarioCandidatemasterΔ (95% CI of mean)
startup:insecure-bank:iast:Agent13.98 s13.93 s[-0.4%; +1.2%] (no difference)
startup:insecure-bank:tracing:Agent12.87 s12.88 s[-0.8%; +0.6%] (no difference)
startup:petclinic:appsec:Agent16.88 s16.78 s[-0.4%; +1.6%] (no difference)
startup:petclinic:iast:Agent16.83 s16.90 s[-1.2%; +0.3%] (no difference)
startup:petclinic:profiling:Agent16.73 s16.00 s[+0.2%; +8.9%] (maybe worse)
startup:petclinic:sca:Agent16.92 s16.78 s[+0.0%; +1.6%] (maybe worse)
startup:petclinic:tracing:Agent15.95 s16.26 s[-2.8%; -1.0%] (maybe better)

Commit:de9dcb31 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

Fold the v1.53.0 -> 1.64.0 sweep results into the SpanCreationBenchmark and
TraceAssemblyBenchmark javadoc so the trend is discoverable from the source
without re-running. Allocation (gc.alloc.rate.norm B/op) carries a net Δ%;
throughput (ops/us) is included but flagged directional-only and given no Δ%
since it has no reliable trend (laptop thermals + per-fork inlining bimodality).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@datadog-datadog-prod-us1

This comment has been minimized.

@dougqh
dougqh marked this pull request as ready for review July 13, 2026 15:14
@dougqh
dougqh requested a review from a team as a code ownerJuly 13, 2026 15:14
@dougqh
dougqh requested a review from ygreeJuly 13, 2026 15:14

@datadog-datadog-prod-us1datadog-datadog-prod-us1Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

Pure benchmark-only PR (no production code changes): all four new JMH files are measurement infrastructure. Every critical API contract was verified statically — DropWriter correctly implements all five Writer methods, all Tags constants exist, asChildOf(root) compiles because CoreTracer.buildSpan() returns the concrete CoreSpanBuilder (which has asChildOf(AgentSpan)) not the SpanBuilder interface (which has only asChildOf(AgentSpanContext)), and the Thread.startVirtualThread MethodHandle pattern was confirmed correct on JDK 21. No behavioral regressions are possible: this PR adds only benchmarks.

Was this helpful? React 👍 or 👎

📊 Validated against 6 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit e92967b · What is Autotest? · Any feedback? Reach out in #autotest

dougqhand others added 2 commits July 16, 2026 11:15
A real jdbc span is never "servlet.request"; using a distinct
"database.query" operation name keeps the web- and DB-shaped span
benchmarks from being conflated by operation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that the DB-shaped span has its own JDBC_OPERATION_NAME, name the
web/server operation constant symmetrically for clarity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@PerfectSlayerPerfectSlayer added tag: no release notes Changes to exclude from release notes and removed tag: no release note labels Jul 20, 2026

@sarahchen6sarahchen6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Left a few comments, but otherwise looks good. Pre-approving !

*/
final class DropWriter implements Writer {
@Override
public void write(List<DDSpan> trace) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this writer leverage JMH's blackhole like PendingTraceWrite and BlackholeWriter? IIUC this will keep unused code active for the benchmarks

@dougqhdougqhJul 21, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yeah, I think that makes sense. I'd held off initially because I was debating having a way to cover both the foreground / application thread work and the background / serializer & stats thread work.

But either way, using a Blackhole here probably makes sense.
I'm confident that spans weren't escaping, so the results shouldn't change.

So I think I'm going to land the PR as is -- then I'll do a re-run over night just to confirm that the results haven't changed significantly. If they have I'll do another PR to refresh the results.

*
* <p>Same conventions as {@link SpanCreationBenchmark}: {@link DropWriter} isolates front-half
* allocation; read alloc ({@code -prof gc}) as the anchor, throughput as directional; logging must
* be forced to WARN or DEBUG-line allocation corrupts the numbers. Drift-stable v1.53→master

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A point brought up by Codex is that all of these benchmarks inherit from https://github.com/DataDog/dd-trace-java/blob/master/dd-trace-core/src/test/resources/logback.xml#L12 which sets the logging level to DEBUG, but at least TraceAssemblyBenchmark here expects logging to be at the WARN level to not corrupt numbers. The recommended fix is to neutralize the logging setting for all of these benchmarks with:

@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn")

WDYT?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yeah, that's a good point -- and is a problem that I ended up hitting in another Claude session. Might as well just fix it for good.

* no serialization or agent I/O leaking into the {@code -prof gc} number.
*
* <p>Drift-stable: implements only the five-method {@link Writer} interface, unchanged
* v1.53→master.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"master" is used in descriptions throughout, but this reference will always be changing -- I think we should specify 1.64 instead to be clear that that's what the latest tested version is, or something like "master at the time of this commit"

@dougqhdougqhJul 21, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The reference to master is largely intentional. My aim is that this benchmark stays valid as a way compare performance over time -- basically as long as we maintain binary compatibility.

Benchmarks that depend on newer APIs will go in their own benchmark classes, since they won't work on older dd-trace-java versions.

dougqhand others added 2 commits July 21, 2026 12:08
… benchmarks
DropWriter.write() was a true no-op, letting the JIT treat the
finish()-triggered write as dead code; hand the trace to a Blackhole
instead. Also force -DTEST_LOG_LEVEL=warn on all three benchmark
classes' forks, since logback.xml defaults to DEBUG and the benchmarks'
own javadoc requires WARN to keep DEBUG logging from corrupting the
-prof gc allocation numbers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dougqh
dougqh enabled auto-merge July 21, 2026 16:24
@dougqh
dougqh added this pull request to the merge queueJul 22, 2026
@dd-octo-sts

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351Bot commented Jul 22, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-22 07:33:56 UTC ℹ️ Start processing command /merge


2026-07-22 07:34:01 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-07-22 08:31:19 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Jul 22, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot merged commit acdb5f0 into masterJul 22, 2026
587 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot deleted the dougqh/span-creation-bench branch July 22, 2026 08:31
@github-actionsgithub-actionsBot added this to the 1.65.0 milestone Jul 22, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: coreTracer coretag: ai generatedLargely based on code generated by an AI or LLMtag: no release notesChanges to exclude from release notestype: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@dougqh@sarahchen6@PerfectSlayer