Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: reduce hashmap allocations #1178

Merged
merged 6 commits into from
Oct 24, 2024
Merged

Conversation

toddbaert
Copy link
Member

@toddbaert toddbaert commented Oct 22, 2024

Continuation of #1156; further reduces allocations, particularly of ImmutableContext and HashMap.

HashMap instances are reduced by more than 66%, memory usage decreased by another 40% in the benchmark.

Open benchmark.txt for results.

: 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.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is way uglier than before with a nice nested call to .merge() but this results in way less memory usage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see it, we're creating a new ImmutableContext for each merge() invocation. It seems like we could optimize this a bit more by skipping the merging of empty contexts. How do you feel about that? Here's an example of what I have in mind:

        final EvaluationContext apiContext = openfeatureApi.getEvaluationContext();
        final EvaluationContext clientContext = this.getEvaluationContext();
        final EvaluationContext transactionContext = openfeatureApi.getTransactionContext();
        final List<EvaluationContext> contextsToMerge = new ArrayList<>();
        contextsToMerge.add(apiContext);
        contextsToMerge.add(transactionContext);
        contextsToMerge.add(clientContext);
        contextsToMerge.add(invocationContext);

        EvaluationContext merged = new ImmutableContext();
        for (EvaluationContext evaluationContext : contextsToMerge) {
            if (evaluationContext != null && !evaluationContext.isEmpty()) {
                merged = merged.merge(evaluationContext);
            }
        }
        return merged;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But even if we do so, in the worst case (all contexts are not empty), we create 5 ImmutableContext objects per invocation.
What about pulling the merge logic out into a factory method on the 'ImmutableContext' that accepts 4 named contexts to merge?

Copy link
Member Author

@toddbaert toddbaert Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ssharaev @guidobrei I've done both of these things, as well as:

  • use a new variadic method that looks nicer
  • refactor to move the static merge function from Structure to EvaluationContext

see: f3e0686

*
* @return all attributes on the structure into a Map
*/
Map<String, Value> asUnmodifiableMap();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make good use of this internally when we want to just get the attributes as a map but don't care if we actually copy them or not - it just returns an immutable view, not a copy.

Copy link

codecov bot commented Oct 22, 2024

Codecov Report

Attention: Patch coverage is 88.67925% with 6 lines in your changes missing coverage. Please review.

Project coverage is 93.26%. Comparing base (7a1eb9b) to head (0bddca9).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...in/java/dev/openfeature/sdk/EvaluationContext.java 70.58% 2 Missing and 3 partials ⚠️
...ain/java/dev/openfeature/sdk/ImmutableContext.java 87.50% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1178      +/-   ##
============================================
+ Coverage     93.01%   93.26%   +0.25%     
- Complexity      436      442       +6     
============================================
  Files            40       41       +1     
  Lines          1016     1025       +9     
  Branches         84       85       +1     
============================================
+ Hits            945      956      +11     
  Misses           43       43              
+ Partials         28       26       -2     
Flag Coverage Δ
unittests 93.26% <88.67%> (+0.25%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@toddbaert toddbaert changed the title chore: reduce hashmap allocations perf: reduce hashmap allocations Oct 22, 2024
benchmark.txt Outdated
+totalAllocatedBytes: 138762960.000 bytes
+totalAllocatedInstances: 4474389.000 instances
0.132 s/op
+totalAllocatedBytes: 105727792.000 bytes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, great result! 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better now with #1178 (comment)

@@ -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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're already in an ImmutableContext and there's nothing to add, why not just return this?

This goes in the direction @ssharaev suggested in https://github.com/open-feature/java-sdk/pull/1178/files#r1811813866

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because then it's not a new instance, and I think always returning a new object from this method is less surprising than sometimes returning the same one.

src/main/java/dev/openfeature/sdk/MutableContext.java Outdated Show resolved Hide resolved
src/main/java/dev/openfeature/sdk/MutableContext.java Outdated Show resolved Hide resolved
: 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But even if we do so, in the worst case (all contexts are not empty), we create 5 ImmutableContext objects per invocation.
What about pulling the merge logic out into a factory method on the 'ImmutableContext' that accepts 4 named contexts to merge?

@toddbaert
Copy link
Member Author

toddbaert commented Oct 23, 2024

@guidobrei @ssharaev Please re-review; according to your advice (including @guidobrei 's comment here) I've made additional improvements. Please note, that during this process I merged an improvement to the benchmark to include more non-empty context data at various levels, so we could better gauge improvements in merging allocations.

With your additional feedback, we now even have a more dramatic improvement:

before:

                 +totalAllocatedBytes:       244216640.000 bytes
                 +totalAllocatedInstances: 7264791.000 instances

after:

                 +totalAllocatedBytes:       139359040.000 bytes
                 +totalAllocatedInstances: 4452140.000 instances

Signed-off-by: Todd Baert <[email protected]>
Copy link
Member

@aepfli aepfli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry, I tried reviewing this on my mobile, but I failed half way through ;) I will give it a shot later. There is one comment, which might also reduce the amount of collections again. (Not sure, if this is maybe handled by the function call anyways)

* @return immutable map
*/
public Map<String, Value> asUnmodifiableMap() {
return Collections.unmodifiableMap(attributes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth to check if this is an unmodifiable nap, before wrapping it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to SO, Collections.unmodifiableMap already does this.

@@ -107,41 +113,6 @@ default Object convertValue(Value value) {
throw new ValueNotConvertableError();
}

/**
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal is very technically a breaking change, but it was never intended to be public and it's very awkward to use. It has more to do with EvaluationContexts not Structures anyway and I doubt it's widely used.

Copy link

sonarcloud bot commented Oct 24, 2024

@toddbaert toddbaert merged commit fd7659a into main Oct 24, 2024
9 of 10 checks passed
@toddbaert toddbaert deleted the chore/more-mem-improvements branch October 24, 2024 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants