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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,6 +16,11 @@
* String#getBytes(java.nio.charset.Charset)}.
*
* <p>The cache is thread safe.
*
* <p>{@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.
Expand Down Expand Up @@ -64,6 +70,7 @@
* provide better cache utilization.
*/
@ThreadSafe
@BackgroundOnly
@SuppressFBWarnings(
value = "IS2_INCONSISTENT_SYNC",
justification =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package datadog.communication.serialization;

import datadog.trace.api.function.BackgroundOnly;
import java.nio.charset.StandardCharsets;
import javax.annotation.concurrent.ThreadSafe;

Expand All @@ -13,6 +14,11 @@
* String#getBytes(java.nio.charset.Charset)}.
*
* <p>The cache is thread safe.
*
* <p>{@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
Expand Down Expand Up @@ -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;

Expand Down
16 changes: 16 additions & 0 deletions docs/instrumentation_design_guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -16,6 +17,13 @@ public interface DDCache<K, V> {
/** 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.
*
* <p>{@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<K, V> consumer);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,6 +18,14 @@
* computeIfAbsent</code> is idempotent, or otherwise you might not get back the value you expect
* from a cache lookup.
*
* <p>{@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 <K> key type
* @param <V> value type
*/
Expand Down Expand Up @@ -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<K, ? extends V> producer) {
if (key == null) {
Expand Down Expand Up @@ -99,11 +110,13 @@ public final V computeIfAbsent(K key, Function<K, ? extends V> producer) {
return value;
}

@ForegroundSafe
@Override
public void clear() {
Arrays.fill(elements, null);
}

@BackgroundOnly
@Override
public void visit(BiConsumer<K, V> consumer) {
for (Pair<K, V> e : elements) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,69 @@
* <p>The two markers are <b>not symmetric</b> -- see {@link ForegroundSafe} for why it, not this
* one, is the strictly stronger guarantee.
*
* <p>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.
*
* <p><b>On a type</b> ({@link ElementType#TYPE}): every method of this type is background-only
* unless a method-level {@link ForegroundSafe} widens it.
*
* <p><b>On a method</b> ({@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.
*
* <p><b>Inheritance direction.</b> An override may only narrow a supertype's <em>declared</em>
* 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 <b>unannotated</b> 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.
*
* <p><b>Checker contract.</b> 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.
*
* <ul>
* <li><b>Trigger:</b> a call site in code that is unannotated or {@link ForegroundSafe} and is
* reachable from an application/foreground call site (a span lifecycle method, an
* instrumentation advice body, anything on the app thread), into a symbol (type or method)
* annotated {@code @BackgroundOnly}.
* <li><b>Not a trigger:</b> a call from code that is itself {@code @BackgroundOnly} -- calls
* between background-only code are fine, only the foreground-to-background crossing is the
* violation. Also not a trigger: a call into a symbol that carries neither marker -- the
* absence of an annotation is not itself a finding, only a call that provably crosses a
* declared {@code @BackgroundOnly} boundary counts.
* <li><b>Violation example:</b> a span-lifecycle method or advice body calling {@code
* sharedUtf8Cache.getUtf8(value)} where {@code sharedUtf8Cache}'s declared type is annotated
* {@code @BackgroundOnly}.
* <li><b>Compliant example:</b> the same call made only from the background serializer thread
* that owns the cache, or from a method itself marked {@code @BackgroundOnly}.
* <li><b>Also a trigger, at the declaration site:</b> a method overriding an explicitly {@link
* ForegroundSafe} supertype/interface method that marks the override {@code @BackgroundOnly}
* -- see "Inheritance direction" above. Flag this the moment it appears; it does not require
* a foreground call site to exist yet. Overriding an <b>unannotated</b> supertype/interface
* method with {@code @BackgroundOnly} is not a trigger -- the supertype made no promise to
* break.
* <li><b>Out of scope (v1):</b> resolution is grep-only today (there is no APT-generated manifest
* yet -- {@code APMLP-1645}), so a callee outside the diff costs a grep per unfamiliar symbol
* rather than a lookup; reflection and dynamic-proxy call sites are not resolved at all.
* <li><b>Propagation across an interface boundary</b> (concrete implementation to the interface
* method it implements, and one hop further into a default method that calls it) is a bounded
* extension of the direct-call trigger above, not a separate rule -- see the {@code
* dd-apm-sdk-review} skill's performance override, addendum {@code background-only-contract},
* for its exact two-hop scope.
* </ul>
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface BackgroundOnly {}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,21 @@
* <p><b>On a method</b> ({@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.
*
* <p><b>Inheritance direction:</b> 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 <em>unannotated</em> 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.
*
* <p><b>Checker contract.</b> This annotation is not itself a trigger -- it is what makes a call
* site <em>not</em> 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 {}
Loading