-
Notifications
You must be signed in to change notification settings - Fork 51
feat: Layered context to reduce memory churn #1717
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
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
# Conflicts: # src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1717 +/- ##
============================================
+ Coverage 91.69% 93.72% +2.03%
- Complexity 517 597 +80
============================================
Files 51 53 +2
Lines 1264 1403 +139
Branches 112 150 +38
============================================
+ Hits 1159 1315 +156
+ Misses 68 50 -18
- Partials 37 38 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
| * API-level, Transaction-level, Client-level, Invocation-level, and Hook-level. | ||
| * The contexts are checked in that order for values, with Hook-level having the highest precedence. | ||
| */ | ||
| public class LayeredEvaluationContext implements EvaluationContext { |
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.
Alternatively, I could also implement this as a list of EvaluationContexts, with each list entry representing a context level (index 0 = api, index 1 = transaction, ...) This could maybe make the code cleaner (especially the merging, as I would just need to add another list entry; I don't think merge will/should be called often on this context though), but also less performant. Wdyt?
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.
so theoretically before hooks can also add information for the evaluation context - and this should also be somehow merged into this. would it make sense to simple add all the evaluationContext returned to the layer, and iterate over them as a list?
Question if it is worth it - at least in theory it would be one less object
Signed-off-by: christian.lutnik <[email protected]>
aepfli
left a comment
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.
looks good to me, i only have a nit (regarding optionals) and a question for beforehooks :)
| Optional<EvaluationContext> returnedEvalContext = Optional.ofNullable( | ||
| hook.before(hookContext, data.getHints())) | ||
| .orElse(Optional.empty()); | ||
| if (returnedEvalContext.isPresent()) { |
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 we are using Optionals already, we should use all the available syntax sugar, and switch to .ifPresent and chain as much as possible
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.
That would be nice, but Optionals in Java don't work well enough to get this with type safety. I would need some castings and redundant isPresent checks to get this to work
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 do we need the Optional wrapping on line 61? we want to reduce object churn etc. but we are generating a new Optional all the time - so i am wondering if we really need the Optional or if a null check would be sufficient here
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 tried to change as little as possible of unrelated code so that we can scientifically see the benefit of the new context. Other optimizations will come in a later PR
| * API-level, Transaction-level, Client-level, Invocation-level, and Hook-level. | ||
| * The contexts are checked in that order for values, with Hook-level having the highest precedence. | ||
| */ | ||
| public class LayeredEvaluationContext implements EvaluationContext { |
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.
so theoretically before hooks can also add information for the evaluation context - and this should also be somehow merged into this. would it make sense to simple add all the evaluationContext returned to the layer, and iterate over them as a list?
Question if it is worth it - at least in theory it would be one less object
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
# Conflicts: # src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmarkState.java
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
| if (apiContext != null) { | ||
| keys.addAll(apiContext.keySet()); | ||
| } | ||
| this.keySet = keys; |
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.
Should we make use of the Collections.emptySet() singleton while we have no keys?
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 don't think I understand your comment, we only know that we have no keys at the end of this method
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 was saying perhaps we could more lazily create a key set if there actually is any keys, and otherwise just return the empty set and avoid allocating something, but it's a micro-optimization.
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 how do we know if we have any keys without generating the keyset? And after we generated it, we might as well return it, as we have allocated it anyway
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.
It's the allocation of the HashSet on 107 that I think we can save, possibly, which is a heap object... I think we can do all the null checks and then only create this object if any of them are non null, otherwise just return the singleton, right? Maybe I'm missing something.
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.
could the isEmpty() method be used to determine whether to create the HashSet, or if true, return the singleton?
|
/gemini review |
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.
Code Review
This pull request introduces a LayeredEvaluationContext to reduce memory churn during flag evaluations by avoiding the merging of evaluation contexts into a single map. The implementation looks solid and achieves its goal. I've added one suggestion to improve the performance of asMap(), asUnmodifiableMap(), and asObjectMap() methods in the new LayeredEvaluationContext class by optimizing how the final map is constructed. Overall, this is a great improvement for performance.
| public Map<String, Value> asMap() { | ||
| var keySet = ensureKeySet(); | ||
| var keys = keySet.size(); | ||
| if (keys == 0) { | ||
| return new HashMap<>(0); | ||
| } | ||
| var map = new HashMap<String, Value>(keys); | ||
|
|
||
| for (String key : keySet) { | ||
| map.put(key, getValue(key)); | ||
| } | ||
| return map; | ||
| } | ||
|
|
||
| @Override | ||
| public Map<String, Value> asUnmodifiableMap() { | ||
| var keySet = ensureKeySet(); | ||
| var keys = keySet.size(); | ||
| if (keys == 0) { | ||
| return Collections.emptyMap(); | ||
| } | ||
| var map = new HashMap<String, Value>(keys); | ||
|
|
||
| for (String key : keySet) { | ||
| map.put(key, getValue(key)); | ||
| } | ||
| return Collections.unmodifiableMap(map); | ||
| } | ||
|
|
||
| @Override | ||
| public Map<String, Object> asObjectMap() { | ||
| var keySet = ensureKeySet(); | ||
| var keys = keySet.size(); | ||
| if (keys == 0) { | ||
| return new HashMap<>(0); | ||
| } | ||
| var map = new HashMap<String, Object>(keys); | ||
|
|
||
| for (String key : keySet) { | ||
| map.put(key, convertValue(getValue(key))); | ||
| } | ||
| return map; | ||
| } |
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.
The implementations of asMap(), asUnmodifiableMap(), and asObjectMap() can be made more efficient. The current approach first builds a complete keySet by iterating through all context layers, and then for each key, it calls getValue(), which again iterates through the layers to find the value. This results in multiple traversals of the context layers for each call to these methods.
A more performant approach would be to iterate through the context layers from lowest to highest precedence, adding all entries from each context to a result map. This way, values from higher-precedence contexts will naturally overwrite those from lower-precedence ones, and each context is traversed only once. This also aligns better with the goal of reducing memory churn by avoiding the creation of an intermediate keySet.
@Override
public Map<String, Value> asMap() {
var map = new HashMap<String, Value>();
// Apply from lowest to highest precedence, so that higher precedence contexts overwrite lower ones.
if (apiContext != null) {
map.putAll(apiContext.asMap());
}
if (transactionContext != null) {
map.putAll(transactionContext.asMap());
}
if (clientContext != null) {
map.putAll(clientContext.asMap());
}
if (invocationContext != null) {
map.putAll(invocationContext.asMap());
}
if (hookContexts != null) {
for (EvaluationContext hookContext : hookContexts) {
if (hookContext != null) {
map.putAll(hookContext.asMap());
}
}
}
return map;
}
@Override
public Map<String, Value> asUnmodifiableMap() {
return Collections.unmodifiableMap(asMap());
}
@Override
public Map<String, Object> asObjectMap() {
var map = new HashMap<String, Object>();
// Apply from lowest to highest precedence, so that higher precedence contexts overwrite lower ones.
if (apiContext != null) {
map.putAll(apiContext.asObjectMap());
}
if (transactionContext != null) {
map.putAll(transactionContext.asObjectMap());
}
if (clientContext != null) {
map.putAll(clientContext.asObjectMap());
}
if (invocationContext != null) {
map.putAll(invocationContext.asObjectMap());
}
if (hookContexts != null) {
for (EvaluationContext hookContext : hookContexts) {
if (hookContext != null) {
map.putAll(hookContext.asObjectMap());
}
}
}
return map;
}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.
That really helped a lot
toddbaert
left a comment
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.
@chrfwow looks excellent.
I've added some possible small optimizations, but no blockers.
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
Signed-off-by: christian.lutnik <[email protected]>
|
|
||
| @Override | ||
| public boolean isEmpty() { | ||
| return (hookContexts == null || hookContexts.isEmpty()) |
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 the hookContexts collection contains at least one EvaluationContext, then a call to isEmpty will return false.
But we do not check inside the hookContexts collection that all the EvaluationContexts that it contains are all empty.
should these also be checked for emptiness?
e.g. (hookContexts == null || hookContexts.isEmpty() || hookContexts.stream().allMatch(ec -> ec.isEmpty()))
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.
Nice catch!
Signed-off-by: christian.lutnik <[email protected]>
|
I'll merge this tomorrow unless I hear objections. |
Signed-off-by: christian.lutnik <[email protected]>
|





Introduces layered context to reduce memory churn
Part of #1718