-
Notifications
You must be signed in to change notification settings - Fork 57
perf: eliminate merge allocation in setHooks by accepting hook sources directly #1956
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
|
|
||
| import dev.openfeature.sdk.fixtures.HookFixtures; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
@@ -38,7 +39,13 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() { | |
| var sharedContext = getBaseHookContextForType(FlagValueType.STRING); | ||
| var hookSupportData = new HookSupportData(); | ||
| hookSupportData.evaluationContext = layered; | ||
| hookSupport.setHooks(hookSupportData, Arrays.asList(hook1, hook2), FlagValueType.STRING); | ||
| hookSupport.setHooks( | ||
| hookSupportData, | ||
| Collections.emptyList(), | ||
| Collections.emptyList(), | ||
| Arrays.asList(hook1, hook2), | ||
| Collections.emptyList(), | ||
| FlagValueType.STRING); | ||
| hookSupport.setHookContexts(hookSupportData, sharedContext, layered); | ||
|
|
||
| hookSupport.executeBeforeHooks(hookSupportData); | ||
|
|
@@ -57,7 +64,13 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) { | |
| Hook<?> genericHook = mockGenericHook(); | ||
|
|
||
| var hookSupportData = new HookSupportData(); | ||
| hookSupport.setHooks(hookSupportData, List.of(genericHook), flagValueType); | ||
| hookSupport.setHooks( | ||
| hookSupportData, | ||
| List.of(genericHook), | ||
| Collections.emptyList(), | ||
| Collections.emptyList(), | ||
| Collections.emptyList(), | ||
| flagValueType); | ||
|
|
||
| callAllHooks(hookSupportData); | ||
|
|
||
|
|
@@ -73,7 +86,13 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) { | |
| void shouldPassDataAcrossStages(FlagValueType flagValueType) { | ||
| var testHook = new TestHookWithData(); | ||
| var hookSupportData = new HookSupportData(); | ||
| hookSupport.setHooks(hookSupportData, List.of(testHook), flagValueType); | ||
| hookSupport.setHooks( | ||
| hookSupportData, | ||
| Collections.emptyList(), | ||
| List.of(testHook), | ||
| Collections.emptyList(), | ||
| Collections.emptyList(), | ||
| flagValueType); | ||
| hookSupport.setHookContexts( | ||
| hookSupportData, | ||
| getBaseHookContextForType(flagValueType), | ||
|
|
@@ -102,7 +121,13 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { | |
| var testHook2 = new TestHookWithData(2); | ||
|
|
||
| var hookSupportData = new HookSupportData(); | ||
| hookSupport.setHooks(hookSupportData, List.of(testHook1, testHook2), flagValueType); | ||
| hookSupport.setHooks( | ||
| hookSupportData, | ||
| Collections.emptyList(), | ||
| Collections.emptyList(), | ||
| Collections.emptyList(), | ||
| List.of(testHook1, testHook2), | ||
| flagValueType); | ||
| hookSupport.setHookContexts( | ||
| hookSupportData, | ||
| getBaseHookContextForType(flagValueType), | ||
|
|
@@ -114,6 +139,28 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { | |
| assertHookData(testHook2, 2, "before", "after", "finallyAfter", "error"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should place hooks in provider → options → client → API order") | ||
| void shouldOrderHooksBySource() { | ||
| Hook<?> providerHook = mockGenericHook(); | ||
| Hook<?> optionHook = mockGenericHook(); | ||
| Hook<?> clientHook = mockGenericHook(); | ||
| Hook<?> apiHook = mockGenericHook(); | ||
|
|
||
| var hookSupportData = new HookSupportData(); | ||
| hookSupport.setHooks( | ||
| hookSupportData, | ||
| List.of(providerHook), | ||
| List.of(optionHook), | ||
| List.of(clientHook), | ||
| List.of(apiHook), | ||
| FlagValueType.STRING); | ||
|
|
||
| assertThat(hookSupportData.getHooks()) | ||
| .extracting(Pair::getKey) | ||
| .containsExactly(providerHook, optionHook, clientHook, apiHook); | ||
| } | ||
|
|
||
| @Test | ||
| void hookThatReturnsTheGivenContext_doesNotResultInAStackOverflow() { | ||
| var hookSupportData = new HookSupportData(); | ||
|
|
@@ -132,7 +179,13 @@ public Optional<EvaluationContext> before(HookContext ctx, Map hints) { | |
| var layeredEvaluationContext = | ||
| new LayeredEvaluationContext(evaluationContextWithValue("key", "value"), null, null, null); | ||
| hookSupportData.evaluationContext = layeredEvaluationContext; | ||
| hookSupport.setHooks(hookSupportData, List.of(recursiveHook, emptyHook), FlagValueType.STRING); | ||
| hookSupport.setHooks( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd pass the non empty list in different order for each of these tests, and I think we should also add a test to verify that the hooks are placed in the correct order in the resulting list
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adapted the tests as suggested and added an extra test to ensure order. |
||
| hookSupportData, | ||
| Collections.emptyList(), | ||
| Collections.emptyList(), | ||
| List.of(recursiveHook, emptyHook), | ||
| Collections.emptyList(), | ||
| FlagValueType.STRING); | ||
| hookSupport.setHookContexts( | ||
| hookSupportData, getBaseHookContextForType(FlagValueType.STRING), layeredEvaluationContext); | ||
|
|
||
|
|
||
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.
To prevent potential
NullPointerExceptions and further optimize performance, we should:providerHooksoroptionHooks) or individual hooks within them arenull.ArrayListwith the sum of the sizes of the non-null collections avoids internal array resizing overhead during population.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 don't know which hooks will be added to the array list, so we would be overestimating the size of the list. I'm not sure if this is worth it, and if we can come up with a benchmark that represensts real life 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.
Under 10 hooks, it looks like the runtime will allocate at least that many anyway: https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/ArrayList.java#L119 - if I'm reading this correctly.
I don't think we'll often have more than 10, so I think doing more than what's here now is 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.
Are null checks that Gemini mentions are concern?
As far as I can tell there were none in place previously, which is why I didn't consider adding them.