@samitouri / QOS-React / commits / 42f15b324f

[DevTools][Transition Tracing] onTransitionComplete and onTransitionStart implmentation (#23313)

* add transition name to startTransition Add a transitionName to start transition, store the transition start time and name in the batch config, and pass it to the root on render * Transition Tracing Types and Consts * Root begin work The root operates as a tracing marker that has all transitions on it. This PR only tested the root with one transition so far - Store transitions in memoizedState. Do this in updateHostRoot AND attemptEarlyBailoutIfNoScheduledUpdate. We need to do this in the latter part because even if the root itself doesn't have an update, it could still have new transitions in its transitionLanes map that we need to process. * Transition Tracing commit phase - adds a module scoped pending transition callbacks object that contains all transition callbacks that have not yet been processed. This contains all callbacks before the next paint occurs. - Add code in the mutation phase to: * For the root, if there are transitions that were initialized during this commit in the root transition lanes map, add a transition start call to the pending transition callbacks object. Then, remove the transitions from the root transition lanes map. * For roots, in the commit phase, add a transition complete call We add this code in the mutation phase because we can't add it to the passive phase because then the paint might have occurred before we even know which callbacks to call * Process Callbacks after paint At the end of the commit phase, call scheduleTransitionCallbacks to schedule all pending transition callbacks to be called after paint. Then clear the callbacks

Luna Ruan committed Feb 24, 2022 at 17:28 UTC 42f15b324f50d0fd98322c21646ac3013e30344a
25 files changed +831 -37
packages/react-debug-tools/src/ReactDebugHooks.js
+5 -1
@@ -13,6 +13,7 @@ import type {
13 MutableSourceSubscribeFn,
14 ReactContext,
15 ReactProviderType,
16 + StartTransitionOptions,
17 } from 'shared/ReactTypes';
18 import type {
19 Fiber,
@@ -291,7 +292,10 @@ function useSyncExternalStore<T>(
292 return value;
293 }
294
294 -function useTransition(): [boolean, (() => void) => void] {
295 +function useTransition(): [
296 + boolean,
297 + (callback: () => void, options?: StartTransitionOptions) => void,
298 +] {
299 // useTransition() composes multiple hooks internally.
300 // Advance the current hook index the same number of times
301 // so that subsequent hooks have the right memoized state.
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+9 -1
@@ -226,6 +226,7 @@ import {
226 markSkippedUpdateLanes,
227 getWorkInProgressRoot,
228 pushRenderLanes,
229 + getWorkInProgressTransitions,
230 } from './ReactFiberWorkLoop.new';
231 import {setWorkInProgressVersion} from './ReactMutableSource.new';
232 import {
@@ -1336,6 +1337,10 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1337 }
1338 }
1339
1340 + if (enableTransitionTracing) {
1341 + workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
1342 + }
1343 +
1344 // Caution: React DevTools currently depends on this property
1345 // being called "element".
1346 const nextChildren = nextState.element;
@@ -3495,12 +3500,15 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3500 switch (workInProgress.tag) {
3501 case HostRoot:
3502 pushHostRootContext(workInProgress);
3503 + const root: FiberRoot = workInProgress.stateNode;
3504 if (enableCache) {
3499 - const root: FiberRoot = workInProgress.stateNode;
3505 const cache: Cache = current.memoizedState.cache;
3506 pushCacheProvider(workInProgress, cache);
3507 pushRootCachePool(root);
3508 }
3509 + if (enableTransitionTracing) {
3510 + workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
3511 + }
3512 resetHydrationState();
3513 break;
3514 case HostComponent:
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+9 -1
@@ -226,6 +226,7 @@ import {
226 markSkippedUpdateLanes,
227 getWorkInProgressRoot,
228 pushRenderLanes,
229 + getWorkInProgressTransitions,
230 } from './ReactFiberWorkLoop.old';
231 import {setWorkInProgressVersion} from './ReactMutableSource.old';
232 import {
@@ -1336,6 +1337,10 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1337 }
1338 }
1339
1340 + if (enableTransitionTracing) {
1341 + workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
1342 + }
1343 +
1344 // Caution: React DevTools currently depends on this property
1345 // being called "element".
1346 const nextChildren = nextState.element;
@@ -3495,12 +3500,15 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3500 switch (workInProgress.tag) {
3501 case HostRoot:
3502 pushHostRootContext(workInProgress);
3503 + const root: FiberRoot = workInProgress.stateNode;
3504 if (enableCache) {
3499 - const root: FiberRoot = workInProgress.stateNode;
3505 const cache: Cache = current.memoizedState.cache;
3506 pushCacheProvider(workInProgress, cache);
3507 pushRootCachePool(root);
3508 }
3509 + if (enableTransitionTracing) {
3510 + workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
3511 + }
3512 resetHydrationState();
3513 break;
3514 case HostComponent:
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+43 -7
@@ -40,6 +40,7 @@ import {
40 enableSuspenseLayoutEffectSemantics,
41 enableUpdaterTracking,
42 enableCache,
43 + enableTransitionTracing,
44 } from 'shared/ReactFeatureFlags';
45 import {
46 FunctionComponent,
@@ -132,6 +133,8 @@ import {
133 markCommitTimeOfFallback,
134 enqueuePendingPassiveProfilerEffect,
135 restorePendingUpdaters,
136 + addTransitionStartCallbackToPendingTransition,
137 + addTransitionCompleteCallbackToPendingTransition,
138 } from './ReactFiberWorkLoop.new';
139 import {
140 NoFlags as NoHookEffect,
@@ -156,6 +159,7 @@ import {
159 onCommitUnmount,
160 } from './ReactFiberDevToolsHook.new';
161 import {releaseCache, retainCache} from './ReactFiberCacheComponent.new';
162 +import {clearTransitionsForLanes} from './ReactFiberLane.new';
163
164 let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
165 if (__DEV__) {
@@ -983,8 +987,10 @@ function commitLayoutEffectOnFiber(
987 case IncompleteClassComponent:
988 case ScopeComponent:
989 case OffscreenComponent:
986 - case LegacyHiddenComponent:
990 + case LegacyHiddenComponent: {
991 break;
992 + }
993 +
994 default:
995 throw new Error(
996 'This unit of work tag should not have side-effects. This error is ' +
@@ -2137,13 +2143,13 @@ export function commitMutationEffects(
2143 inProgressRoot = root;
2144 nextEffect = firstChild;
2145
2140 - commitMutationEffects_begin(root);
2146 + commitMutationEffects_begin(root, committedLanes);
2147
2148 inProgressLanes = null;
2149 inProgressRoot = null;
2150 }
2151
2146 -function commitMutationEffects_begin(root: FiberRoot) {
2152 +function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) {
2153 while (nextEffect !== null) {
2154 const fiber = nextEffect;
2155
@@ -2166,17 +2172,17 @@ function commitMutationEffects_begin(root: FiberRoot) {
2172 ensureCorrectReturnPointer(child, fiber);
2173 nextEffect = child;
2174 } else {
2169 - commitMutationEffects_complete(root);
2175 + commitMutationEffects_complete(root, lanes);
2176 }
2177 }
2178 }
2179
2174 -function commitMutationEffects_complete(root: FiberRoot) {
2180 +function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) {
2181 while (nextEffect !== null) {
2182 const fiber = nextEffect;
2183 setCurrentDebugFiberInDEV(fiber);
2184 try {
2179 - commitMutationEffectsOnFiber(fiber, root);
2185 + commitMutationEffectsOnFiber(fiber, root, lanes);
2186 } catch (error) {
2187 reportUncaughtErrorInDEV(error);
2188 captureCommitPhaseError(fiber, fiber.return, error);
@@ -2194,13 +2200,43 @@ function commitMutationEffects_complete(root: FiberRoot) {
2200 }
2201 }
2202
2197 -function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
2203 +function commitMutationEffectsOnFiber(
2204 + finishedWork: Fiber,
2205 + root: FiberRoot,
2206 + lanes: Lanes,
2207 +) {
2208 // TODO: The factoring of this phase could probably be improved. Consider
2209 // switching on the type of work before checking the flags. That's what
2210 // we do in all the other phases. I think this one is only different
2211 // because of the shared reconciliation logic below.
2212 const flags = finishedWork.flags;
2213
2214 + if (enableTransitionTracing) {
2215 + switch (finishedWork.tag) {
2216 + case HostRoot: {
2217 + const state = finishedWork.memoizedState;
2218 + const transitions = state.transitions;
2219 + if (transitions !== null) {
2220 + transitions.forEach(transition => {
2221 + // TODO(luna) Do we want to log TransitionStart in the startTransition callback instead?
2222 + addTransitionStartCallbackToPendingTransition({
2223 + transitionName: transition.name,
2224 + startTime: transition.startTime,
2225 + });
2226 +
2227 + addTransitionCompleteCallbackToPendingTransition({
2228 + transitionName: transition.name,
2229 + startTime: transition.startTime,
2230 + });
2231 + });
2232 +
2233 + clearTransitionsForLanes(root, lanes);
2234 + state.transitions = null;
2235 + }
2236 + }
2237 + }
2238 + }
2239 +
2240 if (flags & ContentReset) {
2241 commitResetTextContent(finishedWork);
2242 }
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+43 -7
@@ -40,6 +40,7 @@ import {
40 enableSuspenseLayoutEffectSemantics,
41 enableUpdaterTracking,
42 enableCache,
43 + enableTransitionTracing,
44 } from 'shared/ReactFeatureFlags';
45 import {
46 FunctionComponent,
@@ -132,6 +133,8 @@ import {
133 markCommitTimeOfFallback,
134 enqueuePendingPassiveProfilerEffect,
135 restorePendingUpdaters,
136 + addTransitionStartCallbackToPendingTransition,
137 + addTransitionCompleteCallbackToPendingTransition,
138 } from './ReactFiberWorkLoop.old';
139 import {
140 NoFlags as NoHookEffect,
@@ -156,6 +159,7 @@ import {
159 onCommitUnmount,
160 } from './ReactFiberDevToolsHook.old';
161 import {releaseCache, retainCache} from './ReactFiberCacheComponent.old';
162 +import {clearTransitionsForLanes} from './ReactFiberLane.old';
163
164 let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
165 if (__DEV__) {
@@ -983,8 +987,10 @@ function commitLayoutEffectOnFiber(
987 case IncompleteClassComponent:
988 case ScopeComponent:
989 case OffscreenComponent:
986 - case LegacyHiddenComponent:
990 + case LegacyHiddenComponent: {
991 break;
992 + }
993 +
994 default:
995 throw new Error(
996 'This unit of work tag should not have side-effects. This error is ' +
@@ -2137,13 +2143,13 @@ export function commitMutationEffects(
2143 inProgressRoot = root;
2144 nextEffect = firstChild;
2145
2140 - commitMutationEffects_begin(root);
2146 + commitMutationEffects_begin(root, committedLanes);
2147
2148 inProgressLanes = null;
2149 inProgressRoot = null;
2150 }
2151
2146 -function commitMutationEffects_begin(root: FiberRoot) {
2152 +function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) {
2153 while (nextEffect !== null) {
2154 const fiber = nextEffect;
2155
@@ -2166,17 +2172,17 @@ function commitMutationEffects_begin(root: FiberRoot) {
2172 ensureCorrectReturnPointer(child, fiber);
2173 nextEffect = child;
2174 } else {
2169 - commitMutationEffects_complete(root);
2175 + commitMutationEffects_complete(root, lanes);
2176 }
2177 }
2178 }
2179
2174 -function commitMutationEffects_complete(root: FiberRoot) {
2180 +function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) {
2181 while (nextEffect !== null) {
2182 const fiber = nextEffect;
2183 setCurrentDebugFiberInDEV(fiber);
2184 try {
2179 - commitMutationEffectsOnFiber(fiber, root);
2185 + commitMutationEffectsOnFiber(fiber, root, lanes);
2186 } catch (error) {
2187 reportUncaughtErrorInDEV(error);
2188 captureCommitPhaseError(fiber, fiber.return, error);
@@ -2194,13 +2200,43 @@ function commitMutationEffects_complete(root: FiberRoot) {
2200 }
2201 }
2202
2197 -function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
2203 +function commitMutationEffectsOnFiber(
2204 + finishedWork: Fiber,
2205 + root: FiberRoot,
2206 + lanes: Lanes,
2207 +) {
2208 // TODO: The factoring of this phase could probably be improved. Consider
2209 // switching on the type of work before checking the flags. That's what
2210 // we do in all the other phases. I think this one is only different
2211 // because of the shared reconciliation logic below.
2212 const flags = finishedWork.flags;
2213
2214 + if (enableTransitionTracing) {
2215 + switch (finishedWork.tag) {
2216 + case HostRoot: {
2217 + const state = finishedWork.memoizedState;
2218 + const transitions = state.transitions;
2219 + if (transitions !== null) {
2220 + transitions.forEach(transition => {
2221 + // TODO(luna) Do we want to log TransitionStart in the startTransition callback instead?
2222 + addTransitionStartCallbackToPendingTransition({
2223 + transitionName: transition.name,
2224 + startTime: transition.startTime,
2225 + });
2226 +
2227 + addTransitionCompleteCallbackToPendingTransition({
2228 + transitionName: transition.name,
2229 + startTime: transition.startTime,
2230 + });
2231 + });
2232 +
2233 + clearTransitionsForLanes(root, lanes);
2234 + state.transitions = null;
2235 + }
2236 + }
2237 + }
2238 + }
2239 +
2240 if (flags & ContentReset) {
2241 commitResetTextContent(finishedWork);
2242 }
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+10 -1
@@ -62,6 +62,7 @@ import {
62 OffscreenComponent,
63 LegacyHiddenComponent,
64 CacheComponent,
65 + TracingMarkerComponent,
66 } from './ReactWorkTags';
67 import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
68 import {
@@ -141,6 +142,7 @@ import {
142 enableCache,
143 enableSuspenseLayoutEffectSemantics,
144 enablePersistentOffscreenHostContainer,
145 + enableTransitionTracing,
146 } from 'shared/ReactFeatureFlags';
147 import {
148 renderDidSuspend,
@@ -1570,8 +1572,15 @@ function completeWork(
1572 }
1573 popCacheProvider(workInProgress, cache);
1574 bubbleProperties(workInProgress);
1573 - return null;
1575 }
1576 + return null;
1577 + }
1578 + case TracingMarkerComponent: {
1579 + if (enableTransitionTracing) {
1580 + // Bubble subtree flags before so we can set the flag property
1581 + bubbleProperties(workInProgress);
1582 + }
1583 + return null;
1584 }
1585 }
1586
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+10 -1
@@ -62,6 +62,7 @@ import {
62 OffscreenComponent,
63 LegacyHiddenComponent,
64 CacheComponent,
65 + TracingMarkerComponent,
66 } from './ReactWorkTags';
67 import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
68 import {
@@ -141,6 +142,7 @@ import {
142 enableCache,
143 enableSuspenseLayoutEffectSemantics,
144 enablePersistentOffscreenHostContainer,
145 + enableTransitionTracing,
146 } from 'shared/ReactFeatureFlags';
147 import {
148 renderDidSuspend,
@@ -1570,8 +1572,15 @@ function completeWork(
1572 }
1573 popCacheProvider(workInProgress, cache);
1574 bubbleProperties(workInProgress);
1573 - return null;
1575 }
1576 + return null;
1577 + }
1578 + case TracingMarkerComponent: {
1579 + if (enableTransitionTracing) {
1580 + // Bubble subtree flags before so we can set the flag property
1581 + bubbleProperties(workInProgress);
1582 + }
1583 + return null;
1584 }
1585 }
1586
packages/react-reconciler/src/ReactFiberHooks.new.js
+24 -4
@@ -12,6 +12,7 @@ import type {
12 MutableSourceGetSnapshotFn,
13 MutableSourceSubscribeFn,
14 ReactContext,
15 + StartTransitionOptions,
16 } from 'shared/ReactTypes';
17 import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
18 import type {Lanes, Lane} from './ReactFiberLane.new';
@@ -31,6 +32,7 @@ import {
32 enableLazyContextPropagation,
33 enableSuspenseLayoutEffectSemantics,
34 enableUseMutableSource,
35 + enableTransitionTracing,
36 } from 'shared/ReactFeatureFlags';
37
38 import {
@@ -110,6 +112,7 @@ import {
112 } from './ReactUpdateQueue.new';
113 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
114 import {getTreeId} from './ReactFiberTreeContext.new';
115 +import {now} from './Scheduler';
116
117 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
118
@@ -1967,7 +1970,7 @@ function rerenderDeferredValue<T>(value: T): T {
1970 return prevValue;
1971 }
1972
1970 -function startTransition(setPending, callback) {
1973 +function startTransition(setPending, callback, options) {
1974 const previousPriority = getCurrentUpdatePriority();
1975 setCurrentUpdatePriority(
1976 higherEventPriority(previousPriority, ContinuousEventPriority),
@@ -1979,6 +1982,13 @@ function startTransition(setPending, callback) {
1982 ReactCurrentBatchConfig.transition = {};
1983 const currentTransition = ReactCurrentBatchConfig.transition;
1984
1985 + if (enableTransitionTracing) {
1986 + if (options !== undefined && options.name !== undefined) {
1987 + ReactCurrentBatchConfig.transition.name = options.name;
1988 + ReactCurrentBatchConfig.transition.startTime = now();
1989 + }
1990 + }
1991 +
1992 if (__DEV__) {
1993 ReactCurrentBatchConfig.transition._updatedFibers = new Set();
1994 }
@@ -1990,6 +2000,7 @@ function startTransition(setPending, callback) {
2000 setCurrentUpdatePriority(previousPriority);
2001
2002 ReactCurrentBatchConfig.transition = prevTransition;
2003 +
2004 if (__DEV__) {
2005 if (prevTransition === null && currentTransition._updatedFibers) {
2006 const updatedFibersCount = currentTransition._updatedFibers.size;
@@ -2006,7 +2017,10 @@ function startTransition(setPending, callback) {
2017 }
2018 }
2019
2009 -function mountTransition(): [boolean, (() => void) => void] {
2020 +function mountTransition(): [
2021 + boolean,
2022 + (callback: () => void, options?: StartTransitionOptions) => void,
2023 +] {
2024 const [isPending, setPending] = mountState(false);
2025 // The `start` method never changes.
2026 const start = startTransition.bind(null, setPending);
@@ -2015,14 +2029,20 @@ function mountTransition(): [boolean, (() => void) => void] {
2029 return [isPending, start];
2030 }
2031
2018 -function updateTransition(): [boolean, (() => void) => void] {
2032 +function updateTransition(): [
2033 + boolean,
2034 + (callback: () => void, options?: StartTransitionOptions) => void,
2035 +] {
2036 const [isPending] = updateState(false);
2037 const hook = updateWorkInProgressHook();
2038 const start = hook.memoizedState;
2039 return [isPending, start];
2040 }
2041
2025 -function rerenderTransition(): [boolean, (() => void) => void] {
2042 +function rerenderTransition(): [
2043 + boolean,
2044 + (callback: () => void, options?: StartTransitionOptions) => void,
2045 +] {
2046 const [isPending] = rerenderState(false);
2047 const hook = updateWorkInProgressHook();
2048 const start = hook.memoizedState;
packages/react-reconciler/src/ReactFiberHooks.old.js
+24 -4
@@ -12,6 +12,7 @@ import type {
12 MutableSourceGetSnapshotFn,
13 MutableSourceSubscribeFn,
14 ReactContext,
15 + StartTransitionOptions,
16 } from 'shared/ReactTypes';
17 import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
18 import type {Lanes, Lane} from './ReactFiberLane.old';
@@ -31,6 +32,7 @@ import {
32 enableLazyContextPropagation,
33 enableSuspenseLayoutEffectSemantics,
34 enableUseMutableSource,
35 + enableTransitionTracing,
36 } from 'shared/ReactFeatureFlags';
37
38 import {
@@ -110,6 +112,7 @@ import {
112 } from './ReactUpdateQueue.old';
113 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
114 import {getTreeId} from './ReactFiberTreeContext.old';
115 +import {now} from './Scheduler';
116
117 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
118
@@ -1967,7 +1970,7 @@ function rerenderDeferredValue<T>(value: T): T {
1970 return prevValue;
1971 }
1972
1970 -function startTransition(setPending, callback) {
1973 +function startTransition(setPending, callback, options) {
1974 const previousPriority = getCurrentUpdatePriority();
1975 setCurrentUpdatePriority(
1976 higherEventPriority(previousPriority, ContinuousEventPriority),
@@ -1979,6 +1982,13 @@ function startTransition(setPending, callback) {
1982 ReactCurrentBatchConfig.transition = {};
1983 const currentTransition = ReactCurrentBatchConfig.transition;
1984
1985 + if (enableTransitionTracing) {
1986 + if (options !== undefined && options.name !== undefined) {
1987 + ReactCurrentBatchConfig.transition.name = options.name;
1988 + ReactCurrentBatchConfig.transition.startTime = now();
1989 + }
1990 + }
1991 +
1992 if (__DEV__) {
1993 ReactCurrentBatchConfig.transition._updatedFibers = new Set();
1994 }
@@ -1990,6 +2000,7 @@ function startTransition(setPending, callback) {
2000 setCurrentUpdatePriority(previousPriority);
2001
2002 ReactCurrentBatchConfig.transition = prevTransition;
2003 +
2004 if (__DEV__) {
2005 if (prevTransition === null && currentTransition._updatedFibers) {
2006 const updatedFibersCount = currentTransition._updatedFibers.size;
@@ -2006,7 +2017,10 @@ function startTransition(setPending, callback) {
2017 }
2018 }
2019
2009 -function mountTransition(): [boolean, (() => void) => void] {
2020 +function mountTransition(): [
2021 + boolean,
2022 + (callback: () => void, options?: StartTransitionOptions) => void,
2023 +] {
2024 const [isPending, setPending] = mountState(false);
2025 // The `start` method never changes.
2026 const start = startTransition.bind(null, setPending);
@@ -2015,14 +2029,20 @@ function mountTransition(): [boolean, (() => void) => void] {
2029 return [isPending, start];
2030 }
2031
2018 -function updateTransition(): [boolean, (() => void) => void] {
2032 +function updateTransition(): [
2033 + boolean,
2034 + (callback: () => void, options?: StartTransitionOptions) => void,
2035 +] {
2036 const [isPending] = updateState(false);
2037 const hook = updateWorkInProgressHook();
2038 const start = hook.memoizedState;
2039 return [isPending, start];
2040 }
2041
2025 -function rerenderTransition(): [boolean, (() => void) => void] {
2042 +function rerenderTransition(): [
2043 + boolean,
2044 + (callback: () => void, options?: StartTransitionOptions) => void,
2045 +] {
2046 const [isPending] = rerenderState(false);
2047 const hook = updateWorkInProgressHook();
2048 const start = hook.memoizedState;
packages/react-reconciler/src/ReactFiberLane.new.js
+77
@@ -8,6 +8,10 @@
8 */
9
10 import type {FiberRoot} from './ReactInternalTypes';
11 +import type {
12 + Transition,
13 + Transitions,
14 +} from './ReactFiberTracingMarkerComponent.new';
15
16 // TODO: Ideally these types would be opaque but that doesn't work well with
17 // our reconciler fork infra, since these leak into non-reconciler packages.
@@ -20,6 +24,7 @@ import {
24 enableSchedulingProfiler,
25 enableUpdaterTracking,
26 allowConcurrentByDefault,
27 + enableTransitionTracing,
28 } from 'shared/ReactFeatureFlags';
29 import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
30 import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
@@ -796,3 +801,75 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
801 lanes &= ~lane;
802 }
803 }
804 +
805 +export function addTransitionToLanesMap(
806 + root: FiberRoot,
807 + transition: Transition,
808 + lane: Lane,
809 +) {
810 + if (enableTransitionTracing) {
811 + const transitionLanesMap = root.transitionLanes;
812 + const index = laneToIndex(lane);
813 + let transitions = transitionLanesMap[index];
814 + if (transitions === null) {
815 + transitions = [];
816 + }
817 + transitions.push(transition);
818 +
819 + transitionLanesMap[index] = transitions;
820 + }
821 +}
822 +
823 +export function getTransitionsForLanes(
824 + root: FiberRoot,
825 + lanes: Lane | Lanes,
826 +): Transitions | null {
827 + if (!enableTransitionTracing) {
828 + return null;
829 + }
830 +
831 + const transitionsForLanes = [];
832 + while (lanes > 0) {
833 + const index = laneToIndex(lanes);
834 + const lane = 1 << index;
835 + const transitions = root.transitionLanes[index];
836 + if (transitions !== null) {
837 + transitions.forEach(transition => {
838 + transitionsForLanes.push(transition);
839 + });
840 + }
841 +
842 + lanes &= ~lane;
843 + }
844 +
845 + if (transitionsForLanes.length === 0) {
846 + return null;
847 + }
848 +
849 + return transitionsForLanes;
850 +}
851 +
852 +export function clearTransitionsForLanes(root: FiberRoot, lanes: Lane | Lanes) {
853 + if (!enableTransitionTracing) {
854 + return;
855 + }
856 +
857 + while (lanes > 0) {
858 + const index = laneToIndex(lanes);
859 + const lane = 1 << index;
860 +
861 + const transitions = root.transitionLanes[index];
862 + if (transitions !== null) {
863 + root.transitionLanes[index] = null;
864 + } else {
865 + if (__DEV__) {
866 + console.error(
867 + 'React Bug: transition lanes accessed out of bounds index: %s',
868 + index.toString(),
869 + );
870 + }
871 + }
872 +
873 + lanes &= ~lane;
874 + }
875 +}
packages/react-reconciler/src/ReactFiberLane.old.js
+77
@@ -8,6 +8,10 @@
8 */
9
10 import type {FiberRoot} from './ReactInternalTypes';
11 +import type {
12 + Transition,
13 + Transitions,
14 +} from './ReactFiberTracingMarkerComponent.old';
15
16 // TODO: Ideally these types would be opaque but that doesn't work well with
17 // our reconciler fork infra, since these leak into non-reconciler packages.
@@ -20,6 +24,7 @@ import {
24 enableSchedulingProfiler,
25 enableUpdaterTracking,
26 allowConcurrentByDefault,
27 + enableTransitionTracing,
28 } from 'shared/ReactFeatureFlags';
29 import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
30 import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
@@ -796,3 +801,75 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
801 lanes &= ~lane;
802 }
803 }
804 +
805 +export function addTransitionToLanesMap(
806 + root: FiberRoot,
807 + transition: Transition,
808 + lane: Lane,
809 +) {
810 + if (enableTransitionTracing) {
811 + const transitionLanesMap = root.transitionLanes;
812 + const index = laneToIndex(lane);
813 + let transitions = transitionLanesMap[index];
814 + if (transitions === null) {
815 + transitions = [];
816 + }
817 + transitions.push(transition);
818 +
819 + transitionLanesMap[index] = transitions;
820 + }
821 +}
822 +
823 +export function getTransitionsForLanes(
824 + root: FiberRoot,
825 + lanes: Lane | Lanes,
826 +): Transitions | null {
827 + if (!enableTransitionTracing) {
828 + return null;
829 + }
830 +
831 + const transitionsForLanes = [];
832 + while (lanes > 0) {
833 + const index = laneToIndex(lanes);
834 + const lane = 1 << index;
835 + const transitions = root.transitionLanes[index];
836 + if (transitions !== null) {
837 + transitions.forEach(transition => {
838 + transitionsForLanes.push(transition);
839 + });
840 + }
841 +
842 + lanes &= ~lane;
843 + }
844 +
845 + if (transitionsForLanes.length === 0) {
846 + return null;
847 + }
848 +
849 + return transitionsForLanes;
850 +}
851 +
852 +export function clearTransitionsForLanes(root: FiberRoot, lanes: Lane | Lanes) {
853 + if (!enableTransitionTracing) {
854 + return;
855 + }
856 +
857 + while (lanes > 0) {
858 + const index = laneToIndex(lanes);
859 + const lane = 1 << index;
860 +
861 + const transitions = root.transitionLanes[index];
862 + if (transitions !== null) {
863 + root.transitionLanes[index] = null;
864 + } else {
865 + if (__DEV__) {
866 + console.error(
867 + 'React Bug: transition lanes accessed out of bounds index: %s',
868 + index.toString(),
869 + );
870 + }
871 + }
872 +
873 + lanes &= ~lane;
874 + }
875 +}
packages/react-reconciler/src/ReactFiberRoot.new.js
+17 -2
@@ -13,6 +13,8 @@ import type {
13 TransitionTracingCallbacks,
14 } from './ReactInternalTypes';
15 import type {RootTag} from './ReactRootTags';
16 +import type {Cache} from './ReactFiberCacheComponent.new';
17 +import type {Transitions} from './ReactFiberTracingMarkerComponent.new';
18
19 import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
20 import {createHostRootFiber} from './ReactFiber.new';
@@ -35,6 +37,12 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.new';
37 import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
38 import {createCache, retainCache} from './ReactFiberCacheComponent.new';
39
40 +export type RootState = {
41 + element: any,
42 + cache: Cache | null,
43 + transitions: Transitions | null,
44 +};
45 +
46 function FiberRootNode(
47 containerInfo,
48 tag,
@@ -85,6 +93,10 @@ function FiberRootNode(
93
94 if (enableTransitionTracing) {
95 this.transitionCallbacks = null;
96 + const transitionLanesMap = (this.transitionLanes = []);
97 + for (let i = 0; i < TotalLanes; i++) {
98 + transitionLanesMap.push(null);
99 + }
100 }
101
102 if (enableProfilerTimer && enableProfilerCommitHooks) {
@@ -165,14 +177,17 @@ export function createFiberRoot(
177 // retained separately.
178 root.pooledCache = initialCache;
179 retainCache(initialCache);
168 - const initialState = {
180 + const initialState: RootState = {
181 element: null,
182 cache: initialCache,
183 + transitions: null,
184 };
185 uninitializedFiber.memoizedState = initialState;
186 } else {
174 - const initialState = {
187 + const initialState: RootState = {
188 element: null,
189 + cache: null,
190 + transitions: null,
191 };
192 uninitializedFiber.memoizedState = initialState;
193 }
packages/react-reconciler/src/ReactFiberRoot.old.js
+17 -2
@@ -13,6 +13,8 @@ import type {
13 TransitionTracingCallbacks,
14 } from './ReactInternalTypes';
15 import type {RootTag} from './ReactRootTags';
16 +import type {Cache} from './ReactFiberCacheComponent.old';
17 +import type {Transitions} from './ReactFiberTracingMarkerComponent.old';
18
19 import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
20 import {createHostRootFiber} from './ReactFiber.old';
@@ -35,6 +37,12 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.old';
37 import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
38 import {createCache, retainCache} from './ReactFiberCacheComponent.old';
39
40 +export type RootState = {
41 + element: any,
42 + cache: Cache | null,
43 + transitions: Transitions | null,
44 +};
45 +
46 function FiberRootNode(
47 containerInfo,
48 tag,
@@ -85,6 +93,10 @@ function FiberRootNode(
93
94 if (enableTransitionTracing) {
95 this.transitionCallbacks = null;
96 + const transitionLanesMap = (this.transitionLanes = []);
97 + for (let i = 0; i < TotalLanes; i++) {
98 + transitionLanesMap.push(null);
99 + }
100 }
101
102 if (enableProfilerTimer && enableProfilerCommitHooks) {
@@ -165,14 +177,17 @@ export function createFiberRoot(
177 // retained separately.
178 root.pooledCache = initialCache;
179 retainCache(initialCache);
168 - const initialState = {
180 + const initialState: RootState = {
181 element: null,
182 cache: initialCache,
183 + transitions: null,
184 };
185 uninitializedFiber.memoizedState = initialState;
186 } else {
174 - const initialState = {
187 + const initialState: RootState = {
188 element: null,
189 + cache: null,
190 + transitions: null,
191 };
192 uninitializedFiber.memoizedState = initialState;
193 }
packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
+64
@@ -6,8 +6,72 @@
6 *
7 * @flow
8 */
9 +
10 +import type {TransitionTracingCallbacks} from './ReactInternalTypes';
11 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 +import {enableTransitionTracing} from 'shared/ReactFeatureFlags';
13 +
14 +export type SuspenseInfo = {name: string | null};
15 +
16 +export type TransitionObject = {
17 + transitionName: string,
18 + startTime: number,
19 +};
20 +
21 +export type PendingTransitionCallbacks = {
22 + transitionStart: Array<TransitionObject> | null,
23 + transitionComplete: Array<TransitionObject> | null,
24 +};
25
26 export type Transition = {
27 + name: string,
28 + startTime: number,
29 +};
30 +
31 +export type BatchConfigTransition = {
32 + name?: string,
33 + startTime?: number,
34 _updatedFibers?: Set<Fiber>,
35 };
36 +
37 +export type Transitions = Array<Transition> | null;
38 +
39 +export type TransitionCallback = 0 | 1;
40 +
41 +export const TransitionStart = 0;
42 +export const TransitionComplete = 1;
43 +
44 +export function processTransitionCallbacks(
45 + pendingTransitions: PendingTransitionCallbacks,
46 + endTime: number,
47 + callbacks: TransitionTracingCallbacks,
48 +): void {
49 + if (enableTransitionTracing) {
50 + if (pendingTransitions !== null) {
51 + const transitionStart = pendingTransitions.transitionStart;
52 + if (transitionStart !== null) {
53 + transitionStart.forEach(transition => {
54 + if (callbacks.onTransitionStart != null) {
55 + callbacks.onTransitionStart(
56 + transition.transitionName,
57 + transition.startTime,
58 + );
59 + }
60 + });
61 + }
62 +
63 + const transitionComplete = pendingTransitions.transitionComplete;
64 + if (transitionComplete !== null) {
65 + transitionComplete.forEach(transition => {
66 + if (callbacks.onTransitionComplete != null) {
67 + callbacks.onTransitionComplete(
68 + transition.transitionName,
69 + transition.startTime,
70 + endTime,
71 + );
72 + }
73 + });
74 + }
75 + }
76 + }
77 +}
packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js new
+77
@@ -0,0 +1,77 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {TransitionTracingCallbacks} from './ReactInternalTypes';
11 +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 +import {enableTransitionTracing} from 'shared/ReactFeatureFlags';
13 +
14 +export type SuspenseInfo = {name: string | null};
15 +
16 +export type TransitionObject = {
17 + transitionName: string,
18 + startTime: number,
19 +};
20 +
21 +export type PendingTransitionCallbacks = {
22 + transitionStart: Array<TransitionObject> | null,
23 + transitionComplete: Array<TransitionObject> | null,
24 +};
25 +
26 +export type Transition = {
27 + name: string,
28 + startTime: number,
29 +};
30 +
31 +export type BatchConfigTransition = {
32 + name?: string,
33 + startTime?: number,
34 + _updatedFibers?: Set<Fiber>,
35 +};
36 +
37 +export type Transitions = Array<Transition> | null;
38 +
39 +export type TransitionCallback = 0 | 1;
40 +
41 +export const TransitionStart = 0;
42 +export const TransitionComplete = 1;
43 +
44 +export function processTransitionCallbacks(
45 + pendingTransitions: PendingTransitionCallbacks,
46 + endTime: number,
47 + callbacks: TransitionTracingCallbacks,
48 +): void {
49 + if (enableTransitionTracing) {
50 + if (pendingTransitions !== null) {
51 + const transitionStart = pendingTransitions.transitionStart;
52 + if (transitionStart !== null) {
53 + transitionStart.forEach(transition => {
54 + if (callbacks.onTransitionStart != null) {
55 + callbacks.onTransitionStart(
56 + transition.transitionName,
57 + transition.startTime,
58 + );
59 + }
60 + });
61 + }
62 +
63 + const transitionComplete = pendingTransitions.transitionComplete;
64 + if (transitionComplete !== null) {
65 + transitionComplete.forEach(transition => {
66 + if (callbacks.onTransitionComplete != null) {
67 + callbacks.onTransitionComplete(
68 + transition.transitionName,
69 + transition.startTime,
70 + endTime,
71 + );
72 + }
73 + });
74 + }
75 + }
76 + }
77 +}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+93
@@ -15,6 +15,11 @@ import type {StackCursor} from './ReactFiberStack.new';
15 import type {Flags} from './ReactFiberFlags';
16 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
17 import type {EventPriority} from './ReactEventPriorities.new';
18 +import type {
19 + PendingTransitionCallbacks,
20 + TransitionObject,
21 + Transitions,
22 +} from './ReactFiberTracingMarkerComponent.new';
23
24 import {
25 warnAboutDeprecatedLifecycles,
@@ -32,6 +37,7 @@ import {
37 enableStrictEffects,
38 enableUpdaterTracking,
39 enableCache,
40 + enableTransitionTracing,
41 } from 'shared/ReactFeatureFlags';
42 import ReactSharedInternals from 'shared/ReactSharedInternals';
43 import is from 'shared/objectIs';
@@ -138,6 +144,8 @@ import {
144 getHighestPriorityLane,
145 addFiberToLanesMap,
146 movePendingFibersToMemoized,
147 + addTransitionToLanesMap,
148 + getTransitionsForLanes,
149 } from './ReactFiberLane.new';
150 import {
151 DiscreteEventPriority,
@@ -231,6 +239,7 @@ import {
239 isLegacyActEnvironment,
240 isConcurrentActEnvironment,
241 } from './ReactFiberAct.new';
242 +import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
243
244 const ceil = Math.ceil;
245
@@ -313,6 +322,51 @@ let workInProgressRootRenderTargetTime: number = Infinity;
322 // suspense heuristics and opt out of rendering more content.
323 const RENDER_TIMEOUT_MS = 500;
324
325 +let workInProgressTransitions: Transitions | null = null;
326 +export function getWorkInProgressTransitions() {
327 + return workInProgressTransitions;
328 +}
329 +
330 +let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null;
331 +
332 +export function addTransitionStartCallbackToPendingTransition(
333 + transition: TransitionObject,
334 +) {
335 + if (enableTransitionTracing) {
336 + if (currentPendingTransitionCallbacks === null) {
337 + currentPendingTransitionCallbacks = {
338 + transitionStart: [],
339 + transitionComplete: null,
340 + };
341 + }
342 +
343 + if (currentPendingTransitionCallbacks.transitionStart === null) {
344 + currentPendingTransitionCallbacks.transitionStart = [];
345 + }
346 +
347 + currentPendingTransitionCallbacks.transitionStart.push(transition);
348 + }
349 +}
350 +
351 +export function addTransitionCompleteCallbackToPendingTransition(
352 + transition: TransitionObject,
353 +) {
354 + if (enableTransitionTracing) {
355 + if (currentPendingTransitionCallbacks === null) {
356 + currentPendingTransitionCallbacks = {
357 + transitionStart: null,
358 + transitionComplete: [],
359 + };
360 + }
361 +
362 + if (currentPendingTransitionCallbacks.transitionComplete === null) {
363 + currentPendingTransitionCallbacks.transitionComplete = [];
364 + }
365 +
366 + currentPendingTransitionCallbacks.transitionComplete.push(transition);
367 + }
368 +}
369 +
370 function resetRenderTimer() {
371 workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
372 }
@@ -515,6 +569,17 @@ export function scheduleUpdateOnFiber(
569 }
570 }
571
572 + if (enableTransitionTracing) {
573 + const transition = ReactCurrentBatchConfig.transition;
574 + if (transition !== null) {
575 + if (transition.startTime === -1) {
576 + transition.startTime = now();
577 + }
578 +
579 + addTransitionToLanesMap(root, transition, lane);
580 + }
581 + }
582 +
583 if (root.isDehydrated && root.tag !== LegacyRoot) {
584 // This root's shell hasn't hydrated yet. Revert to client rendering.
585 if (workInProgressRoot === root) {
@@ -1244,6 +1309,7 @@ export function getExecutionContext(): ExecutionContext {
1309 export function deferredUpdates<A>(fn: () => A): A {
1310 const previousPriority = getCurrentUpdatePriority();
1311 const prevTransition = ReactCurrentBatchConfig.transition;
1312 +
1313 try {
1314 ReactCurrentBatchConfig.transition = null;
1315 setCurrentUpdatePriority(DefaultEventPriority);
@@ -1318,6 +1384,7 @@ export function flushSync(fn) {
1384
1385 const prevTransition = ReactCurrentBatchConfig.transition;
1386 const previousPriority = getCurrentUpdatePriority();
1387 +
1388 try {
1389 ReactCurrentBatchConfig.transition = null;
1390 setCurrentUpdatePriority(DiscreteEventPriority);
@@ -1329,6 +1396,7 @@ export function flushSync(fn) {
1396 } finally {
1397 setCurrentUpdatePriority(previousPriority);
1398 ReactCurrentBatchConfig.transition = prevTransition;
1399 +
1400 executionContext = prevExecutionContext;
1401 // Flush the immediate callbacks that were scheduled during this batch.
1402 // Note that this will happen even if batchedUpdates is higher up
@@ -1617,6 +1685,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1685 }
1686 }
1687
1688 + workInProgressTransitions = getTransitionsForLanes(root, lanes);
1689 prepareFreshStack(root, lanes);
1690 }
1691
@@ -1701,6 +1770,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1770 }
1771 }
1772
1773 + workInProgressTransitions = getTransitionsForLanes(root, lanes);
1774 resetRenderTimer();
1775 prepareFreshStack(root, lanes);
1776 }
@@ -1896,6 +1966,7 @@ function commitRoot(root: FiberRoot, recoverableErrors: null | Array<mixed>) {
1966 // layout phases. Should be able to remove.
1967 const previousUpdateLanePriority = getCurrentUpdatePriority();
1968 const prevTransition = ReactCurrentBatchConfig.transition;
1969 +
1970 try {
1971 ReactCurrentBatchConfig.transition = null;
1972 setCurrentUpdatePriority(DiscreteEventPriority);
@@ -2237,6 +2308,27 @@ function commitRootImpl(
2308 // If layout work was scheduled, flush it now.
2309 flushSyncCallbacks();
2310
2311 + if (enableTransitionTracing) {
2312 + const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
2313 + const prevRootTransitionCallbacks = root.transitionCallbacks;
2314 + if (
2315 + prevPendingTransitionCallbacks !== null &&
2316 + prevRootTransitionCallbacks !== null
2317 + ) {
2318 + // TODO(luna) Refactor this code into the Host Config
2319 + const endTime = now();
2320 + currentPendingTransitionCallbacks = null;
2321 +
2322 + scheduleCallback(IdleSchedulerPriority, () =>
2323 + processTransitionCallbacks(
2324 + prevPendingTransitionCallbacks,
2325 + endTime,
2326 + prevRootTransitionCallbacks,
2327 + ),
2328 + );
2329 + }
2330 + }
2331 +
2332 if (__DEV__) {
2333 if (enableDebugTracing) {
2334 logCommitStopped();
@@ -2286,6 +2378,7 @@ export function flushPassiveEffects(): boolean {
2378 const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2379 const prevTransition = ReactCurrentBatchConfig.transition;
2380 const previousPriority = getCurrentUpdatePriority();
2381 +
2382 try {
2383 ReactCurrentBatchConfig.transition = null;
2384 setCurrentUpdatePriority(priority);
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+93
@@ -15,6 +15,11 @@ import type {StackCursor} from './ReactFiberStack.old';
15 import type {Flags} from './ReactFiberFlags';
16 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
17 import type {EventPriority} from './ReactEventPriorities.old';
18 +import type {
19 + PendingTransitionCallbacks,
20 + TransitionObject,
21 + Transitions,
22 +} from './ReactFiberTracingMarkerComponent.old';
23
24 import {
25 warnAboutDeprecatedLifecycles,
@@ -32,6 +37,7 @@ import {
37 enableStrictEffects,
38 enableUpdaterTracking,
39 enableCache,
40 + enableTransitionTracing,
41 } from 'shared/ReactFeatureFlags';
42 import ReactSharedInternals from 'shared/ReactSharedInternals';
43 import is from 'shared/objectIs';
@@ -138,6 +144,8 @@ import {
144 getHighestPriorityLane,
145 addFiberToLanesMap,
146 movePendingFibersToMemoized,
147 + addTransitionToLanesMap,
148 + getTransitionsForLanes,
149 } from './ReactFiberLane.old';
150 import {
151 DiscreteEventPriority,
@@ -231,6 +239,7 @@ import {
239 isLegacyActEnvironment,
240 isConcurrentActEnvironment,
241 } from './ReactFiberAct.old';
242 +import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old';
243
244 const ceil = Math.ceil;
245
@@ -313,6 +322,51 @@ let workInProgressRootRenderTargetTime: number = Infinity;
322 // suspense heuristics and opt out of rendering more content.
323 const RENDER_TIMEOUT_MS = 500;
324
325 +let workInProgressTransitions: Transitions | null = null;
326 +export function getWorkInProgressTransitions() {
327 + return workInProgressTransitions;
328 +}
329 +
330 +let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null;
331 +
332 +export function addTransitionStartCallbackToPendingTransition(
333 + transition: TransitionObject,
334 +) {
335 + if (enableTransitionTracing) {
336 + if (currentPendingTransitionCallbacks === null) {
337 + currentPendingTransitionCallbacks = {
338 + transitionStart: [],
339 + transitionComplete: null,
340 + };
341 + }
342 +
343 + if (currentPendingTransitionCallbacks.transitionStart === null) {
344 + currentPendingTransitionCallbacks.transitionStart = [];
345 + }
346 +
347 + currentPendingTransitionCallbacks.transitionStart.push(transition);
348 + }
349 +}
350 +
351 +export function addTransitionCompleteCallbackToPendingTransition(
352 + transition: TransitionObject,
353 +) {
354 + if (enableTransitionTracing) {
355 + if (currentPendingTransitionCallbacks === null) {
356 + currentPendingTransitionCallbacks = {
357 + transitionStart: null,
358 + transitionComplete: [],
359 + };
360 + }
361 +
362 + if (currentPendingTransitionCallbacks.transitionComplete === null) {
363 + currentPendingTransitionCallbacks.transitionComplete = [];
364 + }
365 +
366 + currentPendingTransitionCallbacks.transitionComplete.push(transition);
367 + }
368 +}
369 +
370 function resetRenderTimer() {
371 workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
372 }
@@ -515,6 +569,17 @@ export function scheduleUpdateOnFiber(
569 }
570 }
571
572 + if (enableTransitionTracing) {
573 + const transition = ReactCurrentBatchConfig.transition;
574 + if (transition !== null) {
575 + if (transition.startTime === -1) {
576 + transition.startTime = now();
577 + }
578 +
579 + addTransitionToLanesMap(root, transition, lane);
580 + }
581 + }
582 +
583 if (root.isDehydrated && root.tag !== LegacyRoot) {
584 // This root's shell hasn't hydrated yet. Revert to client rendering.
585 if (workInProgressRoot === root) {
@@ -1244,6 +1309,7 @@ export function getExecutionContext(): ExecutionContext {
1309 export function deferredUpdates<A>(fn: () => A): A {
1310 const previousPriority = getCurrentUpdatePriority();
1311 const prevTransition = ReactCurrentBatchConfig.transition;
1312 +
1313 try {
1314 ReactCurrentBatchConfig.transition = null;
1315 setCurrentUpdatePriority(DefaultEventPriority);
@@ -1318,6 +1384,7 @@ export function flushSync(fn) {
1384
1385 const prevTransition = ReactCurrentBatchConfig.transition;
1386 const previousPriority = getCurrentUpdatePriority();
1387 +
1388 try {
1389 ReactCurrentBatchConfig.transition = null;
1390 setCurrentUpdatePriority(DiscreteEventPriority);
@@ -1329,6 +1396,7 @@ export function flushSync(fn) {
1396 } finally {
1397 setCurrentUpdatePriority(previousPriority);
1398 ReactCurrentBatchConfig.transition = prevTransition;
1399 +
1400 executionContext = prevExecutionContext;
1401 // Flush the immediate callbacks that were scheduled during this batch.
1402 // Note that this will happen even if batchedUpdates is higher up
@@ -1617,6 +1685,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1685 }
1686 }
1687
1688 + workInProgressTransitions = getTransitionsForLanes(root, lanes);
1689 prepareFreshStack(root, lanes);
1690 }
1691
@@ -1701,6 +1770,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1770 }
1771 }
1772
1773 + workInProgressTransitions = getTransitionsForLanes(root, lanes);
1774 resetRenderTimer();
1775 prepareFreshStack(root, lanes);
1776 }
@@ -1896,6 +1966,7 @@ function commitRoot(root: FiberRoot, recoverableErrors: null | Array<mixed>) {
1966 // layout phases. Should be able to remove.
1967 const previousUpdateLanePriority = getCurrentUpdatePriority();
1968 const prevTransition = ReactCurrentBatchConfig.transition;
1969 +
1970 try {
1971 ReactCurrentBatchConfig.transition = null;
1972 setCurrentUpdatePriority(DiscreteEventPriority);
@@ -2237,6 +2308,27 @@ function commitRootImpl(
2308 // If layout work was scheduled, flush it now.
2309 flushSyncCallbacks();
2310
2311 + if (enableTransitionTracing) {
2312 + const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
2313 + const prevRootTransitionCallbacks = root.transitionCallbacks;
2314 + if (
2315 + prevPendingTransitionCallbacks !== null &&
2316 + prevRootTransitionCallbacks !== null
2317 + ) {
2318 + // TODO(luna) Refactor this code into the Host Config
2319 + const endTime = now();
2320 + currentPendingTransitionCallbacks = null;
2321 +
2322 + scheduleCallback(IdleSchedulerPriority, () =>
2323 + processTransitionCallbacks(
2324 + prevPendingTransitionCallbacks,
2325 + endTime,
2326 + prevRootTransitionCallbacks,
2327 + ),
2328 + );
2329 + }
2330 + }
2331 +
2332 if (__DEV__) {
2333 if (enableDebugTracing) {
2334 logCommitStopped();
@@ -2286,6 +2378,7 @@ export function flushPassiveEffects(): boolean {
2378 const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2379 const prevTransition = ReactCurrentBatchConfig.transition;
2380 const previousPriority = getCurrentUpdatePriority();
2381 +
2382 try {
2383 ReactCurrentBatchConfig.transition = null;
2384 setCurrentUpdatePriority(priority);
packages/react-reconciler/src/ReactInternalTypes.js
+7 -1
@@ -15,6 +15,7 @@ import type {
15 MutableSourceGetSnapshotFn,
16 MutableSourceVersion,
17 MutableSource,
18 + StartTransitionOptions,
19 } from 'shared/ReactTypes';
20 import type {SuspenseInstance} from './ReactFiberHostConfig';
21 import type {WorkTag} from './ReactWorkTags';
@@ -25,6 +26,7 @@ import type {RootTag} from './ReactRootTags';
26 import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
27 import type {Wakeable} from 'shared/ReactTypes';
28 import type {Cache} from './ReactFiberCacheComponent.old';
29 +import type {Transitions} from './ReactFiberTracingMarkerComponent.new';
30
31 // Unwind Circular: moved from ReactFiberHooks.old
32 export type HookType =
@@ -320,6 +322,7 @@ export type TransitionTracingCallbacks = {
322 // The following fields are only used in transition tracing in Profile builds
323 type TransitionTracingOnlyFiberRootProperties = {|
324 transitionCallbacks: null | TransitionTracingCallbacks,
325 + transitionLanes: Array<Transitions>,
326 |};
327
328 // Exported FiberRoot type includes all properties,
@@ -369,7 +372,10 @@ export type Dispatcher = {|
372 ): void,
373 useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
374 useDeferredValue<T>(value: T): T,
372 - useTransition(): [boolean, (() => void) => void],
375 + useTransition(): [
376 + boolean,
377 + (callback: () => void, options?: StartTransitionOptions) => void,
378 + ],
379 useMutableSource<Source, Snapshot>(
380 source: MutableSource<Source>,
381 getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js new
+101
@@ -0,0 +1,101 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +
11 +let React;
12 +let ReactNoop;
13 +let Scheduler;
14 +let act;
15 +
16 +let useState;
17 +let startTransition;
18 +
19 +describe('ReactInteractionTracing', () => {
20 + beforeEach(() => {
21 + jest.resetModules();
22 +
23 + React = require('react');
24 + ReactNoop = require('react-noop-renderer');
25 + Scheduler = require('scheduler');
26 +
27 + act = require('jest-react').act;
28 +
29 + useState = React.useState;
30 + startTransition = React.startTransition;
31 + });
32 +
33 + function Text({text}) {
34 + Scheduler.unstable_yieldValue(text);
35 + return text;
36 + }
37 +
38 + function advanceTimers(ms) {
39 + // Note: This advances Jest's virtual time but not React's. Use
40 + // ReactNoop.expire for that.
41 + if (typeof ms !== 'number') {
42 + throw new Error('Must specify ms');
43 + }
44 + jest.advanceTimersByTime(ms);
45 + // Wait until the end of the current tick
46 + // We cannot use a timer since we're faking them
47 + return Promise.resolve().then(() => {});
48 + }
49 +
50 + // @gate enableTransitionTracing
51 + it('should correctly trace basic interaction', async () => {
52 + const transitionCallbacks = {
53 + onTransitionStart: (name, startTime) => {
54 + Scheduler.unstable_yieldValue(
55 + `onTransitionStart(${name}, ${startTime})`,
56 + );
57 + },
58 + onTransitionComplete: (name, startTime, endTime) => {
59 + Scheduler.unstable_yieldValue(
60 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
61 + );
62 + },
63 + };
64 +
65 + let navigateToPageTwo;
66 + function App() {
67 + const [navigate, setNavigate] = useState(false);
68 + navigateToPageTwo = () => {
69 + setNavigate(true);
70 + };
71 +
72 + return (
73 + <div>
74 + {navigate ? <Text text="Page Two" /> : <Text text="Page One" />}
75 + </div>
76 + );
77 + }
78 +
79 + const root = ReactNoop.createRoot({transitionCallbacks});
80 + await act(async () => {
81 + root.render(<App />);
82 + ReactNoop.expire(1000);
83 + await advanceTimers(1000);
84 +
85 + expect(Scheduler).toFlushAndYield(['Page One']);
86 +
87 + await act(async () => {
88 + startTransition(() => navigateToPageTwo(), {name: 'page transition'});
89 +
90 + ReactNoop.expire(1000);
91 + await advanceTimers(1000);
92 +
93 + expect(Scheduler).toFlushAndYield([
94 + 'Page Two',
95 + 'onTransitionStart(page transition, 1000)',
96 + 'onTransitionComplete(page transition, 1000, 2000)',
97 + ]);
98 + });
99 + });
100 + });
101 +});
packages/react-server/src/ReactFizzHooks.js
+5 -1
@@ -14,6 +14,7 @@ import type {
14 MutableSourceGetSnapshotFn,
15 MutableSourceSubscribeFn,
16 ReactContext,
17 + StartTransitionOptions,
18 } from 'shared/ReactTypes';
19
20 import type {ResponseState} from './ReactServerFormatConfig';
@@ -505,7 +506,10 @@ function unsupportedStartTransition() {
506 throw new Error('startTransition cannot be called during server rendering.');
507 }
508
508 -function useTransition(): [boolean, (callback: () => void) => void] {
509 +function useTransition(): [
510 + boolean,
511 + (callback: () => void, options?: StartTransitionOptions) => void,
512 +] {
513 resolveCurrentlyRenderingComponent();
514 return [false, unsupportedStartTransition];
515 }
packages/react/src/ReactCurrentBatchConfig.js
+2 -2
@@ -7,10 +7,10 @@
7 * @flow
8 */
9
10 -import type {Transition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent.new';
10 +import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent.new';
11
12 type BatchConfig = {
13 - transition: Transition | null,
13 + transition: BatchConfigTransition | null,
14 };
15 /**
16 * Keeps track of the current batch's configuration such as how long an update
packages/react/src/ReactHooks.js
+5 -1
@@ -13,6 +13,7 @@ import type {
13 MutableSourceGetSnapshotFn,
14 MutableSourceSubscribeFn,
15 ReactContext,
16 + StartTransitionOptions,
17 } from 'shared/ReactTypes';
18
19 import ReactCurrentDispatcher from './ReactCurrentDispatcher';
@@ -158,7 +159,10 @@ export function useDebugValue<T>(
159
160 export const emptyObject = {};
161
161 -export function useTransition(): [boolean, (() => void) => void] {
162 +export function useTransition(): [
163 + boolean,
164 + (callback: () => void, options?: StartTransitionOptions) => void,
165 +] {
166 const dispatcher = resolveDispatcher();
167 return dispatcher.useTransition();
168 }
packages/react/src/ReactStartTransition.js
+14 -1
@@ -6,10 +6,15 @@
6 *
7 * @flow
8 */
9 +import type {StartTransitionOptions} from 'shared/ReactTypes';
10
11 import ReactCurrentBatchConfig from './ReactCurrentBatchConfig';
12 +import {enableTransitionTracing} from 'shared/ReactFeatureFlags';
13
12 -export function startTransition(scope: () => void) {
14 +export function startTransition(
15 + scope: () => void,
16 + options?: StartTransitionOptions,
17 +) {
18 const prevTransition = ReactCurrentBatchConfig.transition;
19 ReactCurrentBatchConfig.transition = {};
20 const currentTransition = ReactCurrentBatchConfig.transition;
@@ -17,6 +22,14 @@ export function startTransition(scope: () => void) {
22 if (__DEV__) {
23 ReactCurrentBatchConfig.transition._updatedFibers = new Set();
24 }
25 +
26 + if (enableTransitionTracing) {
27 + if (options !== undefined && options.name !== undefined) {
28 + ReactCurrentBatchConfig.transition.name = options.name;
29 + ReactCurrentBatchConfig.transition.startTime = -1;
30 + }
31 + }
32 +
33 try {
34 scope();
35 } finally {
packages/shared/ReactTypes.js
+4
@@ -171,3 +171,7 @@ export type OffscreenMode =
171 | 'hidden'
172 | 'unstable-defer-without-hiding'
173 | 'visible';
174 +
175 +export type StartTransitionOptions = {
176 + name?: string,
177 +};
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -26,6 +26,7 @@ export const enableSyncDefaultUpdates = __VARIANT__;
26 export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
27 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
28 export const enableClientRenderFallbackOnHydrationMismatch = __VARIANT__;
29 +export const enableTransitionTracing = __VARIANT__;
30
31 // Enable this flag to help with concurrent mode debugging.
32 // It logs information to the console about React scheduling, rendering, and commit phases.