Skip to content
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

Merged
merged 3 commits into from
Aug 23, 2021

Conversation

Dosant
Copy link
Contributor

@Dosant Dosant commented Aug 20, 2021

Summary

close #108854

Tried to reduce the number of network calls:

Opening the flyout and typing abc:

BEFORE

Screen Shot 2021-08-20 at 17 38 23

AFTER

Screen Shot 2021-08-20 at 17 36 33

There could be other edge cases

@Dosant Dosant added Feature:Data Views Data Views code and UI - index patterns before 8.0 v8.0.0 Team:AppServices release_note:skip Skip the PR/issue when compiling release notes v7.15.0 labels Aug 20, 2021
// to get a stable reference to avoid hooks re-run and reduce number of excessive requests
const [
{
title = schema.title.defaultValue,
Copy link
Contributor Author

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

Copy link
Contributor Author

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

Copy link
Contributor

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!

Copy link
Contributor

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:

  1. You call the hook and ask for the value --> the field is not mounted yet so its value is undefined
  2. In a useEffect() inside <UseField /> the field mounts and say to the form: "I am here!" --> this updates the field value and the useFormData 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 👍

Copy link
Contributor

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 || {});
Copy link
Contributor Author

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

Copy link
Contributor

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;
Copy link
Contributor Author

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, {
Copy link
Contributor Author

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
Copy link
Contributor Author

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(
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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 ^^

Copy link
Contributor

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 😊

Copy link
Contributor

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()"?

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor

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).

Copy link
Contributor Author

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

@Dosant Dosant marked this pull request as ready for review August 20, 2021 18:05
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-app-services (Team:AppServices)

Copy link
Contributor

@mattkime mattkime left a 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,
Copy link
Contributor

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 || {});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oooph, good point!

Copy link
Contributor

@sebelga sebelga left a 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.

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(
Copy link
Contributor

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -225,52 +238,28 @@ const IndexPatternEditorFlyoutContentComponent = ({
let newRollupIndexName: string | undefined;

const fetchIndices = async (query: string = '') => {
const currentRequestRef = {};
Copy link
Contributor

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
  }
}

Copy link
Contributor Author

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

@Dosant Dosant requested a review from sebelga August 23, 2021 12:37
Copy link
Contributor

@sebelga sebelga left a 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! 👍

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
indexPatternEditor 106 107 +1

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
indexPatternEditor 174.0KB 175.7KB +1.7KB

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@kibanamachine
Copy link
Contributor

💚 Backport successful

Status Branch Result
7.15
7.x

The backport PRs will be merged automatically after passing CI.

kibanamachine added a commit that referenced this pull request Aug 23, 2021
kibanamachine added a commit that referenced this pull request Aug 23, 2021
@Dosant Dosant added the bug Fixes for quality problems that affect the customer experience label Aug 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated - use backport:version if exact versions are needed bug Fixes for quality problems that affect the customer experience Feature:Data Views Data Views code and UI - index patterns before 8.0 release_note:skip Skip the PR/issue when compiling release notes v7.15.0 v7.16.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Index Patterns] Excessive resolve_index requests when opening create index pattern flyout
5 participants