Skip to content

Commit 5c86f3a

Browse files
committed
chore: reduce hashmap allocations
Signed-off-by: Todd Baert <[email protected]>
1 parent cedad9c commit 5c86f3a

9 files changed

+254
-158
lines changed

CONTRIBUTING.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@ mvn test -P e2e
3535
There is a small JMH benchmark suite for testing allocations that can be run with:
3636

3737
```sh
38-
mvn -P benchmark test-compile jmh:benchmark -Djmh.f=1 -Djmh.prof='dev.openfeature.sdk.benchmark.AllocationProfiler'
38+
mvn -P benchmark clean compile test-compile jmh:benchmark -Djmh.f=1 -Djmh.prof='dev.openfeature.sdk.benchmark.AllocationProfiler'
3939
```
4040

4141
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.
42+
Note that the ONLY MEANINGFUL RESULTS of this benchmark are the `totalAllocatedBytes` and the `totalAllocatedInstances`.
43+
The `run` score, and maven task time are not relevant since this benchmark is purely memory-related and has nothing to do with speed.
44+
You can also view the heap breakdown to see which objects are taking up the most memory.
4245

4346
## Releasing
4447

benchmark.txt

+184-129
Large diffs are not rendered by default.

src/main/java/dev/openfeature/sdk/AbstractStructure.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ public boolean isEmpty() {
1818
}
1919

2020
AbstractStructure(Map<String, Value> attributes) {
21-
this.attributes = new HashMap<>(attributes);
21+
this.attributes = attributes;
22+
}
23+
24+
/**
25+
* Returns an unmodifiable representation of the internal attribute map.
26+
* @return immutable map
27+
*/
28+
public Map<String, Value> asUnmodifiableMap() {
29+
return Map.copyOf(attributes);
2230
}
2331

2432
/**

src/main/java/dev/openfeature/sdk/ImmutableContext.java

+6-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.HashMap;
44
import java.util.Map;
55
import java.util.function.Function;
6+
67
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
78
import lombok.ToString;
89
import lombok.experimental.Delegate;
@@ -42,7 +43,7 @@ public ImmutableContext(String targetingKey) {
4243
* @param attributes evaluation context attributes
4344
*/
4445
public ImmutableContext(Map<String, Value> attributes) {
45-
this("", attributes);
46+
this(null, attributes);
4647
}
4748

4849
/**
@@ -53,9 +54,7 @@ public ImmutableContext(Map<String, Value> attributes) {
5354
*/
5455
public ImmutableContext(String targetingKey, Map<String, Value> attributes) {
5556
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
56-
final Map<String, Value> actualAttribs = new HashMap<>(attributes);
57-
actualAttribs.put(TARGETING_KEY, new Value(targetingKey));
58-
this.structure = new ImmutableStructure(actualAttribs);
57+
this.structure = new ImmutableStructure(targetingKey, attributes);
5958
} else {
6059
this.structure = new ImmutableStructure(attributes);
6160
}
@@ -79,14 +78,14 @@ public String getTargetingKey() {
7978
@Override
8079
public EvaluationContext merge(EvaluationContext overridingContext) {
8180
if (overridingContext == null || overridingContext.isEmpty()) {
82-
return new ImmutableContext(this.asMap());
81+
return new ImmutableContext(this.asUnmodifiableMap());
8382
}
8483
if (this.isEmpty()) {
85-
return new ImmutableContext(overridingContext.asMap());
84+
return new ImmutableContext(overridingContext.asUnmodifiableMap());
8685
}
8786

8887
return new ImmutableContext(
89-
this.merge(ImmutableStructure::new, this.asMap(), overridingContext.asMap()));
88+
this.merge(ImmutableStructure::new, this.asUnmodifiableMap(), overridingContext.asUnmodifiableMap()));
9089
}
9190

9291
@SuppressWarnings("all")

src/main/java/dev/openfeature/sdk/ImmutableStructure.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ public ImmutableStructure() {
3636
* @param attributes attributes.
3737
*/
3838
public ImmutableStructure(Map<String, Value> attributes) {
39-
super(copyAttributes(attributes));
39+
super(copyAttributes(attributes, null));
40+
}
41+
42+
protected ImmutableStructure(String targetingKey, Map<String, Value> attributes) {
43+
super(copyAttributes(attributes, targetingKey));
4044
}
4145

4246
@Override
@@ -62,11 +66,18 @@ public Map<String, Value> asMap() {
6266
}
6367

6468
private static Map<String, Value> copyAttributes(Map<String, Value> in) {
69+
return copyAttributes(in, null);
70+
}
71+
72+
private static Map<String, Value> copyAttributes(Map<String, Value> in, String targetingKey) {
6573
Map<String, Value> copy = new HashMap<>();
6674
for (Entry<String, Value> entry : in.entrySet()) {
6775
copy.put(entry.getKey(),
6876
Optional.ofNullable(entry.getValue()).map((Value val) -> val.clone()).orElse(null));
6977
}
78+
if (targetingKey != null) {
79+
copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey));
80+
}
7081
return copy;
7182
}
7283

src/main/java/dev/openfeature/sdk/MutableContext.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public MutableContext(String targetingKey) {
3333
}
3434

