diff --git a/communication/src/main/java/datadog/communication/serialization/EncodingCache.java b/communication/src/main/java/datadog/communication/serialization/EncodingCache.java index 4833eb8695a..d0b6bc086b3 100644 --- a/communication/src/main/java/datadog/communication/serialization/EncodingCache.java +++ b/communication/src/main/java/datadog/communication/serialization/EncodingCache.java @@ -1,6 +1,13 @@ package datadog.communication.serialization; +import datadog.trace.api.function.BackgroundOnly; + // TODO @FunctionalInterface +/** + * {@link BackgroundOnly}: every implementation ({@link SimpleUtf8Cache}, {@link + * GenerationalUtf8Cache}) is only safe to call from the writer/serializer thread that owns it. + */ +@BackgroundOnly public interface EncodingCache { byte[] encode(CharSequence s); diff --git a/communication/src/main/java/datadog/communication/serialization/GenerationalUtf8Cache.java b/communication/src/main/java/datadog/communication/serialization/GenerationalUtf8Cache.java index 099ff56e0e0..d0911907116 100644 --- a/communication/src/main/java/datadog/communication/serialization/GenerationalUtf8Cache.java +++ b/communication/src/main/java/datadog/communication/serialization/GenerationalUtf8Cache.java @@ -1,5 +1,6 @@ package datadog.communication.serialization; +import datadog.trace.api.function.BackgroundOnly; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.nio.charset.StandardCharsets; import javax.annotation.concurrent.ThreadSafe; @@ -15,6 +16,11 @@ * String#getBytes(java.nio.charset.Charset)}. * *

The cache is thread safe. + * + *

