From df49024471836cfcb59a8f0531209c659f377f4b Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Mon, 15 Sep 2025 10:58:02 +0200 Subject: [PATCH 1/9] add bench Signed-off-by: christian.lutnik --- .../sdk/benchmark/AllocationBenchmark.java | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index 5bc89d03d..eea91632e 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -32,9 +32,9 @@ public class AllocationBenchmark { // 10K iterations works well with Xmx1024m (we don't want to run out of memory) private static final int ITERATIONS = 10000; - @Benchmark - @BenchmarkMode(Mode.SingleShotTime) - @Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) + //@Benchmark + //@BenchmarkMode(Mode.SingleShotTime) + //@Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) public void run() { OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); @@ -67,4 +67,54 @@ public Optional before(HookContext ctx, Map globalAttrs = new HashMap<>(); + globalAttrs.put("global", new Value(1)); + EvaluationContext globalContext = new ImmutableContext(globalAttrs); + OpenFeatureAPI.getInstance().setEvaluationContext(globalContext); + + Client client = OpenFeatureAPI.getInstance().getClient(); + + Map clientAttrs = new HashMap<>(); + clientAttrs.put("client", new Value(2)); + client.setEvaluationContext(new ImmutableContext(clientAttrs)); + + Map transactionAttr = new HashMap<>(); + transactionAttr.put("trans", new Value(4)); + + Map transactionAttr2 = new HashMap<>(); + transactionAttr2.put("trans2", new Value(5)); + + Map invocationAttrs = new HashMap<>(); + invocationAttrs.put("invoke", new Value(3)); + EvaluationContext invocationContext = new ImmutableContext(invocationAttrs); + + for (int i = 0; i < 100; i++) { + OpenFeatureAPI.getInstance().setTransactionContext(new ImmutableContext(transactionAttr)); + + for (int j = 0; j < 10; j++) { + client.getBooleanValue(BOOLEAN_FLAG_KEY, false); + client.getStringValue(STRING_FLAG_KEY, "default"); + client.getIntegerValue(INT_FLAG_KEY, 0); + client.getDoubleValue(FLOAT_FLAG_KEY, 0.0); + client.getObjectDetails(OBJECT_FLAG_KEY, new Value(new ImmutableStructure()), invocationContext); + } + + OpenFeatureAPI.getInstance().setTransactionContext(new ImmutableContext(transactionAttr2)); + + for (int j = 0; j < 10; j++) { + client.getBooleanValue(BOOLEAN_FLAG_KEY, false); + client.getStringValue(STRING_FLAG_KEY, "default"); + client.getIntegerValue(INT_FLAG_KEY, 0); + client.getDoubleValue(FLOAT_FLAG_KEY, 0.0); + client.getObjectDetails(OBJECT_FLAG_KEY, new Value(new ImmutableStructure()), invocationContext); + } + } + } } From ea3990dc9abe711cfa44efec7312b29dc818b850 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Mon, 15 Sep 2025 11:23:33 +0200 Subject: [PATCH 2/9] fix bench Signed-off-by: christian.lutnik --- .../dev/openfeature/sdk/benchmark/AllocationBenchmark.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index eea91632e..31f817118 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -14,6 +14,7 @@ import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.NoOpProvider; import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator; import dev.openfeature.sdk.Value; import java.util.HashMap; import java.util.Map; @@ -74,6 +75,8 @@ public Optional before(HookContext ctx, Map globalAttrs = new HashMap<>(); globalAttrs.put("global", new Value(1)); EvaluationContext globalContext = new ImmutableContext(globalAttrs); From 9d49b91e464f64ece7d74b95c907620845314c2e Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 15 Oct 2025 15:29:28 +0200 Subject: [PATCH 3/9] merge master Signed-off-by: christian.lutnik --- .../openfeature/sdk/benchmark/AllocationBenchmark.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index d7668b25d..e1a6f489c 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import org.junit.jupiter.api.Test; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -37,9 +38,9 @@ public class AllocationBenchmark { // 10K iterations works well with Xmx1024m (we don't want to run out of memory) private static final int ITERATIONS = 10000; - //@Benchmark - //@BenchmarkMode(Mode.SingleShotTime) - //@Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) + // @Benchmark + // @BenchmarkMode(Mode.SingleShotTime) + // @Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) public void run() { OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); @@ -100,6 +101,7 @@ public Optional before(HookContext ctx, Map Date: Wed, 12 Nov 2025 10:26:22 +0100 Subject: [PATCH 4/9] improve benchmark Signed-off-by: christian.lutnik --- .../sdk/benchmark/AllocationBenchmark.java | 74 ++++++------------- .../benchmark/AllocationBenchmarkState.java | 50 +++++++++++++ 2 files changed, 74 insertions(+), 50 deletions(-) create mode 100644 src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index e1a6f489c..9fdfc5629 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -18,16 +18,15 @@ import dev.openfeature.sdk.ObjectHook; import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.StringHook; -import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator; import dev.openfeature.sdk.Value; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import org.junit.jupiter.api.Test; import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; /** * Runs a large volume of flag evaluations on a VM with 1G memory and GC @@ -99,55 +98,30 @@ public Optional before(HookContext ctx, Map globalAttrs = new HashMap<>(); - globalAttrs.put("global", new Value(1)); - EvaluationContext globalContext = new ImmutableContext(globalAttrs); - OpenFeatureAPI.getInstance().setEvaluationContext(globalContext); - - Client client = OpenFeatureAPI.getInstance().getClient(); - - Map clientAttrs = new HashMap<>(); - clientAttrs.put("client", new Value(2)); - client.setEvaluationContext(new ImmutableContext(clientAttrs)); - - Map transactionAttr = new HashMap<>(); - transactionAttr.put("trans", new Value(4)); - - Map transactionAttr2 = new HashMap<>(); - transactionAttr2.put("trans2", new Value(5)); - - Map invocationAttrs = new HashMap<>(); - invocationAttrs.put("invoke", new Value(3)); - EvaluationContext invocationContext = new ImmutableContext(invocationAttrs); - - for (int i = 0; i < 100; i++) { - OpenFeatureAPI.getInstance().setTransactionContext(new ImmutableContext(transactionAttr)); - - for (int j = 0; j < 10; j++) { - client.getBooleanValue(BOOLEAN_FLAG_KEY, false); - client.getStringValue(STRING_FLAG_KEY, "default"); - client.getIntegerValue(INT_FLAG_KEY, 0); - client.getDoubleValue(FLOAT_FLAG_KEY, 0.0); - client.getObjectDetails(OBJECT_FLAG_KEY, new Value(new ImmutableStructure()), invocationContext); - } + //@Test + public void context(Blackhole blackhole, AllocationBenchmarkState state) { + OpenFeatureAPI.getInstance().setTransactionContext(new ImmutableContext(state.transactionAttr)); + + for (int j = 0; j < 2; j++) { + blackhole.consume(state.client.getBooleanValue(BOOLEAN_FLAG_KEY, false)); + blackhole.consume(state.client.getStringValue(STRING_FLAG_KEY, "default")); + blackhole.consume(state.client.getIntegerValue(INT_FLAG_KEY, 0, state.invocationContext)); + blackhole.consume(state.client.getDoubleValue(FLOAT_FLAG_KEY, 0.0)); + blackhole.consume(state.client.getObjectDetails(OBJECT_FLAG_KEY, new Value(new ImmutableStructure()), + state.invocationContext)); + } - OpenFeatureAPI.getInstance().setTransactionContext(new ImmutableContext(transactionAttr2)); + OpenFeatureAPI.getInstance().setTransactionContext(new ImmutableContext(state.transactionAttr2)); - for (int j = 0; j < 10; j++) { - client.getBooleanValue(BOOLEAN_FLAG_KEY, false); - client.getStringValue(STRING_FLAG_KEY, "default"); - client.getIntegerValue(INT_FLAG_KEY, 0); - client.getDoubleValue(FLOAT_FLAG_KEY, 0.0); - client.getObjectDetails(OBJECT_FLAG_KEY, new Value(new ImmutableStructure()), invocationContext); - } + for (int j = 0; j < 2; j++) { + blackhole.consume(state.client.getBooleanValue(BOOLEAN_FLAG_KEY, false)); + blackhole.consume(state.client.getStringValue(STRING_FLAG_KEY, "default")); + blackhole.consume(state.client.getIntegerValue(INT_FLAG_KEY, 0, state.invocationContext)); + blackhole.consume(state.client.getDoubleValue(FLOAT_FLAG_KEY, 0.0)); + blackhole.consume(state.client.getObjectDetails(OBJECT_FLAG_KEY, new Value(new ImmutableStructure()), + state.invocationContext)); } } } diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java new file mode 100644 index 000000000..56f62fce1 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java @@ -0,0 +1,50 @@ +package dev.openfeature.sdk.benchmark; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.NoOpProvider; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator; +import dev.openfeature.sdk.Value; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import java.util.HashMap; +import java.util.Map; + +@State(Scope.Benchmark) +public class AllocationBenchmarkState { + public final Client client; + public final Map transactionAttr; + public final Map transactionAttr2; + public final EvaluationContext invocationContext; + + public AllocationBenchmarkState(){ + long start = System.currentTimeMillis(); + OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); + long end = System.currentTimeMillis(); + System.out.println("Setup time: " + (end - start) + "ms"); + Map globalAttrs = new HashMap<>(); + globalAttrs.put("global", new Value(1)); + EvaluationContext globalContext = new ImmutableContext(globalAttrs); + OpenFeatureAPI.getInstance().setEvaluationContext(globalContext); + + client = OpenFeatureAPI.getInstance().getClient(); + + Map clientAttrs = new HashMap<>(); + clientAttrs.put("client", new Value(2)); + client.setEvaluationContext(new ImmutableContext(clientAttrs)); + + transactionAttr = new HashMap<>(); + transactionAttr.put("trans", new Value(4)); + + transactionAttr2 = new HashMap<>(); + transactionAttr2.put("trans2", new Value(5)); + + Map invocationAttrs = new HashMap<>(); + invocationAttrs.put("invoke", new Value(3)); + invocationContext = new ImmutableContext(invocationAttrs); + + } +} From 2e9af46afa8f44916f2acdac03c30a6fb38ddcdb Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 12 Nov 2025 11:12:32 +0100 Subject: [PATCH 5/9] improve benchmark Signed-off-by: christian.lutnik --- .../sdk/benchmark/AllocationBenchmark.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index 9fdfc5629..437292fa8 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import org.junit.jupiter.api.Test; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Scope; @@ -99,8 +100,7 @@ public Optional before(HookContext ctx, Map Date: Wed, 19 Nov 2025 11:41:06 +0100 Subject: [PATCH 6/9] add str hook for ctx Signed-off-by: christian.lutnik --- .../sdk/benchmark/AllocationBenchmarkState.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java index 56f62fce1..f923af2ae 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java @@ -2,15 +2,18 @@ import dev.openfeature.sdk.Client; import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.HookContext; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.NoOpProvider; import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.StringHook; import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator; import dev.openfeature.sdk.Value; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.util.HashMap; import java.util.Map; +import java.util.Optional; @State(Scope.Benchmark) public class AllocationBenchmarkState { @@ -19,7 +22,7 @@ public class AllocationBenchmarkState { public final Map transactionAttr2; public final EvaluationContext invocationContext; - public AllocationBenchmarkState(){ + public AllocationBenchmarkState() { long start = System.currentTimeMillis(); OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); @@ -46,5 +49,15 @@ public AllocationBenchmarkState(){ invocationAttrs.put("invoke", new Value(3)); invocationContext = new ImmutableContext(invocationAttrs); + Map hookAttrs = new HashMap<>(); + hookAttrs.put("hook", new Value(30)); + Optional hookCtx = Optional.of(new ImmutableContext(hookAttrs)); + + client.addHooks(new StringHook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return hookCtx; + } + }); } } From c3bdb78cfcf51c83fd3714c382e0911f36b35014 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 25 Nov 2025 13:50:56 +0100 Subject: [PATCH 7/9] a collection of perf enhancements Signed-off-by: christian.lutnik --- .../sdk/FlagEvaluationDetails.java | 26 ++++++++++--------- .../sdk/FlagEvaluationOptions.java | 4 +++ .../dev/openfeature/sdk/ImmutableContext.java | 5 ++-- .../openfeature/sdk/ImmutableMetadata.java | 2 ++ .../openfeature/sdk/ImmutableStructure.java | 16 +++++++----- .../openfeature/sdk/OpenFeatureClient.java | 23 ++++++++-------- .../sdk/benchmark/AllocationBenchmark.java | 21 +++++++-------- .../benchmark/AllocationBenchmarkState.java | 4 +-- 8 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index f1697e309..beeadde15 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java @@ -1,6 +1,5 @@ package dev.openfeature.sdk; -import java.util.Optional; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -26,7 +25,7 @@ public class FlagEvaluationDetails implements BaseEvaluation { private String errorMessage; @Builder.Default - private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); + private ImmutableMetadata flagMetadata = ImmutableMetadata.EMPTY; /** * Generate detail payload from the provider response. @@ -37,15 +36,18 @@ public class FlagEvaluationDetails implements BaseEvaluation { * @return detail payload */ public static FlagEvaluationDetails from(ProviderEvaluation providerEval, String flagKey) { - return FlagEvaluationDetails.builder() - .flagKey(flagKey) - .value(providerEval.getValue()) - .variant(providerEval.getVariant()) - .reason(providerEval.getReason()) - .errorMessage(providerEval.getErrorMessage()) - .errorCode(providerEval.getErrorCode()) - .flagMetadata(Optional.ofNullable(providerEval.getFlagMetadata()) - .orElse(ImmutableMetadata.builder().build())) - .build(); + var flagMetadata = providerEval.getFlagMetadata(); + if (flagMetadata == null) { + flagMetadata = ImmutableMetadata.EMPTY; + } + + return new FlagEvaluationDetails<>( + flagKey, + providerEval.getValue(), + providerEval.getVariant(), + providerEval.getReason(), + providerEval.getErrorCode(), + providerEval.getErrorMessage(), + flagMetadata); } } diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java index 01ecb9b2e..f73bd9631 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java @@ -10,6 +10,10 @@ @lombok.Value @Builder public class FlagEvaluationOptions { + + public static final FlagEvaluationOptions EMPTY = + FlagEvaluationOptions.builder().build(); + @Singular List hooks; diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java index e4916dfca..35f28d4f4 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java @@ -1,6 +1,7 @@ package dev.openfeature.sdk; import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -20,7 +21,7 @@ @SuppressWarnings("PMD.BeanMembersShouldSerialize") public final class ImmutableContext implements EvaluationContext { - public static final ImmutableContext EMPTY = new ImmutableContext(); + public static final ImmutableContext EMPTY = new ImmutableContext(Collections.emptyMap()); @Delegate(excludes = DelegateExclusions.class) private final ImmutableStructure structure; @@ -58,7 +59,7 @@ public ImmutableContext(Map attributes) { * @param attributes evaluation context attributes */ public ImmutableContext(String targetingKey, Map attributes) { - if (targetingKey != null && !targetingKey.trim().isEmpty()) { + if (targetingKey != null && !targetingKey.isBlank()) { this.structure = new ImmutableStructure(targetingKey, attributes); } else { this.structure = new ImmutableStructure(attributes); diff --git a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java index f6c1d742e..a4467bddf 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java @@ -13,6 +13,8 @@ @Slf4j @EqualsAndHashCode public class ImmutableMetadata { + public static final ImmutableMetadata EMPTY = ImmutableMetadata.builder().build(); + private final Map metadata; private ImmutableMetadata(Map metadata) { diff --git a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java index 849359424..ed2e5bd9a 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java @@ -4,7 +4,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; -import java.util.Optional; import java.util.Set; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -69,15 +68,18 @@ private static Map copyAttributes(Map in) { } private static Map copyAttributes(Map in, String targetingKey) { - Map copy = new HashMap<>(); + Map copy; if (in != null) { + var numMappings = in.size() + 1; + copy = new HashMap<>((int) Math.ceil(numMappings / .75)); for (Entry entry : in.entrySet()) { - copy.put( - entry.getKey(), - Optional.ofNullable(entry.getValue()) - .map((Value val) -> val.clone()) - .orElse(null)); + var key = entry.getKey(); + var value = entry.getValue(); + Value cloned = value == null ? null : value.clone(); + copy.put(key, cloned); } + } else { + copy = new HashMap<>(1); } if (targetingKey != null) { copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey)); diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 614bc1e34..2753080ee 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -163,8 +163,13 @@ private FlagEvaluationDetails evaluateFlag( FlagEvaluationDetails details = null; HookSupportData hookSupportData = new HookSupportData(); - var flagOptions = ObjectUtils.defaultIfNull( - options, () -> FlagEvaluationOptions.builder().build()); + FlagEvaluationOptions flagOptions; + if (options == null) { + flagOptions = FlagEvaluationOptions.EMPTY; + } else { + flagOptions = options; + } + hookSupportData.hints = Collections.unmodifiableMap(flagOptions.getHookHints()); try { @@ -320,8 +325,7 @@ public FlagEvaluationDetails getBooleanDetails(String key, Boolean defa @Override public FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx) { - return getBooleanDetails( - key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); + return getBooleanDetails(key, defaultValue, ctx, FlagEvaluationOptions.EMPTY); } @Override @@ -353,8 +357,7 @@ public FlagEvaluationDetails getStringDetails(String key, String default @Override public FlagEvaluationDetails getStringDetails(String key, String defaultValue, EvaluationContext ctx) { - return getStringDetails( - key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); + return getStringDetails(key, defaultValue, ctx, FlagEvaluationOptions.EMPTY); } @Override @@ -386,8 +389,7 @@ public FlagEvaluationDetails getIntegerDetails(String key, Integer defa @Override public FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx) { - return getIntegerDetails( - key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); + return getIntegerDetails(key, defaultValue, ctx, FlagEvaluationOptions.EMPTY); } @Override @@ -451,8 +453,7 @@ public FlagEvaluationDetails getObjectDetails(String key, Value defaultVa @Override public FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, EvaluationContext ctx) { - return getObjectDetails( - key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); + return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.EMPTY); } @Override @@ -463,7 +464,7 @@ public FlagEvaluationDetails getObjectDetails( @Override public ClientMetadata getMetadata() { - return () -> domain; + return this::getDomain; } /** diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index 437292fa8..1b9a9f535 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -22,11 +22,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import org.junit.jupiter.api.Test; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.infra.Blackhole; /** @@ -99,7 +96,7 @@ public Optional before(HookContext ctx, Map Date: Tue, 25 Nov 2025 17:15:00 +0100 Subject: [PATCH 8/9] revert comments Signed-off-by: christian.lutnik --- .../openfeature/sdk/benchmark/AllocationBenchmark.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index 1b9a9f535..c41492577 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -23,7 +23,9 @@ import java.util.Map; import java.util.Optional; import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.infra.Blackhole; /** @@ -35,9 +37,9 @@ public class AllocationBenchmark { // 10K iterations works well with Xmx1024m (we don't want to run out of memory) private static final int ITERATIONS = 10000; - // @Benchmark - // @BenchmarkMode(Mode.SingleShotTime) - // @Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) public void run() { OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); @@ -96,7 +98,7 @@ public Optional before(HookContext ctx, Map Date: Thu, 27 Nov 2025 08:40:52 +0100 Subject: [PATCH 9/9] extract hash map generation Signed-off-by: christian.lutnik --- src/main/java/dev/openfeature/sdk/HashMapUtils.java | 11 +++++++++++ .../java/dev/openfeature/sdk/ImmutableStructure.java | 9 ++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/HashMapUtils.java diff --git a/src/main/java/dev/openfeature/sdk/HashMapUtils.java b/src/main/java/dev/openfeature/sdk/HashMapUtils.java new file mode 100644 index 000000000..88d255c25 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/HashMapUtils.java @@ -0,0 +1,11 @@ +package dev.openfeature.sdk; + +import java.util.HashMap; + +class HashMapUtils { + private HashMapUtils() {} + + static HashMap forEntries(int expectedEntries) { + return new HashMap<>((int) Math.ceil(expectedEntries / .75)); + } +} diff --git a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java index ed2e5bd9a..313e13057 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java @@ -70,8 +70,11 @@ private static Map copyAttributes(Map in) { private static Map copyAttributes(Map in, String targetingKey) { Map copy; if (in != null) { - var numMappings = in.size() + 1; - copy = new HashMap<>((int) Math.ceil(numMappings / .75)); + var numMappings = in.size(); + if (targetingKey != null) { + numMappings++; + } + copy = HashMapUtils.forEntries(numMappings); for (Entry entry : in.entrySet()) { var key = entry.getKey(); var value = entry.getValue(); @@ -79,7 +82,7 @@ private static Map copyAttributes(Map in, String t copy.put(key, cloned); } } else { - copy = new HashMap<>(1); + copy = new HashMap<>(targetingKey == null ? 0 : 1); } if (targetingKey != null) { copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey));