diff --git a/.agents/skills/perf-review/references/checks.md b/.agents/skills/perf-review/references/checks.md index 38b469fbec2..58772f3ff76 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 — 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 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: -- 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/perfcontract/ImpliedFor.java b/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java new file mode 100644 index 00000000000..02fba55c674 --- /dev/null +++ b/internal-api/src/main/java/datadog/perfcontract/ImpliedFor.java @@ -0,0 +1,41 @@ +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}: + * + *

+ * + *

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/Singleton.java b/internal-api/src/main/java/datadog/perfcontract/Singleton.java new file mode 100644 index 00000000000..368f18a7b1b --- /dev/null +++ b/internal-api/src/main/java/datadog/perfcontract/Singleton.java @@ -0,0 +1,26 @@ +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; + +/** + * 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 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. + * + *

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 +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface Singleton {} diff --git a/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java new file mode 100644 index 00000000000..8aacccb78c8 --- /dev/null +++ b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java @@ -0,0 +1,70 @@ +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; + +/** + * 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 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}. + * + *

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 the fields its + * {@link ImpliedFor} implies it for: + * + *

+ * + *

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

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}. + * + *

+ * @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 +@ImpliedFor(ClassValue.class) +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.FIELD) +public @interface StaticLifetime {}