Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0f7cf73

Browse files
author
Brian Vaughn
committedFeb 7, 2020
useMutableSource reduce amount of metadata cached during render
1 parent baad191 commit 0f7cf73

File tree

5 files changed

+75
-81
lines changed

5 files changed

+75
-81
lines changed
 

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

+48-31
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
} from 'shared/ReactMutableSource';
2424

2525
import ReactSharedInternals from 'shared/ReactSharedInternals';
26+
import {HostRoot} from 'shared/ReactWorkTags';
2627

2728
import {NoWork, Sync} from './ReactFiberExpirationTime';
2829
import {readContext} from './ReactFiberNewContext';
@@ -45,7 +46,8 @@ import {
4546
warnIfNotScopedWithMatchingAct,
4647
markRenderEventTimeAndConfig,
4748
markUnprocessedUpdateTime,
48-
getMutableSourceMetadata,
49+
getMutableSourcePendingExpirationTime,
50+
warnAboutUpdateOnUnmountedFiberInDEV,
4951
} from './ReactFiberWorkLoop';
5052

5153
import invariant from 'shared/invariant';
@@ -897,13 +899,15 @@ function useMutableSourceImpl<S>(
897899
}
898900
}
899901

900-
const metadata = getMutableSourceMetadata(source);
902+
const pendingExpirationTime = getMutableSourcePendingExpirationTime(source);
901903

