From d977186f3c5e1a9ffdef7dace5089c26002b1c17 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 25 Sep 2026 12:07:40 -0400 Subject: [PATCH 01/15] Add @StaticLifetime and @Singleton marker annotations @StaticLifetime marks a field that must live for the whole program rather than for its enclosing instance's lifetime (e.g. a cache allocated per-request instead of once). @Singleton is its escape valve, declaring a class has exactly one process-wide instance, so an instance field on it can satisfy @StaticLifetime without being static. Wires a matching check into the perf-review checks doc, mirroring the existing @NoEscape field-storage check. APMLP-1846, APMLP-1847 Co-Authored-By: Claude Sonnet 5 --- .../skills/perf-review/references/checks.md | 5 +- .../datadog/trace/api/function/Singleton.java | 38 +++++++++++ .../trace/api/function/StaticLifetime.java | 64 +++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/function/Singleton.java create mode 100644 internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java diff --git a/.agents/skills/perf-review/references/checks.md b/.agents/skills/perf-review/references/checks.md index abcfbcf073a..a143e3febba 100644 --- a/.agents/skills/perf-review/references/checks.md +++ b/.agents/skills/perf-review/references/checks.md @@ -26,14 +26,15 @@ Format: **pattern** — *expensive when (the interprocedural condition to trace) 6. **FFI / native-boundary crossing on a hot path** *(central to the shared-core effort)* — *a native crossing per-span/per-item (not batched), or transporting strings/objects rather than primitives/IDs* — flag-with-confidence (boundary cost is mechanism-determined; runtime-specific pinning → addendum) — SEV-1/2 (SEV-1 if it blocks/pins under concurrency) — fix: batch (one per flush, not per item); transport interned IDs not strings; keep crossings off the hot/concurrency path. 7. **Escape / allocation-elision defeated** *(Java/Go/.NET/V8 all have a version)* — *a refactor makes a previously-local object escape (stored, returned, captured by a closure, passed to a virtual/non-inlined call) → silent heap allocation on a hot path* — **flag-as-measure** ("may now escape and allocate; verify with an allocation profiler") — SEV-2/3 — fix: keep it local; avoid the escaping store/capture. 8. **`@NoEscape` field-storage violation** — *a field (instance or static, directly or as a generic type argument) declared with an `@NoEscape`-annotated type (`datadog.trace.api.function.NoEscape`), **or** initialized directly from a call to an `@NoEscape`-annotated method — with no comment at the declaration justifying the retention*. The type form marks a concrete type (e.g. `SubSequence`); the method form exists for a return value whose concrete type can't itself carry the annotation (an anonymous class or lambda implementing a JDK interface). The annotation's own javadoc carries a self-contained "Checker contract" section (trigger / not-a-trigger / violation example / compliant example, both forms) written so this can be checked from the diff alone, with no other context needed. The underlying rule is "should", not "must" (RFC-2119 sense): a trigger is a presumptive finding, not an automatic failure — a field with a `// Retained on purpose: `-style comment is compliant. **flag-with-confidence** — SEV-2/3 (SEV-1 if the annotated type/method shares backing storage with something large, per its own javadoc). Current wearers: `SubSequence`, `Maybe` (type form; see J7 below for `SubSequence`'s specific retention-vs-transient discriminator). **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** +9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.trace.api.function.StaticLifetime`) that is not `static final`, not a `static final ClassValue`, and not an instance field declared on a class annotated `@Singleton` (`datadog.trace.api.function.Singleton`)*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache; it only guards fields that opt in. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** ## Deterministic-lint candidates (DON'T spend AI budget — make these real lints) Fixed-signature, mechanically checkable: -- per-call cache / regex / expensive-object creation that should be static/once +- per-call cache / regex / expensive-object creation that should be static/once (the `@StaticLifetime`-annotated subset is checked above; this line covers the still-unannotated general case) - boxing in specific hot APIs - using a string-API where an id-API exists on a hot decorator - the existing convention rules (e.g. don't extract one-shot instrumentation methods to constants) -- *(grows as patterns prove mechanically checkable — migrate them off the AI as they stabilize; the `@NoEscape` field-storage check above belongs here once its checker lands)* +- *(grows as patterns prove mechanically checkable — migrate them off the AI as they stabilize; the `@NoEscape` and `@StaticLifetime` field-storage checks above belong here once their checkers land)* ## Java addendum (JVM-specific — mechanism authored with JIT-developer authority; **calibrate production-priority against your own escalation history**) Refines the universal checks with JVM mechanics. Quarantined here, for the Java audience that has the substrate. diff --git a/internal-api/src/main/java/datadog/trace/api/function/Singleton.java b/internal-api/src/main/java/datadog/trace/api/function/Singleton.java new file mode 100644 index 00000000000..12bdf8a7b61 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/function/Singleton.java @@ -0,0 +1,38 @@ +package datadog.trace.api.function; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a class as having exactly one instance for the life of the process -- constructed once, + * reachable only through a single static field, DI-container registration, or other holder that + * itself guarantees there is no second instance. + * + *

This exists as the escape valve for {@link StaticLifetime}: a field can legitimately live on + * an instance, not as {@code static}, if that instance itself is guaranteed to be a process-wide + * singleton. Without this annotation, {@code @StaticLifetime} would have no way to accept a + * legitimate cache held on a singleton-scoped instance without also accepting one held on an + * ordinary, possibly-repeatedly-constructed instance -- which is exactly the defect shape it exists + * to catch. + * + *

This is a documentation-and-tooling marker; it changes no behavior. + * + *

v1 posture: trusted declaration, not independently verified. This is the same stance + * the other perf-contract annotations take toward {@code static final} itself -- declared, not + * proven. Verifying it for real (a single construction site, or an instance reachable only via one + * static/DI-registered path) is a call-site/construction-graph problem, out of scope for v1. + * Annotating a class that is, in fact, constructed more than once defeats every guarantee + * downstream checks (starting with {@link StaticLifetime}) build on top of this annotation -- apply + * it with the same care as any other unverified perf-contract claim. + * + *

On a class ({@link ElementType#TYPE}): every instance field of this class may serve as + * the process-wide holder {@link StaticLifetime} requires, because the class itself is guaranteed + * to have at most one instance. + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface Singleton {} diff --git a/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java b/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java new file mode 100644 index 00000000000..81d7f9942b8 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java @@ -0,0 +1,64 @@ +package datadog.trace.api.function; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field that must live for the whole program, not merely for the lifetime of its enclosing + * instance -- typically a cache or other expensive object whose entire value comes from being + * amortized across many uses, which a per-instance field can silently defeat if that instance + * itself doesn't live for the whole program. + * + *

The name deliberately echoes Rust's {@code 'static} lifetime rather than the Java keyword + * {@code static} itself: the property this enforces is "lives for the whole program," and a {@link + * java.lang.ClassValue}-backed holder satisfies that without carrying the literal {@code static} + * modifier. + * + *

Motivating defect: a cache constructed per-request, as an instance field on a per-request + * object, instead of once as a {@code static} field -- so the cache was allocated and thrown away + * on every request and never actually amortized anything. It compiled, ran, and passed tests while + * quietly defeating the entire point of caching -- the same silent-failure shape as the other + * perf-contract annotations in this package. + * + *

This is a documentation-and-tooling marker; it changes no behavior. It exists to telegraph the + * constraint to readers and to give a future checker something to verify. The discipline it names + * is not yet enforced; hold to it by hand until the checker lands. + * + *

On a field ({@link ElementType#FIELD}): the field's value must actually live for the + * whole program, not just for as long as its enclosing instance happens to. + * + *

Checker contract. The rule below is written to be machine-checkable -- by a future + * static checker, or in the meantime by an AI reviewer -- without needing to read this class's + * prose above. This is a declaration-scan check: it looks at how the field is declared, not at + * whether it is, in practice, reused enough to be worth its cost, and it does not attempt to find + * unannotated per-instance caches -- it only guards fields that opt in. + * + *

    + *
  • Accepted (satisfies the contract): a {@code static final} field of the cache / + * expensive-object type; a {@code static final} {@link java.lang.ClassValue}{@code } used + * for a per-class one-shot computation; or an instance field, on a class annotated {@link + * Singleton}, whose enclosing instance is thereby guaranteed to be process-wide. + *
  • Trigger (violation): an instance field of the annotated type on a class that is + * not annotated {@link Singleton} -- even if, in practice, the class is only ever + * constructed once per logical "session". This check is declaration-local; it does not + * attempt to prove single construction dynamically, so a plain instance field never satisfies + * the contract on its own. + *
  • Violation example: {@code private final DDCache cache = + * DDCaches.newFixedSizeCache(128);} as an instance field of a class constructed per-request, + * per-call, or otherwise more than once for the life of the process. + *
  • Compliant example (static): {@code private static final DDCache CACHE = + * DDCaches.newFixedSizeCache(128);} + *
  • Compliant example (singleton-scoped instance): {@code @Singleton class Registry { + * private final DDCache cache = DDCaches.newFixedSizeCache(128); }} + *
  • Out of scope (v1): proving a cache is reused enough to be worth its allocation cost; + * catching a non-static cache that isn't annotated with this marker. Widen this contract only + * once a real case proves it insufficient, rather than guessing ahead of one. + *
+ */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.FIELD) +public @interface StaticLifetime {} From 88d3229a9c6a64afe74ca176db3b40e9ce440121 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 25 Sep 2026 12:21:06 -0400 Subject: [PATCH 02/15] Add Checker contract section to @Singleton javadoc Splits narrative vs. machine-checkable rule, matching the convention established by @NoEscape and @StaticLifetime. Co-Authored-By: Claude Sonnet 5 --- .../src/main/java/datadog/trace/api/function/Singleton.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal-api/src/main/java/datadog/trace/api/function/Singleton.java b/internal-api/src/main/java/datadog/trace/api/function/Singleton.java index 12bdf8a7b61..ede3d796e10 100644 --- a/internal-api/src/main/java/datadog/trace/api/function/Singleton.java +++ b/internal-api/src/main/java/datadog/trace/api/function/Singleton.java @@ -31,6 +31,12 @@ *

On a class ({@link ElementType#TYPE}): every instance field of this class may serve as * the process-wide holder {@link StaticLifetime} requires, because the class itself is guaranteed * to have at most one instance. + * + *

Checker contract. This annotation has no violation condition of its own in v1 -- there + * is nothing to flag on the class itself, since the declaration is trusted rather than verified. + * Its only role in automated review is as an input fact to {@link StaticLifetime}'s checker: a + * field otherwise flagged by that check is accepted instead when its enclosing class carries this + * annotation. See {@link StaticLifetime}'s own Checker contract section for the full rule. */ @Documented @Retention(RetentionPolicy.CLASS) From e98399f1353331bba5a97fca4070a8a5ca2145a4 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 25 Sep 2026 12:26:19 -0400 Subject: [PATCH 03/15] Annotate WebFlux route cache with @StaticLifetime Spot-checks the annotation against the real defect it was modeled on: PR #12561 (commit e6b875623e) made this exact DDCache field static after it lived per-instance, unshared, for ~4 years. Serves as the first real call site for @StaticLifetime. Co-Authored-By: Claude Sonnet 5 --- .../springwebflux/server/RouteOnSuccessOrError.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java index 09b0627eb1a..3f8d68a97bd 100644 --- a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java +++ b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java @@ -4,6 +4,7 @@ import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; +import datadog.trace.api.function.StaticLifetime; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import java.util.function.Consumer; import java.util.function.Function; @@ -31,6 +32,9 @@ public class RouteOnSuccessOrError implements Consumer> { .trim()) .replaceAll(""); + // Shared across requests; per-instance would silently defeat the cache (fixed by 12561, see + // datadog.trace.api.function.StaticLifetime for the general pattern this guards against). + @StaticLifetime private static final DDCache PARSED_ROUTE_CACHE = DDCaches.newFixedSizeCache(64); private final RouterFunction routerFunction; From 4acee829ec6a409634a76d16dbb1d34739f97a10 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 25 Sep 2026 12:30:21 -0400 Subject: [PATCH 04/15] Annotate UTF8 tag/value caches with @StaticLifetime TAG_CACHE/VALUE_CACHE in TraceMapperV0_4 and KEY_CACHE/VALUE_CACHE in OtlpCommonProto are already static final -- marking them adds compliant-example coverage alongside RouteOnSuccessOrError's violation-turned-compliant case. Co-Authored-By: Claude Sonnet 5 --- .../datadog/trace/common/writer/ddagent/TraceMapperV0_4.java | 3 +++ .../java/datadog/trace/core/otlp/common/OtlpCommonProto.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java index cbdcbd41433..e27933fbce3 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java @@ -11,6 +11,7 @@ import datadog.communication.serialization.msgpack.MsgPackWriter; import datadog.trace.api.Config; import datadog.trace.api.TagMap; +import datadog.trace.api.function.StaticLifetime; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.common.writer.Payload; @@ -27,11 +28,13 @@ import okhttp3.RequestBody; public final class TraceMapperV0_4 implements TraceMapper { + @StaticLifetime static final SimpleUtf8Cache TAG_CACHE = Config.get().getTagNameUtf8CacheSize() > 0 ? new SimpleUtf8Cache(Config.get().getTagNameUtf8CacheSize()) : null; + @StaticLifetime static final GenerationalUtf8Cache VALUE_CACHE = Config.get().getTagValueUtf8CacheSize() > 0 ? new GenerationalUtf8Cache(Config.get().getTagValueUtf8CacheSize()) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java index 88fc102a335..57783b89134 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java @@ -14,6 +14,7 @@ import datadog.communication.serialization.SimpleUtf8Cache; import datadog.communication.serialization.StreamingBuffer; import datadog.trace.api.Config; +import datadog.trace.api.function.StaticLifetime; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; import java.nio.ByteBuffer; @@ -34,12 +35,14 @@ private OtlpCommonProto() {} public static final int I32_WIRE_TYPE = 5; // use same cache approach for attribute keys as TraceMapperV0_4 + @StaticLifetime private static final SimpleUtf8Cache KEY_CACHE = Config.get().getTagNameUtf8CacheSize() > 0 ? new SimpleUtf8Cache(Config.get().getTagNameUtf8CacheSize()) : null; // use same cache approach for attribute values as TraceMapperV0_4 + @StaticLifetime private static final GenerationalUtf8Cache VALUE_CACHE = Config.get().getTagValueUtf8CacheSize() > 0 ? new GenerationalUtf8Cache(Config.get().getTagValueUtf8CacheSize()) From 982b12aa7eeea93eed754996afe0c0b8300d82d9 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 25 Sep 2026 14:01:30 -0400 Subject: [PATCH 05/15] Revert "Annotate WebFlux route cache with @StaticLifetime" Holding off on applying @StaticLifetime to real cache fields until @SuppressPerfContract (dougqh/perf-contract-suppress, PR #12647) lands -- want the exemption mechanism in place before widening the annotated surface. Co-Authored-By: Claude Sonnet 5 --- .../springwebflux/server/RouteOnSuccessOrError.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java index 3f8d68a97bd..09b0627eb1a 100644 --- a/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java +++ b/dd-java-agent/instrumentation/spring/spring-webflux/spring-webflux-5.0/src/main/java/datadog/trace/instrumentation/springwebflux/server/RouteOnSuccessOrError.java @@ -4,7 +4,6 @@ import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; -import datadog.trace.api.function.StaticLifetime; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import java.util.function.Consumer; import java.util.function.Function; @@ -32,9 +31,6 @@ public class RouteOnSuccessOrError implements Consumer> { .trim()) .replaceAll(""); - // Shared across requests; per-instance would silently defeat the cache (fixed by 12561, see - // datadog.trace.api.function.StaticLifetime for the general pattern this guards against). - @StaticLifetime private static final DDCache PARSED_ROUTE_CACHE = DDCaches.newFixedSizeCache(64); private final RouterFunction routerFunction; From 0848e09635f653f1916bc3aeb6534e8b85f0e258 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 25 Sep 2026 14:01:36 -0400 Subject: [PATCH 06/15] Revert "Annotate UTF8 tag/value caches with @StaticLifetime" Same reason as the WebFlux route-cache revert: hold off until @SuppressPerfContract lands. Co-Authored-By: Claude Sonnet 5 --- .../datadog/trace/common/writer/ddagent/TraceMapperV0_4.java | 3 --- .../java/datadog/trace/core/otlp/common/OtlpCommonProto.java | 3 --- 2 files changed, 6 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java index e27933fbce3..cbdcbd41433 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java @@ -11,7 +11,6 @@ import datadog.communication.serialization.msgpack.MsgPackWriter; import datadog.trace.api.Config; import datadog.trace.api.TagMap; -import datadog.trace.api.function.StaticLifetime; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.common.writer.Payload; @@ -28,13 +27,11 @@ import okhttp3.RequestBody; public final class TraceMapperV0_4 implements TraceMapper { - @StaticLifetime static final SimpleUtf8Cache TAG_CACHE = Config.get().getTagNameUtf8CacheSize() > 0 ? new SimpleUtf8Cache(Config.get().getTagNameUtf8CacheSize()) : null; - @StaticLifetime static final GenerationalUtf8Cache VALUE_CACHE = Config.get().getTagValueUtf8CacheSize() > 0 ? new GenerationalUtf8Cache(Config.get().getTagValueUtf8CacheSize()) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java index 57783b89134..88fc102a335 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpCommonProto.java @@ -14,7 +14,6 @@ import datadog.communication.serialization.SimpleUtf8Cache; import datadog.communication.serialization.StreamingBuffer; import datadog.trace.api.Config; -import datadog.trace.api.function.StaticLifetime; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; import java.nio.ByteBuffer; @@ -35,14 +34,12 @@ private OtlpCommonProto() {} public static final int I32_WIRE_TYPE = 5; // use same cache approach for attribute keys as TraceMapperV0_4 - @StaticLifetime private static final SimpleUtf8Cache KEY_CACHE = Config.get().getTagNameUtf8CacheSize() > 0 ? new SimpleUtf8Cache(Config.get().getTagNameUtf8CacheSize()) : null; // use same cache approach for attribute values as TraceMapperV0_4 - @StaticLifetime private static final GenerationalUtf8Cache VALUE_CACHE = Config.get().getTagValueUtf8CacheSize() > 0 ? new GenerationalUtf8Cache(Config.get().getTagValueUtf8CacheSize()) From c6b03bb84d424dbe0904d6b004704a15574dd66d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 28 Sep 2026 16:41:12 -0400 Subject: [PATCH 07/15] Close StaticLifetime checker-contract gaps: require final, static+non-final Per Bits AI review: the contract accepted "static" fields and singleton-scoped instance fields without requiring final, so a mutable field of either shape could pass while still being reassignable to a fresh, cold instance at runtime -- defeating the amortization guarantee the annotation exists to protect, just via a write instead of via scope. Add an explicit mutability trigger and require final in the Accepted description for both shapes. Co-Authored-By: Claude Sonnet 5 --- .../trace/api/function/StaticLifetime.java | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java b/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java index 81d7f9942b8..d524ae33af0 100644 --- a/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java +++ b/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java @@ -39,23 +39,38 @@ *

    *
  • Accepted (satisfies the contract): a {@code static final} field of the cache / * expensive-object type; a {@code static final} {@link java.lang.ClassValue}{@code } used - * for a per-class one-shot computation; or an instance field, on a class annotated {@link - * Singleton}, whose enclosing instance is thereby guaranteed to be process-wide. - *
  • Trigger (violation): an instance field of the annotated type on a class that is - * not annotated {@link Singleton} -- even if, in practice, the class is only ever + * for a per-class one-shot computation; or a {@code final} instance field, on a class + * annotated {@link Singleton}, whose enclosing instance is thereby guaranteed to be + * process-wide. {@code final} is required in both the static and singleton-scoped-instance + * shapes: a reassignable field can be replaced with a fresh instance at any point, silently + * discarding everything amortized in the old one -- the same defeat this annotation exists to + * catch, just triggered by a write instead of by scope. + *
  • Trigger (violation, scope): an instance field of the annotated type on a class that + * is not annotated {@link Singleton} -- even if, in practice, the class is only ever * constructed once per logical "session". This check is declaration-local; it does not * attempt to prove single construction dynamically, so a plain instance field never satisfies * the contract on its own. - *
  • Violation example: {@code private final DDCache cache = + *
  • Trigger (violation, mutability): a {@code static} field of the annotated type, or an + * instance field on a class annotated {@link Singleton}, that is not also {@code final} -- + * reassignable, so nothing stops a fresh instance from silently replacing the amortized one + * at runtime, regardless of scope. + *
  • Violation example (scope): {@code private final DDCache cache = * DDCaches.newFixedSizeCache(128);} as an instance field of a class constructed per-request, * per-call, or otherwise more than once for the life of the process. + *
  • Violation example (mutability): {@code private static DDCache cache = + * DDCaches.newFixedSizeCache(128);} -- {@code static} but not {@code final}, so any code with + * write access can swap in a fresh, cold cache and discard everything amortized in the old + * one. *
  • Compliant example (static): {@code private static final DDCache CACHE = * DDCaches.newFixedSizeCache(128);} *
  • Compliant example (singleton-scoped instance): {@code @Singleton class Registry { * private final DDCache cache = DDCaches.newFixedSizeCache(128); }} *
  • Out of scope (v1): proving a cache is reused enough to be worth its allocation cost; - * catching a non-static cache that isn't annotated with this marker. Widen this contract only - * once a real case proves it insufficient, rather than guessing ahead of one. + * catching a non-static cache that isn't annotated with this marker; a {@code final} field + * whose referent is itself internally mutable/replaceable (e.g. wraps its state in an {@code + * AtomicReference} it swaps) -- {@code final} is checked on the field, not transitively + * through the object graph. Widen this contract only once a real case proves it insufficient, + * rather than guessing ahead of one. *
*/ @Documented From afb73499a8f1cf84b528d652675f1526e32375a6 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 28 Sep 2026 16:44:03 -0400 Subject: [PATCH 08/15] Fix StaticLifetime's ClassValue paragraph to match its own checker contract Per review: the intro implied a ClassValue-backed holder need not be static itself, contradicting the Accepted list below it, which requires "static final ClassValue". Clarify that it's the per-Class values ClassValue computes that get a free process-wide lifetime, not the holder field referencing the ClassValue instance -- that still has to be static final. Co-Authored-By: Claude Sonnet 5 --- .../datadog/trace/api/function/StaticLifetime.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java b/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java index d524ae33af0..4982f5390dc 100644 --- a/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java +++ b/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java @@ -13,9 +13,13 @@ * itself doesn't live for the whole program. * *

The name deliberately echoes Rust's {@code 'static} lifetime rather than the Java keyword - * {@code static} itself: the property this enforces is "lives for the whole program," and a {@link - * java.lang.ClassValue}-backed holder satisfies that without carrying the literal {@code static} - * modifier. + * {@code static} itself: the property this enforces is "lives for the whole program." A {@link + * java.lang.ClassValue} is the motivating case for that distinction -- each per-{@code Class} value + * it computes lives for the whole program without itself being declared in a {@code static} field + * anywhere, because {@code ClassValue} does its own process-wide caching internally. The holder + * field that points at the {@code ClassValue} instance still needs to be {@code static final} -- + * see the checker contract below -- it's only the individual per-{@code Class} values inside it + * that get their process-wide lifetime for free. * *

Motivating defect: a cache constructed per-request, as an instance field on a per-request * object, instead of once as a {@code static} field -- so the cache was allocated and thrown away From 4ef7e11dbe10beafa2ef471fddc52dd8e30f1489 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 29 Sep 2026 17:05:19 -0400 Subject: [PATCH 09/15] Move StaticLifetime/Singleton into datadog.perfcontract and meta-annotate with @PerfContract Relocates both annotations out of the legacy datadog.trace.api.function package into the new datadog.perfcontract package (introduced by #12647), alongside PerfContract/SuppressPerfContract, and adds @PerfContract so tooling can discover them as perf-contract markers the same way it already discovers Strategy/StrategyConsumer/NoEscape. No other code references either annotation, so no import-site updates are needed. Co-Authored-By: Claude Sonnet 5 --- .../{trace/api/function => perfcontract}/Singleton.java | 7 ++++--- .../api/function => perfcontract}/StaticLifetime.java | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) rename internal-api/src/main/java/datadog/{trace/api/function => perfcontract}/Singleton.java (90%) rename internal-api/src/main/java/datadog/{trace/api/function => perfcontract}/StaticLifetime.java (98%) diff --git a/internal-api/src/main/java/datadog/trace/api/function/Singleton.java b/internal-api/src/main/java/datadog/perfcontract/Singleton.java similarity index 90% rename from internal-api/src/main/java/datadog/trace/api/function/Singleton.java rename to internal-api/src/main/java/datadog/perfcontract/Singleton.java index ede3d796e10..ba28bf73442 100644 --- a/internal-api/src/main/java/datadog/trace/api/function/Singleton.java +++ b/internal-api/src/main/java/datadog/perfcontract/Singleton.java @@ -1,4 +1,4 @@ -package datadog.trace.api.function; +package datadog.perfcontract; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -21,8 +21,8 @@ *

This is a documentation-and-tooling marker; it changes no behavior. * *

v1 posture: trusted declaration, not independently verified. This is the same stance - * the other perf-contract annotations take toward {@code static final} itself -- declared, not - * proven. Verifying it for real (a single construction site, or an instance reachable only via one + * the other perf-contract markers take toward {@code static final} itself -- declared, not proven. + * Verifying it for real (a single construction site, or an instance reachable only via one * static/DI-registered path) is a call-site/construction-graph problem, out of scope for v1. * Annotating a class that is, in fact, constructed more than once defeats every guarantee * downstream checks (starting with {@link StaticLifetime}) build on top of this annotation -- apply @@ -39,6 +39,7 @@ * annotation. See {@link StaticLifetime}'s own Checker contract section for the full rule. */ @Documented +@PerfContract @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface Singleton {} diff --git a/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java similarity index 98% rename from internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java rename to internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java index 4982f5390dc..4da418c863c 100644 --- a/internal-api/src/main/java/datadog/trace/api/function/StaticLifetime.java +++ b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java @@ -1,4 +1,4 @@ -package datadog.trace.api.function; +package datadog.perfcontract; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -25,7 +25,7 @@ * object, instead of once as a {@code static} field -- so the cache was allocated and thrown away * on every request and never actually amortized anything. It compiled, ran, and passed tests while * quietly defeating the entire point of caching -- the same silent-failure shape as the other - * perf-contract annotations in this package. + * perf-contract markers. * *

This is a documentation-and-tooling marker; it changes no behavior. It exists to telegraph the * constraint to readers and to give a future checker something to verify. The discipline it names @@ -78,6 +78,7 @@ * */ @Documented +@PerfContract @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface StaticLifetime {} From 2ec3a991bae0c16b9b80471debcdc784b71bd973 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 7 Oct 2026 09:37:31 -0400 Subject: [PATCH 10/15] Tighten the @Singleton Javadoc Applies a review suggestion: a static holder or DI registration does not by itself guarantee a single instance, and only final instance fields satisfy @StaticLifetime. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/perfcontract/Singleton.java | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/internal-api/src/main/java/datadog/perfcontract/Singleton.java b/internal-api/src/main/java/datadog/perfcontract/Singleton.java index ba28bf73442..368f18a7b1b 100644 --- a/internal-api/src/main/java/datadog/perfcontract/Singleton.java +++ b/internal-api/src/main/java/datadog/perfcontract/Singleton.java @@ -7,36 +7,17 @@ import java.lang.annotation.Target; /** - * Marks a class as having exactly one instance for the life of the process -- constructed once, - * reachable only through a single static field, DI-container registration, or other holder that - * itself guarantees there is no second instance. + * Declares that a class has one instance for the life of the process. All construction paths must + * preserve that guarantee; a static holder or DI registration alone does not prevent another + * instance from being created. * - *

This exists as the escape valve for {@link StaticLifetime}: a field can legitimately live on - * an instance, not as {@code static}, if that instance itself is guaranteed to be a process-wide - * singleton. Without this annotation, {@code @StaticLifetime} would have no way to accept a - * legitimate cache held on a singleton-scoped instance without also accepting one held on an - * ordinary, possibly-repeatedly-constructed instance -- which is exactly the defect shape it exists - * to catch. + *

This marker changes no runtime behavior. Tools trust the declaration without checking + * construction sites. If the class is instantiated more than once, they may accept caches that are + * rebuilt with each instance. * - *

This is a documentation-and-tooling marker; it changes no behavior. - * - *

v1 posture: trusted declaration, not independently verified. This is the same stance - * the other perf-contract markers take toward {@code static final} itself -- declared, not proven. - * Verifying it for real (a single construction site, or an instance reachable only via one - * static/DI-registered path) is a call-site/construction-graph problem, out of scope for v1. - * Annotating a class that is, in fact, constructed more than once defeats every guarantee - * downstream checks (starting with {@link StaticLifetime}) build on top of this annotation -- apply - * it with the same care as any other unverified perf-contract claim. - * - *

On a class ({@link ElementType#TYPE}): every instance field of this class may serve as - * the process-wide holder {@link StaticLifetime} requires, because the class itself is guaranteed - * to have at most one instance. - * - *

Checker contract. This annotation has no violation condition of its own in v1 -- there - * is nothing to flag on the class itself, since the declaration is trusted rather than verified. - * Its only role in automated review is as an input fact to {@link StaticLifetime}'s checker: a - * field otherwise flagged by that check is accepted instead when its enclosing class carries this - * annotation. See {@link StaticLifetime}'s own Checker contract section for the full rule. + *

Checker contract. This annotation has no violation rule of its own. It allows {@link + * StaticLifetime}'s checker to accept {@code final} instance fields declared on the annotated + * class. */ @Documented @PerfContract From b779f87a8c2605c4419d64046508ce47b70c4940 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 7 Oct 2026 13:10:56 -0400 Subject: [PATCH 11/15] Restructure the @StaticLifetime Javadoc around one field rule Applies a review suggestion: a single accepted/violation/out-of-scope rule, an accurate ClassValue note, and block examples. Keeps the note on the name echoing Rust's 'static lifetime. Examples use

 with escaped @ and generics
so javadoc does not read a leading @ as a block tag.

Co-Authored-By: Claude Opus 5.5 
---
 .../datadog/perfcontract/StaticLifetime.java  | 110 ++++++++----------
 1 file changed, 47 insertions(+), 63 deletions(-)

diff --git a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java
index 4da418c863c..6eb31a63189 100644
--- a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java
+++ b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java
@@ -7,75 +7,59 @@
 import java.lang.annotation.Target;
 
 /**
- * Marks a field that must live for the whole program, not merely for the lifetime of its enclosing
- * instance -- typically a cache or other expensive object whose entire value comes from being
- * amortized across many uses, which a per-instance field can silently defeat if that instance
- * itself doesn't live for the whole program.
+ * Marks a field whose value must be retained for the life of the program to share its setup cost
+ * across uses. A cache rebuilt with each request repeats that cost.
  *
- * 

The name deliberately echoes Rust's {@code 'static} lifetime rather than the Java keyword - * {@code static} itself: the property this enforces is "lives for the whole program." A {@link - * java.lang.ClassValue} is the motivating case for that distinction -- each per-{@code Class} value - * it computes lives for the whole program without itself being declared in a {@code static} field - * anywhere, because {@code ClassValue} does its own process-wide caching internally. The holder - * field that points at the {@code ClassValue} instance still needs to be {@code static final} -- - * see the checker contract below -- it's only the individual per-{@code Class} values inside it - * that get their process-wide lifetime for free. + *

The name echoes Rust's {@code 'static} lifetime rather than the Java keyword {@code static}: + * the property is "lives for the whole program," which a {@code final} field on a {@link Singleton} + * satisfies without being {@code static}. * - *

Motivating defect: a cache constructed per-request, as an instance field on a per-request - * object, instead of once as a {@code static} field -- so the cache was allocated and thrown away - * on every request and never actually amortized anything. It compiled, ran, and passed tests while - * quietly defeating the entire point of caching -- the same silent-failure shape as the other - * perf-contract markers. + *

This marker changes no runtime behavior. The declaration rules below guide AI review; no + * static checker currently enforces them. * - *

This is a documentation-and-tooling marker; it changes no behavior. It exists to telegraph the - * constraint to readers and to give a future checker something to verify. The discipline it names - * is not yet enforced; hold to it by hand until the checker lands. - * - *

On a field ({@link ElementType#FIELD}): the field's value must actually live for the - * whole program, not just for as long as its enclosing instance happens to. - * - *

Checker contract. The rule below is written to be machine-checkable -- by a future - * static checker, or in the meantime by an AI reviewer -- without needing to read this class's - * prose above. This is a declaration-scan check: it looks at how the field is declared, not at - * whether it is, in practice, reused enough to be worth its cost, and it does not attempt to find - * unannotated per-instance caches -- it only guards fields that opt in. + *

Checker contract. Inspect only fields annotated {@code @StaticLifetime}: * *

    - *
  • Accepted (satisfies the contract): a {@code static final} field of the cache / - * expensive-object type; a {@code static final} {@link java.lang.ClassValue}{@code } used - * for a per-class one-shot computation; or a {@code final} instance field, on a class - * annotated {@link Singleton}, whose enclosing instance is thereby guaranteed to be - * process-wide. {@code final} is required in both the static and singleton-scoped-instance - * shapes: a reassignable field can be replaced with a fresh instance at any point, silently - * discarding everything amortized in the old one -- the same defeat this annotation exists to - * catch, just triggered by a write instead of by scope. - *
  • Trigger (violation, scope): an instance field of the annotated type on a class that - * is not annotated {@link Singleton} -- even if, in practice, the class is only ever - * constructed once per logical "session". This check is declaration-local; it does not - * attempt to prove single construction dynamically, so a plain instance field never satisfies - * the contract on its own. - *
  • Trigger (violation, mutability): a {@code static} field of the annotated type, or an - * instance field on a class annotated {@link Singleton}, that is not also {@code final} -- - * reassignable, so nothing stops a fresh instance from silently replacing the amortized one - * at runtime, regardless of scope. - *
  • Violation example (scope): {@code private final DDCache cache = - * DDCaches.newFixedSizeCache(128);} as an instance field of a class constructed per-request, - * per-call, or otherwise more than once for the life of the process. - *
  • Violation example (mutability): {@code private static DDCache cache = - * DDCaches.newFixedSizeCache(128);} -- {@code static} but not {@code final}, so any code with - * write access can swap in a fresh, cold cache and discard everything amortized in the old - * one. - *
  • Compliant example (static): {@code private static final DDCache CACHE = - * DDCaches.newFixedSizeCache(128);} - *
  • Compliant example (singleton-scoped instance): {@code @Singleton class Registry { - * private final DDCache cache = DDCaches.newFixedSizeCache(128); }} - *
  • Out of scope (v1): proving a cache is reused enough to be worth its allocation cost; - * catching a non-static cache that isn't annotated with this marker; a {@code final} field - * whose referent is itself internally mutable/replaceable (e.g. wraps its state in an {@code - * AtomicReference} it swaps) -- {@code final} is checked on the field, not transitively - * through the object graph. Widen this contract only once a real case proves it insufficient, - * rather than guessing ahead of one. + *
  • Accepted: a {@code static final} field, or a {@code final} instance field declared + * on a class annotated {@link Singleton}. The singleton declaration is trusted, not verified. + *
  • Violation: any other annotated field. This includes fields without {@code final} and + * instance fields on classes without {@code @Singleton}, even if a class is constructed only + * once per session. + *
  • Out of scope: unannotated fields, how often a value is reused, and replacement of + * state inside the referenced object. The check does not follow references through the object + * graph. *
+ * + *

Requiring {@code final} prevents reassignment from discarding cached state and repeating + * setup. It does not prevent the referenced object from changing its own state. + * + *

A shared {@link java.lang.ClassValue} satisfies the same field rules. Its values are cached + * per class; computation may be repeated under races or after {@link + * java.lang.ClassValue#remove(Class) remove}. This contract covers the shared holder, not the + * lifetime of each cached value. + * + *

Violation examples: an instance cache on a class without {@code @Singleton}, or a static cache + * without {@code final}. + * + *

+ * @StaticLifetime
+ * private final DDCache<String, String> instanceCache = DDCaches.newFixedSizeCache(128);
+ *
+ * @StaticLifetime
+ * private static DDCache<String, String> staticCache = DDCaches.newFixedSizeCache(128);
+ * 
+ * + *

Compliant examples: + * + *

+ * @StaticLifetime
+ * private static final DDCache<String, String> CACHE = DDCaches.newFixedSizeCache(128);
+ *
+ * @Singleton class Registry {
+ *   @StaticLifetime
+ *   private final DDCache<String, String> cache = DDCaches.newFixedSizeCache(128);
+ * }
+ * 
*/ @Documented @PerfContract From 5fca44d8e4d773b2ecc8afb0b9e47695fcb77cb1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 7 Oct 2026 14:54:52 -0400 Subject: [PATCH 12/15] Require final for @Singleton-held fields in the perf-review StaticLifetime check Matches the @StaticLifetime and @Singleton Javadoc: only a final instance field on a @Singleton class is accepted, and ClassValue holders follow the plain static final rule. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/perf-review/references/checks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/perf-review/references/checks.md b/.agents/skills/perf-review/references/checks.md index e393bc48315..5cb5d81411e 100644 --- a/.agents/skills/perf-review/references/checks.md +++ b/.agents/skills/perf-review/references/checks.md @@ -26,7 +26,7 @@ Format: **pattern** — *expensive when (the interprocedural condition to trace) 6. **FFI / native-boundary crossing on a hot path** *(central to the shared-core effort)* — *a native crossing per-span/per-item (not batched), or transporting strings/objects rather than primitives/IDs* — flag-with-confidence (boundary cost is mechanism-determined; runtime-specific pinning → addendum) — SEV-1/2 (SEV-1 if it blocks/pins under concurrency) — fix: batch (one per flush, not per item); transport interned IDs not strings; keep crossings off the hot/concurrency path. 7. **Escape / allocation-elision defeated** *(Java/Go/.NET/V8 all have a version)* — *a refactor makes a previously-local object escape (stored, returned, captured by a closure, passed to a virtual/non-inlined call) → silent heap allocation on a hot path* — **flag-as-measure** ("may now escape and allocate; verify with an allocation profiler") — SEV-2/3 — fix: keep it local; avoid the escaping store/capture. 8. **`@NoEscape` field-storage violation** — *a field (instance or static, directly or as a generic type argument) declared with an `@NoEscape`-annotated type (`datadog.trace.api.function.NoEscape`), **or** initialized directly from a call to an `@NoEscape`-annotated method — with no comment at the declaration justifying the retention*. The type form marks a concrete type (e.g. `SubSequence`); the method form exists for a return value whose concrete type can't itself carry the annotation (an anonymous class or lambda implementing a JDK interface). The annotation's own javadoc carries a self-contained "Checker contract" section (trigger / not-a-trigger / violation example / compliant example, both forms) written so this can be checked from the diff alone, with no other context needed. The underlying rule is "should", not "must" (RFC-2119 sense): a trigger is a presumptive finding, not an automatic failure — compliant if the declaration carries either a `// Retained on purpose: `-style comment, or `@SuppressPerfContract(value = NoEscape.class, reason = "...")` (`datadog.perfcontract.SuppressPerfContract` — the generic exemption mechanism shared by every perf-contract marker, not `@NoEscape`-specific), or a named canned-exception annotation itself meta-annotated `@SuppressPerfContract` with `NoEscape.class` among its `value` (e.g. a hypothetical `@Borrowed`) — treat the meta-annotation's own `reason` as satisfying the requirement. Don't flag a `SuppressPerfContract`/canned-exception use as non-compliant just for being terse; that mirrors the annotation's own Checker contract. **flag-with-confidence** — SEV-2/3 (SEV-1 if the annotated type/method shares backing storage with something large, per its own javadoc). Current wearers: `SubSequence`, `Maybe` (type form; see J7 below for `SubSequence`'s specific retention-vs-transient discriminator). **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** -9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`) that is not `static final`, not a `static final ClassValue`, and not an instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`)*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache; it only guards fields that opt in. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** +9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`) that is neither `static final` nor a `final` instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`); a shared `ClassValue` holder follows the same rule*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache; it only guards fields that opt in. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's `final` instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** ## Deterministic-lint candidates (DON'T spend AI budget — make these real lints) Fixed-signature, mechanically checkable: From 36646d1fc02c7a3359d2fdfcba04764f67477608 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 7 Oct 2026 15:07:43 -0400 Subject: [PATCH 13/15] Add @ImpliedFor so perf contracts can cover types we don't own @ImpliedFor on a contract annotation lists types the contract is implied for. @StaticLifetime now applies to every ClassValue and ThreadLocal field, since a per-instance one discards its per-class or per-thread values with each instance. The perf-review rubric checks the implied types too. Co-Authored-By: Claude Opus 5.5 --- .../skills/perf-review/references/checks.md | 2 +- .../java/datadog/perfcontract/ImpliedFor.java | 42 +++++++++++++++++++ .../datadog/perfcontract/StaticLifetime.java | 21 ++++++---- 3 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java diff --git a/.agents/skills/perf-review/references/checks.md b/.agents/skills/perf-review/references/checks.md index 5cb5d81411e..e58920cf56c 100644 --- a/.agents/skills/perf-review/references/checks.md +++ b/.agents/skills/perf-review/references/checks.md @@ -26,7 +26,7 @@ Format: **pattern** — *expensive when (the interprocedural condition to trace) 6. **FFI / native-boundary crossing on a hot path** *(central to the shared-core effort)* — *a native crossing per-span/per-item (not batched), or transporting strings/objects rather than primitives/IDs* — flag-with-confidence (boundary cost is mechanism-determined; runtime-specific pinning → addendum) — SEV-1/2 (SEV-1 if it blocks/pins under concurrency) — fix: batch (one per flush, not per item); transport interned IDs not strings; keep crossings off the hot/concurrency path. 7. **Escape / allocation-elision defeated** *(Java/Go/.NET/V8 all have a version)* — *a refactor makes a previously-local object escape (stored, returned, captured by a closure, passed to a virtual/non-inlined call) → silent heap allocation on a hot path* — **flag-as-measure** ("may now escape and allocate; verify with an allocation profiler") — SEV-2/3 — fix: keep it local; avoid the escaping store/capture. 8. **`@NoEscape` field-storage violation** — *a field (instance or static, directly or as a generic type argument) declared with an `@NoEscape`-annotated type (`datadog.trace.api.function.NoEscape`), **or** initialized directly from a call to an `@NoEscape`-annotated method — with no comment at the declaration justifying the retention*. The type form marks a concrete type (e.g. `SubSequence`); the method form exists for a return value whose concrete type can't itself carry the annotation (an anonymous class or lambda implementing a JDK interface). The annotation's own javadoc carries a self-contained "Checker contract" section (trigger / not-a-trigger / violation example / compliant example, both forms) written so this can be checked from the diff alone, with no other context needed. The underlying rule is "should", not "must" (RFC-2119 sense): a trigger is a presumptive finding, not an automatic failure — compliant if the declaration carries either a `// Retained on purpose: `-style comment, or `@SuppressPerfContract(value = NoEscape.class, reason = "...")` (`datadog.perfcontract.SuppressPerfContract` — the generic exemption mechanism shared by every perf-contract marker, not `@NoEscape`-specific), or a named canned-exception annotation itself meta-annotated `@SuppressPerfContract` with `NoEscape.class` among its `value` (e.g. a hypothetical `@Borrowed`) — treat the meta-annotation's own `reason` as satisfying the requirement. Don't flag a `SuppressPerfContract`/canned-exception use as non-compliant just for being terse; that mirrors the annotation's own Checker contract. **flag-with-confidence** — SEV-2/3 (SEV-1 if the annotated type/method shares backing storage with something large, per its own javadoc). Current wearers: `SubSequence`, `Maybe` (type form; see J7 below for `SubSequence`'s specific retention-vs-transient discriminator). **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** -9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`) that is neither `static final` nor a `final` instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`); a shared `ClassValue` holder follows the same rule*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache; it only guards fields that opt in. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's `final` instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** +9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`), **or** an unannotated field whose declared type is listed in the contract's `@ImpliedFor` (`datadog.perfcontract.ImpliedFor` — currently `ClassValue` and `ThreadLocal`, subtypes included), that is neither `static final` nor a `final` instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`)*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache of a non-implied type; it only guards fields that opt in or whose type implies the contract. A deliberate per-instance `ClassValue`/`ThreadLocal` is compliant with `@SuppressPerfContract(value = StaticLifetime.class, reason = "...")`. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's `final` instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** ## Deterministic-lint candidates (DON'T spend AI budget — make these real lints) Fixed-signature, mechanically checkable: diff --git a/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java b/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java new file mode 100644 index 00000000000..a02e0b4e33d --- /dev/null +++ b/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java @@ -0,0 +1,42 @@ +package datadog.perfcontract; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Applies a {@link PerfContract} to types that cannot carry it themselves, such as JDK or + * third-party classes. Annotate the contract annotation with the types its rule is implied for. + * + *

