From 093e04b49d028a973649e9469694f6e85e5bf41a Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 Nov 2023 19:03:12 -0600 Subject: [PATCH 1/4] Add more unit tests for `resultEqualityCheck` --- src/types.ts | 5 +- src/weakMapMemoize.ts | 14 +- test/benchmarks/orderOfExecution.bench.ts | 426 ++++---- test/benchmarks/weakMapMemoize.bench.ts | 972 +++++++++--------- test/computationComparisons.spec.tsx | 118 ++- test/examples.test.ts | 440 ++++---- test/reselect.bench.ts | 434 ++++---- test/testUtils.ts | 1140 ++++++++++----------- 8 files changed, 1805 insertions(+), 1744 deletions(-) diff --git a/src/types.ts b/src/types.ts index 645b74f92..eb88c1f0e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -466,10 +466,7 @@ export type FunctionType = Extract */ export type ExtractReturnType = { [Index in keyof FunctionsArray]: FunctionsArray[Index] extends FunctionsArray[number] - ? FallbackIfUnknown< - FallbackIfUnknown, any>, - any - > + ? FallbackIfUnknown, any> : never } diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index 0e6d771b3..cc7e4193d 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -156,7 +156,7 @@ export function weakMapMemoize( let fnNode = createCacheNode() const { resultEqualityCheck } = options - let lastResult: WeakRef | undefined = undefined + let lastResult: ReturnType | undefined let resultsCount = 0 @@ -212,16 +212,12 @@ export function weakMapMemoize( terminatedNode.s = TERMINATED if (resultEqualityCheck) { - const lastResultValue = lastResult?.deref() ?? lastResult - if (lastResultValue && resultEqualityCheck(lastResultValue, result)) { - result = lastResultValue - resultsCount-- + if (lastResult != null && resultEqualityCheck(lastResult, result)) { + result = lastResult + resultsCount !== 0 && resultsCount-- } - const needsWeakRef = - (typeof result === 'object' && result !== null) || - typeof result === 'function' - lastResult = needsWeakRef ? new WeakRef(result) : result + lastResult = result } terminatedNode.v = result return result diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts index 210b489d7..034ba81e6 100644 --- a/test/benchmarks/orderOfExecution.bench.ts +++ b/test/benchmarks/orderOfExecution.bench.ts @@ -1,213 +1,213 @@ -import type { OutputSelector, Selector } from 'reselect' -import { createSelector, defaultMemoize } from 'reselect' -import type { Options } from 'tinybench' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { - countRecomputations, - expensiveComputation, - logFunctionInfo, - logSelectorRecomputations, - resetSelector, - runMultipleTimes, - setFunctionNames, - setupStore, - toggleCompleted, - toggleRead -} from '../testUtils' - -describe('Less vs more computation in input selectors', () => { - const store = setupStore() - const runSelector = (selector: Selector) => { - runMultipleTimes(selector, 100, store.getState()) - } - const selectorLessInInput = createSelector( - [(state: RootState) => state.todos], - todos => { - expensiveComputation() - return todos.filter(todo => todo.completed) - } - ) - const selectorMoreInInput = createSelector( - [ - (state: RootState) => { - expensiveComputation() - return state.todos - } - ], - todos => todos.filter(todo => todo.completed) - ) - - const nonMemoized = countRecomputations((state: RootState) => { - expensiveComputation() - return state.todos.filter(todo => todo.completed) - }) - const commonOptions: Options = { - iterations: 10, - time: 0 - } - setFunctionNames({ selectorLessInInput, selectorMoreInInput, nonMemoized }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - beforeEach: () => { - store.dispatch(toggleRead(1)) - }, - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorLessInInput, - () => { - runSelector(selectorLessInInput) - }, - createOptions(selectorLessInInput, commonOptions) - ) - bench( - selectorMoreInInput, - () => { - runSelector(selectorMoreInInput) - }, - createOptions(selectorMoreInInput, commonOptions) - ) - bench( - nonMemoized, - () => { - runSelector(nonMemoized) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - nonMemoized.resetRecomputations() - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logFunctionInfo(nonMemoized, nonMemoized.recomputations()) - } - } - } - } - ) -}) - -// This benchmark is made to test to see at what point it becomes beneficial -// to use reselect to memoize a function that is a plain field accessor. -describe('Reselect vs standalone memoization for field access', () => { - const store = setupStore() - const runSelector = (selector: Selector) => { - runMultipleTimes(selector, 1_000_000, store.getState()) - } - const commonOptions: Options = { - // warmupIterations: 0, - // warmupTime: 0, - // iterations: 10, - // time: 0 - } - const fieldAccessorWithReselect = createSelector( - [(state: RootState) => state.users], - users => users.appSettings - ) - const fieldAccessorWithMemoize = countRecomputations( - defaultMemoize((state: RootState) => { - return state.users.appSettings - }) - ) - const nonMemoizedAccessor = countRecomputations( - (state: RootState) => state.users.appSettings - ) - - setFunctionNames({ - fieldAccessorWithReselect, - fieldAccessorWithMemoize, - nonMemoizedAccessor - }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - fieldAccessorWithReselect, - () => { - runSelector(fieldAccessorWithReselect) - }, - createOptions(fieldAccessorWithReselect, commonOptions) - ) - bench( - fieldAccessorWithMemoize, - () => { - runSelector(fieldAccessorWithMemoize) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - fieldAccessorWithMemoize.resetRecomputations() - fieldAccessorWithMemoize.clearCache() - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logFunctionInfo( - fieldAccessorWithMemoize, - fieldAccessorWithMemoize.recomputations() - ) - } - } - } - } - ) - bench( - nonMemoizedAccessor, - () => { - runSelector(nonMemoizedAccessor) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - nonMemoizedAccessor.resetRecomputations() - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logFunctionInfo( - nonMemoizedAccessor, - nonMemoizedAccessor.recomputations() - ) - } - } - } - } - ) -}) +import type { OutputSelector, Selector } from 'reselect' +import { createSelector, defaultMemoize } from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + countRecomputations, + expensiveComputation, + logFunctionInfo, + logSelectorRecomputations, + resetSelector, + runMultipleTimes, + setFunctionNames, + setupStore, + toggleCompleted, + toggleRead +} from '../testUtils' + +describe('Less vs more computation in input selectors', () => { + const store = setupStore() + const runSelector = (selector: Selector) => { + runMultipleTimes(selector, 100, store.getState()) + } + const selectorLessInInput = createSelector( + [(state: RootState) => state.todos], + todos => { + expensiveComputation() + return todos.filter(todo => todo.completed) + } + ) + const selectorMoreInInput = createSelector( + [ + (state: RootState) => { + expensiveComputation() + return state.todos + } + ], + todos => todos.filter(todo => todo.completed) + ) + + const nonMemoized = countRecomputations((state: RootState) => { + expensiveComputation() + return state.todos.filter(todo => todo.completed) + }) + const commonOptions: Options = { + iterations: 10, + time: 0 + } + setFunctionNames({ selectorLessInInput, selectorMoreInInput, nonMemoized }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + beforeEach: () => { + store.dispatch(toggleRead(1)) + }, + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorLessInInput, + () => { + runSelector(selectorLessInInput) + }, + createOptions(selectorLessInInput, commonOptions) + ) + bench( + selectorMoreInInput, + () => { + runSelector(selectorMoreInInput) + }, + createOptions(selectorMoreInInput, commonOptions) + ) + bench( + nonMemoized, + () => { + runSelector(nonMemoized) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + nonMemoized.resetRecomputations() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo(nonMemoized, nonMemoized.recomputations()) + } + } + } + } + ) +}) + +// This benchmark is made to test to see at what point it becomes beneficial +// to use reselect to memoize a function that is a plain field accessor. +describe('Reselect vs standalone memoization for field access', () => { + const store = setupStore() + const runSelector = (selector: Selector) => { + runMultipleTimes(selector, 1_000_000, store.getState()) + } + const commonOptions: Options = { + // warmupIterations: 0, + // warmupTime: 0, + // iterations: 10, + // time: 0 + } + const fieldAccessorWithReselect = createSelector( + [(state: RootState) => state.users], + users => users.appSettings + ) + const fieldAccessorWithMemoize = countRecomputations( + defaultMemoize((state: RootState) => { + return state.users.appSettings + }) + ) + const nonMemoizedAccessor = countRecomputations( + (state: RootState) => state.users.appSettings + ) + + setFunctionNames({ + fieldAccessorWithReselect, + fieldAccessorWithMemoize, + nonMemoizedAccessor + }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + fieldAccessorWithReselect, + () => { + runSelector(fieldAccessorWithReselect) + }, + createOptions(fieldAccessorWithReselect, commonOptions) + ) + bench( + fieldAccessorWithMemoize, + () => { + runSelector(fieldAccessorWithMemoize) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + fieldAccessorWithMemoize.resetRecomputations() + fieldAccessorWithMemoize.clearCache() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo( + fieldAccessorWithMemoize, + fieldAccessorWithMemoize.recomputations() + ) + } + } + } + } + ) + bench( + nonMemoizedAccessor, + () => { + runSelector(nonMemoizedAccessor) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + nonMemoizedAccessor.resetRecomputations() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo( + nonMemoizedAccessor, + nonMemoizedAccessor.recomputations() + ) + } + } + } + } + ) +}) diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts index a64bb93f5..fe4a4381a 100644 --- a/test/benchmarks/weakMapMemoize.bench.ts +++ b/test/benchmarks/weakMapMemoize.bench.ts @@ -1,486 +1,486 @@ -import type { OutputSelector, Selector } from 'reselect' -import { - unstable_autotrackMemoize as autotrackMemoize, - createSelector, - weakMapMemoize -} from 'reselect' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { - logSelectorRecomputations, - resetSelector, - setFunctionNames, - setupStore -} from '../testUtils' - -import type { Options } from 'tinybench' - -describe('Parametric selectors: weakMapMemoize vs others', () => { - const store = setupStore() - const state = store.getState() - const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const runSelector = (selector: S) => { - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - } - const selectorDefault = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - const selectorDefaultWithCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { memoizeOptions: { maxSize: 30 } } - ) - const selectorDefaultWithArgsCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoizeOptions: { maxSize: 30 } } - ) - const selectorDefaultWithBothCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { memoize: weakMapMemoize } - ) - const selectorAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { memoize: autotrackMemoize } - ) - const selectorArgsAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: autotrackMemoize } - ) - const selectorBothAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } - ) - const selectorArgsWeakMap = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: weakMapMemoize } - ) - const selectorBothWeakMap = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } - ) - const nonMemoizedSelector = (state: RootState, id: number) => { - return state.todos.find(todo => todo.id === id) - } - setFunctionNames({ - selectorDefault, - selectorDefaultWithCacheSize, - selectorDefaultWithArgsCacheSize, - selectorDefaultWithBothCacheSize, - selectorWeakMap, - selectorArgsWeakMap, - selectorBothWeakMap, - selectorAutotrack, - selectorArgsAutotrack, - selectorBothAutotrack, - nonMemoizedSelector - }) - - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorDefault, - () => { - runSelector(selectorDefault) - }, - createOptions(selectorDefault, commonOptions) - ) - bench( - selectorDefaultWithCacheSize, - () => { - runSelector(selectorDefaultWithCacheSize) - }, - createOptions(selectorDefaultWithCacheSize, commonOptions) - ) - bench( - selectorDefaultWithArgsCacheSize, - () => { - runSelector(selectorDefaultWithArgsCacheSize) - }, - createOptions(selectorDefaultWithArgsCacheSize, commonOptions) - ) - bench( - selectorDefaultWithBothCacheSize, - () => { - runSelector(selectorDefaultWithBothCacheSize) - }, - createOptions(selectorDefaultWithBothCacheSize, commonOptions) - ) - bench( - selectorWeakMap, - () => { - runSelector(selectorWeakMap) - }, - createOptions(selectorWeakMap, commonOptions) - ) - bench( - selectorArgsWeakMap, - () => { - runSelector(selectorArgsWeakMap) - }, - createOptions(selectorArgsWeakMap, commonOptions) - ) - bench( - selectorBothWeakMap, - () => { - runSelector(selectorBothWeakMap) - }, - createOptions(selectorBothWeakMap, commonOptions) - ) - bench( - selectorAutotrack, - () => { - runSelector(selectorAutotrack) - }, - createOptions(selectorAutotrack, commonOptions) - ) - bench( - selectorArgsAutotrack, - () => { - runSelector(selectorArgsAutotrack) - }, - createOptions(selectorArgsAutotrack, commonOptions) - ) - bench( - selectorBothAutotrack, - () => { - runSelector(selectorBothAutotrack) - }, - createOptions(selectorBothAutotrack, commonOptions) - ) - bench( - nonMemoizedSelector, - () => { - runSelector(nonMemoizedSelector) - }, - { ...commonOptions } - ) -}) - -// describe('weakMapMemoize vs defaultMemoize with maxSize', () => { -// const store = setupStore() -// const state = store.getState() -// const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) -// const commonOptions: Options = { -// iterations: 10, -// time: 0 -// } -// const runSelector = (selector: S) => { -// arrayOfNumbers.forEach(num => { -// selector(state, num) -// }) -// arrayOfNumbers.forEach(num => { -// selector(state, num) -// }) -// } -// const selectorDefaultWithCacheSize = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { memoizeOptions: { maxSize: 30 } } -// ) -// const selectorDefaultWithArgsCacheSize = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { argsMemoizeOptions: { maxSize: 30 } } -// ) -// const selectorDefaultWithBothCacheSize = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } -// ) -// const selectorWeakMap = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { memoize: weakMapMemoize } -// ) -// const selectorArgsWeakMap = createSelector( -// (state: RootState) => state.todos, -// (state: RootState, id: number) => id, -// (todos, id) => todos.map(todo => todo.id === id), -// { argsMemoize: weakMapMemoize } -// ) -// const selectorBothWeakMap = createSelector( -// (state: RootState) => state.todos, -// (state: RootState, id: number) => id, -// (todos, id) => todos.map(todo => todo.id === id), -// { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } -// ) -// const nonMemoizedSelector = (state: RootState, id: number) => { -// return state.todos.map(todo => todo.id === id) -// } -// setFunctionNames({ -// selectorDefaultWithCacheSize, -// selectorDefaultWithArgsCacheSize, -// selectorDefaultWithBothCacheSize, -// selectorWeakMap, -// selectorArgsWeakMap, -// selectorBothWeakMap, -// nonMemoizedSelector -// }) -// const createOptions = ( -// selector: S, -// commonOptions: Options = {} -// ) => { -// const options: Options = { -// setup: (task, mode) => { -// if (mode === 'warmup') return -// resetSelector(selector) -// task.opts = { -// afterAll: () => { -// logSelectorRecomputations(selector) -// } -// } -// } -// } -// return { ...commonOptions, ...options } -// } -// bench( -// selectorDefaultWithCacheSize, -// () => { -// runSelector(selectorDefaultWithCacheSize) -// }, -// createOptions(selectorDefaultWithCacheSize, commonOptions) -// ) -// bench( -// selectorDefaultWithArgsCacheSize, -// () => { -// runSelector(selectorDefaultWithArgsCacheSize) -// }, -// createOptions(selectorDefaultWithArgsCacheSize, commonOptions) -// ) -// bench( -// selectorDefaultWithBothCacheSize, -// () => { -// runSelector(selectorDefaultWithBothCacheSize) -// }, -// createOptions(selectorDefaultWithBothCacheSize, commonOptions) -// ) -// bench( -// selectorWeakMap, -// () => { -// runSelector(selectorWeakMap) -// }, -// createOptions(selectorWeakMap, commonOptions) -// ) -// bench( -// selectorArgsWeakMap, -// () => { -// runSelector(selectorArgsWeakMap) -// }, -// createOptions(selectorArgsWeakMap, commonOptions) -// ) -// bench( -// selectorBothWeakMap, -// () => { -// runSelector(selectorBothWeakMap) -// }, -// createOptions(selectorBothWeakMap, commonOptions) -// ) -// bench( -// nonMemoizedSelector, -// () => { -// runSelector(nonMemoizedSelector) -// }, -// { ...commonOptions } -// ) -// }) - -describe('Simple selectors: weakMapMemoize vs others', () => { - const store = setupStore() - const commonOptions: Options = { - // warmupIterations: 0, - // warmupTime: 0, - // iterations: 10, - // time: 0 - } - const selectTodoIdsDefault = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const selectTodoIdsWeakMap = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize } - ) - const selectTodoIdsAutotrack = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { memoize: autotrackMemoize } - ) - - setFunctionNames({ - selectTodoIdsDefault, - selectTodoIdsWeakMap, - selectTodoIdsAutotrack - }) - - const createOptions = (selector: S) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - - bench( - selectTodoIdsDefault, - () => { - selectTodoIdsDefault(store.getState()) - }, - createOptions(selectTodoIdsDefault) - ) - bench( - selectTodoIdsWeakMap, - () => { - selectTodoIdsWeakMap(store.getState()) - }, - createOptions(selectTodoIdsWeakMap) - ) - bench( - selectTodoIdsAutotrack, - () => { - selectTodoIdsAutotrack(store.getState()) - }, - createOptions(selectTodoIdsAutotrack) - ) -}) - -describe.skip('weakMapMemoize memory leak', () => { - const store = setupStore() - const state = store.getState() - const arrayOfNumbers = Array.from( - { length: 2_000_000 }, - (num, index) => index - ) - const commonOptions: Options = { - warmupIterations: 0, - warmupTime: 0, - iterations: 1, - time: 0 - } - const runSelector = (selector: S) => { - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - } - const selectorDefault = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id) - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id), - { memoize: weakMapMemoize } - ) - const selectorArgsWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize } - ) - const selectorBothWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } - ) - setFunctionNames({ - selectorDefault, - selectorWeakMap, - selectorArgsWeakMap, - selectorBothWeakMap - }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorDefault, - () => { - runSelector(selectorDefault) - }, - createOptions(selectorDefault, commonOptions) - ) - bench( - selectorWeakMap, - () => { - runSelector(selectorWeakMap) - }, - createOptions(selectorWeakMap, commonOptions) - ) - bench.skip( - selectorArgsWeakMap, - () => { - runSelector(selectorArgsWeakMap) - }, - createOptions(selectorArgsWeakMap, commonOptions) - ) - bench.skip( - selectorBothWeakMap, - () => { - runSelector(selectorBothWeakMap) - }, - createOptions(selectorBothWeakMap, commonOptions) - ) -}) +import type { OutputSelector, Selector } from 'reselect' +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector, + weakMapMemoize +} from 'reselect' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + logSelectorRecomputations, + resetSelector, + setFunctionNames, + setupStore +} from '../testUtils' + +import type { Options } from 'tinybench' + +describe('Parametric selectors: weakMapMemoize vs others', () => { + const store = setupStore() + const state = store.getState() + const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + } + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const selectorDefaultWithCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithArgsCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithBothCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { memoize: autotrackMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: autotrackMemoize } + ) + const selectorBothAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } + ) + const selectorArgsWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + const nonMemoizedSelector = (state: RootState, id: number) => { + return state.todos.find(todo => todo.id === id) + } + setFunctionNames({ + selectorDefault, + selectorDefaultWithCacheSize, + selectorDefaultWithArgsCacheSize, + selectorDefaultWithBothCacheSize, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap, + selectorAutotrack, + selectorArgsAutotrack, + selectorBothAutotrack, + nonMemoizedSelector + }) + + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) + ) + bench( + selectorDefaultWithCacheSize, + () => { + runSelector(selectorDefaultWithCacheSize) + }, + createOptions(selectorDefaultWithCacheSize, commonOptions) + ) + bench( + selectorDefaultWithArgsCacheSize, + () => { + runSelector(selectorDefaultWithArgsCacheSize) + }, + createOptions(selectorDefaultWithArgsCacheSize, commonOptions) + ) + bench( + selectorDefaultWithBothCacheSize, + () => { + runSelector(selectorDefaultWithBothCacheSize) + }, + createOptions(selectorDefaultWithBothCacheSize, commonOptions) + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + createOptions(selectorWeakMap, commonOptions) + ) + bench( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + createOptions(selectorArgsWeakMap, commonOptions) + ) + bench( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + createOptions(selectorBothWeakMap, commonOptions) + ) + bench( + selectorAutotrack, + () => { + runSelector(selectorAutotrack) + }, + createOptions(selectorAutotrack, commonOptions) + ) + bench( + selectorArgsAutotrack, + () => { + runSelector(selectorArgsAutotrack) + }, + createOptions(selectorArgsAutotrack, commonOptions) + ) + bench( + selectorBothAutotrack, + () => { + runSelector(selectorBothAutotrack) + }, + createOptions(selectorBothAutotrack, commonOptions) + ) + bench( + nonMemoizedSelector, + () => { + runSelector(nonMemoizedSelector) + }, + { ...commonOptions } + ) +}) + +// describe('weakMapMemoize vs defaultMemoize with maxSize', () => { +// const store = setupStore() +// const state = store.getState() +// const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) +// const commonOptions: Options = { +// iterations: 10, +// time: 0 +// } +// const runSelector = (selector: S) => { +// arrayOfNumbers.forEach(num => { +// selector(state, num) +// }) +// arrayOfNumbers.forEach(num => { +// selector(state, num) +// }) +// } +// const selectorDefaultWithCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoizeOptions: { maxSize: 30 } } +// ) +// const selectorDefaultWithArgsCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoizeOptions: { maxSize: 30 } } +// ) +// const selectorDefaultWithBothCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } +// ) +// const selectorWeakMap = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoize: weakMapMemoize } +// ) +// const selectorArgsWeakMap = createSelector( +// (state: RootState) => state.todos, +// (state: RootState, id: number) => id, +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoize: weakMapMemoize } +// ) +// const selectorBothWeakMap = createSelector( +// (state: RootState) => state.todos, +// (state: RootState, id: number) => id, +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } +// ) +// const nonMemoizedSelector = (state: RootState, id: number) => { +// return state.todos.map(todo => todo.id === id) +// } +// setFunctionNames({ +// selectorDefaultWithCacheSize, +// selectorDefaultWithArgsCacheSize, +// selectorDefaultWithBothCacheSize, +// selectorWeakMap, +// selectorArgsWeakMap, +// selectorBothWeakMap, +// nonMemoizedSelector +// }) +// const createOptions = ( +// selector: S, +// commonOptions: Options = {} +// ) => { +// const options: Options = { +// setup: (task, mode) => { +// if (mode === 'warmup') return +// resetSelector(selector) +// task.opts = { +// afterAll: () => { +// logSelectorRecomputations(selector) +// } +// } +// } +// } +// return { ...commonOptions, ...options } +// } +// bench( +// selectorDefaultWithCacheSize, +// () => { +// runSelector(selectorDefaultWithCacheSize) +// }, +// createOptions(selectorDefaultWithCacheSize, commonOptions) +// ) +// bench( +// selectorDefaultWithArgsCacheSize, +// () => { +// runSelector(selectorDefaultWithArgsCacheSize) +// }, +// createOptions(selectorDefaultWithArgsCacheSize, commonOptions) +// ) +// bench( +// selectorDefaultWithBothCacheSize, +// () => { +// runSelector(selectorDefaultWithBothCacheSize) +// }, +// createOptions(selectorDefaultWithBothCacheSize, commonOptions) +// ) +// bench( +// selectorWeakMap, +// () => { +// runSelector(selectorWeakMap) +// }, +// createOptions(selectorWeakMap, commonOptions) +// ) +// bench( +// selectorArgsWeakMap, +// () => { +// runSelector(selectorArgsWeakMap) +// }, +// createOptions(selectorArgsWeakMap, commonOptions) +// ) +// bench( +// selectorBothWeakMap, +// () => { +// runSelector(selectorBothWeakMap) +// }, +// createOptions(selectorBothWeakMap, commonOptions) +// ) +// bench( +// nonMemoizedSelector, +// () => { +// runSelector(nonMemoizedSelector) +// }, +// { ...commonOptions } +// ) +// }) + +describe('Simple selectors: weakMapMemoize vs others', () => { + const store = setupStore() + const commonOptions: Options = { + // warmupIterations: 0, + // warmupTime: 0, + // iterations: 10, + // time: 0 + } + const selectTodoIdsDefault = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selectTodoIdsWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectTodoIdsAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + + setFunctionNames({ + selectTodoIdsDefault, + selectTodoIdsWeakMap, + selectTodoIdsAutotrack + }) + + const createOptions = (selector: S) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + + bench( + selectTodoIdsDefault, + () => { + selectTodoIdsDefault(store.getState()) + }, + createOptions(selectTodoIdsDefault) + ) + bench( + selectTodoIdsWeakMap, + () => { + selectTodoIdsWeakMap(store.getState()) + }, + createOptions(selectTodoIdsWeakMap) + ) + bench( + selectTodoIdsAutotrack, + () => { + selectTodoIdsAutotrack(store.getState()) + }, + createOptions(selectTodoIdsAutotrack) + ) +}) + +describe.skip('weakMapMemoize memory leak', () => { + const store = setupStore() + const state = store.getState() + const arrayOfNumbers = Array.from( + { length: 2_000_000 }, + (num, index) => index + ) + const commonOptions: Options = { + warmupIterations: 0, + warmupTime: 0, + iterations: 1, + time: 0 + } + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + } + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id) + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + const selectorArgsWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + setFunctionNames({ + selectorDefault, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap + }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + createOptions(selectorWeakMap, commonOptions) + ) + bench.skip( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + createOptions(selectorArgsWeakMap, commonOptions) + ) + bench.skip( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + createOptions(selectorBothWeakMap, commonOptions) + ) +}) diff --git a/test/computationComparisons.spec.tsx b/test/computationComparisons.spec.tsx index 5631cfb67..484306a42 100644 --- a/test/computationComparisons.spec.tsx +++ b/test/computationComparisons.spec.tsx @@ -2,26 +2,23 @@ * @vitest-environment jsdom */ -import { createSelector, weakMapMemoize } from 'reselect' +import * as rtl from '@testing-library/react' import React, { useLayoutEffect, useMemo } from 'react' import type { TypedUseSelectorHook } from 'react-redux' -import { useSelector, Provider, shallowEqual } from 'react-redux' -import * as rtl from '@testing-library/react' - -import type { - OutputSelector, - OutputSelectorFields, - Selector, - defaultMemoize +import { Provider, shallowEqual, useSelector } from 'react-redux' +import { + createSelector, + unstable_autotrackMemoize, + weakMapMemoize } from 'reselect' + +import type { OutputSelector, defaultMemoize } from 'reselect' import type { RootState, Todo } from './testUtils' -import { logSelectorRecomputations } from './testUtils' import { addTodo, - deepClone, - localTest, - toggleCompleted, - setupStore + logSelectorRecomputations, + setupStore, + toggleCompleted } from './testUtils' describe('Computations and re-rendering with React components', () => { @@ -46,8 +43,8 @@ describe('Computations and re-rendering with React components', () => { type SelectTodoIds = OutputSelector< [(state: RootState) => RootState['todos']], number[], - typeof defaultMemoize, - any + typeof defaultMemoize | typeof weakMapMemoize, + typeof defaultMemoize | typeof weakMapMemoize > type SelectTodoById = OutputSelector< @@ -56,8 +53,8 @@ describe('Computations and re-rendering with React components', () => { (state: RootState, id: number) => number ], readonly [todo: Todo | undefined], - typeof defaultMemoize, - any + typeof defaultMemoize | typeof weakMapMemoize, + typeof defaultMemoize | typeof weakMapMemoize > const selectTodos = (state: RootState) => state.todos @@ -170,7 +167,7 @@ describe('Computations and re-rendering with React components', () => { selectTodoIdsResultEquality, selectTodoByIdResultEquality ], - ['weakMap', selectTodoIdsWeakMap, selectTodoByIdWeakMap] as any, + ['weakMap', selectTodoIdsWeakMap, selectTodoByIdWeakMap], [ 'weakMapResultEquality', @@ -183,8 +180,8 @@ describe('Computations and re-rendering with React components', () => { `%s`, async ( name, - selectTodoIds: SelectTodoIds, - selectTodoById: SelectTodoById + selectTodoIds, + selectTodoById ) => { selectTodoIds.resetRecomputations() selectTodoIds.resetDependencyRecomputations() @@ -251,3 +248,82 @@ describe('Computations and re-rendering with React components', () => { } ) }) + +describe('resultEqualityCheck in weakMapMemoize', () => { + test('resultEqualityCheck with shallowEqual', () => { + const store = setupStore() + const state = store.getState() + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + const selectorWeakMapShallow = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { + memoize: weakMapMemoize, + memoizeOptions: { resultEqualityCheck: shallowEqual } + } + ) + const selectorAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: unstable_autotrackMemoize } + ) + const firstResult = selectorWeakMap(store.getState()) + store.dispatch(toggleCompleted(0)) + const secondResult = selectorWeakMap(store.getState()) + expect(firstResult).not.toBe(secondResult) + expect(firstResult).toStrictEqual(secondResult) + const firstResultShallow = selectorWeakMapShallow(store.getState()) + store.dispatch(toggleCompleted(0)) + const secondResultShallow = selectorWeakMapShallow(store.getState()) + expect(firstResultShallow).toBe(secondResultShallow) + const firstResultAutotrack = selectorAutotrack(store.getState()) + store.dispatch(toggleCompleted(0)) + const secondResultAutotrack = selectorAutotrack(store.getState()) + expect(firstResultAutotrack).toBe(secondResultAutotrack) + + const memoized = weakMapMemoize((state: RootState) => + state.todos.map(({ id }) => id) + ) + const memoizedShallow = weakMapMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck: shallowEqual } + ) + expect(memoized.resetResultsCount).to.be.a('function') + expect(memoized.resultsCount).to.be.a('function') + expect(memoized.clearCache).to.be.a('function') + + expect(memoizedShallow.resetResultsCount).to.be.a('function') + expect(memoizedShallow.resultsCount).to.be.a('function') + expect(memoizedShallow.clearCache).to.be.a('function') + + expect(memoized(state)).toBe(memoized(state)) + expect(memoized(state)).toBe(memoized(state)) + expect(memoized(state)).toBe(memoized(state)) + expect(memoized.resultsCount()).toBe(1) + expect(memoized({ ...state })).not.toBe(memoized(state)) + expect(memoized({ ...state })).toStrictEqual(memoized(state)) + expect(memoized.resultsCount()).toBe(3) + expect(memoized({ ...state })).not.toBe(memoized(state)) + expect(memoized({ ...state })).toStrictEqual(memoized(state)) + expect(memoized.resultsCount()).toBe(5) + + expect(memoizedShallow(state)).toBe(memoizedShallow(state)) + expect(memoizedShallow.resultsCount()).toBe(0) + expect(memoizedShallow({ ...state })).toBe(memoizedShallow(state)) + expect(memoizedShallow.resultsCount()).toBe(0) + expect(memoizedShallow({ ...state })).toBe(memoizedShallow(state)) + // We spread the state to force the function to re-run but the + // result maintains the same reference because of `resultEqualityCheck`. + const first = memoizedShallow({ ...state }) + expect(memoizedShallow.resultsCount()).toBe(0) + memoizedShallow({ ...state }) + expect(memoizedShallow.resultsCount()).toBe(0) + const second = memoizedShallow({ ...state }) + expect(memoizedShallow.resultsCount()).toBe(0) + expect(first).toBe(second) + }) +}) diff --git a/test/examples.test.ts b/test/examples.test.ts index f487097e7..8f6218873 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -1,220 +1,220 @@ -import type { - OutputSelector, - Selector, - SelectorArray, - UnknownMemoizer -} from 'reselect' -import { - createSelector, - createSelectorCreator, - defaultMemoize, - unstable_autotrackMemoize as autotrackMemoize, - weakMapMemoize -} from 'reselect' -import { test } from 'vitest' -import type { RootState } from './testUtils' -import { addTodo, setupStore } from './testUtils' - -const store = setupStore() - -const EMPTY_ARRAY: [] = [] - -export const fallbackToEmptyArray = (array: T[]) => { - return array.length === 0 ? EMPTY_ARRAY : array -} - -const selectCompletedTodos = createSelector( - [(state: RootState) => state.todos], - todos => { - return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) - } -) - -const completedTodos = selectCompletedTodos(store.getState()) - -store.dispatch(addTodo({ title: '', description: '' })) - -test('empty array', () => { - expect(completedTodos).toBe(selectCompletedTodos(store.getState())) -}) - -test('identity', () => { - const identity = any>(func: Func) => func - const createNonMemoizedSelector = createSelectorCreator({ - memoize: identity, - argsMemoize: identity - }) - const nonMemoizedSelector = createNonMemoizedSelector( - [(state: RootState) => state.todos], - todos => todos.filter(todo => todo.completed === true), - { inputStabilityCheck: 'never' } - ) - - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - - expect(nonMemoizedSelector.recomputations()).toBe(3) -}) - -test.todo('Top Level Selectors', () => { - type TopLevelSelectors = { - [K in keyof State as K extends string - ? `select${Capitalize}` - : never]: Selector - } - - const topLevelSelectors: TopLevelSelectors = { - selectAlerts: state => state.alerts, - selectTodos: state => state.todos, - selectUsers: state => state.users - } -}) - -test.todo('Find Fastest Selector', () => { - const store = setupStore() - const selectTodoIds = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const findFastestSelector = ( - selector: S, - ...selectorArgs: Parameters - ) => { - const memoizeFuncs = [defaultMemoize, weakMapMemoize, autotrackMemoize] - const results = memoizeFuncs - .map(memoize => { - const alternateSelector = createSelector( - selector.dependencies as [...SelectorArray], - selector.resultFunc, - { memoize } - ) - const start = performance.now() - alternateSelector.apply(null, selectorArgs) - const time = performance.now() - start - return { name: memoize.name, time, selector: alternateSelector } - }) - .sort((a, b) => a.time - b.time) - const fastest = results.reduce((minResult, currentResult) => - currentResult.time < minResult.time ? currentResult : minResult - ) - const ratios = results - .filter(({ time }) => time !== fastest.time) - .map( - ({ time, name }) => - `\x1B[33m \x1B[1m${ - time / fastest.time - }\x1B[0m times faster than \x1B[1;41m${name}\x1B[0m.` - ) - if (fastest.selector.memoize.name !== selector.memoize.name) { - console.warn( - `The memoization method for \x1B[1;41m${ - selector.name - }\x1B[0m is \x1B[31m${ - selector.memoize.name - }\x1B[0m!\nChange it to \x1B[32m\x1B[1m${ - fastest.selector.memoize.name - }\x1B[0m to be more efficient.\nYou should use \x1B[32m\x1B[1m${ - fastest.name - }\x1B[0m because it is${ratios.join('\nand\n')}` - ) - } - return { results, fastest } as const - } -}) - -test('TypedCreateSelector', () => { - type TypedCreateSelector< - State, - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - > = < - InputSelectors extends readonly Selector[], - Result, - OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, - OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction - >( - ...createSelectorArgs: Parameters< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ) => ReturnType< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - const createAppSelector: TypedCreateSelector = createSelector - const selector = createAppSelector( - [state => state.todos, (state, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id)?.completed - ) -}) - -test('createCurriedSelector copy paste pattern', () => { - const state = store.getState() - const currySelector = < - State, - Result, - Params extends readonly any[], - AdditionalFields - >( - selector: ((state: State, ...args: Params) => Result) & AdditionalFields - ) => { - const curriedSelector = (...args: Params) => { - return (state: State) => { - return selector(state, ...args) - } - } - return Object.assign(curriedSelector, selector) - } - - const createCurriedSelector = < - InputSelectors extends SelectorArray, - Result, - OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - >( - ...args: Parameters< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ) => { - return currySelector(createSelector(...args)) - } - const selectTodoById = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - const selectTodoByIdCurried = createCurriedSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - expect(selectTodoById(state, 0)).toStrictEqual( - selectTodoByIdCurried(0)(state) - ) - expect(selectTodoById.argsMemoize).toBe(selectTodoByIdCurried.argsMemoize) - expect(selectTodoById.lastResult()).toBeDefined() - expect(selectTodoByIdCurried.lastResult()).toBeDefined() - expect(selectTodoById.lastResult()).toBe(selectTodoByIdCurried.lastResult()) - expect(selectTodoById.memoize).toBe(selectTodoByIdCurried.memoize) - expect(selectTodoById.memoizedResultFunc(state.todos, 0)).toBe( - selectTodoByIdCurried.memoizedResultFunc(state.todos, 0) - ) - expect(selectTodoById.recomputations()).toBe( - selectTodoByIdCurried.recomputations() - ) - expect(selectTodoById.resultFunc(state.todos, 0)).toBe( - selectTodoByIdCurried.resultFunc(state.todos, 0) - ) -}) +import type { + OutputSelector, + Selector, + SelectorArray, + UnknownMemoizer +} from 'reselect' +import { + createSelector, + createSelectorCreator, + defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { test } from 'vitest' +import type { RootState } from './testUtils' +import { addTodo, setupStore } from './testUtils' + +const store = setupStore() + +const EMPTY_ARRAY: [] = [] + +export const fallbackToEmptyArray = (array: T[]) => { + return array.length === 0 ? EMPTY_ARRAY : array +} + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) + } +) + +const completedTodos = selectCompletedTodos(store.getState()) + +store.dispatch(addTodo({ title: '', description: '' })) + +test('empty array', () => { + expect(completedTodos).toBe(selectCompletedTodos(store.getState())) +}) + +test('identity', () => { + const identity = any>(func: Func) => func + const createNonMemoizedSelector = createSelectorCreator({ + memoize: identity, + argsMemoize: identity + }) + const nonMemoizedSelector = createNonMemoizedSelector( + [(state: RootState) => state.todos], + todos => todos.filter(todo => todo.completed === true), + { inputStabilityCheck: 'never' } + ) + + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + + expect(nonMemoizedSelector.recomputations()).toBe(3) +}) + +test.todo('Top Level Selectors', () => { + type TopLevelSelectors = { + [K in keyof State as K extends string + ? `select${Capitalize}` + : never]: Selector + } + + const topLevelSelectors: TopLevelSelectors = { + selectAlerts: state => state.alerts, + selectTodos: state => state.todos, + selectUsers: state => state.users + } +}) + +test.todo('Find Fastest Selector', () => { + const store = setupStore() + const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const findFastestSelector = ( + selector: S, + ...selectorArgs: Parameters + ) => { + const memoizeFuncs = [defaultMemoize, weakMapMemoize, autotrackMemoize] + const results = memoizeFuncs + .map(memoize => { + const alternateSelector = createSelector( + selector.dependencies as [...SelectorArray], + selector.resultFunc, + { memoize } + ) + const start = performance.now() + alternateSelector.apply(null, selectorArgs) + const time = performance.now() - start + return { name: memoize.name, time, selector: alternateSelector } + }) + .sort((a, b) => a.time - b.time) + const fastest = results.reduce((minResult, currentResult) => + currentResult.time < minResult.time ? currentResult : minResult + ) + const ratios = results + .filter(({ time }) => time !== fastest.time) + .map( + ({ time, name }) => + `\x1B[33m \x1B[1m${ + time / fastest.time + }\x1B[0m times faster than \x1B[1;41m${name}\x1B[0m.` + ) + if (fastest.selector.memoize.name !== selector.memoize.name) { + console.warn( + `The memoization method for \x1B[1;41m${ + selector.name + }\x1B[0m is \x1B[31m${ + selector.memoize.name + }\x1B[0m!\nChange it to \x1B[32m\x1B[1m${ + fastest.selector.memoize.name + }\x1B[0m to be more efficient.\nYou should use \x1B[32m\x1B[1m${ + fastest.name + }\x1B[0m because it is${ratios.join('\nand\n')}` + ) + } + return { results, fastest } as const + } +}) + +test('TypedCreateSelector', () => { + type TypedCreateSelector< + State, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + > = < + InputSelectors extends readonly Selector[], + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + ...createSelectorArgs: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => ReturnType< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + const createAppSelector: TypedCreateSelector = createSelector + const selector = createAppSelector( + [state => state.todos, (state, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id)?.completed + ) +}) + +test('createCurriedSelector copy paste pattern', () => { + const state = store.getState() + const currySelector = < + State, + Result, + Params extends readonly any[], + AdditionalFields + >( + selector: ((state: State, ...args: Params) => Result) & AdditionalFields + ) => { + const curriedSelector = (...args: Params) => { + return (state: State) => { + return selector(state, ...args) + } + } + return Object.assign(curriedSelector, selector) + } + + const createCurriedSelector = < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + >( + ...args: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => { + return currySelector(createSelector(...args)) + } + const selectTodoById = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const selectTodoByIdCurried = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + expect(selectTodoById(state, 0)).toStrictEqual( + selectTodoByIdCurried(0)(state) + ) + expect(selectTodoById.argsMemoize).toBe(selectTodoByIdCurried.argsMemoize) + expect(selectTodoById.lastResult()).toBeDefined() + expect(selectTodoByIdCurried.lastResult()).toBeDefined() + expect(selectTodoById.lastResult()).toBe(selectTodoByIdCurried.lastResult()) + expect(selectTodoById.memoize).toBe(selectTodoByIdCurried.memoize) + expect(selectTodoById.memoizedResultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.memoizedResultFunc(state.todos, 0) + ) + expect(selectTodoById.recomputations()).toBe( + selectTodoByIdCurried.recomputations() + ) + expect(selectTodoById.resultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.resultFunc(state.todos, 0) + ) +}) diff --git a/test/reselect.bench.ts b/test/reselect.bench.ts index d212c821d..419ee1562 100644 --- a/test/reselect.bench.ts +++ b/test/reselect.bench.ts @@ -1,217 +1,217 @@ -import { createSelector } from '@reduxjs/toolkit' -import { bench } from 'vitest' -import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' -import { weakMapMemoize } from '../src/weakMapMemoize' - -const options: NonNullable[2]> = { - iterations: 1_000_000, - time: 100 -} - -describe('bench', () => { - interface State { - todos: { - id: number - completed: boolean - }[] - } - const state: State = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false }, - { id: 2, completed: false }, - { id: 3, completed: false }, - { id: 4, completed: false }, - { id: 5, completed: false }, - { id: 6, completed: false }, - { id: 7, completed: false }, - { id: 8, completed: false }, - { id: 9, completed: false }, - { id: 10, completed: false }, - { id: 11, completed: false }, - { id: 12, completed: false }, - { id: 13, completed: false }, - { id: 14, completed: false }, - { id: 15, completed: false }, - { id: 16, completed: false }, - { id: 17, completed: false }, - { id: 18, completed: false }, - { id: 19, completed: false }, - { id: 20, completed: false }, - { id: 21, completed: false }, - { id: 22, completed: false }, - { id: 23, completed: false }, - { id: 24, completed: false }, - { id: 25, completed: false }, - { id: 26, completed: false }, - { id: 27, completed: false }, - { id: 28, completed: false }, - { id: 29, completed: false }, - { id: 30, completed: false }, - { id: 31, completed: false }, - { id: 32, completed: false }, - { id: 33, completed: false }, - { id: 34, completed: false }, - { id: 35, completed: false }, - { id: 36, completed: false }, - { id: 37, completed: false }, - { id: 38, completed: false }, - { id: 39, completed: false }, - { id: 40, completed: false }, - { id: 41, completed: false }, - { id: 42, completed: false }, - { id: 43, completed: false }, - { id: 44, completed: false }, - { id: 45, completed: false }, - { id: 46, completed: false }, - { id: 47, completed: false }, - { id: 48, completed: false }, - { id: 49, completed: false }, - { id: 50, completed: false }, - { id: 51, completed: false }, - { id: 52, completed: false }, - { id: 53, completed: false }, - { id: 54, completed: false }, - { id: 55, completed: false }, - { id: 56, completed: false }, - { id: 57, completed: false }, - { id: 58, completed: false }, - { id: 59, completed: false }, - { id: 60, completed: false }, - { id: 61, completed: false }, - { id: 62, completed: false }, - { id: 63, completed: false }, - { id: 64, completed: false }, - { id: 65, completed: false }, - { id: 66, completed: false }, - { id: 67, completed: false }, - { id: 68, completed: false }, - { id: 69, completed: false }, - { id: 70, completed: false }, - { id: 71, completed: false }, - { id: 72, completed: false }, - { id: 73, completed: false }, - { id: 74, completed: false }, - { id: 75, completed: false }, - { id: 76, completed: false }, - { id: 77, completed: false }, - { id: 78, completed: false }, - { id: 79, completed: false }, - { id: 80, completed: false }, - { id: 81, completed: false }, - { id: 82, completed: false }, - { id: 83, completed: false }, - { id: 84, completed: false }, - { id: 85, completed: false }, - { id: 86, completed: false }, - { id: 87, completed: false }, - { id: 88, completed: false }, - { id: 89, completed: false }, - { id: 90, completed: false }, - { id: 91, completed: false }, - { id: 92, completed: false }, - { id: 93, completed: false }, - { id: 94, completed: false }, - { id: 95, completed: false }, - { id: 96, completed: false }, - { id: 97, completed: false }, - { id: 98, completed: false }, - { id: 99, completed: false } - ] - } - const selectorDefault = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - const selectorAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) - const selectorWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) - const selectorArgsAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: autotrackMemoize } - ) - const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) - const selectorArgsWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize } - ) - const parametricSelector = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id] - ) - const parametricSelectorWeakMapArgs = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id], - { - argsMemoize: weakMapMemoize - } - ) - bench( - 'selectorDefault', - () => { - selectorDefault(state) - }, - options - ) - - bench( - 'selectorAutotrack', - () => { - selectorAutotrack(state) - }, - options - ) - bench( - 'selectorWeakMap', - () => { - selectorWeakMap(state) - }, - options - ) - bench( - 'selectorArgsAutotrack', - () => { - selectorArgsAutotrack(state) - }, - options - ) - bench( - 'selectorArgsWeakMap', - () => { - selectorArgsWeakMap(state) - }, - options - ) - bench( - 'non-memoized selector', - () => { - nonMemoizedSelector(state) - }, - options - ) - bench( - 'parametricSelector', - () => { - parametricSelector(state, 0) - }, - options - ) - bench( - 'parametricSelectorWeakMapArgs', - () => { - parametricSelectorWeakMapArgs(state, 0) - }, - options - ) -}) +import { createSelector } from '@reduxjs/toolkit' +import { bench } from 'vitest' +import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' +import { weakMapMemoize } from '../src/weakMapMemoize' + +const options: NonNullable[2]> = { + iterations: 1_000_000, + time: 100 +} + +describe('bench', () => { + interface State { + todos: { + id: number + completed: boolean + }[] + } + const state: State = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false }, + { id: 2, completed: false }, + { id: 3, completed: false }, + { id: 4, completed: false }, + { id: 5, completed: false }, + { id: 6, completed: false }, + { id: 7, completed: false }, + { id: 8, completed: false }, + { id: 9, completed: false }, + { id: 10, completed: false }, + { id: 11, completed: false }, + { id: 12, completed: false }, + { id: 13, completed: false }, + { id: 14, completed: false }, + { id: 15, completed: false }, + { id: 16, completed: false }, + { id: 17, completed: false }, + { id: 18, completed: false }, + { id: 19, completed: false }, + { id: 20, completed: false }, + { id: 21, completed: false }, + { id: 22, completed: false }, + { id: 23, completed: false }, + { id: 24, completed: false }, + { id: 25, completed: false }, + { id: 26, completed: false }, + { id: 27, completed: false }, + { id: 28, completed: false }, + { id: 29, completed: false }, + { id: 30, completed: false }, + { id: 31, completed: false }, + { id: 32, completed: false }, + { id: 33, completed: false }, + { id: 34, completed: false }, + { id: 35, completed: false }, + { id: 36, completed: false }, + { id: 37, completed: false }, + { id: 38, completed: false }, + { id: 39, completed: false }, + { id: 40, completed: false }, + { id: 41, completed: false }, + { id: 42, completed: false }, + { id: 43, completed: false }, + { id: 44, completed: false }, + { id: 45, completed: false }, + { id: 46, completed: false }, + { id: 47, completed: false }, + { id: 48, completed: false }, + { id: 49, completed: false }, + { id: 50, completed: false }, + { id: 51, completed: false }, + { id: 52, completed: false }, + { id: 53, completed: false }, + { id: 54, completed: false }, + { id: 55, completed: false }, + { id: 56, completed: false }, + { id: 57, completed: false }, + { id: 58, completed: false }, + { id: 59, completed: false }, + { id: 60, completed: false }, + { id: 61, completed: false }, + { id: 62, completed: false }, + { id: 63, completed: false }, + { id: 64, completed: false }, + { id: 65, completed: false }, + { id: 66, completed: false }, + { id: 67, completed: false }, + { id: 68, completed: false }, + { id: 69, completed: false }, + { id: 70, completed: false }, + { id: 71, completed: false }, + { id: 72, completed: false }, + { id: 73, completed: false }, + { id: 74, completed: false }, + { id: 75, completed: false }, + { id: 76, completed: false }, + { id: 77, completed: false }, + { id: 78, completed: false }, + { id: 79, completed: false }, + { id: 80, completed: false }, + { id: 81, completed: false }, + { id: 82, completed: false }, + { id: 83, completed: false }, + { id: 84, completed: false }, + { id: 85, completed: false }, + { id: 86, completed: false }, + { id: 87, completed: false }, + { id: 88, completed: false }, + { id: 89, completed: false }, + { id: 90, completed: false }, + { id: 91, completed: false }, + { id: 92, completed: false }, + { id: 93, completed: false }, + { id: 94, completed: false }, + { id: 95, completed: false }, + { id: 96, completed: false }, + { id: 97, completed: false }, + { id: 98, completed: false }, + { id: 99, completed: false } + ] + } + const selectorDefault = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id) + ) + const selectorAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + const selectorWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: autotrackMemoize } + ) + const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) + const selectorArgsWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const parametricSelector = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelectorWeakMapArgs = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id], + { + argsMemoize: weakMapMemoize + } + ) + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + + bench( + 'selectorAutotrack', + () => { + selectorAutotrack(state) + }, + options + ) + bench( + 'selectorWeakMap', + () => { + selectorWeakMap(state) + }, + options + ) + bench( + 'selectorArgsAutotrack', + () => { + selectorArgsAutotrack(state) + }, + options + ) + bench( + 'selectorArgsWeakMap', + () => { + selectorArgsWeakMap(state) + }, + options + ) + bench( + 'non-memoized selector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'parametricSelector', + () => { + parametricSelector(state, 0) + }, + options + ) + bench( + 'parametricSelectorWeakMapArgs', + () => { + parametricSelectorWeakMapArgs(state, 0) + }, + options + ) +}) diff --git a/test/testUtils.ts b/test/testUtils.ts index 12c557f5b..cf8290ee0 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,574 +1,566 @@ -import type { PayloadAction } from '@reduxjs/toolkit' -import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' -import { test } from 'vitest' -import type { - AnyFunction, - OutputSelector, - Selector, - SelectorArray, - Simplify -} from '../src/types' - -export interface Todo { - id: number - title: string - description: string - completed: boolean -} - -interface Alert { - id: number - message: string - type: string - read: boolean -} - -interface BillingAddress { - street: string - city: string - state: string - zip: string -} - -interface Address extends BillingAddress { - billing: BillingAddress -} - -interface PushNotification { - enabled: boolean - frequency: string -} - -interface Notifications { - email: boolean - sms: boolean - push: PushNotification -} - -interface Preferences { - newsletter: boolean - notifications: Notifications -} - -interface Login { - lastLogin: string - loginCount: number -} - -interface UserDetails { - name: string - email: string - address: Address - preferences: Preferences -} - -interface User { - id: number - details: UserDetails - status: string - login: Login -} - -interface AppSettings { - theme: string - language: string -} - -interface UserState { - user: User - appSettings: AppSettings -} - -// For long arrays -interface BillingAddress { - street: string - city: string - state: string - zip: string -} - -interface Address extends BillingAddress { - billing: BillingAddress -} - -interface PushNotification { - enabled: boolean - frequency: string -} - -interface Notifications { - email: boolean - sms: boolean - push: PushNotification -} - -interface Preferences { - newsletter: boolean - notifications: Notifications -} - -interface Login { - lastLogin: string - loginCount: number -} - -interface UserDetails { - name: string - email: string - address: Address - preferences: Preferences -} - -interface User { - id: number - details: UserDetails - status: string - login: Login -} - -interface AppSettings { - theme: string - language: string -} - -interface UserState { - user: User - appSettings: AppSettings -} - -let nextTodoId = 0 - -// For long arrays -const todoState = [ - { - id: nextTodoId++, - title: 'Buy groceries', - description: 'Milk, bread, eggs, and fruits', - completed: false - }, - { - id: nextTodoId++, - title: 'Schedule dentist appointment', - description: 'Check available slots for next week', - completed: false - }, - { - id: nextTodoId++, - title: 'Convince the cat to get a job', - description: 'Need extra income for cat treats', - completed: false - }, - { - id: nextTodoId++, - title: 'Figure out if plants are plotting world domination', - description: 'That cactus looks suspicious...', - completed: false - }, - { - id: nextTodoId++, - title: 'Practice telekinesis', - description: 'Try moving the remote without getting up', - completed: false - }, - { - id: nextTodoId++, - title: 'Determine location of El Dorado', - description: 'Might need it for the next vacation', - completed: false - }, - { - id: nextTodoId++, - title: 'Master the art of invisible potato juggling', - description: 'Great party trick', - completed: false - } -] - -export const createTodoItem = () => { - const id = nextTodoId++ - return { - id, - title: `Task ${id}`, - description: `Description for task ${id}`, - completed: false - } -} - -export const pushToTodos = (limit: number) => { - const { length: todoStateLength } = todoState - // const limit = howMany + todoStateLength - for (let i = todoStateLength; i < limit; i++) { - todoState.push(createTodoItem()) - } -} - -pushToTodos(200) - -const alertState = [ - { - id: 0, - message: 'You have an upcoming meeting at 3 PM.', - type: 'reminder', - read: false - }, - { - id: 1, - message: 'New software update available.', - type: 'notification', - read: false - }, - { - id: 3, - message: - 'The plants have been watered, but keep an eye on that shifty cactus.', - type: 'notification', - read: false - }, - { - id: 4, - message: - 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', - type: 'reminder', - read: false - }, - { - id: 5, - message: - 'Expedition to El Dorado is postponed. The treasure map is being updated.', - type: 'notification', - read: false - }, - { - id: 6, - message: - 'Invisible potato juggling championship is tonight. May the best mime win.', - type: 'reminder', - read: false - } -] - -// For nested fields tests -const userState: UserState = { - user: { - id: 0, - details: { - name: 'John Doe', - email: 'john.doe@example.com', - address: { - street: '123 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345', - billing: { - street: '456 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345' - } - }, - preferences: { - newsletter: true, - notifications: { - email: true, - sms: false, - push: { - enabled: true, - frequency: 'daily' - } - } - } - }, - status: 'active', - login: { - lastLogin: '2023-04-30T12:34:56Z', - loginCount: 123 - } - }, - appSettings: { - theme: 'dark', - language: 'en-US' - } -} - -const todoSlice = createSlice({ - name: 'todos', - initialState: todoState, - reducers: { - toggleCompleted: (state, action: PayloadAction) => { - const todo = state.find(todo => todo.id === action.payload) - if (todo) { - todo.completed = !todo.completed - } - }, - - addTodo: (state, action: PayloadAction>) => { - // const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - const newId = nextTodoId++ - state.push({ - ...action.payload, - id: newId, - completed: false - }) - }, - - removeTodo: (state, action: PayloadAction) => { - return state.filter(todo => todo.id !== action.payload) - }, - - updateTodo: (state, action: PayloadAction) => { - const index = state.findIndex(todo => todo.id === action.payload.id) - if (index !== -1) { - state[index] = action.payload - } - }, - - clearCompleted: state => { - return state.filter(todo => !todo.completed) - } - } -}) - -const alertSlice = createSlice({ - name: 'alerts', - initialState: alertState, - reducers: { - markAsRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = true - } - }, - - toggleRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = !alert.read - } - }, - - addAlert: (state, action: PayloadAction>) => { - const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - state.push({ - ...action.payload, - id: newId - }) - }, - - removeAlert: (state, action: PayloadAction) => { - return state.filter(alert => alert.id !== action.payload) - } - } -}) - -const userSlice = createSlice({ - name: 'users', - initialState: userState, - reducers: { - setUserName: (state, action: PayloadAction) => { - state.user.details.name = action.payload - }, - - setUserEmail: (state, action: PayloadAction) => { - state.user.details.email = action.payload - }, - - setAppTheme: (state, action: PayloadAction) => { - state.appSettings.theme = action.payload - }, - - updateUserStatus: (state, action: PayloadAction) => { - state.user.status = action.payload - }, - - updateLoginDetails: ( - state, - action: PayloadAction<{ lastLogin: string; loginCount: number }> - ) => { - state.user.login = { ...state.user.login, ...action.payload } - }, - - updateUserAddress: (state, action: PayloadAction
) => { - state.user.details.address = { - ...state.user.details.address, - ...action.payload - } - }, - - updateBillingAddress: (state, action: PayloadAction) => { - state.user.details.address.billing = { - ...state.user.details.address.billing, - ...action.payload - } - }, - - toggleNewsletterSubscription: state => { - state.user.details.preferences.newsletter = - !state.user.details.preferences.newsletter - }, - - setNotificationPreferences: ( - state, - action: PayloadAction - ) => { - state.user.details.preferences.notifications = { - ...state.user.details.preferences.notifications, - ...action.payload - } - }, - - updateAppLanguage: (state, action: PayloadAction) => { - state.appSettings.language = action.payload - } - } -}) - -const rootReducer = combineReducers({ - [todoSlice.name]: todoSlice.reducer, - [alertSlice.name]: alertSlice.reducer, - [userSlice.name]: userSlice.reducer -}) - -export const setupStore = (preloadedState?: Partial) => { - return configureStore({ reducer: rootReducer, preloadedState }) -} - -export type AppStore = Simplify> - -export type RootState = ReturnType - -export interface LocalTestContext { - store: AppStore - state: RootState -} - -export const { markAsRead, addAlert, removeAlert, toggleRead } = - alertSlice.actions - -export const { - toggleCompleted, - addTodo, - removeTodo, - updateTodo, - clearCompleted -} = todoSlice.actions - -export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions - -// Since Node 16 does not support `structuredClone` -export const deepClone = (object: T): T => - JSON.parse(JSON.stringify(object)) - -export const setFunctionName = (func: AnyFunction, name: string) => { - Object.defineProperty(func, 'name', { value: name }) -} - -export const setFunctionNames = (funcObject: Record) => { - Object.entries(funcObject).forEach(([key, value]) => - setFunctionName(value, key) - ) -} - -const store = setupStore() -const state = store.getState() - -export const localTest = test.extend({ - store, - state -}) - -export const resetSelector = (selector: S) => { - selector.clearCache() - selector.resetRecomputations() - selector.resetDependencyRecomputations() - selector.memoizedResultFunc.clearCache() -} - -export const logRecomputations = (selector: S) => { - console.log( - `${selector.name} result function recalculated:`, - selector.recomputations(), - `time(s)`, - `input selectors recalculated:`, - selector.dependencyRecomputations(), - `time(s)` - ) -} - -export const logSelectorRecomputations = < - S extends OutputSelector ->( - selector: S -) => { - console.log( - `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, - { - resultFunc: selector.recomputations(), - inputSelectors: selector.dependencyRecomputations(), - newResults: - typeof selector.memoizedResultFunc.resultsCount === 'function' - ? selector.memoizedResultFunc.resultsCount() - : undefined - } - ) - // console.log( - // `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, - // `\x1B[33m${selector.recomputations().toLocaleString('en-US')}\x1B[0m`, - // 'time(s)', - // `input selectors recalculated:`, - // `\x1B[33m${selector - // .dependencyRecomputations() - // .toLocaleString('en-US')}\x1B[0m`, - // 'time(s)' - // ) -} - -export const logFunctionInfo = (func: AnyFunction, recomputations: number) => { - console.log( - `\x1B[32m\x1B[1m${func.name}\x1B[0m was called:`, - recomputations, - 'time(s)' - ) -} - -export const safeApply = ( - func: (...args: Params) => Result, - args: Params -) => func.apply(null, args) - -export const countRecomputations = < - Params extends any[], - Result, - AdditionalFields ->( - func: ((...args: Params) => Result) & AdditionalFields -) => { - let recomputations = 0 - const wrapper = (...args: Params) => { - recomputations++ - return safeApply(func, args) - } - return Object.assign( - wrapper, - { - recomputations: () => recomputations, - resetRecomputations: () => (recomputations = 0) - }, - func - ) -} - -export const runMultipleTimes = ( - func: (...args: Params) => any, - times: number, - ...args: Params -) => { - for (let i = 0; i < times; i++) { - safeApply(func, args) - } -} - -export const expensiveComputation = (times = 1_000_000) => { - for (let i = 0; i < times; i++) { - // Do nothing - } -} +import type { PayloadAction } from '@reduxjs/toolkit' +import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' +import { test } from 'vitest' +import type { AnyFunction, OutputSelector, Simplify } from '../src/types' + +export interface Todo { + id: number + title: string + description: string + completed: boolean +} + +interface Alert { + id: number + message: string + type: string + read: boolean +} + +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +// For long arrays +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +let nextTodoId = 0 + +// For long arrays +const todoState = [ + { + id: nextTodoId++, + title: 'Buy groceries', + description: 'Milk, bread, eggs, and fruits', + completed: false + }, + { + id: nextTodoId++, + title: 'Schedule dentist appointment', + description: 'Check available slots for next week', + completed: false + }, + { + id: nextTodoId++, + title: 'Convince the cat to get a job', + description: 'Need extra income for cat treats', + completed: false + }, + { + id: nextTodoId++, + title: 'Figure out if plants are plotting world domination', + description: 'That cactus looks suspicious...', + completed: false + }, + { + id: nextTodoId++, + title: 'Practice telekinesis', + description: 'Try moving the remote without getting up', + completed: false + }, + { + id: nextTodoId++, + title: 'Determine location of El Dorado', + description: 'Might need it for the next vacation', + completed: false + }, + { + id: nextTodoId++, + title: 'Master the art of invisible potato juggling', + description: 'Great party trick', + completed: false + } +] + +export const createTodoItem = () => { + const id = nextTodoId++ + return { + id, + title: `Task ${id}`, + description: `Description for task ${id}`, + completed: false + } +} + +export const pushToTodos = (limit: number) => { + const { length: todoStateLength } = todoState + // const limit = howMany + todoStateLength + for (let i = todoStateLength; i < limit; i++) { + todoState.push(createTodoItem()) + } +} + +pushToTodos(200) + +const alertState = [ + { + id: 0, + message: 'You have an upcoming meeting at 3 PM.', + type: 'reminder', + read: false + }, + { + id: 1, + message: 'New software update available.', + type: 'notification', + read: false + }, + { + id: 3, + message: + 'The plants have been watered, but keep an eye on that shifty cactus.', + type: 'notification', + read: false + }, + { + id: 4, + message: + 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', + type: 'reminder', + read: false + }, + { + id: 5, + message: + 'Expedition to El Dorado is postponed. The treasure map is being updated.', + type: 'notification', + read: false + }, + { + id: 6, + message: + 'Invisible potato juggling championship is tonight. May the best mime win.', + type: 'reminder', + read: false + } +] + +// For nested fields tests +const userState: UserState = { + user: { + id: 0, + details: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + street: '123 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345', + billing: { + street: '456 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345' + } + }, + preferences: { + newsletter: true, + notifications: { + email: true, + sms: false, + push: { + enabled: true, + frequency: 'daily' + } + } + } + }, + status: 'active', + login: { + lastLogin: '2023-04-30T12:34:56Z', + loginCount: 123 + } + }, + appSettings: { + theme: 'dark', + language: 'en-US' + } +} + +const todoSlice = createSlice({ + name: 'todos', + initialState: todoState, + reducers: { + toggleCompleted: (state, action: PayloadAction) => { + const todo = state.find(todo => todo.id === action.payload) + if (todo) { + todo.completed = !todo.completed + } + }, + + addTodo: (state, action: PayloadAction>) => { + // const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + const newId = nextTodoId++ + state.push({ + ...action.payload, + id: newId, + completed: false + }) + }, + + removeTodo: (state, action: PayloadAction) => { + return state.filter(todo => todo.id !== action.payload) + }, + + updateTodo: (state, action: PayloadAction) => { + const index = state.findIndex(todo => todo.id === action.payload.id) + if (index !== -1) { + state[index] = action.payload + } + }, + + clearCompleted: state => { + return state.filter(todo => !todo.completed) + } + } +}) + +const alertSlice = createSlice({ + name: 'alerts', + initialState: alertState, + reducers: { + markAsRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = true + } + }, + + toggleRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = !alert.read + } + }, + + addAlert: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId + }) + }, + + removeAlert: (state, action: PayloadAction) => { + return state.filter(alert => alert.id !== action.payload) + } + } +}) + +const userSlice = createSlice({ + name: 'users', + initialState: userState, + reducers: { + setUserName: (state, action: PayloadAction) => { + state.user.details.name = action.payload + }, + + setUserEmail: (state, action: PayloadAction) => { + state.user.details.email = action.payload + }, + + setAppTheme: (state, action: PayloadAction) => { + state.appSettings.theme = action.payload + }, + + updateUserStatus: (state, action: PayloadAction) => { + state.user.status = action.payload + }, + + updateLoginDetails: ( + state, + action: PayloadAction<{ lastLogin: string; loginCount: number }> + ) => { + state.user.login = { ...state.user.login, ...action.payload } + }, + + updateUserAddress: (state, action: PayloadAction
) => { + state.user.details.address = { + ...state.user.details.address, + ...action.payload + } + }, + + updateBillingAddress: (state, action: PayloadAction) => { + state.user.details.address.billing = { + ...state.user.details.address.billing, + ...action.payload + } + }, + + toggleNewsletterSubscription: state => { + state.user.details.preferences.newsletter = + !state.user.details.preferences.newsletter + }, + + setNotificationPreferences: ( + state, + action: PayloadAction + ) => { + state.user.details.preferences.notifications = { + ...state.user.details.preferences.notifications, + ...action.payload + } + }, + + updateAppLanguage: (state, action: PayloadAction) => { + state.appSettings.language = action.payload + } + } +}) + +const rootReducer = combineReducers({ + [todoSlice.name]: todoSlice.reducer, + [alertSlice.name]: alertSlice.reducer, + [userSlice.name]: userSlice.reducer +}) + +export const setupStore = (preloadedState?: Partial) => { + return configureStore({ reducer: rootReducer, preloadedState }) +} + +export type AppStore = Simplify> + +export type RootState = ReturnType + +export interface LocalTestContext { + store: AppStore + state: RootState +} + +export const { markAsRead, addAlert, removeAlert, toggleRead } = + alertSlice.actions + +export const { + toggleCompleted, + addTodo, + removeTodo, + updateTodo, + clearCompleted +} = todoSlice.actions + +export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions + +// Since Node 16 does not support `structuredClone` +export const deepClone = (object: T): T => + JSON.parse(JSON.stringify(object)) + +export const setFunctionName = (func: AnyFunction, name: string) => { + Object.defineProperty(func, 'name', { value: name }) +} + +export const setFunctionNames = (funcObject: Record) => { + Object.entries(funcObject).forEach(([key, value]) => + setFunctionName(value, key) + ) +} + +const store = setupStore() +const state = store.getState() + +export const localTest = test.extend({ + store, + state +}) + +export const resetSelector = (selector: S) => { + selector.clearCache() + selector.resetRecomputations() + selector.resetDependencyRecomputations() + selector.memoizedResultFunc.clearCache() +} + +export const logRecomputations = (selector: S) => { + console.log( + `${selector.name} result function recalculated:`, + selector.recomputations(), + `time(s)`, + `input selectors recalculated:`, + selector.dependencyRecomputations(), + `time(s)` + ) +} + +export const logSelectorRecomputations = ( + selector: S +) => { + console.log( + `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, + { + resultFunc: selector.recomputations(), + inputSelectors: selector.dependencyRecomputations(), + newResults: + typeof selector.memoizedResultFunc.resultsCount === 'function' + ? selector.memoizedResultFunc.resultsCount() + : undefined + } + ) + // console.log( + // `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, + // `\x1B[33m${selector.recomputations().toLocaleString('en-US')}\x1B[0m`, + // 'time(s)', + // `input selectors recalculated:`, + // `\x1B[33m${selector + // .dependencyRecomputations() + // .toLocaleString('en-US')}\x1B[0m`, + // 'time(s)' + // ) +} + +export const logFunctionInfo = (func: AnyFunction, recomputations: number) => { + console.log( + `\x1B[32m\x1B[1m${func.name}\x1B[0m was called:`, + recomputations, + 'time(s)' + ) +} + +export const safeApply = ( + func: (...args: Params) => Result, + args: Params +) => func.apply(null, args) + +export const countRecomputations = < + Params extends any[], + Result, + AdditionalFields +>( + func: ((...args: Params) => Result) & AdditionalFields +) => { + let recomputations = 0 + const wrapper = (...args: Params) => { + recomputations++ + return safeApply(func, args) + } + return Object.assign( + wrapper, + { + recomputations: () => recomputations, + resetRecomputations: () => (recomputations = 0) + }, + func + ) +} + +export const runMultipleTimes = ( + func: (...args: Params) => any, + times: number, + ...args: Params +) => { + for (let i = 0; i < times; i++) { + safeApply(func, args) + } +} + +export const expensiveComputation = (times = 1_000_000) => { + for (let i = 0; i < times; i++) { + // Do nothing + } +} From 454b17152b4c7a4dc5b3197ed2e583ee5a124dd9 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 Nov 2023 20:41:02 -0600 Subject: [PATCH 2/4] Restore `WeakRef` usage --- src/weakMapMemoize.ts | 21 +++++++++++++++++---- typescript_test/tsconfig.json | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index cc7e4193d..7fa2c4cd3 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -8,6 +8,15 @@ import type { Simplify } from './types' +class StrongRef { + constructor(private value: T) {} + deref() { + return this.value + } +} + +const Ref = WeakRef ?? StrongRef + const UNTERMINATED = 0 const TERMINATED = 1 @@ -156,7 +165,7 @@ export function weakMapMemoize( let fnNode = createCacheNode() const { resultEqualityCheck } = options - let lastResult: ReturnType | undefined + let lastResult: WeakRef | undefined let resultsCount = 0 @@ -212,12 +221,16 @@ export function weakMapMemoize( terminatedNode.s = TERMINATED if (resultEqualityCheck) { - if (lastResult != null && resultEqualityCheck(lastResult, result)) { - result = lastResult + const lastResultValue = lastResult?.deref() ?? lastResult + if (lastResultValue != null && resultEqualityCheck(lastResultValue, result)) { + result = lastResultValue resultsCount !== 0 && resultsCount-- } - lastResult = result + const needsWeakRef = + (typeof result === 'object' && result !== null) || + typeof result === 'function' + lastResult = needsWeakRef ? new Ref(result) : result } terminatedNode.v = result return result diff --git a/typescript_test/tsconfig.json b/typescript_test/tsconfig.json index 93536e656..81026e74a 100644 --- a/typescript_test/tsconfig.json +++ b/typescript_test/tsconfig.json @@ -3,6 +3,7 @@ "module": "commonjs", "strict": true, "target": "ES2015", + "lib": ["ES2021.WeakRef"], "declaration": true, "noEmit": true, "skipLibCheck": true, From 43edb7bf85e854bd92ad7687df71b17b70b75046 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 Nov 2023 21:12:31 -0600 Subject: [PATCH 3/4] Restore whitespace changes --- test/benchmarks/orderOfExecution.bench.ts | 426 +++++----- test/benchmarks/weakMapMemoize.bench.ts | 972 +++++++++++----------- test/examples.test.ts | 440 +++++----- test/reselect.bench.ts | 434 +++++----- 4 files changed, 1136 insertions(+), 1136 deletions(-) diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts index 034ba81e6..210b489d7 100644 --- a/test/benchmarks/orderOfExecution.bench.ts +++ b/test/benchmarks/orderOfExecution.bench.ts @@ -1,213 +1,213 @@ -import type { OutputSelector, Selector } from 'reselect' -import { createSelector, defaultMemoize } from 'reselect' -import type { Options } from 'tinybench' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { - countRecomputations, - expensiveComputation, - logFunctionInfo, - logSelectorRecomputations, - resetSelector, - runMultipleTimes, - setFunctionNames, - setupStore, - toggleCompleted, - toggleRead -} from '../testUtils' - -describe('Less vs more computation in input selectors', () => { - const store = setupStore() - const runSelector = (selector: Selector) => { - runMultipleTimes(selector, 100, store.getState()) - } - const selectorLessInInput = createSelector( - [(state: RootState) => state.todos], - todos => { - expensiveComputation() - return todos.filter(todo => todo.completed) - } - ) - const selectorMoreInInput = createSelector( - [ - (state: RootState) => { - expensiveComputation() - return state.todos - } - ], - todos => todos.filter(todo => todo.completed) - ) - - const nonMemoized = countRecomputations((state: RootState) => { - expensiveComputation() - return state.todos.filter(todo => todo.completed) - }) - const commonOptions: Options = { - iterations: 10, - time: 0 - } - setFunctionNames({ selectorLessInInput, selectorMoreInInput, nonMemoized }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - beforeEach: () => { - store.dispatch(toggleRead(1)) - }, - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorLessInInput, - () => { - runSelector(selectorLessInInput) - }, - createOptions(selectorLessInInput, commonOptions) - ) - bench( - selectorMoreInInput, - () => { - runSelector(selectorMoreInInput) - }, - createOptions(selectorMoreInInput, commonOptions) - ) - bench( - nonMemoized, - () => { - runSelector(nonMemoized) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - nonMemoized.resetRecomputations() - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logFunctionInfo(nonMemoized, nonMemoized.recomputations()) - } - } - } - } - ) -}) - -// This benchmark is made to test to see at what point it becomes beneficial -// to use reselect to memoize a function that is a plain field accessor. -describe('Reselect vs standalone memoization for field access', () => { - const store = setupStore() - const runSelector = (selector: Selector) => { - runMultipleTimes(selector, 1_000_000, store.getState()) - } - const commonOptions: Options = { - // warmupIterations: 0, - // warmupTime: 0, - // iterations: 10, - // time: 0 - } - const fieldAccessorWithReselect = createSelector( - [(state: RootState) => state.users], - users => users.appSettings - ) - const fieldAccessorWithMemoize = countRecomputations( - defaultMemoize((state: RootState) => { - return state.users.appSettings - }) - ) - const nonMemoizedAccessor = countRecomputations( - (state: RootState) => state.users.appSettings - ) - - setFunctionNames({ - fieldAccessorWithReselect, - fieldAccessorWithMemoize, - nonMemoizedAccessor - }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - fieldAccessorWithReselect, - () => { - runSelector(fieldAccessorWithReselect) - }, - createOptions(fieldAccessorWithReselect, commonOptions) - ) - bench( - fieldAccessorWithMemoize, - () => { - runSelector(fieldAccessorWithMemoize) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - fieldAccessorWithMemoize.resetRecomputations() - fieldAccessorWithMemoize.clearCache() - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logFunctionInfo( - fieldAccessorWithMemoize, - fieldAccessorWithMemoize.recomputations() - ) - } - } - } - } - ) - bench( - nonMemoizedAccessor, - () => { - runSelector(nonMemoizedAccessor) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - nonMemoizedAccessor.resetRecomputations() - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logFunctionInfo( - nonMemoizedAccessor, - nonMemoizedAccessor.recomputations() - ) - } - } - } - } - ) -}) +import type { OutputSelector, Selector } from 'reselect' +import { createSelector, defaultMemoize } from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + countRecomputations, + expensiveComputation, + logFunctionInfo, + logSelectorRecomputations, + resetSelector, + runMultipleTimes, + setFunctionNames, + setupStore, + toggleCompleted, + toggleRead +} from '../testUtils' + +describe('Less vs more computation in input selectors', () => { + const store = setupStore() + const runSelector = (selector: Selector) => { + runMultipleTimes(selector, 100, store.getState()) + } + const selectorLessInInput = createSelector( + [(state: RootState) => state.todos], + todos => { + expensiveComputation() + return todos.filter(todo => todo.completed) + } + ) + const selectorMoreInInput = createSelector( + [ + (state: RootState) => { + expensiveComputation() + return state.todos + } + ], + todos => todos.filter(todo => todo.completed) + ) + + const nonMemoized = countRecomputations((state: RootState) => { + expensiveComputation() + return state.todos.filter(todo => todo.completed) + }) + const commonOptions: Options = { + iterations: 10, + time: 0 + } + setFunctionNames({ selectorLessInInput, selectorMoreInInput, nonMemoized }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + beforeEach: () => { + store.dispatch(toggleRead(1)) + }, + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorLessInInput, + () => { + runSelector(selectorLessInInput) + }, + createOptions(selectorLessInInput, commonOptions) + ) + bench( + selectorMoreInInput, + () => { + runSelector(selectorMoreInInput) + }, + createOptions(selectorMoreInInput, commonOptions) + ) + bench( + nonMemoized, + () => { + runSelector(nonMemoized) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + nonMemoized.resetRecomputations() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo(nonMemoized, nonMemoized.recomputations()) + } + } + } + } + ) +}) + +// This benchmark is made to test to see at what point it becomes beneficial +// to use reselect to memoize a function that is a plain field accessor. +describe('Reselect vs standalone memoization for field access', () => { + const store = setupStore() + const runSelector = (selector: Selector) => { + runMultipleTimes(selector, 1_000_000, store.getState()) + } + const commonOptions: Options = { + // warmupIterations: 0, + // warmupTime: 0, + // iterations: 10, + // time: 0 + } + const fieldAccessorWithReselect = createSelector( + [(state: RootState) => state.users], + users => users.appSettings + ) + const fieldAccessorWithMemoize = countRecomputations( + defaultMemoize((state: RootState) => { + return state.users.appSettings + }) + ) + const nonMemoizedAccessor = countRecomputations( + (state: RootState) => state.users.appSettings + ) + + setFunctionNames({ + fieldAccessorWithReselect, + fieldAccessorWithMemoize, + nonMemoizedAccessor + }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + fieldAccessorWithReselect, + () => { + runSelector(fieldAccessorWithReselect) + }, + createOptions(fieldAccessorWithReselect, commonOptions) + ) + bench( + fieldAccessorWithMemoize, + () => { + runSelector(fieldAccessorWithMemoize) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + fieldAccessorWithMemoize.resetRecomputations() + fieldAccessorWithMemoize.clearCache() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo( + fieldAccessorWithMemoize, + fieldAccessorWithMemoize.recomputations() + ) + } + } + } + } + ) + bench( + nonMemoizedAccessor, + () => { + runSelector(nonMemoizedAccessor) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + nonMemoizedAccessor.resetRecomputations() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo( + nonMemoizedAccessor, + nonMemoizedAccessor.recomputations() + ) + } + } + } + } + ) +}) diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts index fe4a4381a..a64bb93f5 100644 --- a/test/benchmarks/weakMapMemoize.bench.ts +++ b/test/benchmarks/weakMapMemoize.bench.ts @@ -1,486 +1,486 @@ -import type { OutputSelector, Selector } from 'reselect' -import { - unstable_autotrackMemoize as autotrackMemoize, - createSelector, - weakMapMemoize -} from 'reselect' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { - logSelectorRecomputations, - resetSelector, - setFunctionNames, - setupStore -} from '../testUtils' - -import type { Options } from 'tinybench' - -describe('Parametric selectors: weakMapMemoize vs others', () => { - const store = setupStore() - const state = store.getState() - const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const runSelector = (selector: S) => { - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - } - const selectorDefault = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - const selectorDefaultWithCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { memoizeOptions: { maxSize: 30 } } - ) - const selectorDefaultWithArgsCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoizeOptions: { maxSize: 30 } } - ) - const selectorDefaultWithBothCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id), - { memoize: weakMapMemoize } - ) - const selectorAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { memoize: autotrackMemoize } - ) - const selectorArgsAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: autotrackMemoize } - ) - const selectorBothAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } - ) - const selectorArgsWeakMap = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: weakMapMemoize } - ) - const selectorBothWeakMap = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.find(todo => todo.id === id), - { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } - ) - const nonMemoizedSelector = (state: RootState, id: number) => { - return state.todos.find(todo => todo.id === id) - } - setFunctionNames({ - selectorDefault, - selectorDefaultWithCacheSize, - selectorDefaultWithArgsCacheSize, - selectorDefaultWithBothCacheSize, - selectorWeakMap, - selectorArgsWeakMap, - selectorBothWeakMap, - selectorAutotrack, - selectorArgsAutotrack, - selectorBothAutotrack, - nonMemoizedSelector - }) - - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorDefault, - () => { - runSelector(selectorDefault) - }, - createOptions(selectorDefault, commonOptions) - ) - bench( - selectorDefaultWithCacheSize, - () => { - runSelector(selectorDefaultWithCacheSize) - }, - createOptions(selectorDefaultWithCacheSize, commonOptions) - ) - bench( - selectorDefaultWithArgsCacheSize, - () => { - runSelector(selectorDefaultWithArgsCacheSize) - }, - createOptions(selectorDefaultWithArgsCacheSize, commonOptions) - ) - bench( - selectorDefaultWithBothCacheSize, - () => { - runSelector(selectorDefaultWithBothCacheSize) - }, - createOptions(selectorDefaultWithBothCacheSize, commonOptions) - ) - bench( - selectorWeakMap, - () => { - runSelector(selectorWeakMap) - }, - createOptions(selectorWeakMap, commonOptions) - ) - bench( - selectorArgsWeakMap, - () => { - runSelector(selectorArgsWeakMap) - }, - createOptions(selectorArgsWeakMap, commonOptions) - ) - bench( - selectorBothWeakMap, - () => { - runSelector(selectorBothWeakMap) - }, - createOptions(selectorBothWeakMap, commonOptions) - ) - bench( - selectorAutotrack, - () => { - runSelector(selectorAutotrack) - }, - createOptions(selectorAutotrack, commonOptions) - ) - bench( - selectorArgsAutotrack, - () => { - runSelector(selectorArgsAutotrack) - }, - createOptions(selectorArgsAutotrack, commonOptions) - ) - bench( - selectorBothAutotrack, - () => { - runSelector(selectorBothAutotrack) - }, - createOptions(selectorBothAutotrack, commonOptions) - ) - bench( - nonMemoizedSelector, - () => { - runSelector(nonMemoizedSelector) - }, - { ...commonOptions } - ) -}) - -// describe('weakMapMemoize vs defaultMemoize with maxSize', () => { -// const store = setupStore() -// const state = store.getState() -// const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) -// const commonOptions: Options = { -// iterations: 10, -// time: 0 -// } -// const runSelector = (selector: S) => { -// arrayOfNumbers.forEach(num => { -// selector(state, num) -// }) -// arrayOfNumbers.forEach(num => { -// selector(state, num) -// }) -// } -// const selectorDefaultWithCacheSize = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { memoizeOptions: { maxSize: 30 } } -// ) -// const selectorDefaultWithArgsCacheSize = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { argsMemoizeOptions: { maxSize: 30 } } -// ) -// const selectorDefaultWithBothCacheSize = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } -// ) -// const selectorWeakMap = createSelector( -// [(state: RootState) => state.todos, (state: RootState, id: number) => id], -// (todos, id) => todos.map(todo => todo.id === id), -// { memoize: weakMapMemoize } -// ) -// const selectorArgsWeakMap = createSelector( -// (state: RootState) => state.todos, -// (state: RootState, id: number) => id, -// (todos, id) => todos.map(todo => todo.id === id), -// { argsMemoize: weakMapMemoize } -// ) -// const selectorBothWeakMap = createSelector( -// (state: RootState) => state.todos, -// (state: RootState, id: number) => id, -// (todos, id) => todos.map(todo => todo.id === id), -// { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } -// ) -// const nonMemoizedSelector = (state: RootState, id: number) => { -// return state.todos.map(todo => todo.id === id) -// } -// setFunctionNames({ -// selectorDefaultWithCacheSize, -// selectorDefaultWithArgsCacheSize, -// selectorDefaultWithBothCacheSize, -// selectorWeakMap, -// selectorArgsWeakMap, -// selectorBothWeakMap, -// nonMemoizedSelector -// }) -// const createOptions = ( -// selector: S, -// commonOptions: Options = {} -// ) => { -// const options: Options = { -// setup: (task, mode) => { -// if (mode === 'warmup') return -// resetSelector(selector) -// task.opts = { -// afterAll: () => { -// logSelectorRecomputations(selector) -// } -// } -// } -// } -// return { ...commonOptions, ...options } -// } -// bench( -// selectorDefaultWithCacheSize, -// () => { -// runSelector(selectorDefaultWithCacheSize) -// }, -// createOptions(selectorDefaultWithCacheSize, commonOptions) -// ) -// bench( -// selectorDefaultWithArgsCacheSize, -// () => { -// runSelector(selectorDefaultWithArgsCacheSize) -// }, -// createOptions(selectorDefaultWithArgsCacheSize, commonOptions) -// ) -// bench( -// selectorDefaultWithBothCacheSize, -// () => { -// runSelector(selectorDefaultWithBothCacheSize) -// }, -// createOptions(selectorDefaultWithBothCacheSize, commonOptions) -// ) -// bench( -// selectorWeakMap, -// () => { -// runSelector(selectorWeakMap) -// }, -// createOptions(selectorWeakMap, commonOptions) -// ) -// bench( -// selectorArgsWeakMap, -// () => { -// runSelector(selectorArgsWeakMap) -// }, -// createOptions(selectorArgsWeakMap, commonOptions) -// ) -// bench( -// selectorBothWeakMap, -// () => { -// runSelector(selectorBothWeakMap) -// }, -// createOptions(selectorBothWeakMap, commonOptions) -// ) -// bench( -// nonMemoizedSelector, -// () => { -// runSelector(nonMemoizedSelector) -// }, -// { ...commonOptions } -// ) -// }) - -describe('Simple selectors: weakMapMemoize vs others', () => { - const store = setupStore() - const commonOptions: Options = { - // warmupIterations: 0, - // warmupTime: 0, - // iterations: 10, - // time: 0 - } - const selectTodoIdsDefault = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const selectTodoIdsWeakMap = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize } - ) - const selectTodoIdsAutotrack = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { memoize: autotrackMemoize } - ) - - setFunctionNames({ - selectTodoIdsDefault, - selectTodoIdsWeakMap, - selectTodoIdsAutotrack - }) - - const createOptions = (selector: S) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - - bench( - selectTodoIdsDefault, - () => { - selectTodoIdsDefault(store.getState()) - }, - createOptions(selectTodoIdsDefault) - ) - bench( - selectTodoIdsWeakMap, - () => { - selectTodoIdsWeakMap(store.getState()) - }, - createOptions(selectTodoIdsWeakMap) - ) - bench( - selectTodoIdsAutotrack, - () => { - selectTodoIdsAutotrack(store.getState()) - }, - createOptions(selectTodoIdsAutotrack) - ) -}) - -describe.skip('weakMapMemoize memory leak', () => { - const store = setupStore() - const state = store.getState() - const arrayOfNumbers = Array.from( - { length: 2_000_000 }, - (num, index) => index - ) - const commonOptions: Options = { - warmupIterations: 0, - warmupTime: 0, - iterations: 1, - time: 0 - } - const runSelector = (selector: S) => { - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - arrayOfNumbers.forEach(num => { - selector(state, num) - }) - } - const selectorDefault = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id) - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id), - { memoize: weakMapMemoize } - ) - const selectorArgsWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize } - ) - const selectorBothWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } - ) - setFunctionNames({ - selectorDefault, - selectorWeakMap, - selectorArgsWeakMap, - selectorBothWeakMap - }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - afterAll: () => { - logSelectorRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorDefault, - () => { - runSelector(selectorDefault) - }, - createOptions(selectorDefault, commonOptions) - ) - bench( - selectorWeakMap, - () => { - runSelector(selectorWeakMap) - }, - createOptions(selectorWeakMap, commonOptions) - ) - bench.skip( - selectorArgsWeakMap, - () => { - runSelector(selectorArgsWeakMap) - }, - createOptions(selectorArgsWeakMap, commonOptions) - ) - bench.skip( - selectorBothWeakMap, - () => { - runSelector(selectorBothWeakMap) - }, - createOptions(selectorBothWeakMap, commonOptions) - ) -}) +import type { OutputSelector, Selector } from 'reselect' +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector, + weakMapMemoize +} from 'reselect' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + logSelectorRecomputations, + resetSelector, + setFunctionNames, + setupStore +} from '../testUtils' + +import type { Options } from 'tinybench' + +describe('Parametric selectors: weakMapMemoize vs others', () => { + const store = setupStore() + const state = store.getState() + const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + } + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const selectorDefaultWithCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithArgsCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithBothCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { memoize: autotrackMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: autotrackMemoize } + ) + const selectorBothAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } + ) + const selectorArgsWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + const nonMemoizedSelector = (state: RootState, id: number) => { + return state.todos.find(todo => todo.id === id) + } + setFunctionNames({ + selectorDefault, + selectorDefaultWithCacheSize, + selectorDefaultWithArgsCacheSize, + selectorDefaultWithBothCacheSize, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap, + selectorAutotrack, + selectorArgsAutotrack, + selectorBothAutotrack, + nonMemoizedSelector + }) + + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) + ) + bench( + selectorDefaultWithCacheSize, + () => { + runSelector(selectorDefaultWithCacheSize) + }, + createOptions(selectorDefaultWithCacheSize, commonOptions) + ) + bench( + selectorDefaultWithArgsCacheSize, + () => { + runSelector(selectorDefaultWithArgsCacheSize) + }, + createOptions(selectorDefaultWithArgsCacheSize, commonOptions) + ) + bench( + selectorDefaultWithBothCacheSize, + () => { + runSelector(selectorDefaultWithBothCacheSize) + }, + createOptions(selectorDefaultWithBothCacheSize, commonOptions) + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + createOptions(selectorWeakMap, commonOptions) + ) + bench( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + createOptions(selectorArgsWeakMap, commonOptions) + ) + bench( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + createOptions(selectorBothWeakMap, commonOptions) + ) + bench( + selectorAutotrack, + () => { + runSelector(selectorAutotrack) + }, + createOptions(selectorAutotrack, commonOptions) + ) + bench( + selectorArgsAutotrack, + () => { + runSelector(selectorArgsAutotrack) + }, + createOptions(selectorArgsAutotrack, commonOptions) + ) + bench( + selectorBothAutotrack, + () => { + runSelector(selectorBothAutotrack) + }, + createOptions(selectorBothAutotrack, commonOptions) + ) + bench( + nonMemoizedSelector, + () => { + runSelector(nonMemoizedSelector) + }, + { ...commonOptions } + ) +}) + +// describe('weakMapMemoize vs defaultMemoize with maxSize', () => { +// const store = setupStore() +// const state = store.getState() +// const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) +// const commonOptions: Options = { +// iterations: 10, +// time: 0 +// } +// const runSelector = (selector: S) => { +// arrayOfNumbers.forEach(num => { +// selector(state, num) +// }) +// arrayOfNumbers.forEach(num => { +// selector(state, num) +// }) +// } +// const selectorDefaultWithCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoizeOptions: { maxSize: 30 } } +// ) +// const selectorDefaultWithArgsCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoizeOptions: { maxSize: 30 } } +// ) +// const selectorDefaultWithBothCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } +// ) +// const selectorWeakMap = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoize: weakMapMemoize } +// ) +// const selectorArgsWeakMap = createSelector( +// (state: RootState) => state.todos, +// (state: RootState, id: number) => id, +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoize: weakMapMemoize } +// ) +// const selectorBothWeakMap = createSelector( +// (state: RootState) => state.todos, +// (state: RootState, id: number) => id, +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } +// ) +// const nonMemoizedSelector = (state: RootState, id: number) => { +// return state.todos.map(todo => todo.id === id) +// } +// setFunctionNames({ +// selectorDefaultWithCacheSize, +// selectorDefaultWithArgsCacheSize, +// selectorDefaultWithBothCacheSize, +// selectorWeakMap, +// selectorArgsWeakMap, +// selectorBothWeakMap, +// nonMemoizedSelector +// }) +// const createOptions = ( +// selector: S, +// commonOptions: Options = {} +// ) => { +// const options: Options = { +// setup: (task, mode) => { +// if (mode === 'warmup') return +// resetSelector(selector) +// task.opts = { +// afterAll: () => { +// logSelectorRecomputations(selector) +// } +// } +// } +// } +// return { ...commonOptions, ...options } +// } +// bench( +// selectorDefaultWithCacheSize, +// () => { +// runSelector(selectorDefaultWithCacheSize) +// }, +// createOptions(selectorDefaultWithCacheSize, commonOptions) +// ) +// bench( +// selectorDefaultWithArgsCacheSize, +// () => { +// runSelector(selectorDefaultWithArgsCacheSize) +// }, +// createOptions(selectorDefaultWithArgsCacheSize, commonOptions) +// ) +// bench( +// selectorDefaultWithBothCacheSize, +// () => { +// runSelector(selectorDefaultWithBothCacheSize) +// }, +// createOptions(selectorDefaultWithBothCacheSize, commonOptions) +// ) +// bench( +// selectorWeakMap, +// () => { +// runSelector(selectorWeakMap) +// }, +// createOptions(selectorWeakMap, commonOptions) +// ) +// bench( +// selectorArgsWeakMap, +// () => { +// runSelector(selectorArgsWeakMap) +// }, +// createOptions(selectorArgsWeakMap, commonOptions) +// ) +// bench( +// selectorBothWeakMap, +// () => { +// runSelector(selectorBothWeakMap) +// }, +// createOptions(selectorBothWeakMap, commonOptions) +// ) +// bench( +// nonMemoizedSelector, +// () => { +// runSelector(nonMemoizedSelector) +// }, +// { ...commonOptions } +// ) +// }) + +describe('Simple selectors: weakMapMemoize vs others', () => { + const store = setupStore() + const commonOptions: Options = { + // warmupIterations: 0, + // warmupTime: 0, + // iterations: 10, + // time: 0 + } + const selectTodoIdsDefault = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selectTodoIdsWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectTodoIdsAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + + setFunctionNames({ + selectTodoIdsDefault, + selectTodoIdsWeakMap, + selectTodoIdsAutotrack + }) + + const createOptions = (selector: S) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + + bench( + selectTodoIdsDefault, + () => { + selectTodoIdsDefault(store.getState()) + }, + createOptions(selectTodoIdsDefault) + ) + bench( + selectTodoIdsWeakMap, + () => { + selectTodoIdsWeakMap(store.getState()) + }, + createOptions(selectTodoIdsWeakMap) + ) + bench( + selectTodoIdsAutotrack, + () => { + selectTodoIdsAutotrack(store.getState()) + }, + createOptions(selectTodoIdsAutotrack) + ) +}) + +describe.skip('weakMapMemoize memory leak', () => { + const store = setupStore() + const state = store.getState() + const arrayOfNumbers = Array.from( + { length: 2_000_000 }, + (num, index) => index + ) + const commonOptions: Options = { + warmupIterations: 0, + warmupTime: 0, + iterations: 1, + time: 0 + } + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + } + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id) + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + const selectorArgsWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + setFunctionNames({ + selectorDefault, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap + }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + createOptions(selectorWeakMap, commonOptions) + ) + bench.skip( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + createOptions(selectorArgsWeakMap, commonOptions) + ) + bench.skip( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + createOptions(selectorBothWeakMap, commonOptions) + ) +}) diff --git a/test/examples.test.ts b/test/examples.test.ts index 8f6218873..f487097e7 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -1,220 +1,220 @@ -import type { - OutputSelector, - Selector, - SelectorArray, - UnknownMemoizer -} from 'reselect' -import { - createSelector, - createSelectorCreator, - defaultMemoize, - unstable_autotrackMemoize as autotrackMemoize, - weakMapMemoize -} from 'reselect' -import { test } from 'vitest' -import type { RootState } from './testUtils' -import { addTodo, setupStore } from './testUtils' - -const store = setupStore() - -const EMPTY_ARRAY: [] = [] - -export const fallbackToEmptyArray = (array: T[]) => { - return array.length === 0 ? EMPTY_ARRAY : array -} - -const selectCompletedTodos = createSelector( - [(state: RootState) => state.todos], - todos => { - return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) - } -) - -const completedTodos = selectCompletedTodos(store.getState()) - -store.dispatch(addTodo({ title: '', description: '' })) - -test('empty array', () => { - expect(completedTodos).toBe(selectCompletedTodos(store.getState())) -}) - -test('identity', () => { - const identity = any>(func: Func) => func - const createNonMemoizedSelector = createSelectorCreator({ - memoize: identity, - argsMemoize: identity - }) - const nonMemoizedSelector = createNonMemoizedSelector( - [(state: RootState) => state.todos], - todos => todos.filter(todo => todo.completed === true), - { inputStabilityCheck: 'never' } - ) - - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - - expect(nonMemoizedSelector.recomputations()).toBe(3) -}) - -test.todo('Top Level Selectors', () => { - type TopLevelSelectors = { - [K in keyof State as K extends string - ? `select${Capitalize}` - : never]: Selector - } - - const topLevelSelectors: TopLevelSelectors = { - selectAlerts: state => state.alerts, - selectTodos: state => state.todos, - selectUsers: state => state.users - } -}) - -test.todo('Find Fastest Selector', () => { - const store = setupStore() - const selectTodoIds = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const findFastestSelector = ( - selector: S, - ...selectorArgs: Parameters - ) => { - const memoizeFuncs = [defaultMemoize, weakMapMemoize, autotrackMemoize] - const results = memoizeFuncs - .map(memoize => { - const alternateSelector = createSelector( - selector.dependencies as [...SelectorArray], - selector.resultFunc, - { memoize } - ) - const start = performance.now() - alternateSelector.apply(null, selectorArgs) - const time = performance.now() - start - return { name: memoize.name, time, selector: alternateSelector } - }) - .sort((a, b) => a.time - b.time) - const fastest = results.reduce((minResult, currentResult) => - currentResult.time < minResult.time ? currentResult : minResult - ) - const ratios = results - .filter(({ time }) => time !== fastest.time) - .map( - ({ time, name }) => - `\x1B[33m \x1B[1m${ - time / fastest.time - }\x1B[0m times faster than \x1B[1;41m${name}\x1B[0m.` - ) - if (fastest.selector.memoize.name !== selector.memoize.name) { - console.warn( - `The memoization method for \x1B[1;41m${ - selector.name - }\x1B[0m is \x1B[31m${ - selector.memoize.name - }\x1B[0m!\nChange it to \x1B[32m\x1B[1m${ - fastest.selector.memoize.name - }\x1B[0m to be more efficient.\nYou should use \x1B[32m\x1B[1m${ - fastest.name - }\x1B[0m because it is${ratios.join('\nand\n')}` - ) - } - return { results, fastest } as const - } -}) - -test('TypedCreateSelector', () => { - type TypedCreateSelector< - State, - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - > = < - InputSelectors extends readonly Selector[], - Result, - OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, - OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction - >( - ...createSelectorArgs: Parameters< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ) => ReturnType< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - const createAppSelector: TypedCreateSelector = createSelector - const selector = createAppSelector( - [state => state.todos, (state, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id)?.completed - ) -}) - -test('createCurriedSelector copy paste pattern', () => { - const state = store.getState() - const currySelector = < - State, - Result, - Params extends readonly any[], - AdditionalFields - >( - selector: ((state: State, ...args: Params) => Result) & AdditionalFields - ) => { - const curriedSelector = (...args: Params) => { - return (state: State) => { - return selector(state, ...args) - } - } - return Object.assign(curriedSelector, selector) - } - - const createCurriedSelector = < - InputSelectors extends SelectorArray, - Result, - OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - >( - ...args: Parameters< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ) => { - return currySelector(createSelector(...args)) - } - const selectTodoById = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - const selectTodoByIdCurried = createCurriedSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - expect(selectTodoById(state, 0)).toStrictEqual( - selectTodoByIdCurried(0)(state) - ) - expect(selectTodoById.argsMemoize).toBe(selectTodoByIdCurried.argsMemoize) - expect(selectTodoById.lastResult()).toBeDefined() - expect(selectTodoByIdCurried.lastResult()).toBeDefined() - expect(selectTodoById.lastResult()).toBe(selectTodoByIdCurried.lastResult()) - expect(selectTodoById.memoize).toBe(selectTodoByIdCurried.memoize) - expect(selectTodoById.memoizedResultFunc(state.todos, 0)).toBe( - selectTodoByIdCurried.memoizedResultFunc(state.todos, 0) - ) - expect(selectTodoById.recomputations()).toBe( - selectTodoByIdCurried.recomputations() - ) - expect(selectTodoById.resultFunc(state.todos, 0)).toBe( - selectTodoByIdCurried.resultFunc(state.todos, 0) - ) -}) +import type { + OutputSelector, + Selector, + SelectorArray, + UnknownMemoizer +} from 'reselect' +import { + createSelector, + createSelectorCreator, + defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { test } from 'vitest' +import type { RootState } from './testUtils' +import { addTodo, setupStore } from './testUtils' + +const store = setupStore() + +const EMPTY_ARRAY: [] = [] + +export const fallbackToEmptyArray = (array: T[]) => { + return array.length === 0 ? EMPTY_ARRAY : array +} + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) + } +) + +const completedTodos = selectCompletedTodos(store.getState()) + +store.dispatch(addTodo({ title: '', description: '' })) + +test('empty array', () => { + expect(completedTodos).toBe(selectCompletedTodos(store.getState())) +}) + +test('identity', () => { + const identity = any>(func: Func) => func + const createNonMemoizedSelector = createSelectorCreator({ + memoize: identity, + argsMemoize: identity + }) + const nonMemoizedSelector = createNonMemoizedSelector( + [(state: RootState) => state.todos], + todos => todos.filter(todo => todo.completed === true), + { inputStabilityCheck: 'never' } + ) + + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + + expect(nonMemoizedSelector.recomputations()).toBe(3) +}) + +test.todo('Top Level Selectors', () => { + type TopLevelSelectors = { + [K in keyof State as K extends string + ? `select${Capitalize}` + : never]: Selector + } + + const topLevelSelectors: TopLevelSelectors = { + selectAlerts: state => state.alerts, + selectTodos: state => state.todos, + selectUsers: state => state.users + } +}) + +test.todo('Find Fastest Selector', () => { + const store = setupStore() + const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const findFastestSelector = ( + selector: S, + ...selectorArgs: Parameters + ) => { + const memoizeFuncs = [defaultMemoize, weakMapMemoize, autotrackMemoize] + const results = memoizeFuncs + .map(memoize => { + const alternateSelector = createSelector( + selector.dependencies as [...SelectorArray], + selector.resultFunc, + { memoize } + ) + const start = performance.now() + alternateSelector.apply(null, selectorArgs) + const time = performance.now() - start + return { name: memoize.name, time, selector: alternateSelector } + }) + .sort((a, b) => a.time - b.time) + const fastest = results.reduce((minResult, currentResult) => + currentResult.time < minResult.time ? currentResult : minResult + ) + const ratios = results + .filter(({ time }) => time !== fastest.time) + .map( + ({ time, name }) => + `\x1B[33m \x1B[1m${ + time / fastest.time + }\x1B[0m times faster than \x1B[1;41m${name}\x1B[0m.` + ) + if (fastest.selector.memoize.name !== selector.memoize.name) { + console.warn( + `The memoization method for \x1B[1;41m${ + selector.name + }\x1B[0m is \x1B[31m${ + selector.memoize.name + }\x1B[0m!\nChange it to \x1B[32m\x1B[1m${ + fastest.selector.memoize.name + }\x1B[0m to be more efficient.\nYou should use \x1B[32m\x1B[1m${ + fastest.name + }\x1B[0m because it is${ratios.join('\nand\n')}` + ) + } + return { results, fastest } as const + } +}) + +test('TypedCreateSelector', () => { + type TypedCreateSelector< + State, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + > = < + InputSelectors extends readonly Selector[], + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + ...createSelectorArgs: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => ReturnType< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + const createAppSelector: TypedCreateSelector = createSelector + const selector = createAppSelector( + [state => state.todos, (state, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id)?.completed + ) +}) + +test('createCurriedSelector copy paste pattern', () => { + const state = store.getState() + const currySelector = < + State, + Result, + Params extends readonly any[], + AdditionalFields + >( + selector: ((state: State, ...args: Params) => Result) & AdditionalFields + ) => { + const curriedSelector = (...args: Params) => { + return (state: State) => { + return selector(state, ...args) + } + } + return Object.assign(curriedSelector, selector) + } + + const createCurriedSelector = < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + >( + ...args: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => { + return currySelector(createSelector(...args)) + } + const selectTodoById = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const selectTodoByIdCurried = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + expect(selectTodoById(state, 0)).toStrictEqual( + selectTodoByIdCurried(0)(state) + ) + expect(selectTodoById.argsMemoize).toBe(selectTodoByIdCurried.argsMemoize) + expect(selectTodoById.lastResult()).toBeDefined() + expect(selectTodoByIdCurried.lastResult()).toBeDefined() + expect(selectTodoById.lastResult()).toBe(selectTodoByIdCurried.lastResult()) + expect(selectTodoById.memoize).toBe(selectTodoByIdCurried.memoize) + expect(selectTodoById.memoizedResultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.memoizedResultFunc(state.todos, 0) + ) + expect(selectTodoById.recomputations()).toBe( + selectTodoByIdCurried.recomputations() + ) + expect(selectTodoById.resultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.resultFunc(state.todos, 0) + ) +}) diff --git a/test/reselect.bench.ts b/test/reselect.bench.ts index 419ee1562..d212c821d 100644 --- a/test/reselect.bench.ts +++ b/test/reselect.bench.ts @@ -1,217 +1,217 @@ -import { createSelector } from '@reduxjs/toolkit' -import { bench } from 'vitest' -import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' -import { weakMapMemoize } from '../src/weakMapMemoize' - -const options: NonNullable[2]> = { - iterations: 1_000_000, - time: 100 -} - -describe('bench', () => { - interface State { - todos: { - id: number - completed: boolean - }[] - } - const state: State = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false }, - { id: 2, completed: false }, - { id: 3, completed: false }, - { id: 4, completed: false }, - { id: 5, completed: false }, - { id: 6, completed: false }, - { id: 7, completed: false }, - { id: 8, completed: false }, - { id: 9, completed: false }, - { id: 10, completed: false }, - { id: 11, completed: false }, - { id: 12, completed: false }, - { id: 13, completed: false }, - { id: 14, completed: false }, - { id: 15, completed: false }, - { id: 16, completed: false }, - { id: 17, completed: false }, - { id: 18, completed: false }, - { id: 19, completed: false }, - { id: 20, completed: false }, - { id: 21, completed: false }, - { id: 22, completed: false }, - { id: 23, completed: false }, - { id: 24, completed: false }, - { id: 25, completed: false }, - { id: 26, completed: false }, - { id: 27, completed: false }, - { id: 28, completed: false }, - { id: 29, completed: false }, - { id: 30, completed: false }, - { id: 31, completed: false }, - { id: 32, completed: false }, - { id: 33, completed: false }, - { id: 34, completed: false }, - { id: 35, completed: false }, - { id: 36, completed: false }, - { id: 37, completed: false }, - { id: 38, completed: false }, - { id: 39, completed: false }, - { id: 40, completed: false }, - { id: 41, completed: false }, - { id: 42, completed: false }, - { id: 43, completed: false }, - { id: 44, completed: false }, - { id: 45, completed: false }, - { id: 46, completed: false }, - { id: 47, completed: false }, - { id: 48, completed: false }, - { id: 49, completed: false }, - { id: 50, completed: false }, - { id: 51, completed: false }, - { id: 52, completed: false }, - { id: 53, completed: false }, - { id: 54, completed: false }, - { id: 55, completed: false }, - { id: 56, completed: false }, - { id: 57, completed: false }, - { id: 58, completed: false }, - { id: 59, completed: false }, - { id: 60, completed: false }, - { id: 61, completed: false }, - { id: 62, completed: false }, - { id: 63, completed: false }, - { id: 64, completed: false }, - { id: 65, completed: false }, - { id: 66, completed: false }, - { id: 67, completed: false }, - { id: 68, completed: false }, - { id: 69, completed: false }, - { id: 70, completed: false }, - { id: 71, completed: false }, - { id: 72, completed: false }, - { id: 73, completed: false }, - { id: 74, completed: false }, - { id: 75, completed: false }, - { id: 76, completed: false }, - { id: 77, completed: false }, - { id: 78, completed: false }, - { id: 79, completed: false }, - { id: 80, completed: false }, - { id: 81, completed: false }, - { id: 82, completed: false }, - { id: 83, completed: false }, - { id: 84, completed: false }, - { id: 85, completed: false }, - { id: 86, completed: false }, - { id: 87, completed: false }, - { id: 88, completed: false }, - { id: 89, completed: false }, - { id: 90, completed: false }, - { id: 91, completed: false }, - { id: 92, completed: false }, - { id: 93, completed: false }, - { id: 94, completed: false }, - { id: 95, completed: false }, - { id: 96, completed: false }, - { id: 97, completed: false }, - { id: 98, completed: false }, - { id: 99, completed: false } - ] - } - const selectorDefault = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - const selectorAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) - const selectorWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) - const selectorArgsAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: autotrackMemoize } - ) - const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) - const selectorArgsWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize } - ) - const parametricSelector = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id] - ) - const parametricSelectorWeakMapArgs = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id], - { - argsMemoize: weakMapMemoize - } - ) - bench( - 'selectorDefault', - () => { - selectorDefault(state) - }, - options - ) - - bench( - 'selectorAutotrack', - () => { - selectorAutotrack(state) - }, - options - ) - bench( - 'selectorWeakMap', - () => { - selectorWeakMap(state) - }, - options - ) - bench( - 'selectorArgsAutotrack', - () => { - selectorArgsAutotrack(state) - }, - options - ) - bench( - 'selectorArgsWeakMap', - () => { - selectorArgsWeakMap(state) - }, - options - ) - bench( - 'non-memoized selector', - () => { - nonMemoizedSelector(state) - }, - options - ) - bench( - 'parametricSelector', - () => { - parametricSelector(state, 0) - }, - options - ) - bench( - 'parametricSelectorWeakMapArgs', - () => { - parametricSelectorWeakMapArgs(state, 0) - }, - options - ) -}) +import { createSelector } from '@reduxjs/toolkit' +import { bench } from 'vitest' +import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' +import { weakMapMemoize } from '../src/weakMapMemoize' + +const options: NonNullable[2]> = { + iterations: 1_000_000, + time: 100 +} + +describe('bench', () => { + interface State { + todos: { + id: number + completed: boolean + }[] + } + const state: State = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false }, + { id: 2, completed: false }, + { id: 3, completed: false }, + { id: 4, completed: false }, + { id: 5, completed: false }, + { id: 6, completed: false }, + { id: 7, completed: false }, + { id: 8, completed: false }, + { id: 9, completed: false }, + { id: 10, completed: false }, + { id: 11, completed: false }, + { id: 12, completed: false }, + { id: 13, completed: false }, + { id: 14, completed: false }, + { id: 15, completed: false }, + { id: 16, completed: false }, + { id: 17, completed: false }, + { id: 18, completed: false }, + { id: 19, completed: false }, + { id: 20, completed: false }, + { id: 21, completed: false }, + { id: 22, completed: false }, + { id: 23, completed: false }, + { id: 24, completed: false }, + { id: 25, completed: false }, + { id: 26, completed: false }, + { id: 27, completed: false }, + { id: 28, completed: false }, + { id: 29, completed: false }, + { id: 30, completed: false }, + { id: 31, completed: false }, + { id: 32, completed: false }, + { id: 33, completed: false }, + { id: 34, completed: false }, + { id: 35, completed: false }, + { id: 36, completed: false }, + { id: 37, completed: false }, + { id: 38, completed: false }, + { id: 39, completed: false }, + { id: 40, completed: false }, + { id: 41, completed: false }, + { id: 42, completed: false }, + { id: 43, completed: false }, + { id: 44, completed: false }, + { id: 45, completed: false }, + { id: 46, completed: false }, + { id: 47, completed: false }, + { id: 48, completed: false }, + { id: 49, completed: false }, + { id: 50, completed: false }, + { id: 51, completed: false }, + { id: 52, completed: false }, + { id: 53, completed: false }, + { id: 54, completed: false }, + { id: 55, completed: false }, + { id: 56, completed: false }, + { id: 57, completed: false }, + { id: 58, completed: false }, + { id: 59, completed: false }, + { id: 60, completed: false }, + { id: 61, completed: false }, + { id: 62, completed: false }, + { id: 63, completed: false }, + { id: 64, completed: false }, + { id: 65, completed: false }, + { id: 66, completed: false }, + { id: 67, completed: false }, + { id: 68, completed: false }, + { id: 69, completed: false }, + { id: 70, completed: false }, + { id: 71, completed: false }, + { id: 72, completed: false }, + { id: 73, completed: false }, + { id: 74, completed: false }, + { id: 75, completed: false }, + { id: 76, completed: false }, + { id: 77, completed: false }, + { id: 78, completed: false }, + { id: 79, completed: false }, + { id: 80, completed: false }, + { id: 81, completed: false }, + { id: 82, completed: false }, + { id: 83, completed: false }, + { id: 84, completed: false }, + { id: 85, completed: false }, + { id: 86, completed: false }, + { id: 87, completed: false }, + { id: 88, completed: false }, + { id: 89, completed: false }, + { id: 90, completed: false }, + { id: 91, completed: false }, + { id: 92, completed: false }, + { id: 93, completed: false }, + { id: 94, completed: false }, + { id: 95, completed: false }, + { id: 96, completed: false }, + { id: 97, completed: false }, + { id: 98, completed: false }, + { id: 99, completed: false } + ] + } + const selectorDefault = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id) + ) + const selectorAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + const selectorWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: autotrackMemoize } + ) + const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) + const selectorArgsWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const parametricSelector = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelectorWeakMapArgs = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id], + { + argsMemoize: weakMapMemoize + } + ) + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + + bench( + 'selectorAutotrack', + () => { + selectorAutotrack(state) + }, + options + ) + bench( + 'selectorWeakMap', + () => { + selectorWeakMap(state) + }, + options + ) + bench( + 'selectorArgsAutotrack', + () => { + selectorArgsAutotrack(state) + }, + options + ) + bench( + 'selectorArgsWeakMap', + () => { + selectorArgsWeakMap(state) + }, + options + ) + bench( + 'non-memoized selector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'parametricSelector', + () => { + parametricSelector(state, 0) + }, + options + ) + bench( + 'parametricSelectorWeakMapArgs', + () => { + parametricSelectorWeakMapArgs(state, 0) + }, + options + ) +}) From fdd979f74921fc0d7f8902ff1283b1dc2f54e899 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 Nov 2023 21:16:45 -0600 Subject: [PATCH 4/4] Revert `testUtils.ts` --- test/testUtils.ts | 1140 +++++++++++++++++++++++---------------------- 1 file changed, 574 insertions(+), 566 deletions(-) diff --git a/test/testUtils.ts b/test/testUtils.ts index cf8290ee0..12c557f5b 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,566 +1,574 @@ -import type { PayloadAction } from '@reduxjs/toolkit' -import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' -import { test } from 'vitest' -import type { AnyFunction, OutputSelector, Simplify } from '../src/types' - -export interface Todo { - id: number - title: string - description: string - completed: boolean -} - -interface Alert { - id: number - message: string - type: string - read: boolean -} - -interface BillingAddress { - street: string - city: string - state: string - zip: string -} - -interface Address extends BillingAddress { - billing: BillingAddress -} - -interface PushNotification { - enabled: boolean - frequency: string -} - -interface Notifications { - email: boolean - sms: boolean - push: PushNotification -} - -interface Preferences { - newsletter: boolean - notifications: Notifications -} - -interface Login { - lastLogin: string - loginCount: number -} - -interface UserDetails { - name: string - email: string - address: Address - preferences: Preferences -} - -interface User { - id: number - details: UserDetails - status: string - login: Login -} - -interface AppSettings { - theme: string - language: string -} - -interface UserState { - user: User - appSettings: AppSettings -} - -// For long arrays -interface BillingAddress { - street: string - city: string - state: string - zip: string -} - -interface Address extends BillingAddress { - billing: BillingAddress -} - -interface PushNotification { - enabled: boolean - frequency: string -} - -interface Notifications { - email: boolean - sms: boolean - push: PushNotification -} - -interface Preferences { - newsletter: boolean - notifications: Notifications -} - -interface Login { - lastLogin: string - loginCount: number -} - -interface UserDetails { - name: string - email: string - address: Address - preferences: Preferences -} - -interface User { - id: number - details: UserDetails - status: string - login: Login -} - -interface AppSettings { - theme: string - language: string -} - -interface UserState { - user: User - appSettings: AppSettings -} - -let nextTodoId = 0 - -// For long arrays -const todoState = [ - { - id: nextTodoId++, - title: 'Buy groceries', - description: 'Milk, bread, eggs, and fruits', - completed: false - }, - { - id: nextTodoId++, - title: 'Schedule dentist appointment', - description: 'Check available slots for next week', - completed: false - }, - { - id: nextTodoId++, - title: 'Convince the cat to get a job', - description: 'Need extra income for cat treats', - completed: false - }, - { - id: nextTodoId++, - title: 'Figure out if plants are plotting world domination', - description: 'That cactus looks suspicious...', - completed: false - }, - { - id: nextTodoId++, - title: 'Practice telekinesis', - description: 'Try moving the remote without getting up', - completed: false - }, - { - id: nextTodoId++, - title: 'Determine location of El Dorado', - description: 'Might need it for the next vacation', - completed: false - }, - { - id: nextTodoId++, - title: 'Master the art of invisible potato juggling', - description: 'Great party trick', - completed: false - } -] - -export const createTodoItem = () => { - const id = nextTodoId++ - return { - id, - title: `Task ${id}`, - description: `Description for task ${id}`, - completed: false - } -} - -export const pushToTodos = (limit: number) => { - const { length: todoStateLength } = todoState - // const limit = howMany + todoStateLength - for (let i = todoStateLength; i < limit; i++) { - todoState.push(createTodoItem()) - } -} - -pushToTodos(200) - -const alertState = [ - { - id: 0, - message: 'You have an upcoming meeting at 3 PM.', - type: 'reminder', - read: false - }, - { - id: 1, - message: 'New software update available.', - type: 'notification', - read: false - }, - { - id: 3, - message: - 'The plants have been watered, but keep an eye on that shifty cactus.', - type: 'notification', - read: false - }, - { - id: 4, - message: - 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', - type: 'reminder', - read: false - }, - { - id: 5, - message: - 'Expedition to El Dorado is postponed. The treasure map is being updated.', - type: 'notification', - read: false - }, - { - id: 6, - message: - 'Invisible potato juggling championship is tonight. May the best mime win.', - type: 'reminder', - read: false - } -] - -// For nested fields tests -const userState: UserState = { - user: { - id: 0, - details: { - name: 'John Doe', - email: 'john.doe@example.com', - address: { - street: '123 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345', - billing: { - street: '456 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345' - } - }, - preferences: { - newsletter: true, - notifications: { - email: true, - sms: false, - push: { - enabled: true, - frequency: 'daily' - } - } - } - }, - status: 'active', - login: { - lastLogin: '2023-04-30T12:34:56Z', - loginCount: 123 - } - }, - appSettings: { - theme: 'dark', - language: 'en-US' - } -} - -const todoSlice = createSlice({ - name: 'todos', - initialState: todoState, - reducers: { - toggleCompleted: (state, action: PayloadAction) => { - const todo = state.find(todo => todo.id === action.payload) - if (todo) { - todo.completed = !todo.completed - } - }, - - addTodo: (state, action: PayloadAction>) => { - // const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - const newId = nextTodoId++ - state.push({ - ...action.payload, - id: newId, - completed: false - }) - }, - - removeTodo: (state, action: PayloadAction) => { - return state.filter(todo => todo.id !== action.payload) - }, - - updateTodo: (state, action: PayloadAction) => { - const index = state.findIndex(todo => todo.id === action.payload.id) - if (index !== -1) { - state[index] = action.payload - } - }, - - clearCompleted: state => { - return state.filter(todo => !todo.completed) - } - } -}) - -const alertSlice = createSlice({ - name: 'alerts', - initialState: alertState, - reducers: { - markAsRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = true - } - }, - - toggleRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = !alert.read - } - }, - - addAlert: (state, action: PayloadAction>) => { - const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - state.push({ - ...action.payload, - id: newId - }) - }, - - removeAlert: (state, action: PayloadAction) => { - return state.filter(alert => alert.id !== action.payload) - } - } -}) - -const userSlice = createSlice({ - name: 'users', - initialState: userState, - reducers: { - setUserName: (state, action: PayloadAction) => { - state.user.details.name = action.payload - }, - - setUserEmail: (state, action: PayloadAction) => { - state.user.details.email = action.payload - }, - - setAppTheme: (state, action: PayloadAction) => { - state.appSettings.theme = action.payload - }, - - updateUserStatus: (state, action: PayloadAction) => { - state.user.status = action.payload - }, - - updateLoginDetails: ( - state, - action: PayloadAction<{ lastLogin: string; loginCount: number }> - ) => { - state.user.login = { ...state.user.login, ...action.payload } - }, - - updateUserAddress: (state, action: PayloadAction
) => { - state.user.details.address = { - ...state.user.details.address, - ...action.payload - } - }, - - updateBillingAddress: (state, action: PayloadAction) => { - state.user.details.address.billing = { - ...state.user.details.address.billing, - ...action.payload - } - }, - - toggleNewsletterSubscription: state => { - state.user.details.preferences.newsletter = - !state.user.details.preferences.newsletter - }, - - setNotificationPreferences: ( - state, - action: PayloadAction - ) => { - state.user.details.preferences.notifications = { - ...state.user.details.preferences.notifications, - ...action.payload - } - }, - - updateAppLanguage: (state, action: PayloadAction) => { - state.appSettings.language = action.payload - } - } -}) - -const rootReducer = combineReducers({ - [todoSlice.name]: todoSlice.reducer, - [alertSlice.name]: alertSlice.reducer, - [userSlice.name]: userSlice.reducer -}) - -export const setupStore = (preloadedState?: Partial) => { - return configureStore({ reducer: rootReducer, preloadedState }) -} - -export type AppStore = Simplify> - -export type RootState = ReturnType - -export interface LocalTestContext { - store: AppStore - state: RootState -} - -export const { markAsRead, addAlert, removeAlert, toggleRead } = - alertSlice.actions - -export const { - toggleCompleted, - addTodo, - removeTodo, - updateTodo, - clearCompleted -} = todoSlice.actions - -export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions - -// Since Node 16 does not support `structuredClone` -export const deepClone = (object: T): T => - JSON.parse(JSON.stringify(object)) - -export const setFunctionName = (func: AnyFunction, name: string) => { - Object.defineProperty(func, 'name', { value: name }) -} - -export const setFunctionNames = (funcObject: Record) => { - Object.entries(funcObject).forEach(([key, value]) => - setFunctionName(value, key) - ) -} - -const store = setupStore() -const state = store.getState() - -export const localTest = test.extend({ - store, - state -}) - -export const resetSelector = (selector: S) => { - selector.clearCache() - selector.resetRecomputations() - selector.resetDependencyRecomputations() - selector.memoizedResultFunc.clearCache() -} - -export const logRecomputations = (selector: S) => { - console.log( - `${selector.name} result function recalculated:`, - selector.recomputations(), - `time(s)`, - `input selectors recalculated:`, - selector.dependencyRecomputations(), - `time(s)` - ) -} - -export const logSelectorRecomputations = ( - selector: S -) => { - console.log( - `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, - { - resultFunc: selector.recomputations(), - inputSelectors: selector.dependencyRecomputations(), - newResults: - typeof selector.memoizedResultFunc.resultsCount === 'function' - ? selector.memoizedResultFunc.resultsCount() - : undefined - } - ) - // console.log( - // `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, - // `\x1B[33m${selector.recomputations().toLocaleString('en-US')}\x1B[0m`, - // 'time(s)', - // `input selectors recalculated:`, - // `\x1B[33m${selector - // .dependencyRecomputations() - // .toLocaleString('en-US')}\x1B[0m`, - // 'time(s)' - // ) -} - -export const logFunctionInfo = (func: AnyFunction, recomputations: number) => { - console.log( - `\x1B[32m\x1B[1m${func.name}\x1B[0m was called:`, - recomputations, - 'time(s)' - ) -} - -export const safeApply = ( - func: (...args: Params) => Result, - args: Params -) => func.apply(null, args) - -export const countRecomputations = < - Params extends any[], - Result, - AdditionalFields ->( - func: ((...args: Params) => Result) & AdditionalFields -) => { - let recomputations = 0 - const wrapper = (...args: Params) => { - recomputations++ - return safeApply(func, args) - } - return Object.assign( - wrapper, - { - recomputations: () => recomputations, - resetRecomputations: () => (recomputations = 0) - }, - func - ) -} - -export const runMultipleTimes = ( - func: (...args: Params) => any, - times: number, - ...args: Params -) => { - for (let i = 0; i < times; i++) { - safeApply(func, args) - } -} - -export const expensiveComputation = (times = 1_000_000) => { - for (let i = 0; i < times; i++) { - // Do nothing - } -} +import type { PayloadAction } from '@reduxjs/toolkit' +import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' +import { test } from 'vitest' +import type { + AnyFunction, + OutputSelector, + Selector, + SelectorArray, + Simplify +} from '../src/types' + +export interface Todo { + id: number + title: string + description: string + completed: boolean +} + +interface Alert { + id: number + message: string + type: string + read: boolean +} + +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +// For long arrays +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +let nextTodoId = 0 + +// For long arrays +const todoState = [ + { + id: nextTodoId++, + title: 'Buy groceries', + description: 'Milk, bread, eggs, and fruits', + completed: false + }, + { + id: nextTodoId++, + title: 'Schedule dentist appointment', + description: 'Check available slots for next week', + completed: false + }, + { + id: nextTodoId++, + title: 'Convince the cat to get a job', + description: 'Need extra income for cat treats', + completed: false + }, + { + id: nextTodoId++, + title: 'Figure out if plants are plotting world domination', + description: 'That cactus looks suspicious...', + completed: false + }, + { + id: nextTodoId++, + title: 'Practice telekinesis', + description: 'Try moving the remote without getting up', + completed: false + }, + { + id: nextTodoId++, + title: 'Determine location of El Dorado', + description: 'Might need it for the next vacation', + completed: false + }, + { + id: nextTodoId++, + title: 'Master the art of invisible potato juggling', + description: 'Great party trick', + completed: false + } +] + +export const createTodoItem = () => { + const id = nextTodoId++ + return { + id, + title: `Task ${id}`, + description: `Description for task ${id}`, + completed: false + } +} + +export const pushToTodos = (limit: number) => { + const { length: todoStateLength } = todoState + // const limit = howMany + todoStateLength + for (let i = todoStateLength; i < limit; i++) { + todoState.push(createTodoItem()) + } +} + +pushToTodos(200) + +const alertState = [ + { + id: 0, + message: 'You have an upcoming meeting at 3 PM.', + type: 'reminder', + read: false + }, + { + id: 1, + message: 'New software update available.', + type: 'notification', + read: false + }, + { + id: 3, + message: + 'The plants have been watered, but keep an eye on that shifty cactus.', + type: 'notification', + read: false + }, + { + id: 4, + message: + 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', + type: 'reminder', + read: false + }, + { + id: 5, + message: + 'Expedition to El Dorado is postponed. The treasure map is being updated.', + type: 'notification', + read: false + }, + { + id: 6, + message: + 'Invisible potato juggling championship is tonight. May the best mime win.', + type: 'reminder', + read: false + } +] + +// For nested fields tests +const userState: UserState = { + user: { + id: 0, + details: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + street: '123 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345', + billing: { + street: '456 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345' + } + }, + preferences: { + newsletter: true, + notifications: { + email: true, + sms: false, + push: { + enabled: true, + frequency: 'daily' + } + } + } + }, + status: 'active', + login: { + lastLogin: '2023-04-30T12:34:56Z', + loginCount: 123 + } + }, + appSettings: { + theme: 'dark', + language: 'en-US' + } +} + +const todoSlice = createSlice({ + name: 'todos', + initialState: todoState, + reducers: { + toggleCompleted: (state, action: PayloadAction) => { + const todo = state.find(todo => todo.id === action.payload) + if (todo) { + todo.completed = !todo.completed + } + }, + + addTodo: (state, action: PayloadAction>) => { + // const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + const newId = nextTodoId++ + state.push({ + ...action.payload, + id: newId, + completed: false + }) + }, + + removeTodo: (state, action: PayloadAction) => { + return state.filter(todo => todo.id !== action.payload) + }, + + updateTodo: (state, action: PayloadAction) => { + const index = state.findIndex(todo => todo.id === action.payload.id) + if (index !== -1) { + state[index] = action.payload + } + }, + + clearCompleted: state => { + return state.filter(todo => !todo.completed) + } + } +}) + +const alertSlice = createSlice({ + name: 'alerts', + initialState: alertState, + reducers: { + markAsRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = true + } + }, + + toggleRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = !alert.read + } + }, + + addAlert: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId + }) + }, + + removeAlert: (state, action: PayloadAction) => { + return state.filter(alert => alert.id !== action.payload) + } + } +}) + +const userSlice = createSlice({ + name: 'users', + initialState: userState, + reducers: { + setUserName: (state, action: PayloadAction) => { + state.user.details.name = action.payload + }, + + setUserEmail: (state, action: PayloadAction) => { + state.user.details.email = action.payload + }, + + setAppTheme: (state, action: PayloadAction) => { + state.appSettings.theme = action.payload + }, + + updateUserStatus: (state, action: PayloadAction) => { + state.user.status = action.payload + }, + + updateLoginDetails: ( + state, + action: PayloadAction<{ lastLogin: string; loginCount: number }> + ) => { + state.user.login = { ...state.user.login, ...action.payload } + }, + + updateUserAddress: (state, action: PayloadAction
) => { + state.user.details.address = { + ...state.user.details.address, + ...action.payload + } + }, + + updateBillingAddress: (state, action: PayloadAction) => { + state.user.details.address.billing = { + ...state.user.details.address.billing, + ...action.payload + } + }, + + toggleNewsletterSubscription: state => { + state.user.details.preferences.newsletter = + !state.user.details.preferences.newsletter + }, + + setNotificationPreferences: ( + state, + action: PayloadAction + ) => { + state.user.details.preferences.notifications = { + ...state.user.details.preferences.notifications, + ...action.payload + } + }, + + updateAppLanguage: (state, action: PayloadAction) => { + state.appSettings.language = action.payload + } + } +}) + +const rootReducer = combineReducers({ + [todoSlice.name]: todoSlice.reducer, + [alertSlice.name]: alertSlice.reducer, + [userSlice.name]: userSlice.reducer +}) + +export const setupStore = (preloadedState?: Partial) => { + return configureStore({ reducer: rootReducer, preloadedState }) +} + +export type AppStore = Simplify> + +export type RootState = ReturnType + +export interface LocalTestContext { + store: AppStore + state: RootState +} + +export const { markAsRead, addAlert, removeAlert, toggleRead } = + alertSlice.actions + +export const { + toggleCompleted, + addTodo, + removeTodo, + updateTodo, + clearCompleted +} = todoSlice.actions + +export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions + +// Since Node 16 does not support `structuredClone` +export const deepClone = (object: T): T => + JSON.parse(JSON.stringify(object)) + +export const setFunctionName = (func: AnyFunction, name: string) => { + Object.defineProperty(func, 'name', { value: name }) +} + +export const setFunctionNames = (funcObject: Record) => { + Object.entries(funcObject).forEach(([key, value]) => + setFunctionName(value, key) + ) +} + +const store = setupStore() +const state = store.getState() + +export const localTest = test.extend({ + store, + state +}) + +export const resetSelector = (selector: S) => { + selector.clearCache() + selector.resetRecomputations() + selector.resetDependencyRecomputations() + selector.memoizedResultFunc.clearCache() +} + +export const logRecomputations = (selector: S) => { + console.log( + `${selector.name} result function recalculated:`, + selector.recomputations(), + `time(s)`, + `input selectors recalculated:`, + selector.dependencyRecomputations(), + `time(s)` + ) +} + +export const logSelectorRecomputations = < + S extends OutputSelector +>( + selector: S +) => { + console.log( + `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, + { + resultFunc: selector.recomputations(), + inputSelectors: selector.dependencyRecomputations(), + newResults: + typeof selector.memoizedResultFunc.resultsCount === 'function' + ? selector.memoizedResultFunc.resultsCount() + : undefined + } + ) + // console.log( + // `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, + // `\x1B[33m${selector.recomputations().toLocaleString('en-US')}\x1B[0m`, + // 'time(s)', + // `input selectors recalculated:`, + // `\x1B[33m${selector + // .dependencyRecomputations() + // .toLocaleString('en-US')}\x1B[0m`, + // 'time(s)' + // ) +} + +export const logFunctionInfo = (func: AnyFunction, recomputations: number) => { + console.log( + `\x1B[32m\x1B[1m${func.name}\x1B[0m was called:`, + recomputations, + 'time(s)' + ) +} + +export const safeApply = ( + func: (...args: Params) => Result, + args: Params +) => func.apply(null, args) + +export const countRecomputations = < + Params extends any[], + Result, + AdditionalFields +>( + func: ((...args: Params) => Result) & AdditionalFields +) => { + let recomputations = 0 + const wrapper = (...args: Params) => { + recomputations++ + return safeApply(func, args) + } + return Object.assign( + wrapper, + { + recomputations: () => recomputations, + resetRecomputations: () => (recomputations = 0) + }, + func + ) +} + +export const runMultipleTimes = ( + func: (...args: Params) => any, + times: number, + ...args: Params +) => { + for (let i = 0; i < times; i++) { + safeApply(func, args) + } +} + +export const expensiveComputation = (times = 1_000_000) => { + for (let i = 0; i < times; i++) { + // Do nothing + } +}