-
-
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
[DataGrid] Performance optimization #1513
Conversation
Test with 1 million rows
Filter by Quantity > 10000 => it shows a clear perf improvment ✅ |
Filter and rendering should be faster. Sorting will not be affected, as this change did not affect the comparator functions. |
@dtassone I think that we should be very careful with using measures in I had a look, using the production versions: SortingIn this reproducible benchmark, I have sorted the quantity column once. https://deploy-preview-1513--material-ui-x.netlify.app/components/data-grid/#commercial-version https://material-ui.com/components/data-grid/#commercial-version It looks the same. If you account for the fact that sorting these very 100,000 rows takes <1ms, I don't see why it wouldn't render in ~100ms. I assume something is really wrong to require x10 more time. FilteringIn this benchmark, I filter the rows with quantity > 1. https://deploy-preview-1513--material-ui-x.netlify.app/components/data-grid/#commercial-version https://material-ui.com/components/data-grid/#commercial-version It looks like the filtering was not working before. This is a great win. However, if you account for the fact that filtering these very 100,000 rows takes <1ms, I don't see why it wouldn't render in ~100ms. I assume something is really wrong to require x10 more time. |
packages/grid/_modules_/grid/models/params/gridColumnHeaderParams.ts
Outdated
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Having a quick look at the bottleneck. The x10 issue with the number sorting seems to come from the code structure, it does a lot of work that ends up trashed. Assuming N is the number of rows doesn't understand why:
I would expect a significant difference by solving the above Regarding the filtering, the issue is simpler than sorting, there is no N log(N) scaling issue. It's more about computing a lot of things that are trashed in the end. |
This comment has been minimized.
This comment has been minimized.
packages/grid/_modules_/grid/components/columnSelection/GridCellCheckboxRenderer.tsx
Outdated
Show resolved
Hide resolved
A summary of what we talk about during today's call
|
packages/storybook/src/stories/playground/customize-components.stories.tsx
Outdated
Show resolved
Hide resolved
I hope the breaking changes are too disruptive. It looks great otherwise. One thing that would be awesome is to add a performance test to make sure we can't completely break it in the future, as we did a month ago. I have pushed two commits to improve the performance of sorting, leveraging the log(N) point I made earlier. I have benchmarked on the "Quantity" column of https://deploy-preview-1513--material-ui-x.netlify.app/components/data-grid/#commercial-version. Enjoy. The performance of filtering is borderline, I couldn't find any obvious win like for sorting, maybe there are, I don't have the time to look. |
(Regarding the PR titles, I have renamed this one. We uppercase after the [label] and renamed optimisation, we use en_US not en_GB. This aims to have a clean and easy to read changelog, nothing more or less) |
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.
One thing that would be awesome is to add a performance test to make sure we can't completely break it in the future, as we did a month ago.
Actually, since we fixed the rerendering logic. I think that it's really important that we add a test for this as well, something in this order. It seems easy to break.
let renderHeaderCount = 0; | ||
const TestCasePerf = () => { | ||
const [cols, setCols] = React.useState<GridColDef[]>([]); | ||
const data = useData(5000, 10); |
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 doesn't seem that we need to render so many rows. I didn't look into how much it contributes to the time spent on this test but I assume it's not negligeable.
@@ -221,5 +221,43 @@ describe('<XGrid /> - Sorting', () => { | |||
const time = Math.round(t1 - t0); | |||
expect(time).to.be.lessThan(300); | |||
}); | |||
|
|||
it('should render maximum twice', async function test() { |
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 don't know what this test is testing but not the rerender fix. It passes on HEAD. I suspect because we test the header and not the cell.
if (data.columns.length) { | ||
const newColumns = [...data.columns]; | ||
newColumns[1].renderHeader = (params) => { | ||
renderHeaderCount += 1; |
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.
As a general note, it would be better to move this into a useEffect, so we can leave React start and interrupts its rendering as he sees fit, and only see when a change is actually committed.
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.
Not sure what you mean.
If I add a useEffect in the TestComponent, I don't think it will be meaningful as it won't reflect the grid internal rendering
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.
In strict mode, React renders twice for each commit. In concurrent mode, React can render but not commit. useEffect allows to reliably assert that a React.memo() logic has pruned the rendering. Feel free to ask Sebastian for more details. It's important to well understand the notion if we want to be able to build the fastest data grid.
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.
While useEffect
would allow React 17.0 to yield rendering, it also introduces tearing. Say we want to render new state. Normally React would render the full UI and let the browser paint it. But now you're basically saying "hey just paint with the new state in certain areas of the UI but with the old state in other parts". And generally we don't want our UI to tear since it makes for a worse UX and harder to reason about the UI/state. It can even lead to infinite loops if you go back and forth with old/new state.
I would avoid tearing at all cost because it will degrade UX and not improve when you work on perf. Even worse, the UX will stay degraded in concurrent React when concurrent React is the solution to the problem. The render will become more "yieldy" i.e. if there's user input during render, React will process that input first before trying to finish rendering.
useEffect
isn't a tool to improve performance. It's just for scheduling side-effects. Not for controlling the render timing. You're in for a world of pain if you think you can build your own scheduler that just considers a single component. React has been trying to build a scheduler for the whole UI for years now. I don't think you can solve that bottom-up.
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.
@eps1lon Thanks for sharing these thoughts, however, I'm not sure we can act on it here.
The problem we are trying to solve is: How to reliably test that a memo logic is working (to avoid regressions)?
I had linked https://github.com/mui-org/material-ui/blob/4a0e2459dc33c5e99df8419fc251db90638d65e7/packages/material-ui-styles/src/makeStyles/makeStyles.test.js#L508 as a way to approach it. What would you recommend?
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 you just want to add
const rendered = React.useRef(1);
React.useEffect(() => {
rendered.current += 1;
});
Then you need to add it in a component that will be rerendered when the grid renders, that could be a header, footer, cell, or column header.
The way I did it, is via the renderCell
and renderHeader
functions, which get called when the cell or header render so it's the same. I can add an intermediary component such as below but I doubt it will add any value
const TestCell = ()=> {
const rendered = React.useRef(1);
React.useEffect(() => {
rendered.current += 1;
});
...
}
renderCell: ()=> <TestCell {...params} />
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 can add an intermediary component such as below but I doubt it will add any value
Yes, this what I had assumed would ensure we reliably test the memo, based on: https://github.com/mui-org/material-ui-x/pull/1513/files#r629548477, e.g. if we are in strict mode, you should get two render call for each "interaction".
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'm not sure we can act on it here.
I was responding to your claims. Neither Damien nor I could make sense of them so I tried to bring some clarity.
How to reliably test that a memo logic is working (to avoid regressions)?
You don't. You monitor performance because that's the feature React.memo
is implementing. You don't test the actual implementation detail.
Outside of the notes I have left, nothing to report :)
@dtassone So no peer reviews (I don't count 😁)? |
😄 Well I took It's a very important PR, as it's touching also the new issues that @m4theushw is looking at now, so we needed it in right now. It's very important to integrate otherwise we will have to refactor new work... |
Edit 2: Must have been a webpack issue of some sort, I thought I restarted the dev server, but after killing it and restarting it, the issue is now resolved.. I can delete this comment if it's deemed to be unhelpful for any future purpose. Edit 1: So it's working fine in the sandbox I created... So I'm still trying to figure out what the issue is. I'll reply back here when I track it down So I'm sure it's something in my code, and I'll try to create a CSB asap, but in a I just tried upgrading to |
@dtassone Why did we change the API of diff --git a/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts b/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts
index 17126faa..b0f4783c 100644
--- a/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts
+++ b/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts
@@ -63,7 +63,7 @@ export function useGridParamsApi(apiRef: GridApiRef) {
value: row[field],
colDef: apiRef.current.getColumn(field),
cellMode: apiRef.current.getCellMode(id, field),
- getValue: apiRef.current.getCellValue,
+ getValue: (...args) => apiRef.current.getCellValue(...args),
api: apiRef.current,
hasFocus: cellFocus !== null && cellFocus.field === field && cellFocus.id === id,
tabIndex: cellTabIndex && cellTabIndex.field === field && cellTabIndex.id === id ? 0 : -1, I couldn't notice a change in the number of React renders nor in the time taken by the filtering to handle 100k rows. |
getValue: (...args) => apiRef.current.getCellValue(...args), You create a new function every time you call |
@dtassone Thanks for the clarification. I have tried to follow to see where the property is used. I guess it saves a rerender if a developer spread all the props into a component that is memo? But isn't the parent Cell, in the core of the data grid, already memo? avoiding the rerenders. So it seems that we can restore the previous logic, is it accurate? |
@dtassone I had a closer look, I'm giving up on the idea. Even if the parent of -params.getValue(params.id, field)
+params.getValue(field) |
Breaking changes
element
,rowIndex
,colIndex
, across all relevant params argument due to their heavy calculation work.params.getValue
across all params argument, due to reference change on each closure, this method is now pointing toapiRef.current.getCellValue
.getValue
in sort string number comparator .setCellFocus
to useid
andfield
to match other api methods.tabIndex
andhasFocus
inGridCellParams
.getRowIndexFromId
togetRowIndex
Work on #1506, #569, #1427
Preview: https://deploy-preview-1513--material-ui-x.netlify.app/components/data-grid/#commercial-version