Skip to content

Commit 6fae379

Browse files
committed
Implement cache cleanup
1 parent bfb4022 commit 6fae379

29 files changed

+1458
-203
lines changed

packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Object {
7373
},
7474
},
7575
"duration": 15,
76-
"effectDuration": null,
76+
"effectDuration": 0,
7777
"fiberActualDurations": Map {
7878
1 => 15,
7979
2 => 15,
@@ -86,7 +86,7 @@ Object {
8686
3 => 3,
8787
4 => 2,
8888
},
89-
"passiveEffectDuration": null,
89+
"passiveEffectDuration": 0,
9090
"priorityLevel": "Immediate",
9191
"timestamp": 15,
9292
"updaters": Array [

packages/react-dom/src/server/ReactPartialRendererHooks.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ export function resetHooksState(): void {
216216
workInProgressHook = null;
217217
}
218218

219+
function getCacheSignal() {
220+
throw new Error('Not implemented.');
221+
}
222+
219223
function getCacheForType<T>(resourceType: () => T): T {
220224
throw new Error('Not implemented.');
221225
}
@@ -551,6 +555,7 @@ export const Dispatcher: DispatcherType = {
551555
};
552556

553557
if (enableCache) {
558+
Dispatcher.getCacheSignal = getCacheSignal;
554559
Dispatcher.getCacheForType = getCacheForType;
555560
Dispatcher.useCacheRefresh = useCacheRefresh;
556561
}

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

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
1818
import {isPrimaryRenderer} from './ReactFiberHostConfig';
1919
import {createCursor, push, pop} from './ReactFiberStack.new';
2020
import {pushProvider, popProvider} from './ReactFiberNewContext.new';
21+
import * as Scheduler from 'scheduler';
2122

22-
export type Cache = Map<() => mixed, mixed>;
23+
export type Cache = {|
24+
controller: AbortController,
25+
data: Map<() => mixed, mixed>,
26+
refCount: number,
27+
|};
2328

2429
export type CacheComponentState = {|
2530
+parent: Cache,
@@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
3136
+pool: Cache,
3237
|};
3338

39+
// Intentionally not named imports because Rollup would
40+
// use dynamic dispatch for CommonJS interop named imports.
41+
const {
42+
unstable_scheduleCallback: scheduleCallback,
43+
unstable_NormalPriority: NormalPriority,
44+
} = Scheduler;
45+
3446
export const CacheContext: ReactContext<Cache> = enableCache
3547
? {
3648
$$typeof: REACT_CONTEXT_TYPE,
@@ -57,6 +69,49 @@ let pooledCache: Cache | null = null;
5769
// cache from the render that suspended.
5870
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
5971

72+
// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
73+
// for retaining the cache once it is in use (retainCache), and releasing the cache
74+
// once it is no longer needed (releaseCache).
75+
export function createCache(): Cache {
76+
const cache: Cache = {
77+
controller: new AbortController(),
78+
data: new Map(),
79+
refCount: 0,
80+
};
81+
82+
return cache;
83+
}
84+
85+
export function retainCache(cache: Cache) {
86+
if (__DEV__) {
87+
if (cache.controller.signal.aborted) {
88+
console.warn(
89+
'A cache instance was retained after it was already freed. ' +
90+
'This likely indicates a bug in React.',
91+
);
92+
}
93+
}
94+
cache.refCount++;
95+
}
96+
97+
// Cleanup a cache instance, potentially freeing it if there are no more references
98+
export function releaseCache(cache: Cache) {
99+
cache.refCount--;
100+
if (__DEV__) {
101+
if (cache.refCount < 0) {
102+
console.warn(
103+
'A cache instance was released after it was already freed. ' +
104+
'This likely indicates a bug in React.',
105+
);
106+
}
107+
}
108+
if (cache.refCount === 0) {
109+
scheduleCallback(NormalPriority, () => {
110+
cache.controller.abort();
111+
});
112+
}
113+
}
114+
60115
export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
61116
if (!enableCache) {
62117
return;
@@ -78,8 +133,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
78133
if (pooledCache !== null) {
79134
return pooledCache;
80135
}
81-
// Create a fresh cache.
82-
pooledCache = new Map();
136+
// Create a fresh cache. The pooled cache must be owned - it is freed
137+
// in releaseRootPooledCache() - but the cache instance handed out
138+
// is retained/released in the commit phase of the component that
139+
// references is (ie the host root, cache boundary, suspense component)
140+
// Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
141+
// whereas the return value of this function is a &Arc<Cache> (borrowed).
142+
pooledCache = createCache();
143+
retainCache(pooledCache);
83144
return pooledCache;
84145
}
85146

@@ -91,7 +152,13 @@ export function pushRootCachePool(root: FiberRoot) {
91152
// from `root.pooledCache`. If it's currently `null`, we will lazily
92153
// initialize it the first type it's requested. However, we only mutate
93154
// the root itself during the complete/unwind phase of the HostRoot.
94-
pooledCache = root.pooledCache;
155+
const rootCache = root.pooledCache;
156+
if (rootCache != null) {
157+
pooledCache = rootCache;
158+
root.pooledCache = null;
159+
} else {
160+
pooledCache = null;
161+
}
95162
}
96163

97164
export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
@@ -157,7 +224,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
157224
if (!enableCache) {
158225
return null;
159226
}
160-
161227
// We check the cache on the stack first, since that's the one any new Caches
162228
// would have accessed.
163229
let pool = pooledCache;

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

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
1818
import {isPrimaryRenderer} from './ReactFiberHostConfig';
1919
import {createCursor, push, pop} from './ReactFiberStack.old';
2020
import {pushProvider, popProvider} from './ReactFiberNewContext.old';
21+
import * as Scheduler from 'scheduler';
2122

22-
export type Cache = Map<() => mixed, mixed>;
23+
export type Cache = {|
24+
controller: AbortController,
25+
data: Map<() => mixed, mixed>,
26+
refCount: number,
27+
|};
2328

2429
export type CacheComponentState = {|
2530
+parent: Cache,
@@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
3136
+pool: Cache,
3237
|};
3338

39+
// Intentionally not named imports because Rollup would
40+
// use dynamic dispatch for CommonJS interop named imports.
41+
const {
42+
unstable_scheduleCallback: scheduleCallback,
43+
unstable_NormalPriority: NormalPriority,
44+
} = Scheduler;
45+
3446
export const CacheContext: ReactContext<Cache> = enableCache
3547
? {
3648
$$typeof: REACT_CONTEXT_TYPE,
@@ -57,6 +69,49 @@ let pooledCache: Cache | null = null;
5769
// cache from the render that suspended.
5870
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
5971

72+
// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
73+
// for retaining the cache once it is in use (retainCache), and releasing the cache
74+
// once it is no longer needed (releaseCache).
75+
export function createCache(): Cache {
76+
const cache: Cache = {
77+
controller: new AbortController(),
78+
data: new Map(),
79+
refCount: 0,
80+
};
81+
82+
return cache;
83+
}
84+
85+
export function retainCache(cache: Cache) {
86+
if (__DEV__) {
87+
if (cache.controller.signal.aborted) {
88+
console.warn(
89+
'A cache instance was retained after it was already freed. ' +
90+
'This likely indicates a bug in React.',
91+
);
92+
}
93+
}
94+
cache.refCount++;
95+
}
96+
97+
// Cleanup a cache instance, potentially freeing it if there are no more references
98+
export function releaseCache(cache: Cache) {
99+
cache.refCount--;
100+
if (__DEV__) {
101+
if (cache.refCount < 0) {
102+
console.warn(
103+
'A cache instance was released after it was already freed. ' +
104+
'This likely indicates a bug in React.',
105+
);
106+
}
107+
}
108+
if (cache.refCount === 0) {
109+
scheduleCallback(NormalPriority, () => {
110+
cache.controller.abort();
111+
});
112+
}
113+
}
114+
60115
export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
61116
if (!enableCache) {
62117
return;
@@ -78,8 +133,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
78133
if (pooledCache !== null) {
79134
return pooledCache;
80135
}
81-
// Create a fresh cache.
82-
pooledCache = new Map();
136+
// Create a fresh cache. The pooled cache must be owned - it is freed
137+
// in releaseRootPooledCache() - but the cache instance handed out
138+
// is retained/released in the commit phase of the component that
139+
// references is (ie the host root, cache boundary, suspense component)
140+
// Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
141+
// whereas the return value of this function is a &Arc<Cache> (borrowed).
142+
pooledCache = createCache();
143+
retainCache(pooledCache);
83144
return pooledCache;
84145
}
85146

@@ -91,7 +152,13 @@ export function pushRootCachePool(root: FiberRoot) {
91152
// from `root.pooledCache`. If it's currently `null`, we will lazily
92153
// initialize it the first type it's requested. However, we only mutate
93154
// the root itself during the complete/unwind phase of the HostRoot.
94-
pooledCache = root.pooledCache;
155+
const rootCache = root.pooledCache;
156+
if (rootCache != null) {
157+
pooledCache = rootCache;
158+
root.pooledCache = null;
159+
} else {
160+
pooledCache = null;
161+
}
95162
}
96163

97164
export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
@@ -157,7 +224,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
157224
if (!enableCache) {
158225
return null;
159226
}
160-
161227
// We check the cache on the stack first, since that's the one any new Caches
162228
// would have accessed.
163229
let pool = pooledCache;

0 commit comments

Comments
 (0)