Skip to content

Commit fd7659a

Browse files
authored
perf: reduce hashmap allocations (#1178)
* chore: reduce hashmap allocations Signed-off-by: Todd Baert <[email protected]>
1 parent 7a1eb9b commit fd7659a

10 files changed

+277
-226
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

+131-131
Large diffs are not rendered by default.

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.HashMap;
44
import java.util.Map;
5+
import java.util.Collections;
56

67
@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" })
78
abstract class AbstractStructure implements Structure {
@@ -18,7 +19,15 @@ public boolean isEmpty() {
1819
}
1920

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

2433
/**

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

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package dev.openfeature.sdk;
22

3+
import java.util.Map;
4+
import java.util.Map.Entry;
5+
import java.util.function.Function;
6+
37
/**
48
* The EvaluationContext is a container for arbitrary contextual data
59
* that can be used as a basis for dynamic evaluation.
@@ -19,4 +23,39 @@ public interface EvaluationContext extends Structure {
1923
* @return resulting merged context
2024
*/
2125
EvaluationContext merge(EvaluationContext overridingContext);
26+
27+
/**
28+
* Recursively merges the overriding map into the base Value map.
29+
* The base map is mutated, the overriding map is not.
30+
* Null maps will cause no-op.
31+
*
32+
* @param newStructure function to create the right structure(s) for Values
33+
* @param base base map to merge
34+
* @param overriding overriding map to merge
35+
*/
36+
static void mergeMaps(Function<Map<String, Value>, Structure> newStructure,
37+
Map<String, Value> base,
38+
Map<String, Value> overriding) {
39+
40+
if (base == null) {
41+
return;
42+
}
43+
if (overriding == null || overriding.isEmpty()) {
44+
return;
45+
}
46+
47+
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
48+
String key = overridingEntry.getKey();
49+
if (overridingEntry.getValue().isStructure() && base.containsKey(key) && base.get(key).isStructure()) {
50+
Structure mergedValue = base.get(key).asStructure();
51+
Structure overridingValue = overridingEntry.getValue().asStructure();
52+
Map<String, Value> newMap = mergedValue.asMap();
53+
mergeMaps(newStructure, newMap,
54+
overridingValue.asUnmodifiableMap());
55+
base.put(key, new Value(newStructure.apply(newMap)));
56+
} else {
57+
base.put(key, overridingEntry.getValue());
58+
}
59+
}
60+
}
2261
}

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

+16-13
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
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;
910

1011
/**
1112
* The EvaluationContext is a container for arbitrary contextual data
1213
* that can be used as a basis for dynamic evaluation.
13-
* The ImmutableContext is an EvaluationContext implementation which is threadsafe, and whose attributes can
14+
* The ImmutableContext is an EvaluationContext implementation which is
15+
* threadsafe, and whose attributes can
1416
* not be modified after instantiation.
1517
*/
1618
@ToString
@@ -21,7 +23,8 @@ public final class ImmutableContext implements EvaluationContext {
2123
private final ImmutableStructure structure;
2224

2325
/**
24-
* Create an immutable context with an empty targeting_key and attributes provided.
26+
* Create an immutable context with an empty targeting_key and attributes
27+
* provided.
2528
*/
2629
public ImmutableContext() {
2730
this(new HashMap<>());
@@ -42,7 +45,7 @@ public ImmutableContext(String targetingKey) {
4245
* @param attributes evaluation context attributes
4346
*/
4447
public ImmutableContext(Map<String, Value> attributes) {
45-
this("", attributes);
48+
this(null, attributes);
4649
}
4750

4851
/**
@@ -53,9 +56,7 @@ public ImmutableContext(Map<String, Value> attributes) {
5356
*/
5457
public ImmutableContext(String targetingKey, Map<String, Value> attributes) {
5558
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);
59+
this.structure = new ImmutableStructure(targetingKey, attributes);
5960
} else {
6061
this.structure = new ImmutableStructure(attributes);
6162
}
@@ -71,31 +72,33 @@ public String getTargetingKey() {
7172
}
7273

7374
/**
74-
* Merges this EvaluationContext object with the passed EvaluationContext, overriding in case of conflict.
75+
* Merges this EvaluationContext object with the passed EvaluationContext,
76+
* overriding in case of conflict.
7577
*
7678
* @param overridingContext overriding context
7779
* @return new, resulting merged context
7880
*/
7981
@Override
8082
public EvaluationContext merge(EvaluationContext overridingContext) {
8183
if (overridingContext == null || overridingContext.isEmpty()) {
82-
return new ImmutableContext(this.asMap());
84+
return new ImmutableContext(this.asUnmodifiableMap());
8385
}
8486
if (this.isEmpty()) {
85-
return new ImmutableContext(overridingContext.asMap());
87+
return new ImmutableContext(overridingContext.asUnmodifiableMap());
8688
}
8789

88-
return new ImmutableContext(
89-
this.merge(ImmutableStructure::new, this.asMap(), overridingContext.asMap()));
90+
Map<String, Value> attributes = this.asMap();
91+
EvaluationContext.mergeMaps(ImmutableStructure::new, attributes,
92+
overridingContext.asUnmodifiableMap());
93+
return new ImmutableContext(attributes);
9094
}
9195

9296
@SuppressWarnings("all")
9397
private static class DelegateExclusions {
9498
@ExcludeFromGeneratedCoverageReport
95-
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
99+
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
96100
Map<String, Value> base,
97101
Map<String, Value> overriding) {
98-
99102
return null;
100103
}
101104
}

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

+6-5
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(null, 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
}
@@ -121,9 +121,10 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
121121
return overridingContext;
122122
}
123123

124-
Map<String, Value> merged = this.merge(
125-
MutableStructure::new, this.asMap(), overridingContext.asMap());
126-
return new MutableContext(merged);
124+
Map<String, Value> attributes = this.asMap();
125+
EvaluationContext.mergeMaps(
126+
MutableStructure::new, attributes, overridingContext.asUnmodifiableMap());
127+
return new MutableContext(attributes);
127128
}
128129

129130
/**

0 commit comments

Comments
 (0)