Pre-construct TagMap.Entry objects in InternalTagsAdder#11555
Conversation
InternalTagsAdder set base.service / version via TagMap.set(tag, value), allocating a fresh TagMap.Entry per span. Both values are fixed for the life of the tracer, and TagMap.Entry objects are safe to share across maps (the OptimizedTagMap collision design relies on it), so build the two Entry objects once in the constructor and reuse them via set(entry). A JFR profile of petclinic (2026-06-03) attributed ~52 allocation samples to InternalTagsAdder.processTags (one Entry per span); this drops them to zero. Re-applies the change from the stale PR #10965 (415 commits behind master, drifted signature) onto current master. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi! 👋 Thanks for your pull request! 🎉 To help us review it, please make sure to:
If you need help, please check our contributing guidelines. |
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 934fd5dc3b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Addresses the Codex review comment on #11555: pre-building the base.service Entry must not change behavior for an explicitly-empty DD_SERVICE. Entry.create rejects empty values, so baseServiceEntry is null in that case; the processTags branch now falls back to set(BASE_SERVICE, ddService) to preserve byte-identical behavior, and the version branch is still reached when the span service also matches the empty configured service. Migrate InternalTagsAdderTest from Groovy/Spock to JUnit 5 (parameterized with @MethodSource) and add regression coverage for the empty-DD_SERVICE case (9 migrated cases + 2 new = 11). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
amarziali
left a comment
There was a problem hiding this comment.
it looks a good idea. I left improvements for the comments specifically that looks a bit too verbosely generated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gression test Preserves the @TableTest versions of the two existing tests that landed on master, and adds the empty-DD_SERVICE regression test (from the PR) as a plain @test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extend the processTags null guard to also exit when ddService.length()==0, which prevents writing _dd.base_service="" via the TagMap.set path that has no empty-value guard (unlike Entry.create). Empty ddService now behaves the same as null/unset. - Remove the manual null+length>0 pre-check before TagMap.Entry.create in the constructor; Entry.create already returns null for null or empty values, so the guard was redundant. - Update the regression test to assert the new early-return behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Handle the per-span "common" tags (base.service / version) via the tag-id fast path. These values are fixed for the tracer's life, so build their TagMap.Entry once and share across every span (Entry is immutable + safe to share) — dropping InternalTagsAdder's per-span Entry allocation to zero (cf. PR #11555, the string-keyed precursor), and making the entries tag-id-bearing so they also land in their positional slot. - TagMap.Entry.create(long, Object)/create(long, CharSequence): tag-id keyed, null/empty-rejecting factories mirroring the String create(). - CoreTagIds.BASE_SERVICE / VERSION (stored range) + resolver entries. - InternalTagsAdder prebuilds baseServiceEntry/versionEntry in its ctor and set()s the shared entry; empty DD_SERVICE early-returns (regression test added). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| public void processTags( | ||
| TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { | ||
| if (spanContext == null || ddService == null) { | ||
| if (spanContext == null || ddService == null || ddService.length() == 0) { |
There was a problem hiding this comment.
when that processor is created (in createEagerChain) the ddService value is taken from Config.getServiceName() and this cannot be neither null neither empty. So it looks that instead adding a check for the size we can also remove the null check. I would suggest to improve the documentation (i.e. also add @NonNull to the constructor and simplify that instructions
There was a problem hiding this comment.
This was primarily out of an abundance of caution. Although, both AI and human reviewers had raised this as potential concern, so it was easiest to just be cautious.
If you are confident that empty won't reach here, then we can change it.
There was a problem hiding this comment.
Good call — confirmed that createEagerChain() always passes Config.getServiceName() which defaults to "unnamed-java-app" and is never null or empty. Changed the constructor param to @Nonnull, dropped the null check on ddService, and simplified the guard in processTags to just spanContext == null.
| // Regression: empty DD_SERVICE is treated the same as unset — processTags exits early and | ||
| // writes no tags, regardless of the span's service name. | ||
| @Test | ||
| void emptyDdServiceWritesNoTags() { |
There was a problem hiding this comment.
This test should not be added. I don't understand why the regression word is used here. And the service name cannot be empty in our tracer. I would suggest to remove it and improve the documentation of this processor as suggested above
There was a problem hiding this comment.
It is a change in behavior of the InternalTagsAdder, so this test was guarded against that change/regression being introduced again. Mostly, it just comes down to the contract being unclear before, so the AI had to be cautious.
There was a problem hiding this comment.
Agreed — removed the test and the @Test import. The contract is now documented by the @Nonnull annotation on the constructor instead.
…ession test Config.getServiceName() always returns a non-null non-empty string (defaults to "unnamed-java-app"), so the null/@empty guard in processTags and the corresponding regression test for empty DD_SERVICE are unnecessary. Replaced @nullable with @nonnull on the constructor param to document the actual contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What Does This Do
InternalTagsAddersets_dd.base_service/versionon spans viaTagMap.set(tag, value), which allocates a freshTagMap.Entryper span. Both values are fixed for the life of the tracer, andTagMap.Entryobjects are explicitly safe to share across maps (theOptimizedTagMapcollision design viaBucketGrouprelies on it). This pre-builds the twoEntryobjects once in the constructor and reuses them viaset(entry).Motivation
A JFR allocation profile of spring-petclinic under load (full agent, 2026-06-03) attributed ~52 allocation samples to
InternalTagsAdder.processTags— oneEntryallocated per span forbase.service. Tag-map handling (TagMap$Entry+OptimizedTagMap) was the single largest non-core tracer allocation theme in that profile, ahead of the CSS metrics system. This change drops theInternalTagsAddercontribution to zero.Notes
dd/pre-construct-tagmap-entries), which had drifted 415 commits behind master and changedprocessTags's signature; this version sits on current master with the unchangedAppendableSpanLinkssignature. Supersedes Reuse TagMap.Entry objects in InternalTagsAdder #10965.Entry.stringValue()for the OBJECT entry resolves toobj.toString()(cached), identical to the priorddService.toString()in theequalsIgnoreCasecomparison. The only edge case — emptyddServicenow yields an early return — is unreachable in production (Config.getServiceName()is never"").InternalTagsAdderTest.groovypasses unchanged.🤖 Generated with Claude Code