diff --git a/packages/toolkit/src/entities/sorted_state_adapter.ts b/packages/toolkit/src/entities/sorted_state_adapter.ts index f40c887a27..aef9ff20bb 100644 --- a/packages/toolkit/src/entities/sorted_state_adapter.ts +++ b/packages/toolkit/src/entities/sorted_state_adapter.ts @@ -499,6 +499,103 @@ export function createSortedStateAdapter( } } + const mergeJackman: MergeFunction = ( + state, + addedItems, + updatedIds, + replacedIds, + ) => { + const entities = state.entities as Record + let ids = state.ids as Id[] + if (replacedIds) { + ids = Array.from(new Set(ids)) + } + const existingSortedItems = ids // Array.from(new Set(state.ids as Id[])) + .map((id) => entities[id]) + .filter(Boolean) + + function findInsertIndex2( + sortedItems: T[], + item: T, + comparisonFunction: Comparer, + lowIndexOverride?: number, + ): number { + let lowIndex = lowIndexOverride ?? 0 + let highIndex = sortedItems.length + while (lowIndex < highIndex) { + const middleIndex = (lowIndex + highIndex) >>> 1 + const currentItem = sortedItems[middleIndex] + if (comparisonFunction(item, currentItem) > 0) { + lowIndex = middleIndex + 1 + } else { + highIndex = middleIndex + } + } + + return lowIndex + } + + if (addedItems.length) { + const newEntities = addedItems.slice().sort(comparer) + + // Insert/overwrite all new/updated + newEntities.forEach((model) => { + entities[selectId(model)] = model + }) + + const firstInstanceId = newEntities[0] + const lastInstanceId = newEntities[newEntities.length - 1] + + const startIndex = findInsertIndex2( + existingSortedItems, + firstInstanceId, + comparer, + ) + const endIndex = findInsertIndex2( + existingSortedItems, + lastInstanceId, + comparer, + startIndex, + ) + + const overlappingExistingIds = existingSortedItems.slice( + startIndex, + endIndex, + ) + let newIdIndexOfLastInsert = 0 + let lastRelativeInsertIndex = 0 + for (let i = 1; i < newEntities.length; i++) { + const relativeInsertIndex = findInsertIndex2( + overlappingExistingIds, + newEntities[i], + comparer, + lastRelativeInsertIndex, + ) + if (lastRelativeInsertIndex !== relativeInsertIndex) { + const insertIndex = + startIndex + newIdIndexOfLastInsert + lastRelativeInsertIndex + const arrayToInsert = newEntities.slice(newIdIndexOfLastInsert, i) + existingSortedItems.splice(insertIndex, 0, ...arrayToInsert) + newIdIndexOfLastInsert = i + lastRelativeInsertIndex = relativeInsertIndex + } + } + existingSortedItems.splice( + startIndex + newIdIndexOfLastInsert + lastRelativeInsertIndex, + 0, + ...newEntities.slice(newIdIndexOfLastInsert), + ) + } else if (updatedIds?.size) { + existingSortedItems.sort(comparer) + } + + const newSortedIds = existingSortedItems.map(selectId) + + if (!areArraysEqual(ids, newSortedIds)) { + state.ids = newSortedIds + } + } + const mergeFunction: MergeFunction = mergeInsertion function resortEntities( diff --git a/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts b/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts index cfdded6a08..f1b34c051d 100644 --- a/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts +++ b/packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts @@ -592,8 +592,8 @@ describe('Sorted State Adapter', () => { }) it('should minimize the amount of sorting work needed', () => { - const INITIAL_ITEMS = 100_000 - const ADDED_ITEMS = 1000 + const INITIAL_ITEMS = 10_000 + const ADDED_ITEMS = 1_000 type Entity = { id: string; name: string; position: number } @@ -663,6 +663,8 @@ describe('Sorted State Adapter', () => { store.dispatch(entitySlice.actions.upsertMany(initialItems)) }) + expect(numSorts).toBeLessThan(INITIAL_ITEMS * 20) + measureComparisons('Insert One (random)', () => { store.dispatch( entitySlice.actions.upsertOne({ @@ -673,6 +675,8 @@ describe('Sorted State Adapter', () => { ) }) + expect(numSorts).toBeLessThan(50) + measureComparisons('Insert One (middle)', () => { store.dispatch( entitySlice.actions.upsertOne({ @@ -683,6 +687,8 @@ describe('Sorted State Adapter', () => { ) }) + expect(numSorts).toBeLessThan(50) + measureComparisons('Insert One (end)', () => { store.dispatch( entitySlice.actions.upsertOne({ @@ -693,11 +699,15 @@ describe('Sorted State Adapter', () => { ) }) + expect(numSorts).toBeLessThan(50) + const addedItems = generateItems(ADDED_ITEMS) measureComparisons('Add Many', () => { store.dispatch(entitySlice.actions.addMany(addedItems)) }) + expect(numSorts).toBeLessThan(ADDED_ITEMS * 20) + // These numbers will vary because of the randomness, but generally // with 10K items the old code had 200K+ sort calls, while the new code // is around 130K sort calls. @@ -718,6 +728,12 @@ describe('Sorted State Adapter', () => { ) }) + const SORTING_COUNT_BUFFER = 100 + + expect(numSorts).toBeLessThan( + INITIAL_ITEMS + ADDED_ITEMS + SORTING_COUNT_BUFFER, + ) + measureComparisons('Update One (middle)', () => { store.dispatch( // Move this middle item near the end @@ -730,6 +746,10 @@ describe('Sorted State Adapter', () => { ) }) + expect(numSorts).toBeLessThan( + INITIAL_ITEMS + ADDED_ITEMS + SORTING_COUNT_BUFFER, + ) + measureComparisons('Update One (replace)', () => { store.dispatch( // Move this middle item near the end @@ -743,6 +763,10 @@ describe('Sorted State Adapter', () => { ) }) + expect(numSorts).toBeLessThan( + INITIAL_ITEMS + ADDED_ITEMS + SORTING_COUNT_BUFFER, + ) + // The old code was around 120K, the new code is around 10K. // expect(numSorts).toBeLessThan(25_000) })