{@link BackgroundOnly}: the eden/tenured promotion and recalibration bookkeeping costs more + * than the {@code getBytes} call it replaces, so the saving only shows up as reduced allocation/GC + * pressure on the thread that pays it -- confine it to the background serializer thread, not an + * application thread. */ /* * Cache works by using a 2-level promotion based scheme. @@ -64,6 +70,7 @@ * provide better cache utilization. */ @ThreadSafe +@BackgroundOnly @SuppressFBWarnings( value = "IS2_INCONSISTENT_SYNC", justification = diff --git a/communication/src/main/java/datadog/communication/serialization/SimpleUtf8Cache.java b/communication/src/main/java/datadog/communication/serialization/SimpleUtf8Cache.java index 8eb12d48465..2398c4d4e05 100644 --- a/communication/src/main/java/datadog/communication/serialization/SimpleUtf8Cache.java +++ b/communication/src/main/java/datadog/communication/serialization/SimpleUtf8Cache.java @@ -1,5 +1,6 @@ package datadog.communication.serialization; +import datadog.trace.api.function.BackgroundOnly; import java.nio.charset.StandardCharsets; import javax.annotation.concurrent.ThreadSafe; @@ -13,6 +14,11 @@ * String#getBytes(java.nio.charset.Charset)}. * *

The cache is thread safe. + * + *

{@link BackgroundOnly}: the bookkeeping (hit counting, LFU eviction scan) costs more than the + * {@code getBytes} call it replaces, so the saving only shows up as reduced allocation/GC pressure + * on the thread that pays it -- confine it to the background serializer thread, not an application + * thread. */ /* * Thread safety is achieved through using CacheEntry objects where the key data @@ -43,6 +49,7 @@ * a LFU: least frequently used eviction policy is used to free up a slot. */ @ThreadSafe +@BackgroundOnly public final class SimpleUtf8Cache implements EncodingCache { static final int MAX_CAPACITY = 1024; diff --git a/docs/instrumentation_design_guidelines.md b/docs/instrumentation_design_guidelines.md index 305ff651439..6e4676e49d1 100644 --- a/docs/instrumentation_design_guidelines.md +++ b/docs/instrumentation_design_guidelines.md @@ -81,3 +81,19 @@ Context ctx = Context.current(); // GOOD - use the bytecode bridge, static-imported Context ctx = currentContext(); ``` + +### 4. Foreground/background thread cost contract + +**Why it matters:** + +- Advice bodies and span lifecycle methods run on the application (foreground) thread, where added cost is + customer-visible latency, as opposed to a background thread the tracer owns and paces itself +- Code meant to run only on a background thread must never become reachable from a foreground call site + +**What to do:** + +Mark code with `datadog.trace.api.function.ForegroundSafe` (cheap enough for either thread) or `.BackgroundOnly` +(must never be reached from the foreground) where the distinction matters, and see their Javadoc for the full +contract, including the inheritance-narrowing rule for overrides. This is a documentation-and-tooling convention +today (not yet compiler- or runtime-enforced); it is checked by hand and by the `dd-apm-sdk-review` skill's +performance lens. diff --git a/internal-api/src/main/java/datadog/trace/api/cache/DDCache.java b/internal-api/src/main/java/datadog/trace/api/cache/DDCache.java index d1cd8024847..6a39b5bda96 100644 --- a/internal-api/src/main/java/datadog/trace/api/cache/DDCache.java +++ b/internal-api/src/main/java/datadog/trace/api/cache/DDCache.java @@ -1,5 +1,6 @@ package datadog.trace.api.cache; +import datadog.trace.api.function.BackgroundOnly; import java.util.function.BiConsumer; import java.util.function.Function; @@ -16,6 +17,13 @@ public interface DDCache { /** Clear the cache. */ void clear(); - /** Visits elements currently in the cache; for debugging/triage purposes. */ + /** + * Visits elements currently in the cache; for debugging/triage purposes. + * + *

{@link BackgroundOnly}: every implementation invokes the caller-supplied {@code consumer} + * once per entry over a full scan, so the cost is unbounded regardless of the implementation's + * own scan shape. + */ + @BackgroundOnly void visit(BiConsumer consumer); } diff --git a/internal-api/src/main/java/datadog/trace/api/cache/FixedSizeCache.java b/internal-api/src/main/java/datadog/trace/api/cache/FixedSizeCache.java index a8f5fc5f3fa..3baef9a2d13 100644 --- a/internal-api/src/main/java/datadog/trace/api/cache/FixedSizeCache.java +++ b/internal-api/src/main/java/datadog/trace/api/cache/FixedSizeCache.java @@ -1,6 +1,8 @@ package datadog.trace.api.cache; import datadog.trace.api.Pair; +import datadog.trace.api.function.BackgroundOnly; +import datadog.trace.api.function.ForegroundSafe; import java.util.Arrays; import java.util.function.BiConsumer; import java.util.function.Function; @@ -16,6 +18,14 @@ * computeIfAbsent is idempotent, or otherwise you might not get back the value you expect * from a cache lookup. * + *

{@link ForegroundSafe} applies to {@code computeIfAbsent}'s probing -- at most 3 slot checks + * against a fixed-size array, no growth, no eviction sweep -- and to {@code clear()}, a plain array + * fill over the same fixed size. Neither covers the caller-supplied {@code producer} passed to + * {@code computeIfAbsent}, whose cost is the caller's responsibility exactly as with any + * higher-order method. {@code visit()} is {@link BackgroundOnly}: it invokes an arbitrary + * caller-supplied {@code consumer} once per entry, so its cost is unbounded regardless of the + * cache's own (bounded) scan. + * * @param key type * @param value type */ @@ -66,6 +76,7 @@ static int calculateSize(int capacity) { * @param producer how to create a cached value base on the key if the lookup fails * @return the cached or created and stored value */ + @ForegroundSafe @Override public final V computeIfAbsent(K key, Function producer) { if (key == null) { @@ -99,11 +110,13 @@ public final V computeIfAbsent(K key, Function producer) { return value; } + @ForegroundSafe @Override public void clear() { Arrays.fill(elements, null); } + @BackgroundOnly @Override public void visit(BiConsumer consumer) { for (Pair e : elements) { diff --git a/internal-api/src/main/java/datadog/trace/api/function/BackgroundOnly.java b/internal-api/src/main/java/datadog/trace/api/function/BackgroundOnly.java index 242652be53d..495cceff866 100644 --- a/internal-api/src/main/java/datadog/trace/api/function/BackgroundOnly.java +++ b/internal-api/src/main/java/datadog/trace/api/function/BackgroundOnly.java @@ -20,14 +20,69 @@ *

The two markers are not symmetric -- see {@link ForegroundSafe} for why it, not this * one, is the strictly stronger guarantee. * + *

Orthogonal to {@code @ThreadSafe} (JSR-305): that annotation says concurrent calls from + * multiple callers are safe; this one says which category of caller is valid at all -- background + * threads (there may be more than one), never the foreground/application thread. A class can, and + * often will, carry both. + * *

On a type ({@link ElementType#TYPE}): every method of this type is background-only * unless a method-level {@link ForegroundSafe} widens it. * *

On a method ({@link ElementType#METHOD}): this method specifically is background-only, * regardless of what the enclosing type declares -- a method-level marker always wins over the * type-level one. + * + *

Inheritance direction. An override may only narrow a supertype's declared + * cost, never widen it -- the same variance rule as a covariant return type, applied to a cost + * contract instead of a value type, and the same logic LSP applies to pre/postconditions: only a + * promise the supertype actually made can be broken by an override. A method overriding a + * {@code @BackgroundOnly} supertype/interface method may itself be marked {@link ForegroundSafe} if + * that override happens to be cheap: every caller holding a reference typed to the supertype + * already assumed the worse (background-only) case, so a cheaper override can't surprise them. The + * reverse can't be done safely when the supertype method is explicitly {@link ForegroundSafe}: + * overriding it and marking the override {@code @BackgroundOnly} breaks the promise for every + * existing caller holding a supertype-typed reference, without their code changing at all -- this + * is a violation at the declaration site itself, independent of whether any foreground call site + * currently exists in the diff. An unannotated supertype/interface method makes no cost + * promise at all, so marking an override {@code @BackgroundOnly} breaks nothing -- no caller + * holding the supertype-typed reference had assumed either case. + * + *

Checker contract. The rule below is written to be machine-checkable -- by a future + * static checker, or in the meantime by an AI reviewer (see the {@code dd-apm-sdk-review} skill's + * performance override, addendum J16) -- without needing to read this class's prose above. + * + *

*/ @Documented -@Retention(RetentionPolicy.SOURCE) +@Retention(RetentionPolicy.CLASS) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface BackgroundOnly {} diff --git a/internal-api/src/main/java/datadog/trace/api/function/ForegroundSafe.java b/internal-api/src/main/java/datadog/trace/api/function/ForegroundSafe.java index b1c0985dc3c..1bb1ce5d2df 100644 --- a/internal-api/src/main/java/datadog/trace/api/function/ForegroundSafe.java +++ b/internal-api/src/main/java/datadog/trace/api/function/ForegroundSafe.java @@ -29,8 +29,21 @@ *

On a method ({@link ElementType#METHOD}): this method specifically is foreground-safe, * regardless of what the enclosing type declares -- a method-level marker always wins over the * type-level one. + * + *

Inheritance direction: like a behavioral-subtyping (LSP) contract, only an actual + * promise from the supertype can be broken -- an override may narrow a {@link BackgroundOnly} + * supertype/interface method to {@code @ForegroundSafe} (a cheaper override can't surprise a caller + * who already assumed the worse case) but may never widen an explicitly {@code @ForegroundSafe} + * supertype method to {@link BackgroundOnly}. An unannotated supertype method makes no + * promise either way, so marking an override {@link BackgroundOnly} is not a violation -- see + * {@link BackgroundOnly}'s "Inheritance direction" for the full rule. + * + *

Checker contract. This annotation is not itself a trigger -- it is what makes a call + * site not suspect. See {@link BackgroundOnly}'s checker contract for the actual rule: + * code marked {@code @ForegroundSafe} (or carrying no marker) is exactly the caller side of the + * violation that contract flags when it reaches a {@code @BackgroundOnly} symbol. */ @Documented -@Retention(RetentionPolicy.SOURCE) +@Retention(RetentionPolicy.CLASS) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface ForegroundSafe {}