[DataViews] Fix excessive resolve_index requests in create data view flyout#109500
[DataViews] Fix excessive resolve_index requests in create data view flyout#109500Dosant merged 3 commits intoelastic:masterfrom
resolve_index requests in create data view flyout#109500Conversation
| // 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.
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.
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.
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.
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 theuseFormDatahook 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.
If resolving the behavior is more difficult than expected it would be good to document it.
| try { | ||
| const response = await http.get('/api/rollup/indices'); | ||
| if (isMounted.current) { | ||
| setRollupIndicesCapabilities(response || {}); |
There was a problem hiding this comment.
re-assigning {} created a new object and caused effects re-run
|
|
||
| const fetchIndices = async (query: string = '') => { | ||
| const currentRequestRef = {}; | ||
| currentLoadingMatchedIndicesRef.current = currentRequestRef; |
There was a problem hiding this comment.
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.
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.
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.
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.
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.
You can see in the documentation I wrote how this will be implemented here
There was a problem hiding this comment.
@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.
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.
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.
This is where fetch №2 happens:
The side effect inside the parent component
There was a problem hiding this comment.
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.
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.
(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) |
mattkime
left a comment
There was a problem hiding this comment.
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.
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!
| try { | ||
| const response = await http.get('/api/rollup/indices'); | ||
| if (isMounted.current) { | ||
| setRollupIndicesCapabilities(response || {}); |
sebelga
left a comment
There was a problem hiding this comment.
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 (#79607)
- I just opened this one: add an option to indicate that a validation 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.
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.
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
| let newRollupIndexName: string | undefined; | ||
|
|
||
| const fetchIndices = async (query: string = '') => { | ||
| const currentRequestRef = {}; |
There was a problem hiding this comment.
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.
looks good, easier to understand then the trick with ref to an object
sebelga
left a comment
There was a problem hiding this comment.
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: |
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