Skip to content

Commit

Permalink
Adjust start and end params to cover whole page(s). Cache responses i…
Browse files Browse the repository at this point in the history
…n chunks based on the pagination model and props. Combine cache entries when needed
  • Loading branch information
arminmeh committed Aug 16, 2024
1 parent 33b0459 commit 99cd596
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 20 deletions.
104 changes: 92 additions & 12 deletions packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { GridGetRowsParams, GridGetRowsResponse } from '../../../models';

type GridDataSourceCacheDefaultConfig = {
export type GridDataSourceCacheDefaultConfig = {
/**
* Time To Live for each cache entry in milliseconds.
* After this time the cache entry will become stale and the next query will result in cache miss.
* @default 300000 (5 minutes)
*/
ttl?: number;
/**
* The number of rows to store in each cache entry. If not set, the whole array will be stored in a single cache entry.
* Setting this value to smallest page size will result in better cache hit rate.
* Has no effect if cursor pagination is used.
* @default undefined
*/
chunkSize?: number;
};

function getKey(params: GridGetRowsParams) {
return JSON.stringify([
params.paginationModel,
params.filterModel,
params.sortModel,
params.groupKeys,
Expand All @@ -21,32 +27,106 @@ function getKey(params: GridGetRowsParams) {
}

export class GridDataSourceCacheDefault {
private cache: Record<string, { value: GridGetRowsResponse; expiry: number }>;
private cache: Record<
string,
{
value: GridGetRowsResponse;
expiry: number;
chunk: { startIndex: string | number; endIndex: number };
}
>;

private ttl: number;

constructor({ ttl = 300000 }: GridDataSourceCacheDefaultConfig) {
private chunkSize: number;

private getChunkRanges = (params: GridGetRowsParams) => {
if (this.chunkSize < 1 || typeof params.start !== 'number') {
return [{ startIndex: params.start, endIndex: params.end }];
}

// split the range into chunks
const chunkRanges = [];
for (let i = params.start; i < params.end; i += this.chunkSize) {
const endIndex = Math.min(i + this.chunkSize - 1, params.end);
chunkRanges.push({ startIndex: i, endIndex });
}

return chunkRanges;
};

constructor({ chunkSize, ttl = 300000 }: GridDataSourceCacheDefaultConfig) {
this.cache = {};
this.ttl = ttl;
this.chunkSize = chunkSize || 0;
}

set(key: GridGetRowsParams, value: GridGetRowsResponse) {
const keyString = getKey(key);
const chunks = this.getChunkRanges(key);
const expiry = Date.now() + this.ttl;
this.cache[keyString] = { value, expiry };

chunks.forEach((chunk) => {
const isLastChunk = chunk.endIndex === key.end;
const keyString = getKey({ ...key, start: chunk.startIndex, end: chunk.endIndex });
const chunkValue: GridGetRowsResponse = {
...value,
pageInfo: {
...value.pageInfo,
// If the original response had page info, update that information for all but last chunk and keep the original value for the last chunk
hasNextPage:
(value.pageInfo?.hasNextPage !== undefined && !isLastChunk) ||
value.pageInfo?.hasNextPage,
nextCursor:
value.pageInfo?.nextCursor !== undefined && !isLastChunk
? value.rows[chunk.endIndex + 1].id
: value.pageInfo?.nextCursor,
},
rows:
typeof chunk.startIndex !== 'number' || typeof key.start !== 'number'
? value.rows
: value.rows.slice(chunk.startIndex - key.start, chunk.endIndex - key.start + 1),
};

this.cache[keyString] = { value: chunkValue, expiry, chunk };
});
}

get(key: GridGetRowsParams): GridGetRowsResponse | undefined {
const keyString = getKey(key);
const entry = this.cache[keyString];
if (!entry) {
const chunks = this.getChunkRanges(key);

const startChunk = chunks.findIndex((chunk) => chunk.startIndex === key.start);
const endChunk = chunks.findIndex((chunk) => chunk.endIndex === key.end);

// If desired range cannot fit completely in chunks, then it is a cache miss
if (startChunk === -1 || endChunk === -1) {
return undefined;
}
if (Date.now() > entry.expiry) {
delete this.cache[keyString];

const cachedResponses = [];

for (let i = startChunk; i <= endChunk; i += 1) {
const keyString = getKey({ ...key, start: chunks[i].startIndex, end: chunks[i].endIndex });
const entry = this.cache[keyString];
const isCacheValid = entry?.value && Date.now() < entry.expiry;
cachedResponses.push(isCacheValid ? entry?.value : null);
}

// If any of the chunks is missing, then it is a cache miss
if (cachedResponses.some((response) => response === null)) {
return undefined;
}
return entry.value;

// Merge the chunks into a single response
return (cachedResponses as GridGetRowsResponse[]).reduce(
(acc: GridGetRowsResponse, response) => {
return {
rows: [...acc.rows, ...response.rows],
rowCount: response.rowCount,
pageInfo: response.pageInfo,
};
},
{ rows: [], rowCount: 0, pageInfo: {} },
);
}

clear() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
GridDataSourceGroupNode,
useGridSelector,
GridRowId,
gridPaginationModelSelector,
} from '@mui/x-data-grid';
import {
GridGetRowsParams,
Expand All @@ -19,7 +20,7 @@ import { gridGetRowsParamsSelector, gridDataSourceErrorsSelector } from './gridD
import { GridDataSourceApi, GridDataSourceApiBase, GridDataSourcePrivateApi } from './interfaces';
import { NestedDataManager, RequestStatus, runIf } from './utils';
import { GridDataSourceCache } from '../../../models';
import { GridDataSourceCacheDefault } from './cache';
import { GridDataSourceCacheDefault, GridDataSourceCacheDefaultConfig } from './cache';

const INITIAL_STATE = {
loading: {},
Expand All @@ -32,11 +33,14 @@ const noopCache: GridDataSourceCache = {
set: () => {},
};

function getCache(cacheProp?: GridDataSourceCache | null) {
function getCache(
cacheProp?: GridDataSourceCache | null,
options: GridDataSourceCacheDefaultConfig = {},
) {
if (cacheProp === null) {
return noopCache;
}
return cacheProp ?? new GridDataSourceCacheDefault({});
return cacheProp ?? new GridDataSourceCacheDefault(options);
}

export const dataSourceStateInitializer: GridStateInitializer = (state) => {
Expand All @@ -56,13 +60,15 @@ export const useGridDataSource = (
| 'sortingMode'
| 'filterMode'
| 'paginationMode'
| 'pageSizeOptions'
| 'treeData'
| 'lazyLoading'
>,
) => {
const nestedDataManager = useLazyRef<NestedDataManager, void>(
() => new NestedDataManager(apiRef),
).current;
const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector);
const groupsToAutoFetch = useGridSelector(apiRef, gridRowGroupsToFetchSelector);
const scheduledGroups = React.useRef<number>(0);

Expand All @@ -71,8 +77,38 @@ export const useGridDataSource = (

const onError = props.unstable_onDataSourceError;

const cacheChunkSize = React.useMemo(() => {
const sortedPageSizeOptions = props.pageSizeOptions
.map((option) => (typeof option === 'number' ? option : option.value))
.sort((a, b) => a - b);

return Math.min(paginationModel.pageSize, sortedPageSizeOptions[0]);
}, [paginationModel.pageSize, props.pageSizeOptions]);

const [cache, setCache] = React.useState<GridDataSourceCache>(() =>
getCache(props.unstable_dataSourceCache),
getCache(props.unstable_dataSourceCache, {
chunkSize: cacheChunkSize,
}),
);

// Adjust the render context range to fit the pagination model's page size
// First row index should be decreased to the start of the page, end row index should be increased to the end of the page or the last row
const adjustRowParams = React.useCallback(
(params: Pick<GridGetRowsParams, 'start' | 'end'>) => {
if (typeof params.start !== 'number') {
return params;
}

const rowCount = apiRef.current.state.pagination.rowCount;
return {
start: params.start - (params.start % paginationModel.pageSize),
end: Math.min(
params.end + paginationModel.pageSize - (params.end % paginationModel.pageSize) - 1,
rowCount - 1,
),
};
},
[apiRef, paginationModel],
);

const fetchRows = React.useCallback(
Expand Down Expand Up @@ -150,10 +186,10 @@ export const useGridDataSource = (

const fetchRowBatch = React.useCallback(
(fetchParams: GridGetRowsParams) => {
rowFetchSlice.current = { start: Number(fetchParams.start), end: fetchParams.end };
rowFetchSlice.current = adjustRowParams(fetchParams);
return fetchRows();
},
[fetchRows],
[adjustRowParams, fetchRows],
);

const fetchRowChildren = React.useCallback<GridDataSourcePrivateApi['fetchRowChildren']>(
Expand Down Expand Up @@ -319,9 +355,11 @@ export const useGridDataSource = (
isFirstRender.current = false;
return;
}
const newCache = getCache(props.unstable_dataSourceCache);
const newCache = getCache(props.unstable_dataSourceCache, {
chunkSize: cacheChunkSize,
});
setCache((prevCache) => (prevCache !== newCache ? newCache : prevCache));
}, [props.unstable_dataSourceCache]);
}, [props.unstable_dataSourceCache, cacheChunkSize]);

React.useEffect(() => {
if (props.unstable_dataSource) {
Expand Down

0 comments on commit 99cd596

Please sign in to comment.