Skip to content

Commit

Permalink
perf: reduce hashmap allocations (#1178)
Browse files Browse the repository at this point in the history
* chore: reduce hashmap allocations

Signed-off-by: Todd Baert <[email protected]>
  • Loading branch information
toddbaert authored Oct 24, 2024
1 parent 7a1eb9b commit fd7659a
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 226 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
262 changes: 131 additions & 131 deletions benchmark.txt

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion src/main/java/dev/openfeature/sdk/AbstractStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Collections;

@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" })
abstract class AbstractStructure implements Structure {
Expand All @@ -18,7 +19,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 Collections.unmodifiableMap(attributes);
}

/**
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/dev/openfeature/sdk/EvaluationContext.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package dev.openfeature.sdk;

import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;

/**
* The EvaluationContext is a container for arbitrary contextual data
* that can be used as a basis for dynamic evaluation.
Expand All @@ -19,4 +23,39 @@ public interface EvaluationContext extends Structure {
* @return resulting merged context
*/
EvaluationContext merge(EvaluationContext overridingContext);

/**
* Recursively merges the overriding map into the base Value map.
* The base map is mutated, the overriding map is not.
* Null maps will cause no-op.
*
* @param newStructure function to create the right structure(s) for Values
* @param base base map to merge
* @param overriding overriding map to merge
*/
static void mergeMaps(Function<Map<String, Value>, Structure> newStructure,
Map<String, Value> base,
Map<String, Value> overriding) {

if (base == null) {
return;
}
if (overriding == null || overriding.isEmpty()) {
return;
}

for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
String key = overridingEntry.getKey();
if (overridingEntry.getValue().isStructure() && base.containsKey(key) && base.get(key).isStructure()) {
Structure mergedValue = base.get(key).asStructure();
Structure overridingValue = overridingEntry.getValue().asStructure();
Map<String, Value> newMap = mergedValue.asMap();
mergeMaps(newStructure, newMap,
overridingValue.asUnmodifiableMap());
base.put(key, new Value(newStructure.apply(newMap)));
} else {
base.put(key, overridingEntry.getValue());
}
}
}
}
29 changes: 16 additions & 13 deletions src/main/java/dev/openfeature/sdk/ImmutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
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;

/**
* The EvaluationContext is a container for arbitrary contextual data
* that can be used as a basis for dynamic evaluation.
* The ImmutableContext is an EvaluationContext implementation which is threadsafe, and whose attributes can
* The ImmutableContext is an EvaluationContext implementation which is
* threadsafe, and whose attributes can
* not be modified after instantiation.
*/
@ToString
Expand All @@ -21,7 +23,8 @@ public final class ImmutableContext implements EvaluationContext {
private final ImmutableStructure structure;

/**
* Create an immutable context with an empty targeting_key and attributes provided.
* Create an immutable context with an empty targeting_key and attributes
* provided.
*/
public ImmutableContext() {
this(new HashMap<>());
Expand All @@ -42,7 +45,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 +56,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 @@ -71,31 +72,33 @@ public String getTargetingKey() {
}

/**
* Merges this EvaluationContext object with the passed EvaluationContext, overriding in case of conflict.
* Merges this EvaluationContext object with the passed EvaluationContext,
* overriding in case of conflict.
*
* @param overridingContext overriding context
* @return new, resulting merged context
*/
@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()));
Map<String, Value> attributes = this.asMap();
EvaluationContext.mergeMaps(ImmutableStructure::new, attributes,
overridingContext.asUnmodifiableMap());
return new ImmutableContext(attributes);
}

@SuppressWarnings("all")
private static class DelegateExclusions {
@ExcludeFromGeneratedCoverageReport
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
Map<String, Value> base,
Map<String, Value> overriding) {

return null;
}
}
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
11 changes: 6 additions & 5 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(null, 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 @@ -121,9 +121,10 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
return overridingContext;
}

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

/**
Expand Down
Loading

0 comments on commit fd7659a

Please sign in to comment.