Skip to content

Commit 3786506

Browse files
authored
Merge pull request #699 from aryaemami59/fix-weakMapMemoize-resultEqualityCheck
Fix `resultEqualityCheck` behavior in `weakMapMemoize`
2 parents b856266 + cc884cc commit 3786506

File tree

7 files changed

+746
-49
lines changed

7 files changed

+746
-49
lines changed

src/weakMapMemoize.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -227,25 +227,29 @@ export function weakMapMemoize<Func extends AnyFunction>(
227227
// Allow errors to propagate
228228
result = func.apply(null, arguments as unknown as any[])
229229
resultsCount++
230-
}
231230

232-
terminatedNode.s = TERMINATED
231+
if (resultEqualityCheck) {
232+
const lastResultValue = lastResult?.deref?.() ?? lastResult
233233

234-
if (resultEqualityCheck) {
235-
const lastResultValue = lastResult?.deref?.() ?? lastResult
236-
if (
237-
lastResultValue != null &&
238-
resultEqualityCheck(lastResultValue as ReturnType<Func>, result)
239-
) {
240-
result = lastResultValue
241-
resultsCount !== 0 && resultsCount--
242-
}
234+
if (
235+
lastResultValue != null &&
236+
resultEqualityCheck(lastResultValue as ReturnType<Func>, result)
237+
) {
238+
result = lastResultValue
239+
240+
resultsCount !== 0 && resultsCount--
241+
}
242+
243+
const needsWeakRef =
244+
(typeof result === 'object' && result !== null) ||
245+
typeof result === 'function'
243246

244-
const needsWeakRef =
245-
(typeof result === 'object' && result !== null) ||
246-
typeof result === 'function'
247-
lastResult = needsWeakRef ? new Ref(result) : result
247+
lastResult = needsWeakRef ? new Ref(result) : result
248+
}
248249
}
250+
251+
terminatedNode.s = TERMINATED
252+
249253
terminatedNode.v = result
250254
return result
251255
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import type { AnyFunction } from '@internal/types'
2+
import type { OutputSelector, Selector } from 'reselect'
3+
import {
4+
createSelector,
5+
lruMemoize,
6+
referenceEqualityCheck,
7+
weakMapMemoize
8+
} from 'reselect'
9+
import type { Options } from 'tinybench'
10+
import { bench } from 'vitest'
11+
import {
12+
logSelectorRecomputations,
13+
setFunctionNames,
14+
setupStore,
15+
toggleCompleted,
16+
type RootState
17+
} from '../testUtils'
18+
19+
describe('memoize functions performance with resultEqualityCheck set to referenceEqualityCheck vs. without resultEqualityCheck', () => {
20+
describe('comparing selectors created with createSelector', () => {
21+
const store = setupStore()
22+
23+
const arrayOfNumbers = Array.from({ length: 1_000 }, (num, index) => index)
24+
25+
const commonOptions: Options = {
26+
iterations: 10_000,
27+
time: 0
28+
}
29+
30+
const runSelector = <S extends Selector>(selector: S) => {
31+
arrayOfNumbers.forEach(num => {
32+
selector(store.getState())
33+
})
34+
}
35+
36+
const createAppSelector = createSelector.withTypes<RootState>()
37+
38+
const selectTodoIdsWeakMap = createAppSelector(
39+
[state => state.todos],
40+
todos => todos.map(({ id }) => id)
41+
)
42+
43+
const selectTodoIdsWeakMapWithResultEqualityCheck = createAppSelector(
44+
[state => state.todos],
45+
todos => todos.map(({ id }) => id),
46+
{
47+
memoizeOptions: { resultEqualityCheck: referenceEqualityCheck },
48+
argsMemoizeOptions: { resultEqualityCheck: referenceEqualityCheck }
49+
}
50+
)
51+
52+
const selectTodoIdsLru = createAppSelector(
53+
[state => state.todos],
54+
todos => todos.map(({ id }) => id),
55+
{ memoize: lruMemoize, argsMemoize: lruMemoize }
56+
)
57+
58+
const selectTodoIdsLruWithResultEqualityCheck = createAppSelector(
59+
[state => state.todos],
60+
todos => todos.map(({ id }) => id),
61+
{
62+
memoize: lruMemoize,
63+
memoizeOptions: { resultEqualityCheck: referenceEqualityCheck },
64+
argsMemoize: lruMemoize,
65+
argsMemoizeOptions: { resultEqualityCheck: referenceEqualityCheck }
66+
}
67+
)
68+
69+
const selectors = {
70+
selectTodoIdsWeakMap,
71+
selectTodoIdsWeakMapWithResultEqualityCheck,
72+
selectTodoIdsLru,
73+
selectTodoIdsLruWithResultEqualityCheck
74+
}
75+
76+
setFunctionNames(selectors)
77+
78+
const createOptions = <S extends OutputSelector>(selector: S) => {
79+
const options: Options = {
80+
setup: (task, mode) => {
81+
if (mode === 'warmup') return
82+
83+
task.opts = {
84+
beforeEach: () => {
85+
store.dispatch(toggleCompleted(1))
86+
},
87+
88+
afterAll: () => {
89+
logSelectorRecomputations(selector)
90+
}
91+
}
92+
}
93+
}
94+
return { ...commonOptions, ...options }
95+
}
96+
97+
Object.values(selectors).forEach(selector => {
98+
bench(
99+
selector,
100+
() => {
101+
runSelector(selector)
102+
},
103+
createOptions(selector)
104+
)
105+
})
106+
})
107+
108+
describe('comparing selectors created with memoize functions', () => {
109+
const store = setupStore()
110+
111+
const arrayOfNumbers = Array.from(
112+
{ length: 100_000 },
113+
(num, index) => index
114+
)
115+
116+
const commonOptions: Options = {
117+
iterations: 1000,
118+
time: 0
119+
}
120+
121+
const runSelector = <S extends Selector>(selector: S) => {
122+
arrayOfNumbers.forEach(num => {
123+
selector(store.getState())
124+
})
125+
}
126+
127+
const selectTodoIdsWeakMap = weakMapMemoize((state: RootState) =>
128+
state.todos.map(({ id }) => id)
129+
)
130+
131+
const selectTodoIdsWeakMapWithResultEqualityCheck = weakMapMemoize(
132+
(state: RootState) => state.todos.map(({ id }) => id),
133+
{ resultEqualityCheck: referenceEqualityCheck }
134+
)
135+
136+
const selectTodoIdsLru = lruMemoize((state: RootState) =>
137+
state.todos.map(({ id }) => id)
138+
)
139+
140+
const selectTodoIdsLruWithResultEqualityCheck = lruMemoize(
141+
(state: RootState) => state.todos.map(({ id }) => id),
142+
{ resultEqualityCheck: referenceEqualityCheck }
143+
)
144+
145+
const memoizedFunctions = {
146+
selectTodoIdsWeakMap,
147+
selectTodoIdsWeakMapWithResultEqualityCheck,
148+
selectTodoIdsLru,
149+
selectTodoIdsLruWithResultEqualityCheck
150+
}
151+
152+
setFunctionNames(memoizedFunctions)
153+
154+
const createOptions = <
155+
Func extends AnyFunction & { resultsCount: () => number }
156+
>(
157+
memoizedFunction: Func
158+
) => {
159+
const options: Options = {
160+
setup: (task, mode) => {
161+
if (mode === 'warmup') return
162+
163+
task.opts = {
164+
beforeEach: () => {
165+
store.dispatch(toggleCompleted(1))
166+
},
167+
168+
afterAll: () => {
169+
console.log(
170+
memoizedFunction.name,
171+
memoizedFunction.resultsCount()
172+
)
173+
}
174+
}
175+
}
176+
}
177+
return { ...commonOptions, ...options }
178+
}
179+
180+
Object.values(memoizedFunctions).forEach(memoizedFunction => {
181+
bench(
182+
memoizedFunction,
183+
() => {
184+
runSelector(memoizedFunction)
185+
},
186+
createOptions(memoizedFunction)
187+
)
188+
})
189+
})
190+
})