A contract is normally opt-in: it guards only declarations that carry it. Some types have the + * contract by their nature. A non-{@code static} {@link ClassValue} field, for example, discards + * its per-class cache with each instance, yet nobody annotates the broken field. Listing {@code + * ClassValue} here lets tools check every such field. + * + *

This marker changes no runtime behavior. {@link RetentionPolicy#CLASS} lets tools read the + * implied types from the contract's class file, like the contract itself. + * + *

Checker contract. For a contract annotation carrying {@code @ImpliedFor}: + * + *

    + *
  • Field contracts: a field whose declared type is a listed type, or a subtype of one, + * is checked as if it carried the contract. + *
  • Type contracts: a listed type is treated as if it carried the contract. + *
  • Suppression: {@link SuppressPerfContract} exempts an implied finding exactly as it + * exempts an explicit one. + *
  • Invalid: {@code @ImpliedFor} on an annotation that is not itself annotated {@link + * PerfContract}. + *
+ * + *

Add a type only when a real case shows that every declaration of it needs the contract. + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.ANNOTATION_TYPE) +public @interface ImpliedFor { + + /** The types the annotated contract is implied for. */ + Class[] value(); +} diff --git a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java index 6eb31a63189..55588e3c922 100644 --- a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java +++ b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java @@ -17,7 +17,9 @@ *

