Skip to content

Commit

Permalink
chore: reduce hashmap allocations
Browse files Browse the repository at this point in the history
Signed-off-by: Todd Baert <[email protected]>
  • Loading branch information
toddbaert committed Oct 22, 2024
1 parent cedad9c commit 5c86f3a
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 158 deletions.
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ mvn test -P e2e
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'
mvn -P benchmark clean compile 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.
Note that the ONLY MEANINGFUL RESULTS of this benchmark are the `totalAllocatedBytes` and the `totalAllocatedInstances`.
The `run` score, and maven task time are not relevant since this benchmark is purely memory-related and has nothing to do with speed.
You can also view the heap breakdown to see which objects are taking up the most memory.

## Releasing

Expand Down
313 changes: 184 additions & 129 deletions benchmark.txt

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/main/java/dev/openfeature/sdk/AbstractStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ public boolean isEmpty() {
}

AbstractStructure(Map<String, Value> attributes) {
this.attributes = new HashMap<>(attributes);
this.attributes = attributes;
}

/**
* Returns an unmodifiable representation of the internal attribute map.
* @return immutable map
*/
public Map<String, Value> asUnmodifiableMap() {
return Map.copyOf(attributes);
}

/**
Expand Down
13 changes: 6 additions & 7 deletions src/main/java/dev/openfeature/sdk/ImmutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
import lombok.ToString;
import lombok.experimental.Delegate;
Expand Down Expand Up @@ -42,7 +43,7 @@ public ImmutableContext(String targetingKey) {
* @param attributes evaluation context attributes
*/
public ImmutableContext(Map<String, Value> attributes) {
this("", attributes);
this(null, attributes);
}

/**
Expand All @@ -53,9 +54,7 @@ public ImmutableContext(Map<String, Value> attributes) {
*/
public ImmutableContext(String targetingKey, Map<String, Value> attributes) {
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
final Map<String, Value> actualAttribs = new HashMap<>(attributes);
actualAttribs.put(TARGETING_KEY, new Value(targetingKey));
this.structure = new ImmutableStructure(actualAttribs);
this.structure = new ImmutableStructure(targetingKey, attributes);
} else {
this.structure = new ImmutableStructure(attributes);
}
Expand All @@ -79,14 +78,14 @@ public String getTargetingKey() {
@Override
public EvaluationContext merge(EvaluationContext overridingContext) {
if (overridingContext == null || overridingContext.isEmpty()) {
return new ImmutableContext(this.asMap());
return new ImmutableContext(this.asUnmodifiableMap());
}
if (this.isEmpty()) {
return new ImmutableContext(overridingContext.asMap());
return new ImmutableContext(overridingContext.asUnmodifiableMap());
}

return new ImmutableContext(
this.merge(ImmutableStructure::new, this.asMap(), overridingContext.asMap()));
this.merge(ImmutableStructure::new, this.asUnmodifiableMap(), overridingContext.asUnmodifiableMap()));
}

@SuppressWarnings("all")
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/dev/openfeature/sdk/ImmutableStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ public ImmutableStructure() {
* @param attributes attributes.
*/
public ImmutableStructure(Map<String, Value> attributes) {
super(copyAttributes(attributes));
super(copyAttributes(attributes, null));
}

protected ImmutableStructure(String targetingKey, Map<String, Value> attributes) {
super(copyAttributes(attributes, targetingKey));
}

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

private static Map<String, Value> copyAttributes(Map<String, Value> in) {
return copyAttributes(in, null);
}

private static Map<String, Value> copyAttributes(Map<String, Value> in, String targetingKey) {
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));
}
if (targetingKey != null) {
copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey));
}
return copy;
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/java/dev/openfeature/sdk/MutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public MutableContext(String targetingKey) {
}

public MutableContext(Map<String, Value> attributes) {
this("", attributes);
this("", new HashMap<>(attributes));
}

