perf: add heap benchmark and reduce allocations (#1156)
* chore: add heap benchmark and reduce allocations Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
parent
b144763299
commit
90088188c9
|
|
@ -30,6 +30,16 @@ to run alone:
|
|||
mvn test -P e2e
|
||||
```
|
||||
|
||||
## Benchmarking
|
||||
|
||||
There is a small JMH benchmark suite for testing allocations that can be run with:
|
||||
|
||||
```sh
|
||||
mvn -P benchmark test-compile jmh:benchmark -Djmh.f=1 -Djmh.prof='dev.openfeature.sdk.benchmark.AllocationProfiler'
|
||||
```
|
||||
|
||||
If you are concerned about the repercussions of a change on memory usage, run this an compare the results to the committed. `benchmark.txt` file.
|
||||
|
||||
## Releasing
|
||||
|
||||
See [releasing](./docs/release.md).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
[INFO] Scanning for projects...
|
||||
[INFO]
|
||||
[INFO] ------------------------< dev.openfeature:sdk >-------------------------
|
||||
[INFO] Building OpenFeature Java SDK 1.12.0
|
||||
[INFO] from pom.xml
|
||||
[INFO] --------------------------------[ jar ]---------------------------------
|
||||
[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)'
|
||||
[INFO]
|
||||
[INFO] >>> jmh:0.2.2:benchmark (default-cli) > process-test-resources @ sdk >>>
|
||||
[INFO]
|
||||
[INFO] --- checkstyle:3.5.0:check (validate) @ sdk ---
|
||||
[INFO] Starting audit...
|
||||
Audit done.
|
||||
[INFO] You have 0 Checkstyle violations.
|
||||
[INFO]
|
||||
[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk ---
|
||||
[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:resources (default-resources) @ sdk ---
|
||||
[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources
|
||||
[INFO]
|
||||
[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk ---
|
||||
[INFO] Recompiling the module because of changed source code.
|
||||
[INFO] Compiling 65 source files with javac [debug target 1.8] to target/classes
|
||||
[WARNING] bootstrap class path not set in conjunction with -source 8
|
||||
[WARNING] source value 8 is obsolete and will be removed in a future release
|
||||
[WARNING] target value 8 is obsolete and will be removed in a future release
|
||||
[WARNING] To suppress warnings about obsolete options, use -Xlint:-options.
|
||||
[INFO] Annotation processing is enabled because one or more processors were found
|
||||
on the class path. A future release of javac may disable annotation processing
|
||||
unless at least one processor is specified by name (-processor), or a search
|
||||
path is specified (--processor-path, --processor-module-path), or annotation
|
||||
processing is enabled explicitly (-proc:only, -proc:full).
|
||||
Use -Xlint:-options to suppress this message.
|
||||
Use -proc:none to disable annotation processing.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/MutableStructure.java:[19,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/ImmutableStructure.java:[22,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/EventDetails.java:[9,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java:[27,26] finalize() in java.lang.Object has been deprecated and marked for removal
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/NoOpProvider.java: Some input files use or override a deprecated API.
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/NoOpProvider.java: Recompile with -Xlint:deprecation for details.
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Some input files use unchecked or unsafe operations.
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Recompile with -Xlint:unchecked for details.
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ sdk ---
|
||||
[INFO] Copying 2 resources from src/test/resources to target/test-classes
|
||||
[INFO]
|
||||
[INFO] <<< jmh:0.2.2:benchmark (default-cli) < process-test-resources @ sdk <<<
|
||||
[INFO]
|
||||
[INFO]
|
||||
[INFO] --- jmh:0.2.2:benchmark (default-cli) @ sdk ---
|
||||
[INFO] Changes detected - recompiling the module!
|
||||
[INFO] Compiling 52 source files to /home/todd/git/java-sdk/target/test-classes
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/LockingTest.java: Some input files use or override a deprecated API.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/LockingTest.java: Recompile with -Xlint:deprecation for details.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java: Some input files use unchecked or unsafe operations.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java: Recompile with -Xlint:unchecked for details.
|
||||
[INFO] Executing the JMH benchmarks
|
||||
# JMH version: 1.37
|
||||
# VM version: JDK 21.0.4, OpenJDK 64-Bit Server VM, 21.0.4+7
|
||||
# VM invoker: /usr/lib/jvm/java-21-openjdk/bin/java
|
||||
# VM options: -Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
|
||||
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||
# Warmup: <none>
|
||||
# Measurement: 1 iterations, single-shot each
|
||||
# Timeout: 10 min per iteration
|
||||
# Threads: 1 thread
|
||||
# Benchmark mode: Single shot invocation time
|
||||
# Benchmark: dev.openfeature.sdk.benchmark.AllocationBenchmark.run
|
||||
|
||||
# Run progress: 0.00% complete, ETA 00:00:00
|
||||
# Fork: 1 of 1
|
||||
[0.001s][warning][gc,init] Consider setting -Xms equal to -Xmx to avoid resizing hiccups
|
||||
[0.001s][warning][gc,init] Consider enabling -XX:+AlwaysPreTouch to avoid memory commit hiccups
|
||||
Iteration 1: num #instances #bytes class name (module)
|
||||
-------------------------------------------------------
|
||||
1: 1146984 55055232 java.util.HashMap (java.base@21.0.4)
|
||||
2: 700056 11200896 java.util.HashMap$EntrySet (java.base@21.0.4)
|
||||
3: 47757 9295888 [B (java.base@21.0.4)
|
||||
4: 305989 8105752 [Ljava.lang.Object; (java.base@21.0.4)
|
||||
5: 482225 7715600 dev.openfeature.sdk.ImmutableStructure
|
||||
6: 472225 7555600 dev.openfeature.sdk.ImmutableContext
|
||||
7: 100000 4000000 dev.openfeature.sdk.HookContext
|
||||
8: 100000 4000000 dev.openfeature.sdk.HookContext$HookContextBuilder
|
||||
9: 154 2995712 [Ljdk.internal.vm.FillerElement; (java.base@21.0.4)
|
||||
10: 122807 2947368 java.util.ArrayList (java.base@21.0.4)
|
||||
11: 50000 2000000 dev.openfeature.sdk.FlagEvaluationDetails
|
||||
12: 50000 2000000 dev.openfeature.sdk.ProviderEvaluation
|
||||
13: 50002 1600064 java.util.Collections$UnmodifiableMap (java.base@21.0.4)
|
||||
14: 100001 1600016 dev.openfeature.sdk.NoOpProvider$$Lambda/0x000074760c02fa78
|
||||
15: 50000 1600000 [Ljava.util.List; (java.base@21.0.4)
|
||||
16: 100000 1600000 dev.openfeature.sdk.ImmutableMetadata
|
||||
17: 100000 1600000 dev.openfeature.sdk.ImmutableMetadata$ImmutableMetadataBuilder
|
||||
18: 100000 1600000 dev.openfeature.sdk.OpenFeatureClient$$Lambda/0x000074760c0821f8
|
||||
19: 43808 1401856 java.util.ArrayList$Itr (java.base@21.0.4)
|
||||
20: 50000 1200000 dev.openfeature.sdk.FlagEvaluationOptions
|
||||
21: 56919 910704 java.util.Optional (java.base@21.0.4)
|
||||
22: 34754 834096 dev.openfeature.sdk.FlagEvaluationOptions$FlagEvaluationOptionsBuilder
|
||||
23: 4489 679248 [I (java.base@21.0.4)
|
||||
24: 26554 637296 java.lang.String (java.base@21.0.4)
|
||||
25: 12462 598176 dev.openfeature.sdk.FlagEvaluationDetails$FlagEvaluationDetailsBuilder
|
||||
26: 13748 549920 dev.openfeature.sdk.ProviderEvaluation$ProviderEvaluationBuilder
|
||||
27: 16418 394032 dev.openfeature.sdk.HookSupport$$Lambda/0x000074760c081230
|
||||
28: 1461 390008 [J (java.base@21.0.4)
|
||||
29: 24033 384528 dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock$$Lambda/0x000074760c02eae8
|
||||
30: 14591 350184 dev.openfeature.sdk.HookSupport$$Lambda/0x000074760c081000
|
||||
31: 2355 288104 java.lang.Class (java.base@21.0.4)
|
||||
32: 8141 260512 java.util.HashMap$EntryIterator (java.base@21.0.4)
|
||||
33: 4610 258160 jdk.internal.org.objectweb.asm.SymbolTable$Entry (java.base@21.0.4)
|
||||
34: 10001 240024 java.lang.Double (java.base@21.0.4)
|
||||
35: 2502 180144 java.lang.reflect.Field (java.base@21.0.4)
|
||||
36: 10000 160000 dev.openfeature.sdk.Value
|
||||
37: 6004 144096 java.lang.StringBuilder (java.base@21.0.4)
|
||||
38: 179 139928 [Ljdk.internal.org.objectweb.asm.SymbolTable$Entry; (java.base@21.0.4)
|
||||
39: 3824 122368 java.util.concurrent.ConcurrentHashMap$Node (java.base@21.0.4)
|
||||
40: 48 122168 [C (java.base@21.0.4)
|
||||
41: 1440 113512 [S (java.base@21.0.4)
|
||||
42: 1201 105688 java.lang.reflect.Method (java.base@21.0.4)
|
||||
43: 3030 79616 [Ljava.lang.Class; (java.base@21.0.4)
|
||||
44: 1349 75544 jdk.internal.org.objectweb.asm.Label (java.base@21.0.4)
|
||||
45: 1550 74400 java.lang.invoke.MemberName (java.base@21.0.4)
|
||||
46: 332 74368 jdk.internal.org.objectweb.asm.MethodWriter (java.base@21.0.4)
|
||||
47: 1794 71760 java.lang.invoke.MethodType (java.base@21.0.4)
|
||||
48: 1089 69696 java.net.URL (java.base@21.0.4)
|
||||
49: 2011 64352 java.util.HashMap$Node (java.base@21.0.4)
|
||||
50: 121 50512 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@21.0.4)
|
||||
51: 3140 50240 jdk.internal.util.StrongReferenceKey (java.base@21.0.4)
|
||||
52: 491 49608 [Ljava.util.HashMap$Node; (java.base@21.0.4)
|
||||
53: 1057 42280 java.io.ObjectStreamField (java.base@21.0.4)
|
||||
54: 1225 39200 java.io.File (java.base@21.0.4)
|
||||
55: 779 37392 jdk.internal.org.objectweb.asm.Frame (java.base@21.0.4)
|
||||
56: 243 25272 java.util.jar.JarFile$JarFileEntry (java.base@21.0.4)
|
||||
57: 793 25224 [Ljava.lang.String; (java.base@21.0.4)
|
||||
58: 622 24880 java.lang.NoSuchFieldException (java.base@21.0.4)
|
||||
59: 571 22840 java.util.LinkedHashMap$Entry (java.base@21.0.4)
|
||||
60: 473 22704 jdk.internal.ref.CleanerImpl$PhantomCleanableRef (java.base@21.0.4)
|
||||
61: 689 22048 jdk.internal.util.WeakReferenceKey (java.base@21.0.4)
|
||||
62: 824 19776 jdk.internal.org.objectweb.asm.ByteVector (java.base@21.0.4)
|
||||
63: 248 18848 [Ljava.lang.ref.SoftReference; (java.base@21.0.4)
|
||||
64: 117 17784 jdk.internal.org.objectweb.asm.ClassWriter (java.base@21.0.4)
|
||||
65: 380 16824 [Ljava.lang.invoke.LambdaForm$Name; (java.base@21.0.4)
|
||||
66: 625 15000 java.lang.Long (java.base@21.0.4)
|
||||
67: 463 14816 java.lang.invoke.LambdaForm$Name (java.base@21.0.4)
|
||||
68: 903 14448 java.lang.Object (java.base@21.0.4)
|
||||
69: 198 14256 java.lang.reflect.Constructor (java.base@21.0.4)
|
||||
70: 249 13944 java.util.zip.ZipFile$ZipFileInputStream (java.base@21.0.4)
|
||||
71: 334 13360 jdk.internal.org.objectweb.asm.Handler (java.base@21.0.4)
|
||||
72: 202 12928 java.util.concurrent.ConcurrentHashMap (java.base@21.0.4)
|
||||
73: 201 12864 jdk.internal.org.objectweb.asm.FieldWriter (java.base@21.0.4)
|
||||
74: 316 12640 java.util.WeakHashMap$Entry (java.base@21.0.4)
|
||||
75: 102 12240 java.io.ObjectStreamClass (java.base@21.0.4)
|
||||
76: 249 11952 java.util.zip.ZipFile$ZipFileInflaterInputStream (java.base@21.0.4)
|
||||
77: 359 11488 jdk.internal.org.objectweb.asm.Type (java.base@21.0.4)
|
||||
78: 464 11136 jdk.internal.org.objectweb.asm.Edge (java.base@21.0.4)
|
||||
79: 463 11112 java.lang.invoke.ResolvedMethodName (java.base@21.0.4)
|
||||
80: 341 10912 jdk.internal.math.FDBigInteger (java.base@21.0.4)
|
||||
81: 94 10728 [Ljava.lang.reflect.Field; (java.base@21.0.4)
|
||||
82: 266 10640 java.lang.NoSuchMethodException (java.base@21.0.4)
|
||||
83: 266 10640 java.security.CodeSource (java.base@21.0.4)
|
||||
84: 264 10560 sun.security.util.KnownOIDs (java.base@21.0.4)
|
||||
85: 218 10464 java.lang.invoke.DirectMethodHandle$Constructor (java.base@21.0.4)
|
||||
86: 75 10200 sun.nio.fs.UnixFileAttributes (java.base@21.0.4)
|
||||
87: 123 9840 jdk.internal.event.DeserializationEvent (java.base@21.0.4)
|
||||
88: 245 9800 java.lang.ref.SoftReference (java.base@21.0.4)
|
||||
89: 115 9200 [Ljava.util.WeakHashMap$Entry; (java.base@21.0.4)
|
||||
90: 368 8832 java.lang.module.ModuleDescriptor$Exports (java.base@21.0.4)
|
||||
91: 63 8384 [Ljava.lang.invoke.MethodHandle; (java.base@21.0.4)
|
||||
92: 146 8176 java.io.FileCleanable (java.base@21.0.4)
|
||||
93: 125 8000 java.lang.Class$ReflectionData (java.base@21.0.4)
|
||||
94: 322 7728 java.util.ImmutableCollections$Set12 (java.base@21.0.4)
|
||||
95: 120 7680 jdk.internal.org.objectweb.asm.SymbolTable (java.base@21.0.4)
|
||||
96: 69 7176 java.lang.invoke.InnerClassLambdaMetafactory (java.base@21.0.4)
|
||||
97: 144 6912 jdk.internal.org.objectweb.asm.AnnotationWriter (java.base@21.0.4)
|
||||
98: 167 6680 jdk.internal.loader.URLClassPath$JarLoader$2 (java.base@21.0.4)
|
||||
99: 196 6272 java.lang.invoke.MethodHandles$Lookup (java.base@21.0.4)
|
||||
100: 156 6240 java.util.StringJoiner (java.base@21.0.4)
|
||||
101: 153 6120 java.io.FileDescriptor (java.base@21.0.4)
|
||||
102: 126 6048 java.lang.invoke.LambdaForm (java.base@21.0.4)
|
||||
103: 77 6016 [Ljava.lang.reflect.Method; (java.base@21.0.4)
|
||||
104: 249 5976 java.util.zip.ZipFile$InflaterCleanupAction (java.base@21.0.4)
|
||||
105: 370 5920 java.lang.Byte (java.base@21.0.4)
|
||||
106: 74 5920 java.util.zip.ZipFile$Source (java.base@21.0.4)
|
||||
107: 82 5720 [Ljava.io.ObjectStreamField; (java.base@21.0.4)
|
||||
108: 40 5640 [Ljava.lang.ClassValue$Entry; (java.base@21.0.4)
|
||||
109: 234 5616 java.util.jar.Attributes$Name (java.base@21.0.4)
|
||||
110: 174 5568 java.util.concurrent.locks.ReentrantLock$NonfairSync (java.base@21.0.4)
|
||||
111: 98 5488 java.lang.Module (java.base@21.0.4)
|
||||
112: 219 5256 java.lang.PublicMethods$MethodList (java.base@21.0.4)
|
||||
113: 65 5200 java.net.URI (java.base@21.0.4)
|
||||
114: 215 5104 [Ljdk.internal.org.objectweb.asm.Type; (java.base@21.0.4)
|
||||
115: 158 5056 java.lang.invoke.MethodTypeForm (java.base@21.0.4)
|
||||
116: 152 4864 java.nio.file.attribute.FileTime (java.base@21.0.4)
|
||||
117: 301 4816 java.util.HashSet (java.base@21.0.4)
|
||||
118: 75 4800 java.util.zip.Inflater (java.base@21.0.4)
|
||||
truncated...
|
||||
Total 4474389 138762960
|
||||
|
||||
0.113 s/op
|
||||
+totalAllocatedBytes: 138762960.000 bytes
|
||||
+totalAllocatedInstances: 4474389.000 instances
|
||||
+totalHeap: 521412608.000 bytes
|
||||
|
||||
|
||||
|
||||
Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalAllocatedBytes":
|
||||
138762960.000 bytes
|
||||
|
||||
Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalAllocatedInstances":
|
||||
4474389.000 instances
|
||||
|
||||
Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalHeap":
|
||||
521412608.000 bytes
|
||||
|
||||
|
||||
# Run complete. Total time: 00:00:00
|
||||
|
||||
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
|
||||
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
|
||||
experiments, perform baseline and negative tests that provide experimental control, make sure
|
||||
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
|
||||
Do not assume the numbers tell you what you want them to tell.
|
||||
|
||||
NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
|
||||
extra caution when trusting the results, look into the generated code to check the benchmark still
|
||||
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
|
||||
different JVMs are already problematic, the performance difference caused by different Blackhole
|
||||
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.
|
||||
|
||||
Benchmark Mode Cnt Score Error Units
|
||||
AllocationBenchmark.run ss 0.113 s/op
|
||||
AllocationBenchmark.run:+totalAllocatedBytes ss 138762960.000 bytes
|
||||
AllocationBenchmark.run:+totalAllocatedInstances ss 4474389.000 instances
|
||||
AllocationBenchmark.run:+totalHeap ss 521412608.000 bytes
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] Total time: 8.073 s
|
||||
[INFO] Finished at: 2024-10-10T12:26:18-04:00
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
28
pom.xml
28
pom.xml
|
|
@ -1,5 +1,5 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>dev.openfeature</groupId>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
|
||||
<junit.jupiter.version>5.11.2</junit.jupiter.version>
|
||||
<!-- exclusion expression for e2e tests -->
|
||||
<!-- exclusion expression for e2e tests -->
|
||||
<testExclusions>**/e2e/*.java</testExclusions>
|
||||
<module-name>${project.groupId}.${project.artifactId}</module-name>
|
||||
</properties>
|
||||
|
|
@ -146,6 +146,13 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>1.37</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
|
|
@ -473,7 +480,7 @@
|
|||
<version>3.10.1</version>
|
||||
<configuration>
|
||||
<failOnWarnings>true</failOnWarnings>
|
||||
<doclint>all,-missing</doclint> <!-- ignore missing javadoc, these are enforced with more customizability in the checkstyle plugin -->
|
||||
<doclint>all,-missing</doclint> <!-- ignore missing javadoc, these are enforced with more customizability in the checkstyle plugin -->
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
|
|
@ -507,6 +514,19 @@
|
|||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>benchmark</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pw.krejci</groupId>
|
||||
<artifactId>jmh-maven-plugin</artifactId>
|
||||
<version>0.2.2</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>e2e</id>
|
||||
<properties>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ abstract class AbstractStructure implements Structure {
|
|||
|
||||
protected final Map<String, Value> attributes;
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return attributes == null || attributes.size() == 0;
|
||||
}
|
||||
|
||||
AbstractStructure() {
|
||||
this.attributes = new HashMap<>();
|
||||
}
|
||||
|
|
@ -32,4 +37,5 @@ abstract class AbstractStructure implements Structure {
|
|||
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
|
||||
HashMap::putAll);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -19,11 +17,7 @@ class HookSupport {
|
|||
|
||||
public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
Stream<EvaluationContext> result = callBeforeHooks(flagValueType, hookCtx, hooks, hints);
|
||||
return hookCtx.getCtx().merge(
|
||||
result.reduce(hookCtx.getCtx(), (EvaluationContext accumulated, EvaluationContext current) -> {
|
||||
return accumulated.merge(current);
|
||||
}));
|
||||
return callBeforeHooks(flagValueType, hookCtx, hooks, hints);
|
||||
}
|
||||
|
||||
public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details,
|
||||
|
|
@ -46,10 +40,11 @@ class HookSupport {
|
|||
String hookMethod,
|
||||
Consumer<Hook<T>> hookCode) {
|
||||
if (hooks != null) {
|
||||
hooks
|
||||
.stream()
|
||||
.filter(hook -> hook.supportsFlagValueType(flagValueType))
|
||||
.forEach(hook -> executeChecked(hook, hookCode, hookMethod));
|
||||
for (Hook hook : hooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
executeChecked(hook, hookCode, hookMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,29 +63,29 @@ class HookSupport {
|
|||
FlagValueType flagValueType, List<Hook> hooks,
|
||||
Consumer<Hook<T>> hookCode) {
|
||||
if (hooks != null) {
|
||||
hooks
|
||||
.stream()
|
||||
.filter(hook -> hook.supportsFlagValueType(flagValueType))
|
||||
.forEach(hookCode::accept);
|
||||
for (Hook hook : hooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
hookCode.accept(hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<EvaluationContext> callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
|
||||
private EvaluationContext callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
|
||||
List<Hook> hooks, Map<String, Object> hints) {
|
||||
// These traverse backwards from normal.
|
||||
List<Hook> reversedHooks = IntStream
|
||||
.range(0, hooks.size())
|
||||
.map(i -> hooks.size() - 1 - i)
|
||||
.mapToObj(hooks::get)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return reversedHooks
|
||||
.stream()
|
||||
.filter(hook -> hook.supportsFlagValueType(flagValueType))
|
||||
.map(hook -> hook.before(hookCtx, hints))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.map(EvaluationContext.class::cast);
|
||||
List<Hook> reversedHooks = new ArrayList<>(hooks);
|
||||
Collections.reverse(reversedHooks);
|
||||
EvaluationContext context = hookCtx.getCtx();
|
||||
for (Hook hook : reversedHooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
Optional<EvaluationContext> optional = Optional.ofNullable(hook.before(hookCtx, hints))
|
||||
.orElse(Optional.empty());
|
||||
if (optional.isPresent()) {
|
||||
context = context.merge(optional.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,9 +78,12 @@ public final class ImmutableContext implements EvaluationContext {
|
|||
*/
|
||||
@Override
|
||||
public EvaluationContext merge(EvaluationContext overridingContext) {
|
||||
if (overridingContext == null) {
|
||||
if (overridingContext == null || overridingContext.isEmpty()) {
|
||||
return new ImmutableContext(this.asMap());
|
||||
}
|
||||
if (this.isEmpty()) {
|
||||
return new ImmutableContext(overridingContext.asMap());
|
||||
}
|
||||
|
||||
return new ImmutableContext(
|
||||
this.merge(ImmutableStructure::new, this.asMap(), overridingContext.asMap()));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package dev.openfeature.sdk;
|
|||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
|
|
@ -35,14 +36,7 @@ public final class ImmutableStructure extends AbstractStructure {
|
|||
* @param attributes attributes.
|
||||
*/
|
||||
public ImmutableStructure(Map<String, Value> attributes) {
|
||||
super(new HashMap<>(attributes.entrySet()
|
||||
.stream()
|
||||
.collect(HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(),
|
||||
Optional.ofNullable(entry.getValue())
|
||||
.map(Value::clone)
|
||||
.orElse(null)),
|
||||
HashMap::putAll)));
|
||||
super(copyAttributes(attributes));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -53,7 +47,7 @@ public final class ImmutableStructure extends AbstractStructure {
|
|||
// getters
|
||||
@Override
|
||||
public Value getValue(String key) {
|
||||
Value value = this.attributes.get(key);
|
||||
Value value = attributes.get(key);
|
||||
return value != null ? value.clone() : null;
|
||||
}
|
||||
|
||||
|
|
@ -64,14 +58,16 @@ public final class ImmutableStructure extends AbstractStructure {
|
|||
*/
|
||||
@Override
|
||||
public Map<String, Value> asMap() {
|
||||
return attributes
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(),
|
||||
Optional.ofNullable(entry.getValue())
|
||||
.map(Value::clone)
|
||||
.orElse(null)),
|
||||
HashMap::putAll);
|
||||
return copyAttributes(attributes);
|
||||
}
|
||||
|
||||
private static Map<String, Value> copyAttributes(Map<String, Value> in) {
|
||||
Map<String, Value> copy = new HashMap<>();
|
||||
for (Entry<String, Value> entry : in.entrySet()) {
|
||||
copy.put(entry.getKey(),
|
||||
Optional.ofNullable(entry.getValue()).map((Value val) -> val.clone()).orElse(null));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,8 +114,11 @@ public class MutableContext implements EvaluationContext {
|
|||
*/
|
||||
@Override
|
||||
public EvaluationContext merge(EvaluationContext overridingContext) {
|
||||
if (overridingContext == null) {
|
||||
return new MutableContext(this.asMap());
|
||||
if (overridingContext == null || overridingContext.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
if (this.isEmpty()) {
|
||||
return overridingContext;
|
||||
}
|
||||
|
||||
Map<String, Value> merged = this.merge(
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ public class MutableStructure extends AbstractStructure {
|
|||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return this.attributes.keySet();
|
||||
return attributes.keySet();
|
||||
}
|
||||
|
||||
// getters
|
||||
@Override
|
||||
public Value getValue(String key) {
|
||||
return this.attributes.get(key);
|
||||
return attributes.get(key);
|
||||
}
|
||||
|
||||
// adders
|
||||
|
|
@ -87,6 +87,6 @@ public class MutableStructure extends AbstractStructure {
|
|||
*/
|
||||
@Override
|
||||
public Map<String, Value> asMap() {
|
||||
return new HashMap<>(this.attributes);
|
||||
return new HashMap<>(attributes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ import static dev.openfeature.sdk.Value.objectToValue;
|
|||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public interface Structure {
|
||||
|
||||
/**
|
||||
* Boolean indicating if this structure is empty.
|
||||
* @return boolean for emptiness
|
||||
*/
|
||||
boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Get all keys.
|
||||
*
|
||||
|
|
@ -113,7 +119,14 @@ public interface Structure {
|
|||
default <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
|
||||
|
||||
if (base.isEmpty()) {
|
||||
return overriding;
|
||||
}
|
||||
if (overriding.isEmpty()) {
|
||||
return base;
|
||||
}
|
||||
|
||||
final Map<String, Value> merged = new HashMap<>(base);
|
||||
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
|
||||
String key = overridingEntry.getKey();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
package dev.openfeature.sdk.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
|
|
@ -64,9 +62,10 @@ public class ObjectUtils {
|
|||
*/
|
||||
@SafeVarargs
|
||||
public static <T> List<T> merge(List<T>... sources) {
|
||||
return Arrays
|
||||
.stream(sources)
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toList());
|
||||
List<T> merged = new ArrayList<>();
|
||||
for (List<T> source : sources) {
|
||||
merged.addAll(source);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package dev.openfeature.sdk.benchmark;
|
||||
|
||||
import static dev.openfeature.sdk.testutils.TestFlagsUtils.BOOLEAN_FLAG_KEY;
|
||||
import static dev.openfeature.sdk.testutils.TestFlagsUtils.FLOAT_FLAG_KEY;
|
||||
import static dev.openfeature.sdk.testutils.TestFlagsUtils.INT_FLAG_KEY;
|
||||
import static dev.openfeature.sdk.testutils.TestFlagsUtils.OBJECT_FLAG_KEY;
|
||||
import static dev.openfeature.sdk.testutils.TestFlagsUtils.STRING_FLAG_KEY;
|
||||
|
||||
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.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
|
||||
import dev.openfeature.sdk.Client;
|
||||
import dev.openfeature.sdk.EvaluationContext;
|
||||
import dev.openfeature.sdk.Hook;
|
||||
import dev.openfeature.sdk.HookContext;
|
||||
import dev.openfeature.sdk.ImmutableContext;
|
||||
import dev.openfeature.sdk.ImmutableStructure;
|
||||
import dev.openfeature.sdk.NoOpProvider;
|
||||
import dev.openfeature.sdk.OpenFeatureAPI;
|
||||
import dev.openfeature.sdk.Value;
|
||||
|
||||
/**
|
||||
* Runs a large volume of flag evaluations on a VM with 1G memory and GC
|
||||
* completely disabled so we can take a heap-dump.
|
||||
*/
|
||||
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" })
|
||||
public void run() {
|
||||
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider());
|
||||
Client client = OpenFeatureAPI.getInstance().getClient();
|
||||
client.addHooks(new Hook<Object>() {
|
||||
@Override
|
||||
public Optional<EvaluationContext> before(HookContext<Object> ctx, Map<String, Object> hints) {
|
||||
return Optional.ofNullable(new ImmutableContext());
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
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()), new ImmutableContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package dev.openfeature.sdk.benchmark;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.LineNumberReader;
|
||||
import java.io.PrintStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.openjdk.jmh.infra.BenchmarkParams;
|
||||
import org.openjdk.jmh.infra.IterationParams;
|
||||
import org.openjdk.jmh.profile.InternalProfiler;
|
||||
import org.openjdk.jmh.results.AggregationPolicy;
|
||||
import org.openjdk.jmh.results.IterationResult;
|
||||
import org.openjdk.jmh.results.Result;
|
||||
import org.openjdk.jmh.results.ScalarResult;
|
||||
import org.openjdk.jmh.util.Utils;
|
||||
|
||||
/**
|
||||
* Takes a heap dump (using JMAP from a separate process) after a benchmark;
|
||||
* only useful if GC is disabled during the benchmark.
|
||||
*/
|
||||
public class AllocationProfiler implements InternalProfiler {
|
||||
|
||||
public static class AllocationTotals {
|
||||
long instances;
|
||||
long bytes;
|
||||
|
||||
public AllocationTotals(long instances, long bytes) {
|
||||
this.instances = instances;
|
||||
this.bytes = bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Max memory heap profiler";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams) {
|
||||
// intentionally left blank
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Result> afterIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams,
|
||||
IterationResult result) {
|
||||
|
||||
long totalHeap = Runtime.getRuntime().totalMemory();
|
||||
AllocationTotals allocationTotals = AllocationProfiler.printHeapHistogram(System.out, 120);
|
||||
|
||||
Collection<ScalarResult> results = new ArrayList<>();
|
||||
results.add(new ScalarResult("+totalHeap", totalHeap, "bytes", AggregationPolicy.MAX));
|
||||
results.add(new ScalarResult("+totalAllocatedInstances", allocationTotals.instances, "instances",
|
||||
AggregationPolicy.MAX));
|
||||
results.add(new ScalarResult("+totalAllocatedBytes", allocationTotals.bytes, "bytes", AggregationPolicy.MAX));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static String getJmapExcutable() {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
String jreDir = File.separator + "jre";
|
||||
if (javaHome.endsWith(jreDir)) {
|
||||
javaHome = javaHome.substring(0, javaHome.length() - jreDir.length());
|
||||
}
|
||||
return (javaHome +
|
||||
File.separator +
|
||||
"bin" +
|
||||
File.separator +
|
||||
"jmap" +
|
||||
(Utils.isWindows() ? ".exe" : ""));
|
||||
}
|
||||
|
||||
// runs JMAP executable in a new process to collect a heap dump
|
||||
// heavily inspired by: https://github.com/cache2k/cache2k-benchmark/blob/master/jmh-suite/src/main/java/org/cache2k/benchmark/jmh/HeapProfiler.java
|
||||
private static AllocationTotals printHeapHistogram(PrintStream out, int maxLines) {
|
||||
long totalBytes = 0;
|
||||
long totalInstances = 0;
|
||||
boolean partial = false;
|
||||
try {
|
||||
Process jmapProcess = Runtime.getRuntime().exec(new String[] {
|
||||
getJmapExcutable(),
|
||||
"-histo:live",
|
||||
Long.toString(Utils.getPid()) });
|
||||
InputStream in = jmapProcess.getInputStream();
|
||||
LineNumberReader r = new LineNumberReader(new InputStreamReader(in));
|
||||
String line;
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
PrintStream printStream = new PrintStream(buffer);
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (line.startsWith("Total")) {
|
||||
printStream.println(line);
|
||||
String[] tokens = line.split("\\s+");
|
||||
totalInstances += Long.parseLong(tokens[1]);
|
||||
totalBytes = Long.parseLong(tokens[2]);
|
||||
} else if (r.getLineNumber() <= maxLines) {
|
||||
printStream.println(line);
|
||||
} else {
|
||||
if (!partial) {
|
||||
printStream.println("truncated...");
|
||||
}
|
||||
partial = true;
|
||||
}
|
||||
}
|
||||
r.close();
|
||||
in.close();
|
||||
printStream.close();
|
||||
byte[] histogramOutput = buffer.toByteArray();
|
||||
buffer = new ByteArrayOutputStream();
|
||||
printStream = new PrintStream(buffer);
|
||||
printStream.write(histogramOutput);
|
||||
printStream.println();
|
||||
printStream.close();
|
||||
out.write(buffer.toByteArray());
|
||||
} catch (Exception ex) {
|
||||
System.err.println("ForcedGcMemoryProfiler: error attaching / reading histogram");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return new AllocationTotals(totalInstances, totalBytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,33 +16,41 @@ import static dev.openfeature.sdk.Structure.mapToStructure;
|
|||
@UtilityClass
|
||||
public class TestFlagsUtils {
|
||||
|
||||
public static final String BOOLEAN_FLAG_KEY = "boolean-flag";
|
||||
public static final String STRING_FLAG_KEY = "string-flag";
|
||||
public static final String INT_FLAG_KEY = "integer-flag";
|
||||
public static final String FLOAT_FLAG_KEY = "float-flag";
|
||||
public static final String OBJECT_FLAG_KEY = "object-flag";
|
||||
public static final String CONTEXT_AWARE_FLAG_KEY = "context-aware";
|
||||
public static final String WRONG_FLAG_KEY = "wrong-flag";
|
||||
|
||||
/**
|
||||
* Building flags for testing purposes.
|
||||
* @return map of flags
|
||||
*/
|
||||
public static Map<String, Flag<?>> buildFlags() {
|
||||
Map<String, Flag<?>> flags = new HashMap<>();
|
||||
flags.put("boolean-flag", Flag.builder()
|
||||
flags.put(BOOLEAN_FLAG_KEY, Flag.builder()
|
||||
.variant("on", true)
|
||||
.variant("off", false)
|
||||
.defaultVariant("on")
|
||||
.build());
|
||||
flags.put("string-flag", Flag.builder()
|
||||
flags.put(STRING_FLAG_KEY, Flag.builder()
|
||||
.variant("greeting", "hi")
|
||||
.variant("parting", "bye")
|
||||
.defaultVariant("greeting")
|
||||
.build());
|
||||
flags.put("integer-flag", Flag.builder()
|
||||
flags.put(INT_FLAG_KEY, Flag.builder()
|
||||
.variant("one", 1)
|
||||
.variant("ten", 10)
|
||||
.defaultVariant("ten")
|
||||
.build());
|
||||
flags.put("float-flag", Flag.builder()
|
||||
flags.put(FLOAT_FLAG_KEY, Flag.builder()
|
||||
.variant("tenth", 0.1)
|
||||
.variant("half", 0.5)
|
||||
.defaultVariant("half")
|
||||
.build());
|
||||
flags.put("object-flag", Flag.builder()
|
||||
flags.put(OBJECT_FLAG_KEY, Flag.builder()
|
||||
.variant("empty", new HashMap<>())
|
||||
.variant("template", new Value(mapToStructure(ImmutableMap.of(
|
||||
"showImages", new Value(true),
|
||||
|
|
@ -51,7 +59,7 @@ public class TestFlagsUtils {
|
|||
))))
|
||||
.defaultVariant("template")
|
||||
.build());
|
||||
flags.put("context-aware", Flag.<String>builder()
|
||||
flags.put(CONTEXT_AWARE_FLAG_KEY, Flag.<String>builder()
|
||||
.variant("internal", "INTERNAL")
|
||||
.variant("external", "EXTERNAL")
|
||||
.defaultVariant("external")
|
||||
|
|
@ -63,7 +71,7 @@ public class TestFlagsUtils {
|
|||
}
|
||||
})
|
||||
.build());
|
||||
flags.put("wrong-flag", Flag.builder()
|
||||
flags.put(WRONG_FLAG_KEY, Flag.builder()
|
||||
.variant("one", "uno")
|
||||
.variant("two", "dos")
|
||||
.defaultVariant("one")
|
||||
|
|
|
|||
Loading…
Reference in New Issue