Skip to content
Draft
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
3 changes: 3 additions & 0 deletions dd-trace-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ jmh {
if (project.hasProperty('jmh.profilers')) {
profilers = project.property('jmh.profilers').tokenize(',')
}
if (project.hasProperty('jmh.jvmArgs')) {
jvmArgsAppend = project.property('jmh.jvmArgs').tokenize(' ')
}
if (project.hasProperty('testJvm')) {
def testJvmSpec = new TestJvmSpec(project)
jvm = testJvmSpec.javaTestLauncher.map { it.executablePath.asFile.absolutePath }
Expand Down
16 changes: 14 additions & 2 deletions dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.api.KnownTags;
import datadog.trace.api.Pair;
import datadog.trace.api.SizingHint;
import datadog.trace.api.SizingHintTable;
import datadog.trace.api.TagMap;
import datadog.trace.api.TraceConfig;
import datadog.trace.api.civisibility.config.BazelMode;
Expand Down Expand Up @@ -659,7 +661,8 @@ private CoreTracer(

// Dense known-tag store (experimental, OFF by default): registering the KnownTagCodec resolver
// flips the dense store live so known tags store without a per-tag Entry. Gated by a system
// property for A/B benchmarking; when off, keyOf stays a no-op and tag storage is byte-identical
// property for A/B benchmarking; when off, keyOf stays a no-op and tag storage is
// byte-identical
// to today. Promote to a Config flag if this becomes a permanent rollout.
if (Boolean.getBoolean("dd.trace.dense.tags.enabled")) {
KnownTags.init();
Expand Down Expand Up @@ -2189,6 +2192,14 @@ protected static final DDSpanContext buildSpanContext(
requestContextDataIast = builderRequestContextDataIast;
}

// Per-type dense-store sizing: an entry (local-root) span carries the trace-metadata /
// enriching tags a child doesn't, so pick the lane by whether we have a local parent. A
// resolved hint sizes the span's TagMap and self-tunes on finish; null (no/unkeyable
// operation
// name) falls back to the generic default capacity.
final boolean entrySpan = !(resolvedParentSpanContext instanceof DDSpanContext);
final SizingHint sizingHint = SizingHintTable.hintFor(operationName, entrySpan);

// some attributes are inherited from the parent
context =
new DDSpanContext(
Expand Down Expand Up @@ -2217,7 +2228,8 @@ protected static final DDSpanContext buildSpanContext(
tracer.profilingContextIntegration,
tracer.injectBaggageAsTags,
tracer.injectLinksAsTags,
mergedTracerTagsNeedsIntercept ? null : mergedTracerTags);
mergedTracerTagsNeedsIntercept ? null : mergedTracerTags,
sizingHint);

// By setting the tags on the context we apply decorators to any tags that have been set via
// the builder. This is the order that the tags were added previously, but maybe the `tags`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public boolean isFinished() {
private void finishAndAddToTrace(final long durationNano) {
// ensure a min duration of 1
if (DURATION_NANO_UPDATER.compareAndSet(this, 0, Math.max(1, durationNano))) {
context.recordDenseSize();
setLongRunningVersion(-this.longRunningVersion);
SpanWrapper wrapper = getWrapper();
if (wrapper != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import datadog.trace.api.DDTraceId;
import datadog.trace.api.Functions;
import datadog.trace.api.ProcessTags;
import datadog.trace.api.SizingHint;
import datadog.trace.api.TagMap;
import datadog.trace.api.cache.DDCache;
import datadog.trace.api.cache.DDCaches;
Expand All @@ -21,6 +22,7 @@
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.api.internal.TraceSegment;
import datadog.trace.api.internal.VisibleForTesting;
import datadog.trace.api.sampling.PrioritySampling;
import datadog.trace.api.sampling.SamplingMechanism;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
Expand Down Expand Up @@ -138,6 +140,12 @@ public class DDSpanContext
*/
private final TagMap unsafeTags;

// Per-operation sizing hint (from SizingHintTable, keyed by operation name) this span was sized
// from, if any. Held so the span can feed its final dense-store size back on finish (recordSize)
// -- the self-tuning loop that lets the reused hint converge to the operation's real known-tag
// high-water mark. Null when unsized.
private final SizingHint sizingHint;

/** The service name is required, otherwise the span are dropped by the agent */
private volatile String serviceName;

Expand Down Expand Up @@ -198,6 +206,8 @@ public class DDSpanContext
*/
private volatile Map<String, Object> metaStruct = EMPTY_META_STRUCT;

@Deprecated
@VisibleForTesting
public DDSpanContext(
final DDTraceId traceId,
final long spanId,
Expand Down Expand Up @@ -244,9 +254,12 @@ public DDSpanContext(
ProfilingContextIntegration.NoOp.INSTANCE,
true,
true,
null,
null);
}

@Deprecated
@VisibleForTesting
public DDSpanContext(
final DDTraceId traceId,
final long spanId,
Expand Down Expand Up @@ -295,10 +308,13 @@ public DDSpanContext(
ProfilingContextIntegration.NoOp.INSTANCE,
injectBaggageAsTags,
injectLinksAsTags,
null,
null);
}

/** Back-compat ctor (no read-through parent); delegates with a null parent. */
@Deprecated
@VisibleForTesting
public DDSpanContext(
final DDTraceId traceId,
final long spanId,
Expand Down Expand Up @@ -351,6 +367,7 @@ public DDSpanContext(
profilingContextIntegration,
injectBaggageAsTags,
injectLinksAsTags,
null,
null);
}

Expand Down Expand Up @@ -380,7 +397,8 @@ public DDSpanContext(
final ProfilingContextIntegration profilingContextIntegration,
final boolean injectBaggageAsTags,
final boolean injectLinksAsTags,
final TagMap readThroughParent) {
final TagMap readThroughParent,
final SizingHint sizingHint) {

assert traceCollector != null;
this.traceCollector = traceCollector;
Expand Down Expand Up @@ -412,7 +430,8 @@ public DDSpanContext(
this.unsafeTags =
readThroughParent != null
? TagMap.createFromParent(readThroughParent)
: TagMap.create(capacity);
: sizingHint != null ? TagMap.create(sizingHint) : TagMap.create(capacity);
this.sizingHint = sizingHint;

// must set this before setting the service and resource names below
this.profilingContextIntegration = profilingContextIntegration;
Expand Down Expand Up @@ -963,6 +982,21 @@ public void setTag(final String tag, final String value) {
}
}

/**
* Feeds this span's final dense-store size back into the sizingHint it was created from (if any).
* Called once at span finish; lets a reused hint self-tune to the operation's observed known-tag
* high-water mark, so subsequent spans of the operation are sized correctly.
*/
void recordDenseSize() {
if (sizingHint != null) {
// Intentionally unsynchronized: recordSize is benign-racy by design (monotonic-max on a plain
// int), and a stale knownCount read only ever costs one extra growth on a later span. The
// lock
// would add nothing but contention on the finish path.
unsafeTags.recordSize(sizingHint);
}
}

public void setTag(TagMap.EntryReader entry) {
if (entry == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public class DenseStoreAllocBenchmark {

private String[] keys;
private String[] values;
// Per-operation sizing hint seeded to this scenario's known-tag count -- what SizingHintTable
// supplies.
private SizingHint sizingHint;

@Setup(Level.Trial)
public void setup() {
Expand All @@ -108,8 +111,11 @@ public void setup() {
this.keys[i] = i < knownCount ? KNOWN[i] : "custom.tag." + i;
this.values[i] = "value-" + i;
}
// Size the dense store to the known-tag count (the "end state" per-operation sizing).
this.sizingHint = new SizingHint("bench", 0, Math.max(knownCount, 1));
}

/** Current: generic default dense capacity (KNOWN_INIT_CAP=12) -- over-provisions small types. */
@Benchmark
public TagMap buildMap() {
TagMap m = TagMap.create(16);
Expand All @@ -119,6 +125,16 @@ public TagMap buildMap() {
return m;
}

/** End state: dense store sized per-operation via a SizingHint -- no over-provision. */
@Benchmark
public TagMap buildMapSized() {
TagMap m = TagMap.create(sizingHint);
for (int i = 0; i < tagCount; i++) {
m.set(keys[i], values[i]);
}
return m;
}

@Benchmark
public void buildAndSerialize(Blackhole bh) {
TagMap m = TagMap.create(16);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package datadog.trace.util;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

/**
* Directional: is the {@link FlatHashtable} hit-path lookup (what a span pays per create, all hits
* after warmup) cheap vs a {@link HashMap}? Concrete-typed {@code static final} helper so the
* static-poly specialization is in play. One op = one pass over the op-name set. Single-threaded,
* short — a rough signal, not a verdict (real numbers on the box).
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3, time = 1)
@Fork(1)
@Threads(1)
public class FlatHashtableBenchmark {

static final class StrHelper extends FlatHashtable.StringHelper<String> {
@Override
public boolean matches(String key, String value) {
return key == value || key.equals(value);
}

@Override
public String create(String key) {
return key; // store the key itself as the (self-identifying) entry
}
}

private static final StrHelper HELPER = new StrHelper();

private static final String[] KEYS = {
"servlet.request",
"database.query",
"http.request",
"grpc.client",
"kafka.produce",
"kafka.consume",
"jdbc.query",
"spring.handler",
"servlet.forward",
"okhttp.request"
};

private String[] table;
private Map<String, String> map;

@Setup
public void setup() {
table = FlatHashtable.create(String.class, KEYS.length);
map = new HashMap<>(KEYS.length * 2);
for (String k : KEYS) {
FlatHashtable.getOrCreate(table, k, HELPER);
map.put(k, k);
}
}

/** FlatHashtable all-hit lookups (concrete helper → specialized). */
@Benchmark
public void flatGet(Blackhole bh) {
for (String k : KEYS) {
bh.consume(FlatHashtable.get(table, k, HELPER));
}
}

/** Steady-state span-create shape: get-then-getOrCreate, all hits. */
@Benchmark
public void flatGetOrCreate(Blackhole bh) {
for (String k : KEYS) {
bh.consume(FlatHashtable.getOrCreate(table, k, HELPER));
}
}

/** Baseline: HashMap lookups over the same keys. */
@Benchmark
public void hashMapGet(Blackhole bh) {
for (String k : KEYS) {
bh.consume(map.get(k));
}
}
}
27 changes: 27 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/SizingHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package datadog.trace.api;

import datadog.trace.util.FlatHashtable;

/**
* {@link FlatHashtable} policy for the per-operation {@link SizingHint} table: keys by operation
* name, entries are {@code SizingHint}s carrying that name plus its cached spread hash. Stateless —
* held by {@link SizingHintTable} as a concrete-typed {@code static final} singleton so {@code
* FlatHashtable.get}/{@code getOrCreate} specialize (devirtualize + inline) at the call site.
*
* <p>Extends {@link FlatHashtable.StringHelper}, which seals the spread {@code hash}; this class
* only supplies {@code matches} and {@code create}. Both use the inherited {@code hash} so the
* cached {@link SizingHint#labelHash} is always the same spread the probe used.
*/
final class SizingHelper extends FlatHashtable.StringHelper<SizingHint> {
@Override
public boolean matches(String key, SizingHint value) {
// int gate on the cached hash before equals; op-names are usually interned literals, so `==` is
// the common hit.
return value.labelHash == hash(key) && (key == value.label || key.equals(value.label));
}

@Override
public SizingHint create(String key) {
return new SizingHint(key, hash(key), SizingHintTable.SEED_SIZE);
}
}
49 changes: 49 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/SizingHint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package datadog.trace.api;

/**
* Opaque per-operation dense-store sizing hint, and a self-contained {@link
* datadog.trace.util.FlatHashtable} slot: it carries everything the probe compares ({@link #label}
* + cached {@link #labelHash}) plus the tuned payload ({@link #size}). Holding key, hash, and value
* in ONE object behind ONE array slot is deliberate — entry publication is a single reference
* store, so a reader sees {@code null} or a complete entry (never a torn one), and the {@code
* final} identity fields are visible even under racy publication (JMM final-field guarantee). That
* sidesteps the memory-ordering / visibility problems parallel key/hash/value arrays would create,
* no volatile or atomics.
*
* <p>Opaque to everything outside {@code datadog.trace.api}: no public members. {@link TagMap}
* reads {@link #size} to size a fresh dense store and writes it back (monotonic-max) at a terminal
* point; {@code SizingHelper} mints and compares by {@link #label}/{@link #labelHash}. Callers only
* ever hold the reference.
*
* <p>{@link #labelHash} is supplied by the helper (a single spread source — {@code
* FlatHashtable.StringHelper.hash}) so the cached gate always matches the probe hash.
*/
public final class SizingHint {
// Identity: final => safely published under a racy single-reference store. `label` is the
// operation
// name (typically an interned literal, so the `==` fast-path usually hits). `labelHash` is the
// helper's spread hash, cached to gate `equals` with an int compare during probing.
final String label;
final int labelHash;

// Payload: the tuned dense-store size. Plain racy int, updated monotonic-max — a stale/lost read
// only mis-sizes an array (over/under-provision), never corrupts tag data, so no synchronization.
int size;

// When true, {@code size} is fixed and recordSize won't grow it. For the shared default /
// overflow
// hint (a HETEROGENEOUS catch-all for operation-less / over-budget spans): self-tuning it via
// monotonic-max would converge to the max across unlike sharers and over-provision the lean ones.
final boolean capped;

SizingHint(String label, int labelHash, int seedSize) {
this(label, labelHash, seedSize, false);
}

SizingHint(String label, int labelHash, int seedSize, boolean capped) {
this.label = label;
this.labelHash = labelHash;
this.size = seedSize;
this.capped = capped;
}
}
Loading
Loading