diff --git a/.agents/skills/perf-review/references/checks.md b/.agents/skills/perf-review/references/checks.md index 38b469fbec2..e393bc48315 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`) 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.** ## 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/Singleton.java b/internal-api/src/main/java/datadog/perfcontract/Singleton.java new file mode 100644 index 00000000000..ba28bf73442 --- /dev/null +++ b/internal-api/src/main/java/datadog/perfcontract/Singleton.java @@ -0,0 +1,45 @@ +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 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 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. + */ +@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..4da418c863c --- /dev/null +++ b/internal-api/src/main/java/datadog/perfcontract/StaticLifetime.java @@ -0,0 +1,84 @@ +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 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." 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 + * 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 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. + * + *

+ */ +@Documented +@PerfContract +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.FIELD) +public @interface StaticLifetime {}