test/computationComparisons.spec.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,18 +304,18 @@ describe('resultEqualityCheck in weakMapMemoize', () => {
304304
expect(memoized.resultsCount()).toBe(5)
305305

306306
expect(memoizedShallow(state)).toBe(memoizedShallow(state))
307-
expect(memoizedShallow.resultsCount()).toBe(0)
307+
expect(memoizedShallow.resultsCount()).toBe(1)
308308
expect(memoizedShallow({ ...state })).toBe(memoizedShallow(state))
309-
expect(memoizedShallow.resultsCount()).toBe(0)
309+
expect(memoizedShallow.resultsCount()).toBe(1)
310310
expect(memoizedShallow({ ...state })).toBe(memoizedShallow(state))
311311
// We spread the state to force the function to re-run but the
312312
// result maintains the same reference because of `resultEqualityCheck`.
313313
const first = memoizedShallow({ ...state })
314-
expect(memoizedShallow.resultsCount()).toBe(0)
314+
expect(memoizedShallow.resultsCount()).toBe(1)
315315
memoizedShallow({ ...state })
316-
expect(memoizedShallow.resultsCount()).toBe(0)
316+
expect(memoizedShallow.resultsCount()).toBe(1)
317317
const second = memoizedShallow({ ...state })
318-
expect(memoizedShallow.resultsCount()).toBe(0)
318+
expect(memoizedShallow.resultsCount()).toBe(1)
319319
expect(first).toBe(second)
320320
})
321321
})