This marker changes no runtime behavior. The declaration rules below guide AI review; no * static checker currently enforces them. * - *

Checker contract. Inspect only fields annotated {@code @StaticLifetime}: + *

Checker contract. Inspect fields annotated {@code @StaticLifetime}, and fields whose + * declared type is one listed in this annotation's {@link ImpliedFor} (or a subtype of one), as if + * they were annotated: * *

    *
  • Accepted: a {@code static final} field, or a {@code final} instance field declared @@ -25,18 +27,20 @@ *
  • Violation: any other annotated field. This includes fields without {@code final} and * instance fields on classes without {@code @Singleton}, even if a class is constructed only * once per session. - *
  • Out of scope: unannotated fields, how often a value is reused, and replacement of - * state inside the referenced object. The check does not follow references through the object - * graph. + *
  • Out of scope: unannotated fields of types not listed in {@link ImpliedFor}, how + * often a value is reused, and replacement of state inside the referenced object. The check + * does not follow references through the object graph. *
* *

Requiring {@code final} prevents reassignment from discarding cached state and repeating * setup. It does not prevent the referenced object from changing its own state. * - *

A shared {@link java.lang.ClassValue} satisfies the same field rules. Its values are cached - * per class; computation may be repeated under races or after {@link - * java.lang.ClassValue#remove(Class) remove}. This contract covers the shared holder, not the - * lifetime of each cached value. + *

{@link ClassValue} and {@link ThreadLocal} fields are checked without annotation: a + * per-instance one discards its per-class or per-thread values with each instance. A {@code + * ClassValue}'s values are cached per class; computation may be repeated under races or after + * {@link ClassValue#remove(Class) remove}. This contract covers the shared holder, not the lifetime + * of each cached value. A deliberate per-instance holder needs {@link SuppressPerfContract} with a + * reason. * *

Violation examples: an instance cache on a class without {@code @Singleton}, or a static cache * without {@code final}. @@ -63,6 +67,7 @@ */ @Documented @PerfContract +@ImpliedFor({ClassValue.class, ThreadLocal.class}) @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface StaticLifetime {} From be3bda7e18825eaed741ce91172c9885920cc91c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 7 Oct 2026 15:29:37 -0400 Subject: [PATCH 14/15] Keep the implied-type rule in one place StaticLifetime and the perf-review rubric now point to @ImpliedFor instead of restating its matching rule and type list, and @ImpliedFor drops the unused type-contract clause until a type contract adopts it. Co-Authored-By: Claude Opus 5.5 --- .../skills/perf-review/references/checks.md | 2 +- .../java/datadog/perfcontract/ImpliedFor.java | 5 ++--- .../datadog/perfcontract/StaticLifetime.java | 21 ++++++++----------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/.agents/skills/perf-review/references/checks.md b/.agents/skills/perf-review/references/checks.md index e58920cf56c..49b45d50c95 100644 --- a/.agents/skills/perf-review/references/checks.md +++ b/.agents/skills/perf-review/references/checks.md @@ -26,7 +26,7 @@ Format: **pattern** — *expensive when (the interprocedural condition to trace) 6. **FFI / native-boundary crossing on a hot path** *(central to the shared-core effort)* — *a native crossing per-span/per-item (not batched), or transporting strings/objects rather than primitives/IDs* — flag-with-confidence (boundary cost is mechanism-determined; runtime-specific pinning → addendum) — SEV-1/2 (SEV-1 if it blocks/pins under concurrency) — fix: batch (one per flush, not per item); transport interned IDs not strings; keep crossings off the hot/concurrency path. 7. **Escape / allocation-elision defeated** *(Java/Go/.NET/V8 all have a version)* — *a refactor makes a previously-local object escape (stored, returned, captured by a closure, passed to a virtual/non-inlined call) → silent heap allocation on a hot path* — **flag-as-measure** ("may now escape and allocate; verify with an allocation profiler") — SEV-2/3 — fix: keep it local; avoid the escaping store/capture. 8. **`@NoEscape` field-storage violation** — *a field (instance or static, directly or as a generic type argument) declared with an `@NoEscape`-annotated type (`datadog.trace.api.function.NoEscape`), **or** initialized directly from a call to an `@NoEscape`-annotated method — with no comment at the declaration justifying the retention*. The type form marks a concrete type (e.g. `SubSequence`); the method form exists for a return value whose concrete type can't itself carry the annotation (an anonymous class or lambda implementing a JDK interface). The annotation's own javadoc carries a self-contained "Checker contract" section (trigger / not-a-trigger / violation example / compliant example, both forms) written so this can be checked from the diff alone, with no other context needed. The underlying rule is "should", not "must" (RFC-2119 sense): a trigger is a presumptive finding, not an automatic failure — compliant if the declaration carries either a `// Retained on purpose: `-style comment, or `@SuppressPerfContract(value = NoEscape.class, reason = "...")` (`datadog.perfcontract.SuppressPerfContract` — the generic exemption mechanism shared by every perf-contract marker, not `@NoEscape`-specific), or a named canned-exception annotation itself meta-annotated `@SuppressPerfContract` with `NoEscape.class` among its `value` (e.g. a hypothetical `@Borrowed`) — treat the meta-annotation's own `reason` as satisfying the requirement. Don't flag a `SuppressPerfContract`/canned-exception use as non-compliant just for being terse; that mirrors the annotation's own Checker contract. **flag-with-confidence** — SEV-2/3 (SEV-1 if the annotated type/method shares backing storage with something large, per its own javadoc). Current wearers: `SubSequence`, `Maybe` (type form; see J7 below for `SubSequence`'s specific retention-vs-transient discriminator). **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** -9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`), **or** an unannotated field whose declared type is listed in the contract's `@ImpliedFor` (`datadog.perfcontract.ImpliedFor` — currently `ClassValue` and `ThreadLocal`, subtypes included), that is neither `static final` nor a `final` instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`)*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache of a non-implied type; it only guards fields that opt in or whose type implies the contract. A deliberate per-instance `ClassValue`/`ThreadLocal` is compliant with `@SuppressPerfContract(value = StaticLifetime.class, reason = "...")`. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's `final` instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** +9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`), **or** an unannotated field whose declared type is listed in the contract's `@ImpliedFor` (`datadog.perfcontract.ImpliedFor`; read the types from the `@ImpliedFor` on `StaticLifetime` itself, subtypes included), that is neither `static final` nor a `final` instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`)*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache of a non-implied type; it only guards fields that opt in or whose type implies the contract. A deliberate per-instance `ClassValue`/`ThreadLocal` is compliant with `@SuppressPerfContract(value = StaticLifetime.class, reason = "...")`. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's `final` instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** ## Deterministic-lint candidates (DON'T spend AI budget — make these real lints) Fixed-signature, mechanically checkable: diff --git a/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java b/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java index a02e0b4e33d..02fba55c674 100644 --- a/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java +++ b/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java @@ -21,9 +21,8 @@ *

Checker contract. For a contract annotation carrying {@code @ImpliedFor}: * *

    - *
  • Field contracts: a field whose declared type is a listed type, or a subtype of one, - * is checked as if it carried the contract. - *
  • Type contracts: a listed type is treated as if it carried the contract. + *
  • Implied fields: a field whose declared type is a listed type, or a subtype of one, + * is checked as if it carried the contract. Only field contracts use this so far. *
  • Suppression: {@link SuppressPerfContract} exempts an implied finding exactly as it * exempts an explicit one. *
  • Invalid: {@code @ImpliedFor} on an annotation that is not itself annotated {@link diff --git a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java index 55588e3c922..2b1194c27fc 100644 --- a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java +++ b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java @@ -17,9 +17,8 @@ *

    This marker changes no runtime behavior. The declaration rules below guide AI review; no * static checker currently enforces them. * - *

    Checker contract. Inspect fields annotated {@code @StaticLifetime}, and fields whose - * declared type is one listed in this annotation's {@link ImpliedFor} (or a subtype of one), as if - * they were annotated: + *

    Checker contract. Inspect fields annotated {@code @StaticLifetime}, and the fields its + * {@link ImpliedFor} implies it for: * *

      *
    • Accepted: a {@code static final} field, or a {@code final} instance field declared @@ -27,20 +26,18 @@ *
    • Violation: any other annotated field. This includes fields without {@code final} and * instance fields on classes without {@code @Singleton}, even if a class is constructed only * once per session. - *
    • Out of scope: unannotated fields of types not listed in {@link ImpliedFor}, how - * often a value is reused, and replacement of state inside the referenced object. The check - * does not follow references through the object graph. + *
    • Out of scope: unannotated fields that {@link ImpliedFor} does not cover, how often a + * value is reused, and replacement of state inside the referenced object. The check does not + * follow references through the object graph. *
    * *

    Requiring {@code final} prevents reassignment from discarding cached state and repeating * setup. It does not prevent the referenced object from changing its own state. * - *

    {@link ClassValue} and {@link ThreadLocal} fields are checked without annotation: a - * per-instance one discards its per-class or per-thread values with each instance. A {@code - * ClassValue}'s values are cached per class; computation may be repeated under races or after - * {@link ClassValue#remove(Class) remove}. This contract covers the shared holder, not the lifetime - * of each cached value. A deliberate per-instance holder needs {@link SuppressPerfContract} with a - * reason. + *

    The implied types are holders whose per-instance form discards its cached values with each + * instance. A {@link ClassValue}'s values are cached per class; computation may be repeated under + * races or after {@link ClassValue#remove(Class) remove}. This contract covers the shared holder, + * not the lifetime of each cached value. * *

    Violation examples: an instance cache on a class without {@code @Singleton}, or a static cache * without {@code final}. From 014549b641ac9d05eefb70287a21340fafea2981 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 7 Oct 2026 15:35:07 -0400 Subject: [PATCH 15/15] Imply @StaticLifetime for ClassValue only, not ThreadLocal A per-instance ThreadLocal is often deliberate per-owner state (about a third of ThreadLocal fields on master), so implying the contract for it would flag correct code. ClassValue fields are static 17:1. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/perf-review/references/checks.md | 2 +- .../src/main/java/datadog/perfcontract/StaticLifetime.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/perf-review/references/checks.md b/.agents/skills/perf-review/references/checks.md index 49b45d50c95..58772f3ff76 100644 --- a/.agents/skills/perf-review/references/checks.md +++ b/.agents/skills/perf-review/references/checks.md @@ -26,7 +26,7 @@ Format: **pattern** — *expensive when (the interprocedural condition to trace) 6. **FFI / native-boundary crossing on a hot path** *(central to the shared-core effort)* — *a native crossing per-span/per-item (not batched), or transporting strings/objects rather than primitives/IDs* — flag-with-confidence (boundary cost is mechanism-determined; runtime-specific pinning → addendum) — SEV-1/2 (SEV-1 if it blocks/pins under concurrency) — fix: batch (one per flush, not per item); transport interned IDs not strings; keep crossings off the hot/concurrency path. 7. **Escape / allocation-elision defeated** *(Java/Go/.NET/V8 all have a version)* — *a refactor makes a previously-local object escape (stored, returned, captured by a closure, passed to a virtual/non-inlined call) → silent heap allocation on a hot path* — **flag-as-measure** ("may now escape and allocate; verify with an allocation profiler") — SEV-2/3 — fix: keep it local; avoid the escaping store/capture. 8. **`@NoEscape` field-storage violation** — *a field (instance or static, directly or as a generic type argument) declared with an `@NoEscape`-annotated type (`datadog.trace.api.function.NoEscape`), **or** initialized directly from a call to an `@NoEscape`-annotated method — with no comment at the declaration justifying the retention*. The type form marks a concrete type (e.g. `SubSequence`); the method form exists for a return value whose concrete type can't itself carry the annotation (an anonymous class or lambda implementing a JDK interface). The annotation's own javadoc carries a self-contained "Checker contract" section (trigger / not-a-trigger / violation example / compliant example, both forms) written so this can be checked from the diff alone, with no other context needed. The underlying rule is "should", not "must" (RFC-2119 sense): a trigger is a presumptive finding, not an automatic failure — compliant if the declaration carries either a `// Retained on purpose: `-style comment, or `@SuppressPerfContract(value = NoEscape.class, reason = "...")` (`datadog.perfcontract.SuppressPerfContract` — the generic exemption mechanism shared by every perf-contract marker, not `@NoEscape`-specific), or a named canned-exception annotation itself meta-annotated `@SuppressPerfContract` with `NoEscape.class` among its `value` (e.g. a hypothetical `@Borrowed`) — treat the meta-annotation's own `reason` as satisfying the requirement. Don't flag a `SuppressPerfContract`/canned-exception use as non-compliant just for being terse; that mirrors the annotation's own Checker contract. **flag-with-confidence** — SEV-2/3 (SEV-1 if the annotated type/method shares backing storage with something large, per its own javadoc). Current wearers: `SubSequence`, `Maybe` (type form; see J7 below for `SubSequence`'s specific retention-vs-transient discriminator). **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** -9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`), **or** an unannotated field whose declared type is listed in the contract's `@ImpliedFor` (`datadog.perfcontract.ImpliedFor`; read the types from the `@ImpliedFor` on `StaticLifetime` itself, subtypes included), that is neither `static final` nor a `final` instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`)*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache of a non-implied type; it only guards fields that opt in or whose type implies the contract. A deliberate per-instance `ClassValue`/`ThreadLocal` is compliant with `@SuppressPerfContract(value = StaticLifetime.class, reason = "...")`. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's `final` instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** +9. **`@StaticLifetime` field-lifetime violation** — *a field annotated `@StaticLifetime` (`datadog.perfcontract.StaticLifetime`), **or** an unannotated field whose declared type is listed in the contract's `@ImpliedFor` (`datadog.perfcontract.ImpliedFor`; read the types from the `@ImpliedFor` on `StaticLifetime` itself, subtypes included), that is neither `static final` nor a `final` instance field declared on a class annotated `@Singleton` (`datadog.perfcontract.Singleton`)*. Declaration-scan only, same shape as the `@NoEscape` field-storage check above — it does not attempt to prove a field is actually reused enough to be worth caching, and it does not catch an unannotated per-instance cache of a non-implied type; it only guards fields that opt in or whose type implies the contract. A deliberate per-instance holder of an implied type is compliant with `@SuppressPerfContract(value = StaticLifetime.class, reason = "...")`. `@Singleton`, applied to a class, is a trusted, unverified declaration (v1 posture — the same stance the other perf-contract markers take toward `static final` itself) that the class has exactly one process-wide instance; declaring it satisfies `@StaticLifetime` for that class's `final` instance fields. **flag-with-confidence** — SEV-2/3, motivated by a real production defect (a per-request-constructed cache that never actually amortized anything — see the annotation's own javadoc for the full trigger/accepted-form table). Refines the "per-call cache / expensive-object creation that should be static/once" deterministic-lint candidate below by giving that pattern's opted-in subset an explicit annotation and trigger; the unannotated general case is still that line's job. **No lint enforces this yet — the AI reviewer is the only check, so this stays here (not under deterministic-lint candidates below) until a checker lands and it can migrate down.** ## Deterministic-lint candidates (DON'T spend AI budget — make these real lints) Fixed-signature, mechanically checkable: diff --git a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java index 2b1194c27fc..8aacccb78c8 100644 --- a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java +++ b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java @@ -64,7 +64,7 @@ */ @Documented @PerfContract -@ImpliedFor({ClassValue.class, ThreadLocal.class}) +@ImpliedFor(ClassValue.class) @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface StaticLifetime {}