Skip to content

Conversation

lauri865
Copy link
Contributor

@lauri865 lauri865 commented Jun 12, 2025

Fixes #18342 (the first part of it)

Adds a new applySorting?: boolean property to GridAggregationFunction

The only thing that probably needs additional polish is this:

useGridEvent(apiRef, 'filteredRowsSet', applyAggregation);
useGridEvent(apiRef, 'sortedRowsSet', applyAggregation);

These can in some instances (on mount) be triggered sequentially if I remember correctly (correct me if I'm wrong), so might be some room for optimisation.

Changes:

  • Optimized columns updates (-800ms with a large amount of columns)
  • Optimized column group creation (50-100x improvement under large number of columns)
  • Improved aggregation performance (from 6000ms+ -> 275ms in my test case)
  • Removed pivotModeChange control state, because pivotMode is not strictly controlled, and in an uncontrolled mode, it delays updates by 1 render
  • Fixed a case whereby rows are improperly restored (props.rows takes overrides any subsequent changes made via the apiRef)
  • Created windowed and chunked aggregation logic, which calculates the current viewport instantly, and the rest in chunks to avoid locking the main thread. Chunksize logic could be improved further; one idea is to continue with the chunks until we hit X amount of ms, or recalculate chunkSize based on how long the previous chunk took.
  • Deduplicated calls to applyAggregation (currently it was called many times in a row)
  • Support for applySorting in aggregation functions
  • Expose field to getCellValue in aggregation functions to make custom logic possible, especially in the context of pivoting where fields are automatically generated
  • Fixed: column groups based on numbers were not sorted correctly, as all headernames were coerced to strings
  • Fixed: pivoting doesn't initialize when rows are updated with setRows while isLoading internal state is still true. Common pattern would be to set rows within useLayoutEffect when isLoading is changing (to avoid flickering), and that can't be caught by the current logic since an upperlevel useLayoutEffect will always run before the update of the internal state. Deferred initializing nonPivotDataRef to the first row update (works with all of setRows, updateRows and props.rows) instead of listening to the isLoading change of internal state.

Performance updates; before:

before-agg.mp4

After:

smooth-operator.mp4

@mui-bot
Copy link

mui-bot commented Jun 12, 2025

Deploy preview: https://deploy-preview-18348--material-ui-x.netlify.app/

Bundle size report

Total Size Change: 🔺+10.5KB(+0.08%) - Total Gzip Change: 🔺+3.64KB(+0.09%)
Files: 122 total (0 added, 0 removed, 6 changed)

Show details for 100 more bundles (22 more not shown)

@mui/x-data-grid-premiumparsed: 🔺+3.25KB(+0.57%) gzip: 🔺+1.07KB(+0.66%)
@mui/x-data-grid-premium/DataGridPremiumparsed: 🔺+3.21KB(+0.61%) gzip: 🔺+1.2KB(+0.79%)
@mui/x-data-gridparsed: 🔺+1.17KB(+0.31%) gzip: 🔺+399B(+0.36%)
@mui/x-data-grid-proparsed: 🔺+1.17KB(+0.26%) gzip: 🔺+383B(+0.29%)
@mui/x-data-grid-pro/DataGridProparsed: 🔺+855B(+0.19%) gzip: 🔺+290B(+0.23%)
@mui/x-data-grid/DataGridparsed: 🔺+855B(+0.23%) gzip: 🔺+295B(+0.28%)
@mui/x-chartsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-proparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/BarChartProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/ChartContainerProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/ChartDataProviderProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/ChartsToolbarProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/ChartZoomSliderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/FunnelChartparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/Heatmapparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/LineChartProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/PieChartProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/RadarChartProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts-pro/ScatterChartProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/BarChartparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartContainerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartDataProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsAxisparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsAxisHighlightparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsClipPathparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsGridparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLabelparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLegendparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsOverlayparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsReferenceLineparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsSurfaceparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsTextparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsTooltipparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsXAxisparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsYAxisparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/Gaugeparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/LineChartparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/PieChartparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/RadarChartparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ScatterChartparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/SparkLineChartparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/Toolbarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickersparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-proparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDateFnsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDateFnsJalaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDayjsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterLuxonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentHijriparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentJalaaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangeCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerDayparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerDay2parsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DesktopDateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DesktopDateTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DesktopTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/LocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileDateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileDateTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputDateRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputDateTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/PickersRangeCalendarHeaderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputDateRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputDateTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/StaticDateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/TimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsBaseparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsJalaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDayjsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterLuxonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentHijriparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentJalaaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateTimeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DayCalendarSkeletonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopDatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopDateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DigitalClockparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/LocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileDatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileDateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MonthCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MultiSectionDigitalClockparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickerDay2parsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickersActionBarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickersCalendarHeaderparsed: 0B(0.00%) gzip: 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against de6d7b0

@lauri865
Copy link
Contributor Author

lauri865 commented Jun 12, 2025

Now also exposed field to getCellValue that's required to pull the correct data for pivoting, fixing the second part of the issue.
I also exposed api in aggregationFunction.apply, as I've felt many times that I could build more flexible and performant aggregations if only I had more context to go by.

@lauri865
Copy link
Contributor Author

Enabling pivot seems to trigger 5 instances of "filteredRowsSet" (each of them retriggers aggregations). And the same for "sortedRowsSet for whatever reason @cherniavskii

Adding applySorting to the mix causes a visible slowdown.

Just press the toggle button and check console:
https://stackblitz.com/edit/g4dsphrl-sxhvdgqz?file=src%2FDemo.tsx,package.json

@lauri865
Copy link
Contributor Author

I tried deduplicating some more expensive calls, but probably broke some tests (not sure if for functionality, but at least it will complain about act().

One thing I still don't understand though – when stopping pivoting, columns get rehydrated 6 times:
6x MUI X: useGridColumns - Columns pipe processing have changed, regenerating the columns

@lauri865
Copy link
Contributor Author

Did a bunch of optimisations in aggregation calculations. For a a large pivot table (10k rows, pivoted by date (value) and commodity in the dataset, summing "total in usd") – computation time went down from 6000ms+ to 375ms. A big culprit was nested function calls to access row values, which we can skip for pivoting + doing another full recursion over all rows/columns. isNumber custom function wrapper also added quite significant overhead for summing.

Can be still polished a bit I guess.

@lauri865
Copy link
Contributor Author

From 375ms -> 260ms in my test case now that I stoped storing undefined values. I guess it's fair to assume that undefined has no meaning in aggreations, nulls might (although even that is slightly questionable, and could be optionally toggled). And there's tons of undefined values in pivoting at least.

@lauri865
Copy link
Contributor Author

Fixed the last issue as well with cascading hydrateColumns updates by re-registering the pipeApplier when pivotMode changes. So the slow restore after pivoting in the after video should also hopefully be fixed now.

@zannager zannager added the scope: data grid Changes related to the data grid. label Jun 13, 2025
@lauri865
Copy link
Contributor Author

Generalized the aggregation now for any depth. For traversal, we only fetch direct leaf values, otherwise merge the precomputed values on group level, instead of going through the nested tree over and over again.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 4, 2025
Otherwise, columns are not regenerated and groupingColDef is basically ignored
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 11, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 14, 2025
@@ -65,6 +69,8 @@ export const useGridPipeProcessing = (apiRef: RefObject<GridPrivateApiCommon>) =
isRunning.current = false;
}, []);

