22
33import dev .openfeature .flagd .evaluator .comparison .MinimalInProcessResolver ;
44import dev .openfeature .sdk .EvaluationContext ;
5- import dev .openfeature .sdk .MutableContext ;
5+ import dev .openfeature .sdk .ImmutableContext ;
6+ import dev .openfeature .sdk .LayeredEvaluationContext ;
67import dev .openfeature .sdk .ProviderEvaluation ;
8+ import dev .openfeature .sdk .Value ;
79import org .openjdk .jmh .annotations .*;
810import org .openjdk .jmh .infra .Blackhole ;
911
12+ import java .util .HashMap ;
13+ import java .util .Map ;
1014import java .util .Random ;
1115import java .util .concurrent .TimeUnit ;
1216
1317/**
1418 * JMH benchmarks comparing old JsonLogic resolver vs new WASM evaluator.
1519 *
20+ * <p>Both resolvers use {@link LayeredEvaluationContext} (the production API path),
21+ * which mirrors how the flagd provider constructs contexts with API, client,
22+ * invocation, and transaction layers. This enables context key filtering and
23+ * flag index evaluation on the WASM side.
24+ *
1625 * <p>Covers single-threaded comparison (X1, X2), context size sweep (X3),
17- * and concurrent comparison (X4).
26+ * concurrent comparison at 4 threads (X4) and 16 threads (X5 ).
1827 *
1928 * <p><b>Running the benchmarks:</b>
2029 * <pre>
21- * ./mvnw clean package
22- * java -jar target/benchmarks.jar ComparisonBenchmark
30+ * ./mvnw clean package -DskipTests
31+ * java -jar target/benchmarks.jar ComparisonBenchmark -prof gc
2332 *
2433 * # Single-threaded only:
2534 * java -jar target/benchmarks.jar "ComparisonBenchmark.X[12]_.*"
2635 *
2736 * # Concurrent only:
28- * java -jar target/benchmarks.jar "ComparisonBenchmark.X4_ .*"
37+ * java -jar target/benchmarks.jar "ComparisonBenchmark.X[45]_ .*"
2938 * </pre>
3039 */
3140@ BenchmarkMode ({Mode .Throughput , Mode .AverageTime })
@@ -69,30 +78,32 @@ public class ComparisonBenchmark {
6978 " }\n " +
7079 "}" ;
7180
72- // Context JSON strings for the new evaluator
73- private static final String EMPTY_CONTEXT_JSON = "{}" ;
81+ /** Build a LayeredEvaluationContext matching the flagd provider pattern. */
82+ private static EvaluationContext layeredContext (
83+ EvaluationContext apiCtx ,
84+ EvaluationContext clientCtx ,
85+ EvaluationContext invocationCtx ) {
86+ return new LayeredEvaluationContext (apiCtx , clientCtx , invocationCtx , ImmutableContext .EMPTY );
87+ }
7488
75- private static final String SMALL_CONTEXT_JSON = "{" +
76- "\" targetingKey\" : \" user-123\" , " +
77- "\" tier\" : \" premium\" , " +
78- "\" role\" : \" admin\" , " +
79- "\" region\" : \" us-east\" , " +
80- "\" score\" : 85" +
81- "}" ;
89+ /** Build an ImmutableContext from a targeting key and attribute map. */
90+ private static ImmutableContext immutableCtx (String targetingKey , Map <String , Value > attrs ) {
91+ return new ImmutableContext (targetingKey , attrs );
92+ }
8293
8394 @ State (Scope .Benchmark )
8495 public static class ComparisonState {
8596 FlagEvaluator newEvaluator ;
8697 MinimalInProcessResolver oldResolver ;
8798
88- // EvaluationContext objects for the old resolver
99+ // Shared API-level context (global attributes like environment/service)
100+ ImmutableContext apiContext ;
101+
102+ // Layered contexts for benchmarks
89103 EvaluationContext emptyContext ;
90104 EvaluationContext smallContext ;
91105 EvaluationContext largeContext ;
92106
93- // JSON strings for the new evaluator
94- String largeContextJson ;
95-
96107 @ Setup (Level .Trial )
97108 public void setup () {
98109 try {
@@ -104,67 +115,48 @@ public void setup() {
104115 oldResolver = new MinimalInProcessResolver ();
105116 oldResolver .loadFlags (FLAG_CONFIG );
106117
107- // Build contexts for old resolver
108- emptyContext = new MutableContext ();
109-
110- smallContext = new MutableContext ()
111- .add ("targetingKey" , "user-123" )
112- .add ("tier" , "premium" )
113- .add ("role" , "admin" )
114- .add ("region" , "us-east" )
115- .add ("score" , 85 );
116-
117- MutableContext large = new MutableContext ()
118- .add ("targetingKey" , "user-123" )
119- .add ("tier" , "premium" )
120- .add ("role" , "admin" )
121- .add ("region" , "us-east" )
122- .add ("score" , 85 );
123- for (int i = 0 ; i < 100 ; i ++) {
124- switch (i % 4 ) {
125- case 0 :
126- large .add ("attr_" + i , "value-" + i );
127- break ;
128- case 1 :
129- large .add ("attr_" + i , i * 7 );
130- break ;
131- case 2 :
132- large .add ("attr_" + i , i % 2 == 0 );
133- break ;
134- case 3 :
135- large .add ("attr_" + i , i * 1.5 );
136- break ;
137- }
138- }
139- largeContext = large ;
140-
141- // Build large context JSON for the new evaluator
142- StringBuilder sb = new StringBuilder (4096 );
143- sb .append ("{" );
144- sb .append ("\" targetingKey\" : \" user-123\" , " );
145- sb .append ("\" tier\" : \" premium\" , " );
146- sb .append ("\" role\" : \" admin\" , " );
147- sb .append ("\" region\" : \" us-east\" , " );
148- sb .append ("\" score\" : 85" );
118+ // API-level context (set once at provider init)
119+ Map <String , Value > apiAttrs = new HashMap <>();
120+ apiAttrs .put ("environment" , new Value ("production" ));
121+ apiAttrs .put ("service" , new Value ("checkout" ));
122+ apiContext = new ImmutableContext (apiAttrs );
123+
124+ // Empty invocation context — only API-level attributes
125+ emptyContext = layeredContext (apiContext , ImmutableContext .EMPTY , ImmutableContext .EMPTY );
126+
127+ // Small invocation context (4 user attributes)
128+ Map <String , Value > smallAttrs = new HashMap <>();
129+ smallAttrs .put ("tier" , new Value ("premium" ));
130+ smallAttrs .put ("role" , new Value ("admin" ));
131+ smallAttrs .put ("region" , new Value ("us-east" ));
132+ smallAttrs .put ("score" , new Value (85 ));
133+ smallContext = layeredContext (apiContext , ImmutableContext .EMPTY ,
134+ immutableCtx ("user-123" , smallAttrs ));
135+
136+ // Large invocation context (100+ attributes)
137+ Map <String , Value > largeAttrs = new HashMap <>();
138+ largeAttrs .put ("tier" , new Value ("premium" ));
139+ largeAttrs .put ("role" , new Value ("admin" ));
140+ largeAttrs .put ("region" , new Value ("us-east" ));
141+ largeAttrs .put ("score" , new Value (85 ));
149142 for (int i = 0 ; i < 100 ; i ++) {
150- sb .append (", " );
151143 switch (i % 4 ) {
152144 case 0 :
153- sb . append ( " \" attr_"). append ( i ). append ( " \" : \" value-"). append ( i ). append ( " \" " );
145+ largeAttrs . put ( " attr_" + i , new Value ( " value-" + i ) );
154146 break ;
155147 case 1 :
156- sb . append ( " \" attr_"). append ( i ). append ( " \" : " ). append (i * 7 );
148+ largeAttrs . put ( " attr_" + i , new Value (i * 7 ) );
157149 break ;
158150 case 2 :
159- sb . append ( " \" attr_"). append ( i ). append ( " \" : " ). append (i % 2 == 0 ? "true" : "false" );
151+ largeAttrs . put ( " attr_" + i , new Value (i % 2 == 0 ) );
160152 break ;
161153 case 3 :
162- sb . append ( " \" attr_"). append ( i ). append ( " \" : " ). append (i * 1.5 );
154+ largeAttrs . put ( " attr_" + i , new Value (i * 1.5 ) );
163155 break ;
164156 }
165157 }
166- sb . append ( "}" );
167- largeContextJson = sb . toString ( );
158+ largeContext = layeredContext ( apiContext , ImmutableContext . EMPTY ,
159+ immutableCtx ( "user-123" , largeAttrs ) );
168160 } catch (Exception e ) {
169161 throw new RuntimeException ("Failed to setup comparison state" , e );
170162 }
@@ -195,7 +187,7 @@ public void X1_old_simple(ComparisonState state, Blackhole bh) {
195187 public void X1_new_simple (ComparisonState state , Blackhole bh ) {
196188 try {
197189 EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
198- Boolean .class , "simple-bool" , EMPTY_CONTEXT_JSON );
190+ Boolean .class , "simple-bool" , state . emptyContext );
199191 bh .consume (result .getValue ());
200192 bh .consume (result .getVariant ());
201193 } catch (Exception e ) {
@@ -220,7 +212,7 @@ public void X2_old_targeting(ComparisonState state, Blackhole bh) {
220212 public void X2_new_targeting (ComparisonState state , Blackhole bh ) {
221213 try {
222214 EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
223- Boolean .class , "targeted-access" , SMALL_CONTEXT_JSON );
215+ Boolean .class , "targeted-access" , state . smallContext );
224216 bh .consume (result .getValue ());
225217 bh .consume (result .getVariant ());
226218 } catch (Exception e ) {
@@ -265,7 +257,7 @@ public void X3_old_largeContext(ComparisonState state, Blackhole bh) {
265257 public void X3_new_emptyContext (ComparisonState state , Blackhole bh ) {
266258 try {
267259 EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
268- Boolean .class , "targeted-access" , EMPTY_CONTEXT_JSON );
260+ Boolean .class , "targeted-access" , state . emptyContext );
269261 bh .consume (result .getValue ());
270262 bh .consume (result .getVariant ());
271263 } catch (Exception e ) {
@@ -278,7 +270,7 @@ public void X3_new_emptyContext(ComparisonState state, Blackhole bh) {
278270 public void X3_new_smallContext (ComparisonState state , Blackhole bh ) {
279271 try {
280272 EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
281- Boolean .class , "targeted-access" , SMALL_CONTEXT_JSON );
273+ Boolean .class , "targeted-access" , state . smallContext );
282274 bh .consume (result .getValue ());
283275 bh .consume (result .getVariant ());
284276 } catch (Exception e ) {
@@ -291,7 +283,7 @@ public void X3_new_smallContext(ComparisonState state, Blackhole bh) {
291283 public void X3_new_largeContext (ComparisonState state , Blackhole bh ) {
292284 try {
293285 EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
294- Boolean .class , "targeted-access" , state .largeContextJson );
286+ Boolean .class , "targeted-access" , state .largeContext );
295287 bh .consume (result .getValue ());
296288 bh .consume (result .getVariant ());
297289 } catch (Exception e ) {
@@ -305,18 +297,56 @@ public void X3_new_largeContext(ComparisonState state, Blackhole bh) {
305297
306298 @ State (Scope .Thread )
307299 public static class ThreadContext {
308- Random random ;
309- String simpleContextJson ;
300+ EvaluationContext simpleContext ;
310301 EvaluationContext matchingContext ;
302+ EvaluationContext largeMatchingContext ;
311303
312304 @ Setup (Level .Trial )
313305 public void setup () {
314- random = new Random (42 + Thread .currentThread ().getId ());
315- simpleContextJson = "{\" targetingKey\" : \" user-" + random .nextInt (10000 ) + "\" }" ;
316- matchingContext = new MutableContext ()
317- .add ("role" , "admin" )
318- .add ("tier" , "premium" )
319- .add ("targetingKey" , "user-" + random .nextInt (10000 ));
306+ Random random = new Random (42 + Thread .currentThread ().getId ());
307+ String targetingKey = "user-" + random .nextInt (10000 );
308+
309+ // API-level context (same as ComparisonState)
310+ Map <String , Value > apiAttrs = new HashMap <>();
311+ apiAttrs .put ("environment" , new Value ("production" ));
312+ apiAttrs .put ("service" , new Value ("checkout" ));
313+ ImmutableContext apiCtx = new ImmutableContext (apiAttrs );
314+
315+ // Simple: only targeting key, no invocation attributes
316+ simpleContext = layeredContext (apiCtx , ImmutableContext .EMPTY ,
317+ new ImmutableContext (targetingKey ));
318+
319+ // Matching: role + tier that match the targeting rule
320+ Map <String , Value > matchAttrs = new HashMap <>();
321+ matchAttrs .put ("role" , new Value ("admin" ));
322+ matchAttrs .put ("tier" , new Value ("premium" ));
323+ matchingContext = layeredContext (apiCtx , ImmutableContext .EMPTY ,
324+ immutableCtx (targetingKey , matchAttrs ));
325+
326+ // Large: 100+ attributes with role + tier
327+ Map <String , Value > largeAttrs = new HashMap <>();
328+ largeAttrs .put ("role" , new Value ("admin" ));
329+ largeAttrs .put ("tier" , new Value ("premium" ));
330+ largeAttrs .put ("region" , new Value ("us-east" ));
331+ largeAttrs .put ("score" , new Value (85 ));
332+ for (int i = 0 ; i < 100 ; i ++) {
333+ switch (i % 4 ) {
334+ case 0 :
335+ largeAttrs .put ("attr_" + i , new Value ("value-" + i ));
336+ break ;
337+ case 1 :
338+ largeAttrs .put ("attr_" + i , new Value (i * 7 ));
339+ break ;
340+ case 2 :
341+ largeAttrs .put ("attr_" + i , new Value (i % 2 == 0 ));
342+ break ;
343+ case 3 :
344+ largeAttrs .put ("attr_" + i , new Value (i * 1.5 ));
345+ break ;
346+ }
347+ }
348+ largeMatchingContext = layeredContext (apiCtx , ImmutableContext .EMPTY ,
349+ immutableCtx (targetingKey , largeAttrs ));
320350 }
321351 }
322352
@@ -328,7 +358,7 @@ public void setup() {
328358 @ Threads (4 )
329359 public void X4_old_concurrentSimple (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
330360 ProviderEvaluation <Boolean > result = state .oldResolver .booleanEvaluation (
331- "simple-bool" , false , state . emptyContext );
361+ "simple-bool" , false , ctx . simpleContext );
332362 bh .consume (result );
333363 }
334364
@@ -337,7 +367,7 @@ public void X4_old_concurrentSimple(ComparisonState state, ThreadContext ctx, Bl
337367 public void X4_new_concurrentSimple (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
338368 try {
339369 EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
340- Boolean .class , "simple-bool" , ctx .simpleContextJson );
370+ Boolean .class , "simple-bool" , ctx .simpleContext );
341371 bh .consume (result .getValue ());
342372 bh .consume (result .getVariant ());
343373 } catch (Exception e ) {
@@ -366,6 +396,73 @@ public void X4_new_concurrentTargeting(ComparisonState state, ThreadContext ctx,
366396 }
367397 }
368398
399+ // ========================================================================
400+ // X5: Old vs New under high concurrency (16 threads)
401+ // ========================================================================
402+
403+ @ Benchmark
404+ @ Threads (16 )
405+ public void X5_old_16t_simple (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
406+ ProviderEvaluation <Boolean > result = state .oldResolver .booleanEvaluation (
407+ "simple-bool" , false , ctx .simpleContext );
408+ bh .consume (result );
409+ }
410+
411+ @ Benchmark
412+ @ Threads (16 )
413+ public void X5_new_16t_simple (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
414+ try {
415+ EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
416+ Boolean .class , "simple-bool" , ctx .simpleContext );
417+ bh .consume (result .getValue ());
418+ bh .consume (result .getVariant ());
419+ } catch (Exception e ) {
420+ throw new RuntimeException ("Benchmark failed" , e );
421+ }
422+ }
423+
424+ @ Benchmark
425+ @ Threads (16 )
426+ public void X5_old_16t_targeting (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
427+ ProviderEvaluation <Boolean > result = state .oldResolver .booleanEvaluation (
428+ "targeted-access" , false , ctx .matchingContext );
429+ bh .consume (result );
430+ }
431+
432+ @ Benchmark
433+ @ Threads (16 )
434+ public void X5_new_16t_targeting (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
435+ try {
436+ EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
437+ Boolean .class , "targeted-access" , ctx .matchingContext );
438+ bh .consume (result .getValue ());
439+ bh .consume (result .getVariant ());
440+ } catch (Exception e ) {
441+ throw new RuntimeException ("Benchmark failed" , e );
442+ }
443+ }
444+
445+ @ Benchmark
446+ @ Threads (16 )
447+ public void X5_old_16t_largeContext (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
448+ ProviderEvaluation <Boolean > result = state .oldResolver .booleanEvaluation (
449+ "targeted-access" , false , ctx .largeMatchingContext );
450+ bh .consume (result );
451+ }
452+
453+ @ Benchmark
454+ @ Threads (16 )
455+ public void X5_new_16t_largeContext (ComparisonState state , ThreadContext ctx , Blackhole bh ) {
456+ try {
457+ EvaluationResult <Boolean > result = state .newEvaluator .evaluateFlag (
458+ Boolean .class , "targeted-access" , ctx .largeMatchingContext );
459+ bh .consume (result .getValue ());
460+ bh .consume (result .getVariant ());
461+ } catch (Exception e ) {
462+ throw new RuntimeException ("Benchmark failed" , e );
463+ }
464+ }
465+
369466 /**
370467 * Main method to run benchmarks standalone.
371468 */
0 commit comments