Repository navigation
Conversation
Marks a top-level class whose nest must contain no synthetic accessors (access$* methods or synthetic constructors), which javac generates for cross-nest private access when targeting Java 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lists the synthetic accessors in a top-level class's nest by loading every TopLevel$* class file beside it, so tests can enforce @NoSyntheticAccessors deterministically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CoreSpanBuilder read ten private CoreTracer fields through access$ methods on every span. Make the members used across the nest package-private and mark CoreTracer @NoSyntheticAccessors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
🟢 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. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Its uncovered code is defensive error handling that a normal test run can't reach, so it joins the module's other excluded test helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
The new tests for SyntheticAccessors cover only directory-backed fixtures, leaving the JAR scanner and error branches uncovered. Because the module enforces a mandatory 90% per-class branch and instruction coverage threshold with no exemption for this helper, baseCheck/baseTest -PcheckCoverage will fail until the missing paths are covered or an explicit exclusion is added.
🤖 Bits Code Review · Commit 52b997b
PerfectSlayer
left a comment
There was a problem hiding this comment.
I would rather not open core tracer fields / encapsulation for performance reason.
@PerfectSlayer I understand, but we're not really opening encapsulation to dd-trace-java as a whole. We're opening just to the one package -- not public. We already do that for testing purposes. I'd argue that an end user performance gain is a stronger justification. And while I understand the desire to protect the core, the core is also the most important place performance wise, so we have to find a balance. Admittedly, we should be careful about what we open-up and how. For the immutable fields, I'd prefer to keep them package visible to avoid eating into the inlining depth limit. However for mutable fields, I think we could create package visible helpers in many situations to maintain some encapsulation. We could also scope this field-by-field, method-by-method, but I didn't do that because that makes it easy for someone to introduce a regression. I could also make this work with the SuppressPerfContract annotation - if we want to opt-out certain fields. |
buildSpanContext was a static method on CoreSpanBuilder that read twelve of CoreTracer's private fields through synthetic accessors on every span. As a package-private instance method on CoreTracer it reads them directly, so those fields go back to private. The move is mechanical: the tracer parameter becomes this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SyntheticAccessors now reads the nest's class files with ASM instead of reflecting: only the bytecode says which member an access$ method or synthetic constructor reaches, and @SuppressPerfContract has CLASS retention. An accessor is exempt when its target member carries a suppression naming NoSyntheticAccessors with a non-blank reason, directly or through a reusable suppression annotation. Reports now name each accessor's target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The constructor, invertMap, ShutdownHook's constructor and the three fields ConfigSnapshot reads are used across the nest only during setup or config reloads. They go back to private with a suppression that records why, instead of being opened to the package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@PerfectSlayer I reworked this so it no longer opens anything in
So the contract doesn't push us toward weaker encapsulation. It flags when a nested class reaches into private state, and we either move the code next to the data or record that it's a deliberate, off-hot-path exception. 🤖 Generated with Claude Code |
What Does This Do
Adds a
@NoSyntheticAccessorsperf contract, a test helper that enforces it, and applies it toCoreTracer.@NoSyntheticAccessors(datadog.perfcontract): on a top-level class, declares that no class in its nest needs a synthetic accessor. The nest is the class plus everything nested in it, including anonymous and local classes. The rule covers the whole nest because the accessor lands in the class that owns the private member, while the cause is the class that uses it.SyntheticAccessors.assertNone(Class)(utils/test-utils): fails if any class in the nest has an unsuppressed syntheticaccess$*method or synthetic constructor, and names the member each one reaches. It reads the nest's class files with ASM (Byte Buddy's bundled copy, already a test-utils dependency), since only the bytecode says what an accessor reaches and@SuppressPerfContracthasCLASSretention. It finds anonymous and local classes by listing theTopLevel$*class files beside the top-level class. Lambdas and bridge methods aren't counted.@SuppressPerfContract(value = NoSyntheticAccessors.class, reason = ...), directly or through a reusable suppression annotation. That lets a member used across the nest only off the hot path stayprivate, with the reason at the declaration. A blank reason, or a suppression that doesn't name this contract, is reported.CoreTraceris the first class marked. Its nest had 14access$methods and 2 synthetic constructors. Most came fromCoreSpanBuilder.buildSpanContext, a static method that read 12 ofCoreTracer's private fields (idGenerationStrategy,propagationTagsFactory,localRootSpanTagsand others) through accessors on everybuildSpan(). It now lives inCoreTraceras a package-private instance method, so those fields are read directly. The move is mechanical: thetracerparameter becomesthis. The remaining cross-nest members are cold (the constructor,invertMap,ShutdownHook's constructor, and the three fieldsConfigSnapshotreads), so they stayprivatewith suppressions. Compared with master, noCoreTracermember is opened.checks.mdlists this under deterministic-lint candidates as already checked, so the AI reviewer doesn't spend effort on it.Motivation
We compile to Java 8 bytecode, which has no nestmates (JEP 181 arrived in JDK 11). So when one class in a nest uses another's
privatemember, javac generates a staticaccess$NNNmethod and routes the call through it. A private constructor called across the nest also gets a synthetic constructor with an extra parameter typed as a generated class.Each accessor is a real call in the interpreter and C1. C2 inlines them as trivial methods, but each one uses up an inlining level, so on a deep hot chain it can push the real callee past
MaxInlineLevel. Accessors also add methods and classes to load at startup.Accessors also hide coupling: a nested class can reach into its outer class's private state and nothing in the source shows it.
buildSpanContexthad grown into a 290-line method in the builder that read 12 privateCoreTracerfields; the contract turns that kind of reach into a test failure.Unlike the other perf contracts, this one is fully mechanical, so it gets a deterministic check rather than an AI-review rule.
Additional Notes
CLASSretention like the other perf contracts, so reflection can't discover marked classes. Each marked class gets a one-line test callingassertNone. A build-time scan for the annotation could replace that later.dd-trace-coreandinternal-api:TracerHealthMetrics(58),LLMObsSpanMapper(24),DefaultDataStreamsMonitoring(19),PendingTraceBuffer$DelayingPendingTraceBuffer(13). Which of those are on hot paths still needs checking.Contributor Checklist
:utils:test-utils:test(SyntheticAccessorsTest: detects both accessor kinds and names their targets, ignores package-private members and lambdas, honours direct and meta-annotation suppressions, and rejects a blank reason or a suppression of another contract)CoreTracer's suppressions failsCoreTracerSyntheticAccessorsTestwithCoreTracer.access$000 -> CoreTracer.invertMap:dd-trace-core:test, full suite after thebuildSpanContextmove (4,710 tests; the only failures were a timing flake that passes on rerun and two Docker-dependent tests with no local Docker). The new test fails on master with all 16 accessors listed:internal-api:javadoc,:utils:test-utils:javadoc,spotlessApply🤖 Generated with Claude Code