const runAppliersDeferred = useRunOncePerLoop(runAppliers, false, getGroupScopeKey);
Copy link
Member

Choose a reason for hiding this comment

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

cc @romgrk I used the same approach with useRunOncePerLoop to only run appliers once after all pipe processors change. Before that, appliers were called after each pipe processor change, which in some cases hurts performance.
I modified useRunOncePerLoop to support scopes, so that a single hook can be used to schedule multiple appliers independently.
Any objections?

This change naturally inflicted a lot of updates to tests (mostly await mictotasks() at the end of the test to avoid React act warnings).
I think it's fine, and I'll mention this in the changelog and in the docs.

Copy link
Contributor Author

@lauri865 lauri865 Jul 14, 2025

Choose a reason for hiding this comment

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

Is there a case in which this could introduce flickering or other unwanted behaviour – e.g. some changes get flushed to DOM based on updated state, but pipe processing takes place after that (e.g. state changes -> DOM updates based on the change state -> columns get rehydrated only on the next render). Essentially the equivalent of setting state in useEffect vs. useLayoutEffect.

In the case of aggregation it's fine (especially with batched processing now), but broadly applying it to all pipe processors might make be a bit more questionable / tricky.

Alternative approach for this case would be to mark a group as pending for updates, and add a hook as the last entry in useDatagridPremiumComponent that clears the queue / triggers runAppliers.

