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
9 changes: 5 additions & 4 deletions dd-openfeature/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@ java {

dependencies {
api("dev.openfeature:sdk:1.22.0")
api("io.opentelemetry:opentelemetry-api:1.66.0")
api(libs.jackson.core)

testImplementation(libs.bundles.junit5)
testImplementation(libs.bundles.mockito)
}

tasks.withType<JavaCompile>().configureEach {
configureCompiler(JavaVersion.VERSION_11)
}

jmh {
jmhVersion = libs.versions.jmh.get()
}

tasks.withType<JavaCompile>().configureEach {
configureCompiler(JavaVersion.VERSION_11)
}
3 changes: 3 additions & 0 deletions dd-openfeature/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ commons-io:commons-io:2.21.0=spotbugs
de.thetaphi:forbiddenapis:3.11=compileClasspath,jmhCompileClasspath
dev.openfeature:sdk:1.22.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.leangen.geantyref:geantyref:1.3.16=jmhRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-api:1.66.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-common:1.66.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-context:1.66.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jaxen:jaxen:2.0.6=spotbugs
net.bytebuddy:byte-buddy-agent:1.18.12=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.18.12=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package com.datadog.openfeature;

import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

import com.datadog.openfeature.internal.flagevaluation.FlagEvalEvent;
import com.datadog.openfeature.internal.flagevaluation.FlagEvaluationWriter;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.FlagValueType;
import dev.openfeature.sdk.HookContext;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

/**
* Evaluation-thread benchmark for the OpenFeature hook: the inline cost a caller actually pays
* inside FlagEvalLoggingHook.finallyAfter, across context shapes.
*
* <p>This is the counterpart to FlagEvaluationHotPathBenchmark, which measures the writer queue and
* the worker-thread aggregation.
*
* <p>The dominant inline cost under consent-on is DDEvaluator.copyPrunedContext: one bounded walk
* of the caller-owned EvaluationContext. contextCopy isolates that walk so its share of
* hookFinallyAfter is directly readable. Under consent-off the hook skips the copy entirely, which
* hookFinallyAfterConsentOff measures as the protected-path floor.
*
* <p>The writer used here is a no-op that discards events, so no queue or worker cost is included.
*
* <p>Run: {@code ./gradlew :dd-openfeature:jmh -Pjmh.includes=FlagEvalHookHotPathBenchmark}.
*/
@State(Scope.Benchmark)
@Warmup(iterations = 3, time = 2, timeUnit = SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = SECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(NANOSECONDS)
@Fork(value = 1)
public class FlagEvalHookHotPathBenchmark {

/**
* Context shapes. The three 100-leaf shapes (flat/100attrs, nested/10structs_10fields,
* list/10lists_10items) carry the same leaf count under different structure, so the spread
* between them isolates shape cost from leaf count.
*/
@Param({
"flat/0attrs",
"flat/10attrs",
"flat/100attrs",
"nested/10structs_10fields",
"list/10lists_10items"
})
public String shape;

private HookContext<Object> hookContext;
private FlagEvaluationDetails<Object> consentOnDetails;
private FlagEvaluationDetails<Object> consentOffDetails;
private FlagEvalLoggingHook<Object> hook;

@Setup(Level.Trial)
public void setUp() {
final MutableContext ctx = buildContext(shape);
hookContext =
HookContext.builder()
.flagKey("bench-flag")
.type(FlagValueType.STRING)
.defaultValue("default")
.ctx(ctx)
.build();

consentOnDetails = details(true);
consentOffDetails = details(false);

// Discarding writer: isolates hook-inline cost from queue mechanics.
final FlagEvaluationWriter writer = new NoOpWriter();
hook = new FlagEvalLoggingHook<>(() -> writer);
}

/** Total inline cost under consent-on: scalar extraction, bounded context copy, and enqueue. */
@Benchmark
public void hookFinallyAfter() {
hook.finallyAfter(hookContext, consentOnDetails, Collections.emptyMap());
}

/** Protected-path floor: consent-off skips the context copy, leaving scalar work plus enqueue. */
@Benchmark
public void hookFinallyAfterConsentOff() {
hook.finallyAfter(hookContext, consentOffDetails, Collections.emptyMap());
}

/** The bounded context copy alone - the component that scales with context shape. */
@Benchmark
public void contextCopy(final Blackhole blackhole) {
blackhole.consume(DDEvaluator.copyPrunedContext(hookContext.getCtx()));
}

private static FlagEvaluationDetails<Object> details(final boolean observeFullEvaluationData) {
return FlagEvaluationDetails.builder()
.flagKey("bench-flag")
.value("on-value")
.variant("on")
.reason(Reason.TARGETING_MATCH.name())
.flagMetadata(
ImmutableMetadata.builder()
.addString("allocationKey", "alloc-1")
.addLong("__dd_eval_timestamp_ms", 1_700_000_000_000L)
.addBoolean(
DDEvaluator.METADATA_OBSERVE_FULL_EVALUATION_DATA, observeFullEvaluationData)
.build())
.build();
}

private static MutableContext buildContext(final String shape) {
final MutableContext ctx = new MutableContext("bench-user");
if ("flat/0attrs".equals(shape)) {
return ctx;
}
if ("flat/10attrs".equals(shape)) {
return addFlat(ctx, 10);
}
if ("flat/100attrs".equals(shape)) {
return addFlat(ctx, 100);
}
if ("nested/10structs_10fields".equals(shape)) {
for (int i = 0; i < 10; i++) {
final Map<String, Value> inner = new HashMap<>();
for (int j = 0; j < 10; j++) {
inner.put("field" + j, new Value("value" + j));
}
ctx.add("struct" + i, new ImmutableStructure(inner));
}
return ctx;
}
if ("list/10lists_10items".equals(shape)) {
for (int i = 0; i < 10; i++) {
final List<Value> items = new ArrayList<>(10);
for (int j = 0; j < 10; j++) {
items.add(new Value("value" + j));
}
ctx.add("list" + i, items);
}
return ctx;
}
throw new IllegalArgumentException("unknown benchmark shape: " + shape);
}

private static MutableContext addFlat(final MutableContext ctx, final int count) {
for (int i = 0; i < count; i++) {
ctx.add("field" + i, "value" + i);
}
return ctx;
}

/** Discards events so only hook-inline work is measured. */
private static final class NoOpWriter implements FlagEvaluationWriter {
@Override
public void enqueue(final FlagEvalEvent event) {}

@Override
public boolean hasCapacityForEnqueue() {
return true;
}

@Override
public void countPreQueueOverflow() {}

@Override
public void countContextTruncated(final String reason) {}
}
}
Loading
Loading