Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of cache cleanup #22510

Merged
merged 3 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Object {
},
},
"duration": 15,
"effectDuration": null,
"effectDuration": 0,
"fiberActualDurations": Map {
1 => 15,
2 => 15,
Expand All @@ -86,7 +86,7 @@ Object {
3 => 3,
4 => 2,
},
"passiveEffectDuration": null,
"passiveEffectDuration": 0,
"priorityLevel": "Immediate",
"timestamp": 15,
"updaters": Array [
Expand Down
5 changes: 5 additions & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ export function resetHooksState(): void {
workInProgressHook = null;
}

function getCacheSignal() {
throw new Error('Not implemented.');
}

function getCacheForType<T>(resourceType: () => T): T {
throw new Error('Not implemented.');
}
Expand Down Expand Up @@ -551,6 +555,7 @@ export const Dispatcher: DispatcherType = {
};

if (enableCache) {
Dispatcher.getCacheSignal = getCacheSignal;
Dispatcher.getCacheForType = getCacheForType;
Dispatcher.useCacheRefresh = useCacheRefresh;
}
85 changes: 80 additions & 5 deletions packages/react-reconciler/src/ReactFiberCacheComponent.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {createCursor, push, pop} from './ReactFiberStack.new';
import {pushProvider, popProvider} from './ReactFiberNewContext.new';
import * as Scheduler from 'scheduler';

export type Cache = Map<() => mixed, mixed>;
export type Cache = {|
controller: AbortController,
data: Map<() => mixed, mixed>,
refCount: number,
|};

export type CacheComponentState = {|
+parent: Cache,
Expand All @@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
+pool: Cache,
|};

// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;

export const CacheContext: ReactContext<Cache> = enableCache
? {
$$typeof: REACT_CONTEXT_TYPE,
Expand All @@ -57,6 +69,58 @@ let pooledCache: Cache | null = null;
// cache from the render that suspended.
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);

// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
// for retaining the cache once it is in use (retainCache), and releasing the cache
// once it is no longer needed (releaseCache).
export function createCache(): Cache {
if (!enableCache) {
return (null: any);
}
const cache: Cache = {
josephsavona marked this conversation as resolved.
Show resolved Hide resolved
controller: new AbortController(),
data: new Map(),
refCount: 0,
};

return cache;
}

export function retainCache(cache: Cache) {
if (!enableCache) {
return;
}
if (__DEV__) {
if (cache.controller.signal.aborted) {
console.warn(
'A cache instance was retained after it was already freed. ' +
'This likely indicates a bug in React.',
);
}
}
cache.refCount++;
}

// Cleanup a cache instance, potentially freeing it if there are no more references
export function releaseCache(cache: Cache) {
if (!enableCache) {
return;
}
cache.refCount--;
if (__DEV__) {
if (cache.refCount < 0) {
console.warn(
'A cache instance was released after it was already freed. ' +
'This likely indicates a bug in React.',
);
}
}
if (cache.refCount === 0) {
scheduleCallback(NormalPriority, () => {
cache.controller.abort();
});
}
}

export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
if (!enableCache) {
return;
Expand All @@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
if (pooledCache !== null) {
return pooledCache;
}
// Create a fresh cache.
pooledCache = new Map();
// Create a fresh cache. The pooled cache must be owned - it is freed
// in releaseRootPooledCache() - but the cache instance handed out
// is retained/released in the commit phase of the component that
// references is (ie the host root, cache boundary, suspense component)
// Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
// whereas the return value of this function is a &Arc<Cache> (borrowed).
pooledCache = createCache();
retainCache(pooledCache);
return pooledCache;
}

Expand All @@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) {
// from `root.pooledCache`. If it's currently `null`, we will lazily
// initialize it the first type it's requested. However, we only mutate
// the root itself during the complete/unwind phase of the HostRoot.
pooledCache = root.pooledCache;
const rootCache = root.pooledCache;
if (rootCache != null) {
pooledCache = rootCache;
root.pooledCache = null;
josephsavona marked this conversation as resolved.
Show resolved Hide resolved
} else {
pooledCache = null;
}
}

export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
Expand Down Expand Up @@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
if (!enableCache) {
return null;
}

// We check the cache on the stack first, since that's the one any new Caches
// would have accessed.
let pool = pooledCache;
Expand Down
85 changes: 80 additions & 5 deletions packages/react-reconciler/src/ReactFiberCacheComponent.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {createCursor, push, pop} from './ReactFiberStack.old';
import {pushProvider, popProvider} from './ReactFiberNewContext.old';
import * as Scheduler from 'scheduler';

export type Cache = Map<() => mixed, mixed>;
export type Cache = {|
controller: AbortController,
data: Map<() => mixed, mixed>,
refCount: number,
|};

export type CacheComponentState = {|
+parent: Cache,
Expand All @@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
+pool: Cache,
|};

// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
const {
unstable_scheduleCallback: scheduleCallback,
unstable_NormalPriority: NormalPriority,
} = Scheduler;

export const CacheContext: ReactContext<Cache> = enableCache
? {
$$typeof: REACT_CONTEXT_TYPE,
Expand All @@ -57,6 +69,58 @@ let pooledCache: Cache | null = null;
// cache from the render that suspended.
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);

// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
// for retaining the cache once it is in use (retainCache), and releasing the cache
// once it is no longer needed (releaseCache).
export function createCache(): Cache {
if (!enableCache) {
return (null: any);
}
const cache: Cache = {
controller: new AbortController(),
data: new Map(),
refCount: 0,
};

return cache;
}

export function retainCache(cache: Cache) {
if (!enableCache) {
return;
}
if (__DEV__) {
if (cache.controller.signal.aborted) {
console.warn(
'A cache instance was retained after it was already freed. ' +
'This likely indicates a bug in React.',
);
}
}
cache.refCount++;
}

// Cleanup a cache instance, potentially freeing it if there are no more references
export function releaseCache(cache: Cache) {
if (!enableCache) {
return;
}
cache.refCount--;
if (__DEV__) {
if (cache.refCount < 0) {
console.warn(
'A cache instance was released after it was already freed. ' +
'This likely indicates a bug in React.',
);
}
}
if (cache.refCount === 0) {
scheduleCallback(NormalPriority, () => {
cache.controller.abort();
});
}
}

export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
if (!enableCache) {
return;
Expand All @@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
if (pooledCache !== null) {
return pooledCache;
}
// Create a fresh cache.
pooledCache = new Map();
// Create a fresh cache. The pooled cache must be owned - it is freed
// in releaseRootPooledCache() - but the cache instance handed out
// is retained/released in the commit phase of the component that
// references is (ie the host root, cache boundary, suspense component)
// Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
// whereas the return value of this function is a &Arc<Cache> (borrowed).
pooledCache = createCache();
retainCache(pooledCache);
return pooledCache;
}

Expand All @@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) {
// from `root.pooledCache`. If it's currently `null`, we will lazily
// initialize it the first type it's requested. However, we only mutate
// the root itself during the complete/unwind phase of the HostRoot.
pooledCache = root.pooledCache;
const rootCache = root.pooledCache;
if (rootCache != null) {
pooledCache = rootCache;
root.pooledCache = null;
} else {
pooledCache = null;
}
}

export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
Expand Down Expand Up @@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
if (!enableCache) {
return null;
}

// We check the cache on the stack first, since that's the one any new Caches
// would have accessed.
let pool = pooledCache;
Expand Down
Loading