Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .agents/dd-apm-sdk-review-overrides/reviewers/performance.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Override for `reviewers/performance.md` (in the core skill folder) — read that file first, then this.

Seeded from this repo's former `.agents/skills/perf-review/references/{guide.md,checks.md}` (the previously-shipped, now-retired Java performance rubric; the skill folder was removed to avoid two competing performance-review entry points). Everything language-agnostic from that rubric moved to the core `reviewers/performance.md` (confidence axis, severity model, domain-adjusted severity, universal checks); everything Java-specific is the addenda below (J1-J15), the instrumentation idioms, the deterministic-lint candidates, and the bootstrap note.
Seeded from this repo's former `.agents/skills/perf-review/references/{guide.md,checks.md}` (the previously-shipped, now-retired Java performance rubric; the skill folder was removed to avoid two competing performance-review entry points). Everything language-agnostic from that rubric moved to the core `reviewers/performance.md` (confidence axis, severity model, domain-adjusted severity, universal checks); everything Java-specific is the addenda below (J1-J16), the instrumentation idioms, the deterministic-lint candidates, and the bootstrap note.

# Performance — dd-trace-java specifics

Expand Down Expand Up @@ -30,10 +30,22 @@ Seeded from this repo's former `.agents/skills/perf-review/references/{guide.md,
- **J13 — Defensive copies at internal boundaries** *(refines universal `per-call-allocation`)*. `array.clone()`, `new ArrayList<>(other)`, and similar copies are justified at a real trust boundary (a public API, or genuinely mutable external input), but also when the callee needs a stable snapshot across threads or ownership of the data — a read-only view only blocks mutation *through that view* and still reflects later changes to the backing collection, so swapping in a view where a real copy was needed trades an allocation for an aliasing/concurrency bug. flag-with-confidence only when neither a trust boundary nor a stable-snapshot/ownership-transfer need is established — SEV-2/3. Fix: return a read-only view, or establish a "don't mutate" contract instead of copying.
- **J14 — Capturing lambda allocated on every call, including cache hits** *(refines universal `per-call-allocation`, `escape-elision-defeated`)*. A non-capturing lambda is a cached singleton (zero-alloc); a capturing lambda (closes over a local or `this`) is a new instance per evaluation — but when the call site is inlined and the mapping function isn't retained past the `computeIfAbsent` call, escape analysis can still scalar-replace it on a cache hit. The recurring trap: `map.computeIfAbsent(k, k -> compute())` looks like it allocates the lambda on *every* call including cache hits, but whether it actually survives is optimizer-dependent. flag-as-measure — verify in JFR/an allocation profile that the lambda escapes before treating it as a confirmed cost; only escalate to flag-with-confidence once escape is established (e.g. the mapping function is stored, passed to a virtual call, or the call site is megamorphic) — SEV-2/3. Fix: `get` first, call `computeIfAbsent` only on a miss.
- **J15 — `Optional` construction and primitive boxing outside the JVM cache range** *(refines universal `per-call-allocation`)*. `Optional.empty()` / `OptionalInt.empty()` / `OptionalLong.empty()` / `OptionalDouble.empty()` return cached singletons and don't allocate. A non-empty `Optional*` construction allocates a new instance, but C2 can scalar-replace it when the value is created and immediately consumed in an inlined method — same as J14's local-object case. flag-with-confidence only when the Optional is returned, stored in a field/collection, or otherwise proven to escape; flag-as-measure for local create-and-consume (verify in JFR before treating it as a confirmed cost). Autoboxing a primitive outside the JVM's cached range (`[-128, 127]` for `Integer`/`Long`) likewise allocates when the boxed value escapes: flag-with-confidence then, flag-as-measure for a locally consumed box. Fix: null checks or primitive-typed return values instead of `Optional`; fixed-arity primitive overloads instead of boxing. SEV-2/3.
- **J16 (slug: `background-only-contract`) — `@ForegroundSafe`/`@BackgroundOnly` declared-contract reachability** *(distinct axis — not a refinement of a universal cost check; a declared-contract violation, not an inferred cost)*. `datadog.trace.api.function.ForegroundSafe` and `.BackgroundOnly` (`CLASS`-retention markers) declare a one-way compatibility contract: `@ForegroundSafe` code must be cheap enough to call from either an application (foreground) thread or a background thread; `@BackgroundOnly` code must never be reached from a foreground call site. A method-level annotation always overrides a type-level one on the same element. Flag-with-confidence a diff that adds a call path from `@ForegroundSafe`-or-unannotated foreground code (a span lifecycle method, an instrumentation advice body, anything on the app thread) into a symbol declared `@BackgroundOnly`.

**Inheritance direction is a second, independent trigger — checked at the declaration, not the call site.** The full rule and rationale live in `BackgroundOnly`'s Javadoc ("Inheritance direction" section, mirrored in `ForegroundSafe`'s) — don't restate it here, just apply it: an override may only narrow a supertype's declared cost, never widen it. The checker-facing consequence: overriding a `@ForegroundSafe` or unannotated supertype method and marking the override `@BackgroundOnly` is flag-with-confidence the moment it appears in the diff, whether or not any foreground call site currently reaches it — the reverse direction (narrowing `@BackgroundOnly` to `@ForegroundSafe`) is compliant, not a finding.