/**
Expand All @@ -44,7 +44,7 @@ public MutableContext(Map<String, Value> attributes) {
* @param attributes evaluation context attributes
*/
public MutableContext(String targetingKey, Map<String, Value> attributes) {
this.structure = new MutableStructure(attributes);
this.structure = new MutableStructure(new HashMap<>(attributes));
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
this.structure.attributes.put(TARGETING_KEY, new Value(targetingKey));
}
Expand Down Expand Up @@ -122,7 +122,7 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
}

Map<String, Value> merged = this.merge(
MutableStructure::new, this.asMap(), overridingContext.asMap());
MutableStructure::new, this.asUnmodifiableMap(), overridingContext.asUnmodifiableMap());
return new MutableContext(merged);
}

Expand Down
35 changes: 23 additions & 12 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
() -> FlagEvaluationOptions.builder().build());
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext());

FlagEvaluationDetails<T> details = null;
List<Hook> mergedHooks = null;
Expand Down Expand Up @@ -183,17 +182,29 @@ private static <T> void enrichDetailsWithErrorDefaults(T defaultValue, FlagEvalu
* @return merged evaluation context
*/
private EvaluationContext mergeEvaluationContext(EvaluationContext invocationContext) {
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null
? openfeatureApi.getEvaluationContext()
: new ImmutableContext();
final EvaluationContext clientContext = this.getEvaluationContext() != null
? this.getEvaluationContext()
: new ImmutableContext();
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext() != null
? openfeatureApi.getTransactionContext()
: new ImmutableContext();

return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext)));
// avoid any unnecessary context instantiations and stream usage here; this is call with every evaluation.
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext();
final EvaluationContext clientContext = this.getEvaluationContext();
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext();
final List<EvaluationContext> contextsToMerge = new ArrayList<>();
if (apiContext != null) {
contextsToMerge.add(apiContext);
}
if (transactionContext != null) {
contextsToMerge.add(transactionContext);
}
if (clientContext != null) {
contextsToMerge.add(clientContext);
}
if (invocationContext != null) {
contextsToMerge.add(invocationContext);
}

EvaluationContext merged = new ImmutableContext();
for (EvaluationContext evaluationContext : contextsToMerge) {
merged = merged.merge(evaluationContext);
}
return merged;
}

private <T> ProviderEvaluation<?> createProviderEvaluation(
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/dev/openfeature/sdk/Structure.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public interface Structure {
*/
Map<String, Value> asMap();

/**
* Get all values, as a map of Values.
*
* @return all attributes on the structure into a Map
*/
Map<String, Value> asUnmodifiableMap();


/**
* Get all values, with as a map of Object.
*
Expand Down Expand Up @@ -95,7 +103,7 @@ default Object convertValue(Value value) {

if (value.isStructure()) {
Structure s = value.asStructure();
return s.asMap()
return s.asUnmodifiableMap()
.entrySet()
.stream()
.collect(HashMap::new,
Expand Down Expand Up @@ -126,14 +134,15 @@ default <T extends Structure> Map<String, Value> merge(Function<Map<String, Valu
if (overriding.isEmpty()) {
return base;
}

final Map<String, Value> merged = new HashMap<>(base);
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
String key = overridingEntry.getKey();
if (overridingEntry.getValue().isStructure() && merged.containsKey(key) && merged.get(key).isStructure()) {
Structure mergedValue = merged.get(key).asStructure();
Structure overridingValue = overridingEntry.getValue().asStructure();
Map<String, Value> newMap = this.merge(newStructure, mergedValue.asMap(), overridingValue.asMap());
Map<String, Value> newMap = this.merge(newStructure, mergedValue.asUnmodifiableMap(),
overridingValue.asUnmodifiableMap());
merged.put(key, new Value(newStructure.apply(newMap)));
} else {
merged.put(key, overridingEntry.getValue());
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/dev/openfeature/sdk/Value.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ protected Value clone() {
return new Value(copy);
}
if (this.isStructure()) {
return new Value(new ImmutableStructure(this.asStructure().asMap()));
return new Value(new ImmutableStructure(this.asStructure().asUnmodifiableMap()));
}
if (this.isInstant()) {
Instant copy = Instant.ofEpochMilli(this.asInstant().toEpochMilli());
Expand Down

0 comments on commit 5c86f3a

Please sign in to comment.