3535
public MutableContext(Map<String, Value> attributes) {
36-
this("", attributes);
36+
this("", new HashMap<>(attributes));
3737
}
3838

3939
/**
@@ -44,7 +44,7 @@ public MutableContext(Map<String, Value> attributes) {
4444
* @param attributes evaluation context attributes
4545
*/
4646
public MutableContext(String targetingKey, Map<String, Value> attributes) {
47-
this.structure = new MutableStructure(attributes);
47+
this.structure = new MutableStructure(new HashMap<>(attributes));
4848
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
4949
this.structure.attributes.put(TARGETING_KEY, new Value(targetingKey));
5050
}
@@ -122,7 +122,7 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
122122
}
123123

124124
Map<String, Value> merged = this.merge(
125-
MutableStructure::new, this.asMap(), overridingContext.asMap());
125+
MutableStructure::new, this.asUnmodifiableMap(), overridingContext.asUnmodifiableMap());
126126
return new MutableContext(merged);
127127
}
128128

src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

+23-12
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
110110
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
111111
() -> FlagEvaluationOptions.builder().build());
112112
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
113-
ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext());
114113

115114
FlagEvaluationDetails<T> details = null;
116115
List<Hook> mergedHooks = null;
@@ -183,17 +182,29 @@ private static <T> void enrichDetailsWithErrorDefaults(T defaultValue, FlagEvalu
183182
* @return merged evaluation context
184183
*/
185184
private EvaluationContext mergeEvaluationContext(EvaluationContext invocationContext) {
186-
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null
187-
? openfeatureApi.getEvaluationContext()
188-
: new ImmutableContext();
189-
final EvaluationContext clientContext = this.getEvaluationContext() != null
190-
? this.getEvaluationContext()
191-
: new ImmutableContext();
192-
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext() != null
193-
? openfeatureApi.getTransactionContext()
194-
: new ImmutableContext();
195-
196-
return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext)));
185+
// avoid any unnecessary context instantiations and stream usage here; this is call with every evaluation.
186+
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext();
187+
final EvaluationContext clientContext = this.getEvaluationContext();
188+
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext();
189+
final List<EvaluationContext> contextsToMerge = new ArrayList<>();
190+
if (apiContext != null) {
191+
contextsToMerge.add(apiContext);
192+
}
193+
if (transactionContext != null) {
194+
contextsToMerge.add(transactionContext);
195+
}
196+
if (clientContext != null) {
197+
contextsToMerge.add(clientContext);
198+
}
199+
if (invocationContext != null) {
200+
contextsToMerge.add(invocationContext);
201+
}
202+
203+
EvaluationContext merged = new ImmutableContext();
204+
for (EvaluationContext evaluationContext : contextsToMerge) {
205+
merged = merged.merge(evaluationContext);
206+
}
207+
return merged;
197208
}
198209

199210
private <T> ProviderEvaluation<?> createProviderEvaluation(

src/main/java/dev/openfeature/sdk/Structure.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ public interface Structure {
4646
*/
4747
Map<String, Value> asMap();
4848

49+
/**
50+
* Get all values, as a map of Values.
51+
*
52+
* @return all attributes on the structure into a Map
53+
*/
54+
Map<String, Value> asUnmodifiableMap();
55+
56+
4957
/**
5058
* Get all values, with as a map of Object.
5159
*
@@ -95,7 +103,7 @@ default Object convertValue(Value value) {
95103

96104
if (value.isStructure()) {
97105
Structure s = value.asStructure();
98-
return s.asMap()
106+
return s.asUnmodifiableMap()
99107
.entrySet()
100108
.stream()
101109
.collect(HashMap::new,
@@ -126,14 +134,15 @@ default <T extends Structure> Map<String, Value> merge(Function<Map<String, Valu
126134
if (overriding.isEmpty()) {
127135
return base;
128136
}
129-
137+
130138
final Map<String, Value> merged = new HashMap<>(base);
131139
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
132140
String key = overridingEntry.getKey();
133141
if (overridingEntry.getValue().isStructure() && merged.containsKey(key) && merged.get(key).isStructure()) {
134142
Structure mergedValue = merged.get(key).asStructure();
135143
Structure overridingValue = overridingEntry.getValue().asStructure();
136-
Map<String, Value> newMap = this.merge(newStructure, mergedValue.asMap(), overridingValue.asMap());
144+
Map<String, Value> newMap = this.merge(newStructure, mergedValue.asUnmodifiableMap(),
145+
overridingValue.asUnmodifiableMap());
137146
merged.put(key, new Value(newStructure.apply(newMap)));
138147
} else {
139148
merged.put(key, overridingEntry.getValue());

src/main/java/dev/openfeature/sdk/Value.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ protected Value clone() {
274274
return new Value(copy);
275275
}
276276
if (this.isStructure()) {
277-
return new Value(new ImmutableStructure(this.asStructure().asMap()));
277+
return new Value(new ImmutableStructure(this.asStructure().asUnmodifiableMap()));
278278
}
279279
if (this.isInstant()) {
280280
Instant copy = Instant.ofEpochMilli(this.asInstant().toEpochMilli());

0 commit comments

Comments
 (0)