Severity follows this lens's normal cost-based ladder (§ "Mapping SEV to this skill's P0/P1/P2 scale" above), not a fixed level — the contract crossing tells you *that* the callee's cost is now paid on the wrong thread, not by itself how large that cost is: SEV-2/3 by default (general tracer/background-style overhead moved to the app thread); escalate toward SEV-1/2 when the reached call site is itself on a hot per-span/per-request path, or when the background-only code also does I/O, blocking, or heavy allocation, matching the general "hot-path multiplier" and denominator guidance above. Resolution is grep-only today (`grep -rn "@BackgroundOnly\|@ForegroundSafe"`), including across module boundaries when the callee lives outside the diff — there is no APT-generated manifest yet, so cross-module resolution costs a grep per unfamiliar symbol rather than a lookup (tracked in APMLP-1645). **This is local type-tracking (creation point to usage point within a class/file), not cross-file data-flow.** Extend resolution exactly two hops beyond the direct case, both anchored to files already in hand — never the reverse (enumerating every implementer of an interface to see whether any one of them is `@BackgroundOnly`); that search is unbounded, repo-wide, and belongs to APMLP-1645's real resolver, not this grep-based check:

1. **Concrete → declared interface, method-scoped.** When a foreground call site is typed to an interface `I` rather than the concrete class, and some implementation `C` of `I` — already in hand, i.e. visible in the diff or files handed to you, never found by searching the repo for implementers — is `@BackgroundOnly` (type- or method-level), read `C`'s `implements`/`extends` clause and match `C`'s `@Override` methods against `I`'s method signatures. Propagate the marker only to *that signature* on `I` — not to every method `I` declares. A foreground call to a different, unrelated method on `I` is not a finding. **First check whether the call site's receiver is visibly bound in the diff** (a local `new`/factory-method assignment, a constructor/field initializer, a DI wire-up you can see) **to a specific implementation of `I` other than `C`.** When it is, and that other implementation is not itself `@BackgroundOnly`, resolve against what's actually visible instead of `C` — this is not a finding, since `C` cannot be what that particular reference calls. Reserve hop 1 for the case where the receiver's concrete type isn't visible in hand (a parameter, a field populated elsewhere, a return value from outside the diff) and `C` is the only candidate implementation you have to reason from.
2. **Default method transitively calling a flagged signature.** If `I` declares a default method `d()` whose body calls one of the signatures flagged in step 1, `d()` inherits the flag too. This is a single read of `I`'s own source — do not chase the call further than one interface file.

Neither hop proves the call site's interface reference actually resolves to `C` at runtime when the receiver's concrete type isn't visible (that's still unprovable by grep) — treat a hit from either hop as flag-with-confidence in that case, same as the direct case, since a call site that would only ever reach a *safe* implementer of `I` is the rare case, not the default one to assume absent evidence. But when the diff *does* show the receiver bound to a specific, different, safe implementation, that visible evidence overrides the default assumption — don't flag against a `C` the call site provably can't reach.

**Stay silent, not "probably fine," on an unclassified symbol** — the absence of either annotation on the callee is not itself a finding; only a diff that *provably* crosses a declared `@BackgroundOnly` boundary from a foreground path counts. For a feel of the shape (not an exhaustive list — grep for the current, complete set): `FixedSizeCache` carries `@ForegroundSafe` (bounded-probe implementation only — the `DDCache` interface itself and its other implementations, e.g. `CHMCache`, are deliberately unannotated since their cost isn't uniformly bounded), `SimpleUtf8Cache` carries `@BackgroundOnly`. Fix: remove the call from the foreground path, or move the enclosing work to the background/serializer thread that legitimately owns the `@BackgroundOnly` code.

**J7–J11 route an *existing* universal `per-call-allocation`/`repeat-work-across-calls`/`unbounded-memory` finding to a landed reusable fix — they are not new triggers.** Don't raise a finding you wouldn't have raised anyway.

**Toolkit availability — cite only what exists.** Available today: `Strings`, `SubSequence`, `HashingUtils`, `Hashtable`, `ConcurrentHashtable`, `StringIndex` (all `datadog.trace.util`), `RE2J` (`com.google.re2j`). Coming (name as "coming", don't imply present): `UTF8BytesString.Cache`, wider `IntegerCache`, `DDCache` inlining.
**Toolkit availability — cite only what exists.** Available today: `Strings`, `SubSequence`, `HashingUtils`, `Hashtable`, `ConcurrentHashtable`, `StringIndex` (all `datadog.trace.util`), `RE2J` (`com.google.re2j`), `ForegroundSafe`/`BackgroundOnly` (`datadog.trace.api.function`, scoped only to whatever J16's grep currently turns up — see J16 above, don't assume repo-wide coverage). Coming (name as "coming", don't imply present): `UTF8BytesString.Cache`, wider `IntegerCache`, `DDCache` inlining.

## Instrumentation (ByteBuddy Advice) idioms — dd-trace-java-specific fixes

Expand Down
4 changes: 2 additions & 2 deletions .llm-validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level full --runs 1

# CI-shaped set (8 cases)
# CI-shaped set (13 cases)
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level gate --runs 1

Expand Down Expand Up @@ -110,7 +110,7 @@ that level already selected.
| Level | Cases | Default runs | Use |
|---|---|---|---|
| `minimum` | **1** (`java-perf-lens-wrong-collection-001`) | 2 | First smoke |
| `gate` (default) | **8** listed in `config.yaml` | 2 | CI-shaped |
| `gate` (default) | **13** listed in `config.yaml` | 2 | CI-shaped |
| `full` | **every** case in `suites/` | 2 | Broader pass |

So this command runs **one** case once, not the whole suite:
Expand Down
5 changes: 5 additions & 0 deletions .llm-validation/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ presets:
- java-maintainability-resource-leak-streams
- java-correctness-span-events-list-only
- java-correctness-mapper-state-leak
- java-perf-foreground-calls-background-only-001
- java-perf-foreground-calls-unannotated-002
- java-perf-foreground-calls-method-override-003
- java-perf-foreground-calls-interface-hop-004
- java-perf-inheritance-widen-violation-005
runs: 2
minimum:
cases: ["java-perf-lens-wrong-collection-001"]
Expand Down
Loading