-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[DataViews] Fix excessive resolve_index
requests in create data view flyout
#109500
Conversation
// to get a stable reference to avoid hooks re-run and reduce number of excessive requests | ||
const [ | ||
{ | ||
title = schema.title.defaultValue, |
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 like internally useFormData
has some before init state, where these all returns undefined
even though default values are specified in the schema.
Initial switches between undefined
and ""
caused some redundant effects re-run
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 didn't work with our form lib before, so not sure if there is a better approach
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 did notice this was happening. This seems like a bug with the form lib. What do you think @sebelga ?
👍 on finding a work around!
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 wouldn't say it's a bug, it is more an unpleasant side effect on how the architecture and react hook lifecycle work. I looked into it while working on #109238 but decided to keep that PR focused. It is on my radar and will see what can be done.
For you information this is how the data flow works:
- You call the hook and ask for the value --> the field is not mounted yet so its value is
undefined
- In a
useEffect()
inside<UseField />
the field mounts and say to the form: "I am here!" --> this updates the field value and theuseFormData
hook triggers an update that you receive.
You can pass any number of fields inside the schema (that might not actually exist in the form) but the source of truth of real fields rendered in the form is the DOM (JSX) --> so when a field mounts and say to the form it is there. This is why I don't rely on the schema to return defaultValue because the field might not exist.
For now, the consumer has to do the check manually:
if (title === undefined) {
return;
}
I'll give it more thought to improve this unexpected behaviour 👍
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 resolving the behavior is more difficult than expected it would be good to document it.
@@ -165,7 +176,9 @@ const IndexPatternEditorFlyoutContentComponent = ({ | |||
try { | |||
const response = await http.get('/api/rollup/indices'); | |||
if (isMounted.current) { | |||
setRollupIndicesCapabilities(response || {}); |
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.
re-assigning {}
created a new object and caused effects re-run
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.
oooph, good point!
@@ -225,52 +238,28 @@ const IndexPatternEditorFlyoutContentComponent = ({ | |||
let newRollupIndexName: string | undefined; | |||
|
|||
const fetchIndices = async (query: string = '') => { | |||
const currentRequestRef = {}; | |||
currentLoadingMatchedIndicesRef.current = currentRequestRef; |
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 added this to avoid a race-condition where we can override more recent request with older response
|
||
if (isMounted.current) { | ||
const { matchedIndicesResult, exactMatched } = !isLoadingSources | ||
? await loadMatchedIndices(query, allowHidden, allSources, { |
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.
Had to extract loadMatchedIndices
into a separate function to wrap it into memoize-one
. Otherwise, eslint-hooks complained that it can't resolve args.
I kept it in the current file to keep it simpler.
); | ||
|
||
if (isMounted.current) { | ||
const { matchedIndicesResult, exactMatched } = !isLoadingSources |
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 like we don't need to make a request while isLoadingSources
didn't resolve. I short cut it
// loadMatchedIndices is called both as an side effect inside of a parent component and the inside forms validation functions | ||
// that are challenging to synchronize without a larger refactor | ||
// Use memoizeOne as a caching layer to avoid excessive network requests on each key type | ||
const loadMatchedIndices = memoizeOne( |
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.
memoizeOne
is the simplest way I can think of to share the state between validation and component side-effect.
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 thinking in refactoring the code code once #109238 is merged so I am not sure we need to optimise for now.
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.
You can see in the documentation I wrote how this will be implemented 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.
@sebelga, I've just tried to remove it and I get 3 network requests on each keystroke instead of 1 without it.
Do you think this quick trick doesn't worth it? I also planned to backport to at least 7.15 (looking at it like on a bug).
We can still refactor in 7.16+ when using your new API. wdyt?
I am open to reverting, just want to make sure we consider this ^^
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.
Cool yes the trick is worth it and we can remove it later on 👍 Surprised it is 3 requests and not 2. But still worth 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.
The side effect inside the parent component
Mmmm. Can you point me to that code? The parent component also triggers a "fetchIndices()"?
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 is where fetch №2 happens:
The side effect inside the parent component
kibana/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx
Line 295 in 0a8d42c
reloadMatchedIndices(title);
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 see on L286
useEffect(() => {
reloadMatchedIndices(title);
}, [allowHidden, reloadMatchedIndices, title]);
I am not sure I fully understand why we need this. I thought it was the validator inside the titleField that was triggering the reload. Why do we need also this effect?
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 if I understand correctly, you call twice the reloadMatchedIndices
? Once inside the validation (when title
changes) and once in this useEffect
again when title
changes? It seems that the useEffect
is redundant (it also has allowHidden
dependency which should not be there).
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 also has allowHidden dependency which should not be there).
yes, I agree.
It seems that the useEffect is redundant
Hm, it is indeed might be redundant. I haven't tried to remove it
Pinging @elastic/kibana-app-services (Team:AppServices) |
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.
changes look good and work well!
// to get a stable reference to avoid hooks re-run and reduce number of excessive requests | ||
const [ | ||
{ | ||
title = schema.title.defaultValue, |
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 did notice this was happening. This seems like a bug with the form lib. What do you think @sebelga ?
👍 on finding a work around!
@@ -165,7 +176,9 @@ const IndexPatternEditorFlyoutContentComponent = ({ | |||
try { | |||
const response = await http.get('/api/rollup/indices'); | |||
if (isMounted.current) { | |||
setRollupIndicesCapabilities(response || {}); |
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.
oooph, good point!
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.
Thanks for working on this @Dosant ! I indeed saw those requests and there a few optimisation that the form lib could do to reduce them.
- A long standing one to debounce the validation requests ([Form Lib] Add option to debounce async validations #79607)
- I just opened this one: add an option to indicate that a validation is asynchronous ([Form lib] Add option to indicate that a validator is asynchronous #109628)
As I added in my comment I would revert the changes (memoizeOne())
you made to improve the indirection of loading the indices from the validation as #109238 will solve that problem and will make the code much easier to reason about.
// loadMatchedIndices is called both as an side effect inside of a parent component and the inside forms validation functions | ||
// that are challenging to synchronize without a larger refactor | ||
// Use memoizeOne as a caching layer to avoid excessive network requests on each key type | ||
const loadMatchedIndices = memoizeOne( |
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 thinking in refactoring the code code once #109238 is merged so I am not sure we need to optimise for now.
// loadMatchedIndices is called both as an side effect inside of a parent component and the inside forms validation functions | ||
// that are challenging to synchronize without a larger refactor | ||
// Use memoizeOne as a caching layer to avoid excessive network requests on each key type | ||
const loadMatchedIndices = memoizeOne( |
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.
You can see in the documentation I wrote how this will be implemented here
src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx
Outdated
Show resolved
Hide resolved
@@ -225,52 +238,28 @@ const IndexPatternEditorFlyoutContentComponent = ({ | |||
let newRollupIndexName: string | undefined; | |||
|
|||
const fetchIndices = async (query: string = '') => { | |||
const currentRequestRef = {}; |
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 usually use a counter for this purpose and simply increment it.
const currentLoadingMatchedIndicesRef = useRef(0);
...
const fetchIndices = async (query: string = '') => {
const idx = ++currentLoadingMatchedIndicesRef.current;
...
if (idx === currentLoadingMatchedIndicesRef.current) {
// update states
}
}
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, easier to understand then the trick with ref to an object
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.
Happy to merge with your fix to reduce 3 requests to 1 and refactor once the new form lib API is merged. Great work! 👍
💚 Build Succeeded
Metrics [docs]Module Count
Async chunks
History
To update your PR or re-run it, just comment with: |
…w flyout (#109500) (#109700) Co-authored-by: Anton Dosov <[email protected]>
…w flyout (#109500) (#109701) Co-authored-by: Anton Dosov <[email protected]>
Summary
close #108854
Tried to reduce the number of network calls:
Opening the flyout and typing
abc
:BEFORE
AFTER
There could be other edge cases