Edit: could potentially explain the next comment regarding increased call count (just a guess at this point, haven't dug into it)

Edit 2: also, since formerly runAppliers was called effectively inline in the component body, any state updates from pipe processing will prevent effects from being run. But in the case of the new implementation with useRunOncePerLoop, all effects are run before the appliers, which could (in theory) negate performance gains from deduplicating calls to runAppliers. Check: https://codesandbox.io/p/sandbox/ptnhjf

Copy link
Member

Choose a reason for hiding this comment

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

Is there a case in which this could introduce flickering or other unwanted behaviour – e.g. some changes get flushed to DOM based on updated state, but pipe processing takes place after that (e.g. state changes -> DOM updates based on the change state -> columns get rehydrated only on the next render). Essentially the equivalent of setting state in useEffect vs. useLayoutEffect.

I haven't noticed anything like that.

Alternative approach for this case would be to mark a group as pending for updates, and add a hook as the last entry in useDatagridPremiumComponent that clears the queue / triggers runAppliers.

That's a good idea, I'll give it a try 👍🏻

But in the case of the new implementation with useRunOncePerLoop, all effects are run before the appliers, which could (in theory) negate performance gains from deduplicating calls to runAppliers. Check: codesandbox.io/p/sandbox/ptnhjf

Hmm, interesting. What would be a potential scenario where performance gains will be negated? In the demo you shared, useRunOncePerLoop results in less rerenders and less effect calls:

useRunOncePerLoop inline

// + 2x when sortedRowsSet is fired
const expectedCallCount = reactMajor >= 19 ? 4 : 8;
const expectedCallCount = reactMajor >= 19 ? 6 : 12;
Copy link
Member

Choose a reason for hiding this comment

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

I'm not exactly sure why this needed to be updated...

run appliers after all pipe processors have updated, instead of after each processor updated
@cherniavskii cherniavskii force-pushed the aggregation-with-sorting branch from cf6d47b to 2003195 Compare July 16, 2025 13:35
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 16, 2025
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 16, 2025
@cherniavskii cherniavskii mentioned this pull request Jul 16, 2025
14 tasks
@cherniavskii cherniavskii changed the title [data grid] Pivoting/aggregation performance (+allow sort-dependent aggregations) [DataGridPremium] Support sort-dependent aggregation and improve performance Jul 17, 2025
@cherniavskii cherniavskii merged commit 3ed40cd into mui:master Jul 17, 2025
22 checks passed
@lauri865
Copy link
Contributor Author

Thanks for all the heavy lifting @cherniavskii for making this PR happen – and sorry for all the rework for tests this brought 🙏❤️

@cherniavskii
Copy link
Member

@lauri865 Thank you for making it possible, we appreciate it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: Aggregation Related to the data grid Aggregation feature feature: Pivoting Related to the data grid pivoting feature performance plan: Premium Impact at least one premium user. scope: data grid Changes related to the data grid. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[data grid] Aggregation does not respond to sorting (+getCellValue not usable with pivoting)
7 participants