-
Notifications
You must be signed in to change notification settings - Fork 38
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
Conversation
87e6c5d
to
5c86f3a
Compare
: 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
toEvaluationContext
see: f3e0686
* | ||
* @return all attributes on the structure into a Map | ||
*/ | ||
Map<String, Value> asUnmodifiableMap(); |
There was a problem hiding this comment.
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.
Codecov ReportAttention: Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
benchmark.txt
Outdated
+totalAllocatedBytes: 138762960.000 bytes | ||
+totalAllocatedInstances: 4474389.000 instances | ||
0.132 s/op | ||
+totalAllocatedBytes: 105727792.000 bytes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, great result! 👍
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
: 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. |
There was a problem hiding this comment.
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?
Signed-off-by: Todd Baert <[email protected]>
Signed-off-by: Todd Baert <[email protected]>
Signed-off-by: Todd Baert <[email protected]>
Signed-off-by: Todd Baert <[email protected]>
f3e0686
to
bddf7f2
Compare
@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:
after:
|
Signed-off-by: Todd Baert <[email protected]>
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); | |||
} | |||
|
|||
/** |
There was a problem hiding this comment.
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.
Quality Gate passedIssues Measures |
Continuation of #1156; further reduces allocations, particularly of
ImmutableContext
andHashMap
.HashMap
instances are reduced by more than 66%, memory usage decreased by another 40% in the benchmark.Open
benchmark.txt
for results.