902-
// Is it safe to read from this source during the current render?
903-
// If the source has not yet been subscribed to, we can use the version number to determine this.
904-
// Else we can use the expiration time as an indicator of any future scheduled updates.
905904
let isSafeToReadFromSource = false;
906-
if (metadata.subscriptionCount === 0) {
905+
906+
// Is it safe to read from this source during the current render?
907+
// If the source has pending updates, we can use the current render's expiration
908+
// time to determine if it's safe to read again from the source.
909+
// If there are no pending updates, we can use the work-in-progress version.
910+
if (pendingExpirationTime === null) {
907911
const lastReadVersion = getWorkInProgressVersion(source);
908912
if (lastReadVersion === null) {
909913
// This is the only case where we need to actually update the version number.
@@ -926,8 +930,8 @@ function useMutableSourceImpl<S>(
926930
);
927931

928932
isSafeToReadFromSource =
929-
metadata.expirationTime === NoWork ||
930-
metadata.expirationTime >= expirationTime;
933+
pendingExpirationTime === NoWork ||
934+
pendingExpirationTime >= expirationTime;
931935
}
932936

933937
let prevMemoizedState = ((hook.memoizedState: any): ?MutableSourceState<S>);
@@ -977,22 +981,43 @@ function useMutableSourceImpl<S>(
977981

978982
const create = () => {
979983
const scheduleUpdate = () => {
980-
const currentTime = requestCurrentTimeForUpdate();
981-
const suspenseConfig = requestCurrentSuspenseConfig();
982-
const expirationTime = computeExpirationForFiber(
983-
currentTime,
984-
fiber,
985-
suspenseConfig,
986-
);
984+
let node = fiber;
985+
let root = null;
986+
while (node !== null) {
987+
if (node.tag === HostRoot) {
988+
root = node.stateNode;
989+
break;
990+
}
991+
node = node.return;
992+
}
987993

988-
// Make sure reads during future renders will know there's a pending update.
989-
// This will prevent a higher priority update from reading a newer version of the source,
990-
// and causing a tear between that render and previous renders.
991-
if (expirationTime > metadata.expirationTime) {
992-
metadata.expirationTime = expirationTime;
994+
if (root === null) {
995+
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
996+
return;
993997
}
994998

995-
scheduleWork(fiber, expirationTime);
999+
const alreadyScheduledExpirationTime = root.mutableSourcePendingUpdateMap.get(
1000+
source,
1001+
);
1002+
1003+
// If an update is already scheduled for this source, re-use the same priority.
1004+
if (alreadyScheduledExpirationTime !== undefined) {
1005+
scheduleWork(fiber, alreadyScheduledExpirationTime);
1006+
} else {
1007+
const currentTime = requestCurrentTimeForUpdate();
1008+
const suspenseConfig = requestCurrentSuspenseConfig();
1009+
const expirationTime = computeExpirationForFiber(
1010+
currentTime,
1011+
fiber,
1012+
suspenseConfig,
1013+
);
1014+
scheduleWork(fiber, expirationTime);
1015+
1016+
// Make sure reads during future renders will know there's a pending update.
1017+
// This will prevent a higher priority update from reading a newer version of the source,
1018+
// and causing a tear between that render and previous renders.
1019+
root.mutableSourcePendingUpdateMap.set(source, expirationTime);
1020+
}
9961021
};
9971022

9981023
// Was the source mutated between when we rendered and when we're subscribing?
@@ -1002,16 +1027,8 @@ function useMutableSourceImpl<S>(
10021027
scheduleUpdate();
10031028
}
10041029

1005-
const unsubscribe = subscribe(scheduleUpdate);
1006-
metadata.subscriptionCount++;
1007-
1008-
memoizedState.destroy = () => {
1009-
metadata.subscriptionCount--;
1010-
1011-
// TODO (useMutableSource) If count is 0, flag this source for possible cleanup.
1012-
1013-
unsubscribe();
1014-
};
1030+
// Unsubscribe on destroy.
1031+
memoizedState.destroy = subscribe(scheduleUpdate);
10151032

10161033
return memoizedState.destroy;
10171034
};

‎packages/react-reconciler/src/ReactFiberRoot.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {Thenable} from './ReactFiberWorkLoop';
1515
import type {Interaction} from 'scheduler/src/Tracing';
1616
import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
1717
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
18-
import type {MutableSourceMetadataMap} from 'shared/ReactMutableSource';
18+
import type {MutableSourcePendingUpdateMap} from 'shared/ReactMutableSource';
1919

2020
import {noTimeout} from './ReactFiberHostConfig';
2121
import {createHostRootFiber} from './ReactFiber';
@@ -77,7 +77,7 @@ type BaseFiberRootProperties = {|
7777
lastExpiredTime: ExpirationTime,
7878
// Used by useMutableSource hook to avoid tearing within this root
7979
// when external, mutable sources are read from during render.
80-
mutableSourceMetadata: MutableSourceMetadataMap,
80+
mutableSourcePendingUpdateMap: MutableSourcePendingUpdateMap,
8181
|};
8282

8383
// The following attributes are only used by interaction tracing builds.
@@ -127,7 +127,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
127127
this.nextKnownPendingLevel = NoWork;
128128
this.lastPingedTime = NoWork;
129129
this.lastExpiredTime = NoWork;
130-
this.mutableSourceMetadata = new Map();
130+
this.mutableSourcePendingUpdateMap = new Map();
131131

132132
if (enableSchedulerTracing) {
133133
this.interactionThreadID = unstable_getThreadID();

‎packages/react-reconciler/src/ReactFiberWorkLoop.js

+18-28
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
1414
import type {Interaction} from 'scheduler/src/Tracing';
1515
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
1616
import type {SuspenseState} from './ReactFiberSuspenseComponent';
17-
import type {
18-
MutableSource,
19-
MutableSourceMetadata,
20-
} from 'shared/ReactMutableSource';
17+
import type {MutableSource} from 'shared/ReactMutableSource';
2118

2219
import {
2320
initializeWorkInProgressVersionMap as initializeMutableSourceWorkInProgressVersionMap,
@@ -299,24 +296,14 @@ let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
299296
// receive the same expiration time. Otherwise we get tearing.
300297
let currentEventTime: ExpirationTime = NoWork;
301298

302-
export function getMutableSourceMetadata(
299+
export function getMutableSourcePendingExpirationTime(
303300
source: MutableSource,
304-
): MutableSourceMetadata {
301+
): ExpirationTime | null {
305302
invariant(workInProgressRoot !== null, 'Expected a work-in-progress root.');
306-
307-
let metadata = workInProgressRoot.mutableSourceMetadata.get(source);
308-
if (metadata !== undefined) {
309-
return metadata;
310-
} else {
311-
metadata = {
312-
expirationTime: NoWork,
313-
subscriptionCount: 0,
314-
};
315-
316-
workInProgressRoot.mutableSourceMetadata.set(source, metadata);
317-
318-
return ((metadata: any): MutableSourceMetadata);
319-
}
303+
const expirationTime = workInProgressRoot.mutableSourcePendingUpdateMap.get(
304+
source,
305+
);
306+
return expirationTime !== undefined ? expirationTime : null;
320307
}
321308

322309
export function requestCurrentTimeForUpdate() {
@@ -402,10 +389,7 @@ export function computeExpirationForFiber(
402389
return expirationTime;
403390
}
404391

405-
export function scheduleUpdateOnFiber(
406-
fiber: Fiber,
407-
expirationTime: ExpirationTime,
408-
) {
392+
function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTime) {
409393
checkForNestedUpdates();
410394
warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
411395

@@ -2027,6 +2011,15 @@ function commitRootImpl(root, renderPriorityLevel) {
20272011
nestedUpdateCount = 0;
20282012
}
20292013

2014+
// Remove pending mutable source entries that we've completed processing.
2015+
root.mutableSourcePendingUpdateMap.forEach(
2016+
(pendingExpirationTime, source) => {
2017+
if (pendingExpirationTime <= expirationTime) {
2018+
root.mutableSourcePendingUpdateMap.delete(source);
2019+
}
2020+
},
2021+
);
2022+
20302023
resetMutableSourceWorkInProgressVersionMap();
20312024

20322025
onCommitRoot(finishedWork.stateNode, expirationTime);
@@ -2280,9 +2273,6 @@ function flushPassiveEffectsImpl() {
22802273
finishPendingInteractions(root, expirationTime);
22812274
}
22822275

2283-
// TODO (useMutableSource) Remove metadata for mutable sources that are no longer in use.
2284-
// This check comes after passive effects, because that's when sources are unsubscribed from.
2285-
22862276
executionContext = prevExecutionContext;
22872277

22882278
flushSyncCallbackQueue();
@@ -2626,7 +2616,7 @@ function checkForInterruption(
26262616
}
26272617

26282618
let didWarnStateUpdateForUnmountedComponent: Set<string> | null = null;
2629-
function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
2619+
export function warnAboutUpdateOnUnmountedFiberInDEV(fiber: Fiber) {
26302620
if (__DEV__) {
26312621
const tag = fiber.tag;
26322622
if (

‎packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js

-2
Original file line numberDiff line numberDiff line change
@@ -2098,8 +2098,6 @@ describe('ReactHooksWithNoopRenderer', () => {
20982098
});
20992099
expect(Scheduler).toHaveYielded(['a:two', 'b:two', 'Sync effect']);
21002100
});
2101-
2102-
// TODO (useMutableSource) Edge case: make sure we don't leak on root Map (how to test this without internals?)
21032101
});
21042102

21052103
describe('useCallback', () => {

‎packages/shared/ReactMutableSource.js

+6-17
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,12 @@ export type MutableSourceHookConfig<S> = {|
2020
subscribe: (callback: Function) => () => void,
2121
|};
2222

23-
export type MutableSourceMetadata = {|
24-
// Expiration time of most recently scheduled update.
25-
// Used to determine if a source is safe to read during updates.
26-
// If the render’s expiration time is ≤ this value,
27-
// the source has not changed since the last render and is safe to read from.
28-
expirationTime: ExpirationTime,
29-
30-
// Number of hooks that are subscribed as of the most recently committed render.
31-
// This value is used to determine when a source is no longer in use,
32-
// and should be removed from the root map to avoid a memory leak.
33-
subscriptionCount: number,
34-
|};
35-
36-
export type MutableSourceMetadataMap = Map<
37-
MutableSource,
38-
MutableSourceMetadata,
39-
>;
23+
// Tracks expiration time for all mutable sources with pending updates.
24+
// Used to determine if a source is safe to read during updates.
25+
// If there are no entries in this map for a given source,
26+
// or if the current render’s expiration time is ≤ this value,
27+
// it is safe to read from the source without tearing.
28+
export type MutableSourcePendingUpdateMap = Map<MutableSource, ExpirationTime>;
4029

4130
// Tracks the version of each source at the time it was most recently read.
4231
// Used to determine if a source is safe to read from before it has been subscribed to.

0 commit comments

Comments
 (0)
Please sign in to comment.