Skip to content

Commit

Permalink
Add some assertions for perf expectations
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Apr 20, 2024
1 parent 8db3f7f commit bece99b
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 2 deletions.
97 changes: 97 additions & 0 deletions packages/toolkit/src/entities/sorted_state_adapter.ts
Expand Up @@ -499,6 +499,103 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
}
}

const mergeJackman: MergeFunction = (
state,
addedItems,
updatedIds,
replacedIds,
) => {
const entities = state.entities as Record<Id, T>
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<T>(
sortedItems: T[],
item: T,
comparisonFunction: Comparer<T>,
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(
Expand Down
28 changes: 26 additions & 2 deletions packages/toolkit/src/entities/tests/sorted_state_adapter.test.ts
Expand Up @@ -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 }

Expand Down Expand Up @@ -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({
Expand All @@ -673,6 +675,8 @@ describe('Sorted State Adapter', () => {
)
})

expect(numSorts).toBeLessThan(50)

measureComparisons('Insert One (middle)', () => {
store.dispatch(
entitySlice.actions.upsertOne({
Expand All @@ -683,6 +687,8 @@ describe('Sorted State Adapter', () => {
)
})

expect(numSorts).toBeLessThan(50)

measureComparisons('Insert One (end)', () => {
store.dispatch(
entitySlice.actions.upsertOne({
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
})
Expand Down

0 comments on commit bece99b

Please sign in to comment.