-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
[charts] Plugin selector memoization issue #16575
base: master
Are you sure you want to change the base?
Conversation
Deploy preview: https://deploy-preview-16575--material-ui-x.netlify.app/ |
equalityCheck: complainIfNoMemoize('prop check'), | ||
resultEqualityCheck: complainIfNoMemoize('result check'), |
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.
A possible solution is to just use fastShallowCompare
everywhere. It should fixes all the issues we are seeing.
Another way is to use process.env.NODE_ENV !== 'production' ? complainIfNoMemoize() : Object.is
and we will have to manually add the correct comparison to useSelector
. Though this only fixes result check
, not the prop check
CodSpeed Performance ReportMerging #16575 will not alter performanceComparing Summary
|
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 👍
Not approving because there's some commented code that probably should be used or removed
import { ChartAnyPluginSignature, ChartState, ChartStateCacheKey } from '../models'; | ||
|
||
const complainIfNoMemoize = |
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 use this? I guess it makes sense. Is there any way to remove code when building for a release?
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.
equalityCheck: process.env.NODE_ENV !== 'production' ? compain : fast
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 PR is mostly for discussion on the issue, it does not depict the final solution. 😄
I think the discussion would be more if we want to use fastShallowCompare
directly, which would fix the problems everywhere.
Or if we want to have this complain
function, and manually fix those when we detect them.
if (Array.isArray(a)) { | ||
return fastArrayCompare(a, b); | ||
} | ||
if (b instanceof Object) { | ||
return fastObjectShallowCompare(a, b); | ||
} |
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 logic is a bit confusing to me, why is it checking a
for array and b
for object?
Also the object check should probably be typeof x === 'object' && x !== null
. instanceof Object
will fail for Object.create(null)
, which is required in some cases (e.g. inserting entries by id in an object where the id is a string provided by the user - otherwise ids like 'constructor'
or '__proto__'
could mess up things).
In general I wouldn't advise using the "fast" methods as generic equality check functions, the tradeoff for "fast" is usually correctness in edge-cases. Imo we should maybe even mark those functions as unsafe_fastObjectShallowCompare
to clearly indicate there are invariants that must be taken care of by the caller.
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 logic is a bit confusing to me, why is it checking
a
for array andb
for object?
Mistake
In general I wouldn't advise using the "fast" methods as generic equality check functions, the tradeoff for "fast" is usually correctness in edge-cases. Imo we should maybe even mark those functions as
unsafe_fastObjectShallowCompare
to clearly indicate there are invariants that must be taken care of by the caller.
Should we use a "complete compare" instead?
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.
Depends on the use-case. Maybe I should have written more in the jsdoc, but the fast object compare is only really meant for record objects like { a: 1, b: 2, ... }
, and will possibly fail with anything else (e.g. prototype different from Object
, a Date
, a RegExp
, non-enumerable props, etc). If you can guarantee that it will not receive non-exotic objects, then it's safe to use, but otherwise you should indeed use a "complete compare" focused on robustness/correctness.
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.
Also note that when I used fastObjectShallowCompare
for our selectors in the grid, it was to deal with mutable records that were kept in a ref - in other words, it was a workaround for a hack. Moving forward, I plan that we don't use it anymore for selectors.
Imho, selectors should receive standard arguments, not bag-of-data arguments like { seriesId, etc }
, then the standard args memoization logic will work. It will also improve performance to avoid those allocations. In the grid we use intermediate selectors like @alexfauquette outlined below.
Should we have a two selector contructors like
Because we have different usecases. For example selectors like For selectors that accept objects as an argument like For selectors doing computation. Like
For the second (following code block) I assuem the fact that the selected height and width are the same according to selectorChartContainerWidth = (size) => size.width + size.left + size.right
selectorChartContainerHeight = (size) => height: size.height + size.top + size.bottom
selectorChartContainerSize = createSelector(
[selectorChartContainerWidth, selectorChartContainerHeight],
(width, height) => ({ width, height})
) |
I think we have plenty of use-cases. Generally returning an object that is not memoized is not an issue because we have other selectors up/down the road that fixes it somehow. But any selector that accepts an object will probably not memoize the user's input. Which also means the cache will grow and never get cleaned.
This can create quite a lot of boilerplate for a code that is already very boilerplaty 🫠 |
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
Ensure you open a page with few charts when running locally it or it will slow down your browser a lot
The issue mainly seems to revolve around selectors/hooks that accept objects, eg:
useItemHighlighted({ seriesId })
And selectors that return a new object, eg: