Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
8797b6c
fix aggregation with sorting
lauri865 Oct 10, 2025
ff426d6
fix aggregation reason
lauri865 Oct 10, 2025
8851650
tentative changes
lauri865 Oct 10, 2025
8916698
refactors
lauri865 Oct 10, 2025
8e542b8
use store effect equality check
lauri865 Oct 10, 2025
86d7e49
wip
lauri865 Oct 10, 2025
f2d411b
test opts
lauri865 Oct 13, 2025
067a960
fixes
lauri865 Oct 13, 2025
2653c29
wip
lauri865 Oct 13, 2025
d136a1a
lint
lauri865 Oct 13, 2025
b568d28
fix
lauri865 Oct 13, 2025
7eb1f6a
Merge branch 'master' into aggregation-sort-fix
lauri865 Oct 13, 2025
742b378
fix
lauri865 Oct 13, 2025
900eeec
refactor: virtualizer implemented in subcomponent
lauri865 Oct 13, 2025
a03a136
fix
lauri865 Oct 13, 2025
62b16f0
fixes
lauri865 Oct 13, 2025
30a1e05
console
lauri865 Oct 13, 2025
8f2537b
fixes
lauri865 Oct 13, 2025
352b5c3
fix
lauri865 Oct 13, 2025
0a5f3f0
fix
lauri865 Oct 13, 2025
0d345b5
tst
lauri865 Oct 13, 2025
4cb956b
dim
lauri865 Oct 13, 2025
ac62a97
Merge branch 'master' into aggregation-sort-fix
lauri865 Oct 13, 2025
87db6a2
virt
lauri865 Oct 13, 2025
1bf65c2
fix
lauri865 Oct 13, 2025
402f14d
lint
lauri865 Oct 13, 2025
57c9815
update
lauri865 Oct 13, 2025
1613657
remove unnecessary event handler
lauri865 Oct 13, 2025
a936867
log
lauri865 Oct 13, 2025
29fbafb
fix
lauri865 Oct 13, 2025
fdd510e
add an event for when aggregation finishes
lauri865 Oct 14, 2025
4bffff5
fix
lauri865 Oct 14, 2025
11ee56a
oops
lauri865 Oct 14, 2025
f50fde4
Merge branch 'master' into aggregation-sort-fix
lauri865 Oct 14, 2025
9fbeade
fix
lauri865 Oct 14, 2025
d46a0b3
Merge branch 'aggregation-sort-fix' of https://github.com/lauri865/mu…
lauri865 Oct 14, 2025
12feebd
remove hack for now
lauri865 Oct 14, 2025
1ab86c4
fix
lauri865 Oct 14, 2025
bc96ff9
fix
lauri865 Oct 14, 2025
b43dfdc
fix
lauri865 Oct 14, 2025
5199dfd
deduplicate row spanning calls
lauri865 Oct 14, 2025
9262c19
fixes
lauri865 Oct 14, 2025
806bc9a
fix test
lauri865 Oct 14, 2025
15cc4fb
fix
lauri865 Oct 14, 2025
7d1b7e0
fix keyboard test in the browser
lauri865 Oct 14, 2025
970ea94
fix
lauri865 Oct 14, 2025
25b27ad
test
lauri865 Oct 14, 2025
3b45c7b
test
lauri865 Oct 14, 2025
dae6c7f
empty
cherniavskii Oct 14, 2025
217d960
lint
romgrk Oct 14, 2025
1424219
fix tests
lauri865 Oct 14, 2025
3b070fa
reduce state updates
lauri865 Oct 14, 2025
f09246d
fix
lauri865 Oct 14, 2025
d5619fb
reduce calls to sorting / filtering
lauri865 Oct 14, 2025
12c7e97
update call count
lauri865 Oct 14, 2025
beb0a57
fix
lauri865 Oct 14, 2025
610a270
update call counts
lauri865 Oct 14, 2025
d60b398
lint
lauri865 Oct 15, 2025
e8a5d85
fix
lauri865 Oct 15, 2025
249c567
fix
lauri865 Oct 15, 2025
c87bede
fix rowspan tests
lauri865 Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Member

Choose a reason for hiding this comment

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