test/inputStabilityCheck.spec.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { shallowEqual } from 'react-redux'
2-
import { createSelector, lruMemoize, setGlobalDevModeChecks } from 'reselect'
2+
import {
3+
createSelector,
4+
lruMemoize,
5+
referenceEqualityCheck,
6+
setGlobalDevModeChecks
7+
} from 'reselect'
8+
import type { RootState } from './testUtils'
9+
import { localTest } from './testUtils'
310

411
describe('inputStabilityCheck', () => {
512
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
@@ -164,3 +171,105 @@ describe('inputStabilityCheck', () => {
164171
expect(consoleSpy).not.toHaveBeenCalled()
165172
})
166173
})
174+
175+
describe('the effects of inputStabilityCheck with resultEqualityCheck', () => {
176+
const createAppSelector = createSelector.withTypes<RootState>()
177+
178+
const resultEqualityCheck = vi
179+
.fn(referenceEqualityCheck)
180+
.mockName('resultEqualityCheck')
181+
182+
afterEach(() => {
183+
resultEqualityCheck.mockClear()
184+
})
185+
186+
localTest(
187+
'resultEqualityCheck should not be called with empty objects when inputStabilityCheck is set to once and input selectors are stable',
188+
({ store }) => {
189+
const selectTodoIds = createAppSelector(
190+
[state => state.todos],
191+
todos => todos.map(({ id }) => id),
192+
{
193+
memoizeOptions: { resultEqualityCheck },
194+
devModeChecks: { inputStabilityCheck: 'once' }
195+
}
196+
)
197+
198+
const firstResult = selectTodoIds(store.getState())
199+
200+
expect(resultEqualityCheck).not.toHaveBeenCalled()
201+
202+
const secondResult = selectTodoIds(store.getState())
203+
204+
expect(firstResult).toBe(secondResult)
205+
206+
expect(resultEqualityCheck).not.toHaveBeenCalled()
207+
208+
const thirdResult = selectTodoIds(store.getState())
209+
210+
expect(secondResult).toBe(thirdResult)
211+
212+
expect(resultEqualityCheck).not.toHaveBeenCalled()
213+
}
214+
)
215+
216+
localTest(
217+
'resultEqualityCheck should not be called with empty objects when inputStabilityCheck is set to always and input selectors are stable',
218+
({ store }) => {
219+
const selectTodoIds = createAppSelector(
220+
[state => state.todos],
221+
todos => todos.map(({ id }) => id),
222+
{
223+
memoizeOptions: { resultEqualityCheck },
224+
devModeChecks: { inputStabilityCheck: 'always' }
225+
}
226+
)
227+
228+
const firstResult = selectTodoIds(store.getState())
229+
230+
expect(resultEqualityCheck).not.toHaveBeenCalled()
231+
232+
const secondResult = selectTodoIds(store.getState())
233+
234+
expect(firstResult).toBe(secondResult)
235+
236+
expect(resultEqualityCheck).not.toHaveBeenCalled()
237+
238+
const thirdResult = selectTodoIds(store.getState())
239+
240+
expect(secondResult).toBe(thirdResult)
241+
242+
expect(resultEqualityCheck).not.toHaveBeenCalled()
243+
}
244+
)
245+
246+
localTest(
247+
'resultEqualityCheck should not be called with empty objects when inputStabilityCheck is set to never and input selectors are unstable',
248+
({ store }) => {
249+
const selectTodoIds = createAppSelector(
250+
[state => [...state.todos]],
251+
todos => todos.map(({ id }) => id),
252+
{
253+
memoizeOptions: { resultEqualityCheck },
254+
devModeChecks: { inputStabilityCheck: 'never' }
255+
}
256+
)
257+
258+
const firstResult = selectTodoIds(store.getState())
259+
260+
expect(resultEqualityCheck).not.toHaveBeenCalled()
261+
262+
const secondResult = selectTodoIds(store.getState())
263+
264+
expect(firstResult).toBe(secondResult)
265+
266+
expect(resultEqualityCheck).not.toHaveBeenCalled()
267+
268+
const thirdResult = selectTodoIds(store.getState())
269+
270+
expect(secondResult).toBe(thirdResult)
271+
272+
expect(resultEqualityCheck).not.toHaveBeenCalled()
273+
}
274+
)
275+
})

0 commit comments

Comments
 (0)