Skip to content

Commit 3401e92

Browse files
authored
useMemoCache implementation (#25143)
* useMemoCache impl * test for multiple calls in a component (from custom hook) * Use array of arrays for multiple calls; use alternate/local as the backup * code cleanup * fix internal test * oops we do not support nullable property access * Simplify implementation, still have questions on some of the PR feedback though * Gate all code based on the feature flag * refactor to use updateQueue * address feedback * Try to eliminate size increase in prod bundle * update to retrigger ci
1 parent 0556bab commit 3401e92

File tree

4 files changed

+528
-12
lines changed

4 files changed

+528
-12
lines changed

packages/react-reconciler/src/ReactFiberHooks.new.js

+78-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import type {
1616
Usable,
1717
Thenable,
1818
} from 'shared/ReactTypes';
19-
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
19+
import type {
20+
Fiber,
21+
Dispatcher,
22+
HookType,
23+
MemoCache,
24+
} from './ReactInternalTypes';
2025
import type {Lanes, Lane} from './ReactFiberLane.new';
2126
import type {HookFlags} from './ReactHookEffectTags';
2227
import type {FiberRoot} from './ReactInternalTypes';
@@ -177,6 +182,8 @@ type StoreConsistencyCheck<T> = {
177182
export type FunctionComponentUpdateQueue = {
178183
lastEffect: Effect | null,
179184
stores: Array<StoreConsistencyCheck<any>> | null,
185+
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
186+
memoCache?: MemoCache | null,
180187
};
181188

182189
type BasicStateAction<S> = (S => S) | S;
@@ -710,10 +717,23 @@ function updateWorkInProgressHook(): Hook {
710717
return workInProgressHook;
711718
}
712719

713-
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
714-
return {
715-
lastEffect: null,
716-
stores: null,
720+
// NOTE: defining two versions of this function to avoid size impact when this feature is disabled.
721+
// Previously this function was inlined, the additional `memoCache` property makes it not inlined.
722+
let createFunctionComponentUpdateQueue: () => FunctionComponentUpdateQueue;
723+
if (enableUseMemoCacheHook) {
724+
createFunctionComponentUpdateQueue = () => {
725+
return {
726+
lastEffect: null,
727+
stores: null,
728+
memoCache: null,
729+
};
730+
};
731+
} else {
732+
createFunctionComponentUpdateQueue = () => {
733+
return {
734+
lastEffect: null,
735+
stores: null,
736+
};
717737
};
718738
}
719739

@@ -787,7 +807,59 @@ function use<T>(usable: Usable<T>): T {
787807
}
788808

789809
function useMemoCache(size: number): Array<any> {
790-
throw new Error('Not implemented.');
810+
let memoCache = null;
811+
// Fast-path, load memo cache from wip fiber if already prepared
812+
let updateQueue: FunctionComponentUpdateQueue | null = (currentlyRenderingFiber.updateQueue: any);
813+
if (updateQueue !== null) {
814+
memoCache = updateQueue.memoCache;
815+
}
816+
// Otherwise clone from the current fiber
817+
// TODO: not sure how to access the current fiber here other than going through
818+
// currentlyRenderingFiber.alternate
819+
if (memoCache == null) {
820+
const current: Fiber | null = currentlyRenderingFiber.alternate;
821+
if (current !== null) {
822+
const currentUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
823+
if (currentUpdateQueue !== null) {
824+
const currentMemoCache: ?MemoCache = currentUpdateQueue.memoCache;
825+
if (currentMemoCache != null) {
826+
memoCache = {
827+
data: currentMemoCache.data.map(array => array.slice()),
828+
index: 0,
829+
};
830+
}
831+
}
832+
}
833+
}
834+
// Finally fall back to allocating a fresh instance of the cache
835+
if (memoCache == null) {
836+
memoCache = {
837+
data: [],
838+
index: 0,
839+
};
840+
}
841+
if (updateQueue === null) {
842+
updateQueue = createFunctionComponentUpdateQueue();
843+
currentlyRenderingFiber.updateQueue = updateQueue;
844+
}
845+
updateQueue.memoCache = memoCache;
846+
847+
let data = memoCache.data[memoCache.index];
848+
if (data === undefined) {
849+
data = memoCache.data[memoCache.index] = new Array(size);
850+
} else if (data.length !== size) {
851+
// TODO: consider warning or throwing here
852+
if (__DEV__) {
853+
console.error(
854+
'Expected a constant size argument for each invocation of useMemoCache. ' +
855+
'The previous cache was allocated with size %s but size %s was requested.',
856+
data.length,
857+
size,
858+
);
859+
}
860+
}
861+
memoCache.index++;
862+
return data;
791863
}
792864

793865
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {

packages/react-reconciler/src/ReactFiberHooks.old.js

+78-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import type {
1616
Usable,
1717
Thenable,
1818
} from 'shared/ReactTypes';
19-
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
19+
import type {
20+
Fiber,
21+
Dispatcher,
22+
HookType,
23+
MemoCache,
24+
} from './ReactInternalTypes';
2025
import type {Lanes, Lane} from './ReactFiberLane.old';
2126
import type {HookFlags} from './ReactHookEffectTags';
2227
import type {FiberRoot} from './ReactInternalTypes';
@@ -177,6 +182,8 @@ type StoreConsistencyCheck<T> = {
177182
export type FunctionComponentUpdateQueue = {
178183
lastEffect: Effect | null,
179184
stores: Array<StoreConsistencyCheck<any>> | null,
185+
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
186+
memoCache?: MemoCache | null,
180187
};
181188

182189
type BasicStateAction<S> = (S => S) | S;
@@ -710,10 +717,23 @@ function updateWorkInProgressHook(): Hook {
710717
return workInProgressHook;
711718
}
712719

713-
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
714-
return {
715-
lastEffect: null,
716-
stores: null,
720+
// NOTE: defining two versions of this function to avoid size impact when this feature is disabled.
721+
// Previously this function was inlined, the additional `memoCache` property makes it not inlined.
722+
let createFunctionComponentUpdateQueue: () => FunctionComponentUpdateQueue;
723+
if (enableUseMemoCacheHook) {
724+
createFunctionComponentUpdateQueue = () => {
725+
return {
726+
lastEffect: null,
727+
stores: null,
728+
memoCache: null,
729+
};
730+
};
731+
} else {
732+
createFunctionComponentUpdateQueue = () => {
733+
return {
734+
lastEffect: null,
735+
stores: null,
736+
};
717737
};
718738
}
719739

@@ -787,7 +807,59 @@ function use<T>(usable: Usable<T>): T {
787807
}
788808

789809
function useMemoCache(size: number): Array<any> {
790-
throw new Error('Not implemented.');
810+
let memoCache = null;
811+
// Fast-path, load memo cache from wip fiber if already prepared
812+
let updateQueue: FunctionComponentUpdateQueue | null = (currentlyRenderingFiber.updateQueue: any);
813+
if (updateQueue !== null) {
814+
memoCache = updateQueue.memoCache;
815+
}
816+
// Otherwise clone from the current fiber
817+
// TODO: not sure how to access the current fiber here other than going through
818+
// currentlyRenderingFiber.alternate
819+
if (memoCache == null) {
820+
const current: Fiber | null = currentlyRenderingFiber.alternate;
821+
if (current !== null) {
822+
const currentUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
823+
if (currentUpdateQueue !== null) {
824+
const currentMemoCache: ?MemoCache = currentUpdateQueue.memoCache;
825+
if (currentMemoCache != null) {
826+
memoCache = {
827+
data: currentMemoCache.data.map(array => array.slice()),
828+
index: 0,
829+
};
830+
}
831+
}
832+
}
833+
}
834+
// Finally fall back to allocating a fresh instance of the cache
835+
if (memoCache == null) {
836+
memoCache = {
837+
data: [],
838+
index: 0,
839+
};
840+
}
841+
if (updateQueue === null) {
842+
updateQueue = createFunctionComponentUpdateQueue();
843+
currentlyRenderingFiber.updateQueue = updateQueue;
844+
}
845+
updateQueue.memoCache = memoCache;
846+
847+
let data = memoCache.data[memoCache.index];
848+
if (data === undefined) {
849+
data = memoCache.data[memoCache.index] = new Array(size);
850+
} else if (data.length !== size) {
851+
// TODO: consider warning or throwing here
852+
if (__DEV__) {
853+
console.error(
854+
'Expected a constant size argument for each invocation of useMemoCache. ' +
855+
'The previous cache was allocated with size %s but size %s was requested.',
856+
data.length,
857+
size,
858+
);
859+
}
860+
}
861+
memoCache.index++;
862+
return data;
791863
}
792864

793865
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {

packages/react-reconciler/src/ReactInternalTypes.js

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export type Dependencies = {
6868
...
6969
};
7070

71+
export type MemoCache = {
72+
data: Array<Array<any>>,
73+
index: number,
74+
};
75+
7176
// A Fiber is work on a Component that needs to be done or was done. There can
7277
// be more than one per component.
7378
export type Fiber = {

0 commit comments

Comments
 (0)