Added an experimental refactor that takes mount performance back to the old baselines if not slightly better (<15ms vs. 60m = 4x better; in 3 render passes vs. 5 currently).

Can you share the the details of your setup for measurements?
I'll try to add a benchmark for mount performance to track regressions.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { RefObject } from '@mui/x-internals/types';
import {
useGridInitialization,
useGridInitializeState,
useGridVirtualizer,
useGridClipboard,
useGridColumnMenu,
useGridColumns,
Expand All @@ -24,7 +23,6 @@ import {
useGridRows,
useGridRowsPreProcessors,
rowsStateInitializer,
useGridRowsMeta,
useGridParamsApi,
useGridRowSelection,
useGridSorting,
Expand Down Expand Up @@ -79,6 +77,7 @@ import {
propsStateInitializer,
rowReorderStateInitializer,
type GridConfiguration,
useFirstRender,
} from '@mui/x-data-grid-pro/internals';
import { useGridSelector } from '@mui/x-data-grid-pro';
import { GridPrivateApiPremium } from '../models/gridApiPremium';
Expand Down Expand Up @@ -202,7 +201,6 @@ export const useDataGridPremiumComponent = (
useGridInitializeState(aiAssistantStateInitializer, apiRef, props);
useGridInitializeState(chartsIntegrationStateInitializer, apiRef, props);

useGridVirtualizer(apiRef, props);
useGridSidebar(apiRef, props);
useGridPivoting(apiRef, props, inProps.columns, inProps.rows);
useGridRowGrouping(apiRef, props);
Expand Down Expand Up @@ -231,7 +229,6 @@ export const useDataGridPremiumComponent = (
useGridColumnReorder(apiRef, props);
useGridColumnResize(apiRef, props);
useGridPagination(apiRef, props);
useGridRowsMeta(apiRef, props);
useGridRowReorder(apiRef, props);
useGridScroll(apiRef, props);
useGridInfiniteLoader(apiRef, props);
Expand All @@ -254,6 +251,9 @@ export const useDataGridPremiumComponent = (
useGridPivotingExportState(apiRef);

// Should be the last thing to run, because all pre-processors should have been registered by now.
useFirstRender(() => {
apiRef.current.runAppliersForPendingProcessors();
});
React.useEffect(() => {
apiRef.current.runAppliersForPendingProcessors();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
gridRenderContextSelector,
gridVisibleColumnFieldsSelector,
gridSortModelSelector,
gridRowMaximumTreeDepthSelector,
} from '@mui/x-data-grid-pro';
import {
useGridRegisterPipeProcessor,
Expand Down Expand Up @@ -89,126 +90,128 @@ export const useGridAggregation = (
);

const abortControllerRef = React.useRef<AbortController | null>(null);
const applyAggregation = React.useCallback(
(reason?: 'filter' | 'sort') => {
// Abort previous if any
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;

const aggregationRules = getAggregationRules(
gridColumnLookupSelector(apiRef),
gridAggregationModelSelector(apiRef),
props.aggregationFunctions,
!!props.dataSource,
);
const aggregatedFields = Object.keys(aggregationRules);
const currentAggregationLookup = gridAggregationLookupSelector(apiRef);
const needsSorting = shouldApplySorting(aggregationRules, aggregatedFields);
if (reason === 'sort' && !needsSorting) {
// no need to re-apply aggregation on `sortedRowsSet` if sorting is not needed
return;
}
const applyAggregation = React.useCallback(() => {
// Abort previous if we're proceeding
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;

const renderContext = gridRenderContextSelector(apiRef);
const visibleColumns = gridVisibleColumnFieldsSelector(apiRef);
const aggregationRules = getAggregationRules(
gridColumnLookupSelector(apiRef),
gridAggregationModelSelector(apiRef),
props.aggregationFunctions,
!!props.dataSource,
);
const aggregatedFields = Object.keys(aggregationRules);
const currentAggregationLookup = gridAggregationLookupSelector(apiRef);

const renderContext = gridRenderContextSelector(apiRef);
const visibleColumns = gridVisibleColumnFieldsSelector(apiRef);

const chunks: string[][] = [];
const sortedAggregatedFields = gridSortModelSelector(apiRef)
.map((s) => s.field)
.filter((field) => aggregatedFields.includes(field));
const visibleAggregatedFields = visibleColumns
.slice(renderContext.firstColumnIndex, renderContext.lastColumnIndex + 1)
.filter((field) => aggregatedFields.includes(field));
const visibleAggregatedFieldsWithSort = [
...visibleAggregatedFields,
...sortedAggregatedFields.filter((field) => !visibleAggregatedFields.includes(field)),
];
const hasAggregatedSortedField =
gridRowMaximumTreeDepthSelector(apiRef) > 1 && sortedAggregatedFields.length > 0;

if (visibleAggregatedFields.length > 0) {
chunks.push(visibleAggregatedFieldsWithSort);
}
const otherAggregatedFields = aggregatedFields.filter(
(field) => !visibleAggregatedFieldsWithSort.includes(field),
);

const chunks: string[][] = [];
const visibleAggregatedFields = visibleColumns
.slice(renderContext.firstColumnIndex, renderContext.lastColumnIndex + 1)
.filter((field) => aggregatedFields.includes(field));
if (visibleAggregatedFields.length > 0) {
chunks.push(visibleAggregatedFields);
}
const otherAggregatedFields = aggregatedFields.filter(
(field) => !visibleAggregatedFields.includes(field),
);
const chunkSize = 20; // columns per chunk
for (let i = 0; i < otherAggregatedFields.length; i += chunkSize) {
chunks.push(otherAggregatedFields.slice(i, i + chunkSize));
}

const chunkSize = 20; // columns per chunk
for (let i = 0; i < otherAggregatedFields.length; i += chunkSize) {
chunks.push(otherAggregatedFields.slice(i, i + chunkSize));
}
let chunkIndex = 0;
const aggregationLookup: GridAggregationLookup = {};
let chunkStartTime = performance.now();
const timeLimit = 1000 / 120;

let chunkIndex = 0;
const aggregationLookup: GridAggregationLookup = {};
let chunkStartTime = performance.now();
const timeLimit = 1000 / 120;
const processChunk = () => {
if (abortController.signal.aborted) {
return;
}

const processChunk = () => {
if (abortController.signal.aborted) {
return;
}
const currentChunk = chunks[chunkIndex];
if (!currentChunk) {
apiRef.current.publishEvent('aggregationLookupSet');
abortControllerRef.current = null;
return;
}

const currentChunk = chunks[chunkIndex];
if (!currentChunk) {
const sortModel = gridSortModelSelector(apiRef).map((s) => s.field);
const hasAggregatedSorting = sortModel.some((field) => aggregationRules[field]);
if (hasAggregatedSorting) {
apiRef.current.applySorting();
}
abortControllerRef.current = null;
return;
const applySorting = shouldApplySorting(aggregationRules, currentChunk);

// createAggregationLookup now RETURNS new partial lookup
const partialLookup = createAggregationLookup({
apiRef,
getAggregationPosition: props.getAggregationPosition,
aggregatedFields: currentChunk,
aggregationRules,
aggregationRowsScope: props.aggregationRowsScope,
isDataSource: !!props.dataSource,
applySorting,
});

for (const key of Object.keys(partialLookup)) {
for (const field of Object.keys(partialLookup[key])) {
aggregationLookup[key] ??= {};
aggregationLookup[key][field] = partialLookup[key][field];
}
}

const applySorting = shouldApplySorting(aggregationRules, currentChunk);

// createAggregationLookup now RETURNS new partial lookup
const partialLookup = createAggregationLookup({
apiRef,
getAggregationPosition: props.getAggregationPosition,
aggregatedFields: currentChunk,
aggregationRules,
aggregationRowsScope: props.aggregationRowsScope,
isDataSource: !!props.dataSource,
applySorting,
});

for (const key of Object.keys(partialLookup)) {
for (const field of Object.keys(partialLookup[key])) {
aggregationLookup[key] ??= {};
aggregationLookup[key][field] = partialLookup[key][field];
}
}
apiRef.current.setState((state) => ({
...state,
aggregation: { ...state.aggregation, lookup: { ...aggregationLookup } },
}));

apiRef.current.setState((state) => ({
...state,
aggregation: { ...state.aggregation, lookup: { ...aggregationLookup } },
}));
if (chunkIndex === 0 && hasAggregatedSortedField) {
apiRef.current.applySorting();
}

chunkIndex += 1;
chunkIndex += 1;

if (performance.now() - chunkStartTime < timeLimit) {
processChunk();
return;
}
if (performance.now() - chunkStartTime < timeLimit) {
processChunk();
return;
}

setTimeout(() => {
chunkStartTime = performance.now();
processChunk();
}, 0);
};
setTimeout(() => {
chunkStartTime = performance.now();
processChunk();
}, 0);
};

processChunk();
processChunk();

// processChunk() does nothing if there are no aggregated fields
// make sure that the lookup is empty in this case
if (aggregatedFields.length === 0 && !isObjectEmpty(currentAggregationLookup)) {
apiRef.current.setState((state) => ({
...state,
aggregation: { ...state.aggregation, lookup: {} },
}));
}
},
[
apiRef,
props.getAggregationPosition,
props.aggregationFunctions,
props.aggregationRowsScope,
props.dataSource,
],
);
// processChunk() does nothing if there are no aggregated fields
// make sure that the lookup is empty in this case
if (aggregatedFields.length === 0 && !isObjectEmpty(currentAggregationLookup)) {
apiRef.current.setState((state) => ({
...state,
aggregation: { ...state.aggregation, lookup: {} },
}));
}
}, [
apiRef,
props.getAggregationPosition,
props.aggregationFunctions,
props.aggregationRowsScope,
props.dataSource,
]);

React.useEffect(() => {
return () => {
Expand All @@ -219,7 +222,7 @@ export const useGridAggregation = (
};
}, []);

const deferredApplyAggregation = useRunOncePerLoop(applyAggregation);
const { schedule: deferredApplyAggregation } = useRunOncePerLoop(applyAggregation);

const aggregationApi: GridAggregationApi = {
setAggregationModel,
Expand Down Expand Up @@ -285,7 +288,32 @@ export const useGridAggregation = (
useGridEvent(apiRef, 'aggregationModelChange', checkAggregationRulesDiff);
useGridEvent(apiRef, 'columnsChange', checkAggregationRulesDiff);
useGridEvent(apiRef, 'filteredRowsSet', deferredApplyAggregation);
useGridEvent(apiRef, 'sortedRowsSet', () => deferredApplyAggregation('sort'));

const lastSortModel = React.useRef(gridSortModelSelector(apiRef));
useGridEvent(apiRef, 'sortedRowsSet', () => {
const sortModel = gridSortModelSelector(apiRef);
if (lastSortModel.current === sortModel) {
return;
}
lastSortModel.current = sortModel;

const aggregationRules = getAggregationRules(
gridColumnLookupSelector(apiRef),
gridAggregationModelSelector(apiRef),
props.aggregationFunctions,
!!props.dataSource,
);
const aggregatedFields = Object.keys(aggregationRules);
if (!aggregatedFields.length) {
return;
}
const needsSorting = shouldApplySorting(aggregationRules, aggregatedFields);
if (!needsSorting) {
return;
}

deferredApplyAggregation();
});

/**
* EFFECTS
Expand All @@ -295,4 +323,10 @@ export const useGridAggregation = (
apiRef.current.setAggregationModel(props.aggregationModel);
}
}, [apiRef, props.aggregationModel]);

React.useEffect(() => {
if (props.getAggregationPosition !== undefined) {
deferredApplyAggregation();
}
}, [deferredApplyAggregation, props.getAggregationPosition]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,11 @@ export const useGridChartsIntegration = (
'sortedRowsSet',
runIf(isChartsIntegrationAvailable, () => debouncedHandleRowDataUpdate(syncedChartIds)),
);
useGridEvent(
apiRef,
'aggregationLookupSet',
runIf(isChartsIntegrationAvailable, () => debouncedHandleRowDataUpdate(syncedChartIds)),
);

React.useEffect(() => {
if (!activeChartId && availableChartIds.length > 0) {
Expand Down
Loading
Loading