diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 0ffec00bd3f8d..b7159859060e7 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -13,6 +13,7 @@ import type { MutableSourceSubscribeFn, ReactContext, ReactProviderType, + StartTransitionOptions, } from 'shared/ReactTypes'; import type { Fiber, @@ -291,7 +292,10 @@ function useSyncExternalStore( return value; } -function useTransition(): [boolean, (() => void) => void] { +function useTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { // useTransition() composes multiple hooks internally. // Advance the current hook index the same number of times // so that subsequent hooks have the right memoized state. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 9b8de1df80183..4435fef438938 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -226,6 +226,7 @@ import { markSkippedUpdateLanes, getWorkInProgressRoot, pushRenderLanes, + getWorkInProgressTransitions, } from './ReactFiberWorkLoop.new'; import {setWorkInProgressVersion} from './ReactMutableSource.new'; import { @@ -1336,6 +1337,10 @@ function updateHostRoot(current, workInProgress, renderLanes) { } } + if (enableTransitionTracing) { + workInProgress.memoizedState.transitions = getWorkInProgressTransitions(); + } + // Caution: React DevTools currently depends on this property // being called "element". const nextChildren = nextState.element; @@ -3495,12 +3500,15 @@ function attemptEarlyBailoutIfNoScheduledUpdate( switch (workInProgress.tag) { case HostRoot: pushHostRootContext(workInProgress); + const root: FiberRoot = workInProgress.stateNode; if (enableCache) { - const root: FiberRoot = workInProgress.stateNode; const cache: Cache = current.memoizedState.cache; pushCacheProvider(workInProgress, cache); pushRootCachePool(root); } + if (enableTransitionTracing) { + workInProgress.memoizedState.transitions = getWorkInProgressTransitions(); + } resetHydrationState(); break; case HostComponent: diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 457088598e51b..b037863aa40de 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -226,6 +226,7 @@ import { markSkippedUpdateLanes, getWorkInProgressRoot, pushRenderLanes, + getWorkInProgressTransitions, } from './ReactFiberWorkLoop.old'; import {setWorkInProgressVersion} from './ReactMutableSource.old'; import { @@ -1336,6 +1337,10 @@ function updateHostRoot(current, workInProgress, renderLanes) { } } + if (enableTransitionTracing) { + workInProgress.memoizedState.transitions = getWorkInProgressTransitions(); + } + // Caution: React DevTools currently depends on this property // being called "element". const nextChildren = nextState.element; @@ -3495,12 +3500,15 @@ function attemptEarlyBailoutIfNoScheduledUpdate( switch (workInProgress.tag) { case HostRoot: pushHostRootContext(workInProgress); + const root: FiberRoot = workInProgress.stateNode; if (enableCache) { - const root: FiberRoot = workInProgress.stateNode; const cache: Cache = current.memoizedState.cache; pushCacheProvider(workInProgress, cache); pushRootCachePool(root); } + if (enableTransitionTracing) { + workInProgress.memoizedState.transitions = getWorkInProgressTransitions(); + } resetHydrationState(); break; case HostComponent: diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 5ff6830096687..75553555b928d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -40,6 +40,7 @@ import { enableSuspenseLayoutEffectSemantics, enableUpdaterTracking, enableCache, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -132,6 +133,8 @@ import { markCommitTimeOfFallback, enqueuePendingPassiveProfilerEffect, restorePendingUpdaters, + addTransitionStartCallbackToPendingTransition, + addTransitionCompleteCallbackToPendingTransition, } from './ReactFiberWorkLoop.new'; import { NoFlags as NoHookEffect, @@ -156,6 +159,7 @@ import { onCommitUnmount, } from './ReactFiberDevToolsHook.new'; import {releaseCache, retainCache} from './ReactFiberCacheComponent.new'; +import {clearTransitionsForLanes} from './ReactFiberLane.new'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -983,8 +987,10 @@ function commitLayoutEffectOnFiber( case IncompleteClassComponent: case ScopeComponent: case OffscreenComponent: - case LegacyHiddenComponent: + case LegacyHiddenComponent: { break; + } + default: throw new Error( 'This unit of work tag should not have side-effects. This error is ' + @@ -2137,13 +2143,13 @@ export function commitMutationEffects( inProgressRoot = root; nextEffect = firstChild; - commitMutationEffects_begin(root); + commitMutationEffects_begin(root, committedLanes); inProgressLanes = null; inProgressRoot = null; } -function commitMutationEffects_begin(root: FiberRoot) { +function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) { while (nextEffect !== null) { const fiber = nextEffect; @@ -2166,17 +2172,17 @@ function commitMutationEffects_begin(root: FiberRoot) { ensureCorrectReturnPointer(child, fiber); nextEffect = child; } else { - commitMutationEffects_complete(root); + commitMutationEffects_complete(root, lanes); } } } -function commitMutationEffects_complete(root: FiberRoot) { +function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) { while (nextEffect !== null) { const fiber = nextEffect; setCurrentDebugFiberInDEV(fiber); try { - commitMutationEffectsOnFiber(fiber, root); + commitMutationEffectsOnFiber(fiber, root, lanes); } catch (error) { reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); @@ -2194,13 +2200,43 @@ function commitMutationEffects_complete(root: FiberRoot) { } } -function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) { +function commitMutationEffectsOnFiber( + finishedWork: Fiber, + root: FiberRoot, + lanes: Lanes, +) { // TODO: The factoring of this phase could probably be improved. Consider // switching on the type of work before checking the flags. That's what // we do in all the other phases. I think this one is only different // because of the shared reconciliation logic below. const flags = finishedWork.flags; + if (enableTransitionTracing) { + switch (finishedWork.tag) { + case HostRoot: { + const state = finishedWork.memoizedState; + const transitions = state.transitions; + if (transitions !== null) { + transitions.forEach(transition => { + // TODO(luna) Do we want to log TransitionStart in the startTransition callback instead? + addTransitionStartCallbackToPendingTransition({ + transitionName: transition.name, + startTime: transition.startTime, + }); + + addTransitionCompleteCallbackToPendingTransition({ + transitionName: transition.name, + startTime: transition.startTime, + }); + }); + + clearTransitionsForLanes(root, lanes); + state.transitions = null; + } + } + } + } + if (flags & ContentReset) { commitResetTextContent(finishedWork); } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 769974e048d51..23e9d6070c9a2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -40,6 +40,7 @@ import { enableSuspenseLayoutEffectSemantics, enableUpdaterTracking, enableCache, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -132,6 +133,8 @@ import { markCommitTimeOfFallback, enqueuePendingPassiveProfilerEffect, restorePendingUpdaters, + addTransitionStartCallbackToPendingTransition, + addTransitionCompleteCallbackToPendingTransition, } from './ReactFiberWorkLoop.old'; import { NoFlags as NoHookEffect, @@ -156,6 +159,7 @@ import { onCommitUnmount, } from './ReactFiberDevToolsHook.old'; import {releaseCache, retainCache} from './ReactFiberCacheComponent.old'; +import {clearTransitionsForLanes} from './ReactFiberLane.old'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -983,8 +987,10 @@ function commitLayoutEffectOnFiber( case IncompleteClassComponent: case ScopeComponent: case OffscreenComponent: - case LegacyHiddenComponent: + case LegacyHiddenComponent: { break; + } + default: throw new Error( 'This unit of work tag should not have side-effects. This error is ' + @@ -2137,13 +2143,13 @@ export function commitMutationEffects( inProgressRoot = root; nextEffect = firstChild; - commitMutationEffects_begin(root); + commitMutationEffects_begin(root, committedLanes); inProgressLanes = null; inProgressRoot = null; } -function commitMutationEffects_begin(root: FiberRoot) { +function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) { while (nextEffect !== null) { const fiber = nextEffect; @@ -2166,17 +2172,17 @@ function commitMutationEffects_begin(root: FiberRoot) { ensureCorrectReturnPointer(child, fiber); nextEffect = child; } else { - commitMutationEffects_complete(root); + commitMutationEffects_complete(root, lanes); } } } -function commitMutationEffects_complete(root: FiberRoot) { +function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) { while (nextEffect !== null) { const fiber = nextEffect; setCurrentDebugFiberInDEV(fiber); try { - commitMutationEffectsOnFiber(fiber, root); + commitMutationEffectsOnFiber(fiber, root, lanes); } catch (error) { reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); @@ -2194,13 +2200,43 @@ function commitMutationEffects_complete(root: FiberRoot) { } } -function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) { +function commitMutationEffectsOnFiber( + finishedWork: Fiber, + root: FiberRoot, + lanes: Lanes, +) { // TODO: The factoring of this phase could probably be improved. Consider // switching on the type of work before checking the flags. That's what // we do in all the other phases. I think this one is only different // because of the shared reconciliation logic below. const flags = finishedWork.flags; + if (enableTransitionTracing) { + switch (finishedWork.tag) { + case HostRoot: { + const state = finishedWork.memoizedState; + const transitions = state.transitions; + if (transitions !== null) { + transitions.forEach(transition => { + // TODO(luna) Do we want to log TransitionStart in the startTransition callback instead? + addTransitionStartCallbackToPendingTransition({ + transitionName: transition.name, + startTime: transition.startTime, + }); + + addTransitionCompleteCallbackToPendingTransition({ + transitionName: transition.name, + startTime: transition.startTime, + }); + }); + + clearTransitionsForLanes(root, lanes); + state.transitions = null; + } + } + } + } + if (flags & ContentReset) { commitResetTextContent(finishedWork); } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index bb01e2b8a2a26..50ab1d3d9672a 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -62,6 +62,7 @@ import { OffscreenComponent, LegacyHiddenComponent, CacheComponent, + TracingMarkerComponent, } from './ReactWorkTags'; import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; import { @@ -141,6 +142,7 @@ import { enableCache, enableSuspenseLayoutEffectSemantics, enablePersistentOffscreenHostContainer, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import { renderDidSuspend, @@ -1570,8 +1572,15 @@ function completeWork( } popCacheProvider(workInProgress, cache); bubbleProperties(workInProgress); - return null; } + return null; + } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + // Bubble subtree flags before so we can set the flag property + bubbleProperties(workInProgress); + } + return null; } } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 0d9de679a0f43..81a51eaff8ebc 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -62,6 +62,7 @@ import { OffscreenComponent, LegacyHiddenComponent, CacheComponent, + TracingMarkerComponent, } from './ReactWorkTags'; import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; import { @@ -141,6 +142,7 @@ import { enableCache, enableSuspenseLayoutEffectSemantics, enablePersistentOffscreenHostContainer, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import { renderDidSuspend, @@ -1570,8 +1572,15 @@ function completeWork( } popCacheProvider(workInProgress, cache); bubbleProperties(workInProgress); - return null; } + return null; + } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + // Bubble subtree flags before so we can set the flag property + bubbleProperties(workInProgress); + } + return null; } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 31e716935bc34..3f64a7d8f9178 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -12,6 +12,7 @@ import type { MutableSourceGetSnapshotFn, MutableSourceSubscribeFn, ReactContext, + StartTransitionOptions, } from 'shared/ReactTypes'; import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; @@ -31,6 +32,7 @@ import { enableLazyContextPropagation, enableSuspenseLayoutEffectSemantics, enableUseMutableSource, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import { @@ -110,6 +112,7 @@ import { } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; import {getTreeId} from './ReactFiberTreeContext.new'; +import {now} from './Scheduler'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -1967,7 +1970,7 @@ function rerenderDeferredValue(value: T): T { return prevValue; } -function startTransition(setPending, callback) { +function startTransition(setPending, callback, options) { const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority( higherEventPriority(previousPriority, ContinuousEventPriority), @@ -1979,6 +1982,13 @@ function startTransition(setPending, callback) { ReactCurrentBatchConfig.transition = {}; const currentTransition = ReactCurrentBatchConfig.transition; + if (enableTransitionTracing) { + if (options !== undefined && options.name !== undefined) { + ReactCurrentBatchConfig.transition.name = options.name; + ReactCurrentBatchConfig.transition.startTime = now(); + } + } + if (__DEV__) { ReactCurrentBatchConfig.transition._updatedFibers = new Set(); } @@ -1990,6 +2000,7 @@ function startTransition(setPending, callback) { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + if (__DEV__) { if (prevTransition === null && currentTransition._updatedFibers) { const updatedFibersCount = currentTransition._updatedFibers.size; @@ -2006,7 +2017,10 @@ function startTransition(setPending, callback) { } } -function mountTransition(): [boolean, (() => void) => void] { +function mountTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { const [isPending, setPending] = mountState(false); // The `start` method never changes. const start = startTransition.bind(null, setPending); @@ -2015,14 +2029,20 @@ function mountTransition(): [boolean, (() => void) => void] { return [isPending, start]; } -function updateTransition(): [boolean, (() => void) => void] { +function updateTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { const [isPending] = updateState(false); const hook = updateWorkInProgressHook(); const start = hook.memoizedState; return [isPending, start]; } -function rerenderTransition(): [boolean, (() => void) => void] { +function rerenderTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { const [isPending] = rerenderState(false); const hook = updateWorkInProgressHook(); const start = hook.memoizedState; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index bd9e2488a8841..d55cac9b26c74 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -12,6 +12,7 @@ import type { MutableSourceGetSnapshotFn, MutableSourceSubscribeFn, ReactContext, + StartTransitionOptions, } from 'shared/ReactTypes'; import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.old'; @@ -31,6 +32,7 @@ import { enableLazyContextPropagation, enableSuspenseLayoutEffectSemantics, enableUseMutableSource, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import { @@ -110,6 +112,7 @@ import { } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import {getTreeId} from './ReactFiberTreeContext.old'; +import {now} from './Scheduler'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -1967,7 +1970,7 @@ function rerenderDeferredValue(value: T): T { return prevValue; } -function startTransition(setPending, callback) { +function startTransition(setPending, callback, options) { const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority( higherEventPriority(previousPriority, ContinuousEventPriority), @@ -1979,6 +1982,13 @@ function startTransition(setPending, callback) { ReactCurrentBatchConfig.transition = {}; const currentTransition = ReactCurrentBatchConfig.transition; + if (enableTransitionTracing) { + if (options !== undefined && options.name !== undefined) { + ReactCurrentBatchConfig.transition.name = options.name; + ReactCurrentBatchConfig.transition.startTime = now(); + } + } + if (__DEV__) { ReactCurrentBatchConfig.transition._updatedFibers = new Set(); } @@ -1990,6 +2000,7 @@ function startTransition(setPending, callback) { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + if (__DEV__) { if (prevTransition === null && currentTransition._updatedFibers) { const updatedFibersCount = currentTransition._updatedFibers.size; @@ -2006,7 +2017,10 @@ function startTransition(setPending, callback) { } } -function mountTransition(): [boolean, (() => void) => void] { +function mountTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { const [isPending, setPending] = mountState(false); // The `start` method never changes. const start = startTransition.bind(null, setPending); @@ -2015,14 +2029,20 @@ function mountTransition(): [boolean, (() => void) => void] { return [isPending, start]; } -function updateTransition(): [boolean, (() => void) => void] { +function updateTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { const [isPending] = updateState(false); const hook = updateWorkInProgressHook(); const start = hook.memoizedState; return [isPending, start]; } -function rerenderTransition(): [boolean, (() => void) => void] { +function rerenderTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { const [isPending] = rerenderState(false); const hook = updateWorkInProgressHook(); const start = hook.memoizedState; diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 452107c907b6f..597f996331999 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -8,6 +8,10 @@ */ import type {FiberRoot} from './ReactInternalTypes'; +import type { + Transition, + Transitions, +} from './ReactFiberTracingMarkerComponent.new'; // TODO: Ideally these types would be opaque but that doesn't work well with // our reconciler fork infra, since these leak into non-reconciler packages. @@ -20,6 +24,7 @@ import { enableSchedulingProfiler, enableUpdaterTracking, allowConcurrentByDefault, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; @@ -796,3 +801,75 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) { lanes &= ~lane; } } + +export function addTransitionToLanesMap( + root: FiberRoot, + transition: Transition, + lane: Lane, +) { + if (enableTransitionTracing) { + const transitionLanesMap = root.transitionLanes; + const index = laneToIndex(lane); + let transitions = transitionLanesMap[index]; + if (transitions === null) { + transitions = []; + } + transitions.push(transition); + + transitionLanesMap[index] = transitions; + } +} + +export function getTransitionsForLanes( + root: FiberRoot, + lanes: Lane | Lanes, +): Transitions | null { + if (!enableTransitionTracing) { + return null; + } + + const transitionsForLanes = []; + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + const transitions = root.transitionLanes[index]; + if (transitions !== null) { + transitions.forEach(transition => { + transitionsForLanes.push(transition); + }); + } + + lanes &= ~lane; + } + + if (transitionsForLanes.length === 0) { + return null; + } + + return transitionsForLanes; +} + +export function clearTransitionsForLanes(root: FiberRoot, lanes: Lane | Lanes) { + if (!enableTransitionTracing) { + return; + } + + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + + const transitions = root.transitionLanes[index]; + if (transitions !== null) { + root.transitionLanes[index] = null; + } else { + if (__DEV__) { + console.error( + 'React Bug: transition lanes accessed out of bounds index: %s', + index.toString(), + ); + } + } + + lanes &= ~lane; + } +} diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 8360d2630daca..9f366c9ade886 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -8,6 +8,10 @@ */ import type {FiberRoot} from './ReactInternalTypes'; +import type { + Transition, + Transitions, +} from './ReactFiberTracingMarkerComponent.old'; // TODO: Ideally these types would be opaque but that doesn't work well with // our reconciler fork infra, since these leak into non-reconciler packages. @@ -20,6 +24,7 @@ import { enableSchedulingProfiler, enableUpdaterTracking, allowConcurrentByDefault, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; @@ -796,3 +801,75 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) { lanes &= ~lane; } } + +export function addTransitionToLanesMap( + root: FiberRoot, + transition: Transition, + lane: Lane, +) { + if (enableTransitionTracing) { + const transitionLanesMap = root.transitionLanes; + const index = laneToIndex(lane); + let transitions = transitionLanesMap[index]; + if (transitions === null) { + transitions = []; + } + transitions.push(transition); + + transitionLanesMap[index] = transitions; + } +} + +export function getTransitionsForLanes( + root: FiberRoot, + lanes: Lane | Lanes, +): Transitions | null { + if (!enableTransitionTracing) { + return null; + } + + const transitionsForLanes = []; + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + const transitions = root.transitionLanes[index]; + if (transitions !== null) { + transitions.forEach(transition => { + transitionsForLanes.push(transition); + }); + } + + lanes &= ~lane; + } + + if (transitionsForLanes.length === 0) { + return null; + } + + return transitionsForLanes; +} + +export function clearTransitionsForLanes(root: FiberRoot, lanes: Lane | Lanes) { + if (!enableTransitionTracing) { + return; + } + + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + + const transitions = root.transitionLanes[index]; + if (transitions !== null) { + root.transitionLanes[index] = null; + } else { + if (__DEV__) { + console.error( + 'React Bug: transition lanes accessed out of bounds index: %s', + index.toString(), + ); + } + } + + lanes &= ~lane; + } +} diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 3a4546008ad50..00dd694be4f5f 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -13,6 +13,8 @@ import type { TransitionTracingCallbacks, } from './ReactInternalTypes'; import type {RootTag} from './ReactRootTags'; +import type {Cache} from './ReactFiberCacheComponent.new'; +import type {Transitions} from './ReactFiberTracingMarkerComponent.new'; import {noTimeout, supportsHydration} from './ReactFiberHostConfig'; import {createHostRootFiber} from './ReactFiber.new'; @@ -35,6 +37,12 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.new'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.new'; +export type RootState = { + element: any, + cache: Cache | null, + transitions: Transitions | null, +}; + function FiberRootNode( containerInfo, tag, @@ -85,6 +93,10 @@ function FiberRootNode( if (enableTransitionTracing) { this.transitionCallbacks = null; + const transitionLanesMap = (this.transitionLanes = []); + for (let i = 0; i < TotalLanes; i++) { + transitionLanesMap.push(null); + } } if (enableProfilerTimer && enableProfilerCommitHooks) { @@ -165,14 +177,17 @@ export function createFiberRoot( // retained separately. root.pooledCache = initialCache; retainCache(initialCache); - const initialState = { + const initialState: RootState = { element: null, cache: initialCache, + transitions: null, }; uninitializedFiber.memoizedState = initialState; } else { - const initialState = { + const initialState: RootState = { element: null, + cache: null, + transitions: null, }; uninitializedFiber.memoizedState = initialState; } diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 56ebcd5cccac4..1e561e49facb3 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -13,6 +13,8 @@ import type { TransitionTracingCallbacks, } from './ReactInternalTypes'; import type {RootTag} from './ReactRootTags'; +import type {Cache} from './ReactFiberCacheComponent.old'; +import type {Transitions} from './ReactFiberTracingMarkerComponent.old'; import {noTimeout, supportsHydration} from './ReactFiberHostConfig'; import {createHostRootFiber} from './ReactFiber.old'; @@ -35,6 +37,12 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.old'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.old'; +export type RootState = { + element: any, + cache: Cache | null, + transitions: Transitions | null, +}; + function FiberRootNode( containerInfo, tag, @@ -85,6 +93,10 @@ function FiberRootNode( if (enableTransitionTracing) { this.transitionCallbacks = null; + const transitionLanesMap = (this.transitionLanes = []); + for (let i = 0; i < TotalLanes; i++) { + transitionLanesMap.push(null); + } } if (enableProfilerTimer && enableProfilerCommitHooks) { @@ -165,14 +177,17 @@ export function createFiberRoot( // retained separately. root.pooledCache = initialCache; retainCache(initialCache); - const initialState = { + const initialState: RootState = { element: null, cache: initialCache, + transitions: null, }; uninitializedFiber.memoizedState = initialState; } else { - const initialState = { + const initialState: RootState = { element: null, + cache: null, + transitions: null, }; uninitializedFiber.memoizedState = initialState; } diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js index 15349ce2d68da..aad0c912c5318 100644 --- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js @@ -6,8 +6,72 @@ * * @flow */ + +import type {TransitionTracingCallbacks} from './ReactInternalTypes'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import {enableTransitionTracing} from 'shared/ReactFeatureFlags'; + +export type SuspenseInfo = {name: string | null}; + +export type TransitionObject = { + transitionName: string, + startTime: number, +}; + +export type PendingTransitionCallbacks = { + transitionStart: Array | null, + transitionComplete: Array | null, +}; export type Transition = { + name: string, + startTime: number, +}; + +export type BatchConfigTransition = { + name?: string, + startTime?: number, _updatedFibers?: Set, }; + +export type Transitions = Array | null; + +export type TransitionCallback = 0 | 1; + +export const TransitionStart = 0; +export const TransitionComplete = 1; + +export function processTransitionCallbacks( + pendingTransitions: PendingTransitionCallbacks, + endTime: number, + callbacks: TransitionTracingCallbacks, +): void { + if (enableTransitionTracing) { + if (pendingTransitions !== null) { + const transitionStart = pendingTransitions.transitionStart; + if (transitionStart !== null) { + transitionStart.forEach(transition => { + if (callbacks.onTransitionStart != null) { + callbacks.onTransitionStart( + transition.transitionName, + transition.startTime, + ); + } + }); + } + + const transitionComplete = pendingTransitions.transitionComplete; + if (transitionComplete !== null) { + transitionComplete.forEach(transition => { + if (callbacks.onTransitionComplete != null) { + callbacks.onTransitionComplete( + transition.transitionName, + transition.startTime, + endTime, + ); + } + }); + } + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js new file mode 100644 index 0000000000000..aad0c912c5318 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {TransitionTracingCallbacks} from './ReactInternalTypes'; +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import {enableTransitionTracing} from 'shared/ReactFeatureFlags'; + +export type SuspenseInfo = {name: string | null}; + +export type TransitionObject = { + transitionName: string, + startTime: number, +}; + +export type PendingTransitionCallbacks = { + transitionStart: Array | null, + transitionComplete: Array | null, +}; + +export type Transition = { + name: string, + startTime: number, +}; + +export type BatchConfigTransition = { + name?: string, + startTime?: number, + _updatedFibers?: Set, +}; + +export type Transitions = Array | null; + +export type TransitionCallback = 0 | 1; + +export const TransitionStart = 0; +export const TransitionComplete = 1; + +export function processTransitionCallbacks( + pendingTransitions: PendingTransitionCallbacks, + endTime: number, + callbacks: TransitionTracingCallbacks, +): void { + if (enableTransitionTracing) { + if (pendingTransitions !== null) { + const transitionStart = pendingTransitions.transitionStart; + if (transitionStart !== null) { + transitionStart.forEach(transition => { + if (callbacks.onTransitionStart != null) { + callbacks.onTransitionStart( + transition.transitionName, + transition.startTime, + ); + } + }); + } + + const transitionComplete = pendingTransitions.transitionComplete; + if (transitionComplete !== null) { + transitionComplete.forEach(transition => { + if (callbacks.onTransitionComplete != null) { + callbacks.onTransitionComplete( + transition.transitionName, + transition.startTime, + endTime, + ); + } + }); + } + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 71bce2613251e..a529454dcd37c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -15,6 +15,11 @@ import type {StackCursor} from './ReactFiberStack.new'; import type {Flags} from './ReactFiberFlags'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; import type {EventPriority} from './ReactEventPriorities.new'; +import type { + PendingTransitionCallbacks, + TransitionObject, + Transitions, +} from './ReactFiberTracingMarkerComponent.new'; import { warnAboutDeprecatedLifecycles, @@ -32,6 +37,7 @@ import { enableStrictEffects, enableUpdaterTracking, enableCache, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -138,6 +144,8 @@ import { getHighestPriorityLane, addFiberToLanesMap, movePendingFibersToMemoized, + addTransitionToLanesMap, + getTransitionsForLanes, } from './ReactFiberLane.new'; import { DiscreteEventPriority, @@ -231,6 +239,7 @@ import { isLegacyActEnvironment, isConcurrentActEnvironment, } from './ReactFiberAct.new'; +import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new'; const ceil = Math.ceil; @@ -313,6 +322,51 @@ let workInProgressRootRenderTargetTime: number = Infinity; // suspense heuristics and opt out of rendering more content. const RENDER_TIMEOUT_MS = 500; +let workInProgressTransitions: Transitions | null = null; +export function getWorkInProgressTransitions() { + return workInProgressTransitions; +} + +let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null; + +export function addTransitionStartCallbackToPendingTransition( + transition: TransitionObject, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: [], + transitionComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.transitionStart === null) { + currentPendingTransitionCallbacks.transitionStart = []; + } + + currentPendingTransitionCallbacks.transitionStart.push(transition); + } +} + +export function addTransitionCompleteCallbackToPendingTransition( + transition: TransitionObject, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionComplete: [], + }; + } + + if (currentPendingTransitionCallbacks.transitionComplete === null) { + currentPendingTransitionCallbacks.transitionComplete = []; + } + + currentPendingTransitionCallbacks.transitionComplete.push(transition); + } +} + function resetRenderTimer() { workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS; } @@ -515,6 +569,17 @@ export function scheduleUpdateOnFiber( } } + if (enableTransitionTracing) { + const transition = ReactCurrentBatchConfig.transition; + if (transition !== null) { + if (transition.startTime === -1) { + transition.startTime = now(); + } + + addTransitionToLanesMap(root, transition, lane); + } + } + if (root.isDehydrated && root.tag !== LegacyRoot) { // This root's shell hasn't hydrated yet. Revert to client rendering. if (workInProgressRoot === root) { @@ -1244,6 +1309,7 @@ export function getExecutionContext(): ExecutionContext { export function deferredUpdates(fn: () => A): A { const previousPriority = getCurrentUpdatePriority(); const prevTransition = ReactCurrentBatchConfig.transition; + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(DefaultEventPriority); @@ -1318,6 +1384,7 @@ export function flushSync(fn) { const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(DiscreteEventPriority); @@ -1329,6 +1396,7 @@ export function flushSync(fn) { } finally { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + executionContext = prevExecutionContext; // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up @@ -1617,6 +1685,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } } + workInProgressTransitions = getTransitionsForLanes(root, lanes); prepareFreshStack(root, lanes); } @@ -1701,6 +1770,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } } + workInProgressTransitions = getTransitionsForLanes(root, lanes); resetRenderTimer(); prepareFreshStack(root, lanes); } @@ -1896,6 +1966,7 @@ function commitRoot(root: FiberRoot, recoverableErrors: null | Array) { // layout phases. Should be able to remove. const previousUpdateLanePriority = getCurrentUpdatePriority(); const prevTransition = ReactCurrentBatchConfig.transition; + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(DiscreteEventPriority); @@ -2237,6 +2308,27 @@ function commitRootImpl( // If layout work was scheduled, flush it now. flushSyncCallbacks(); + if (enableTransitionTracing) { + const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks; + const prevRootTransitionCallbacks = root.transitionCallbacks; + if ( + prevPendingTransitionCallbacks !== null && + prevRootTransitionCallbacks !== null + ) { + // TODO(luna) Refactor this code into the Host Config + const endTime = now(); + currentPendingTransitionCallbacks = null; + + scheduleCallback(IdleSchedulerPriority, () => + processTransitionCallbacks( + prevPendingTransitionCallbacks, + endTime, + prevRootTransitionCallbacks, + ), + ); + } + } + if (__DEV__) { if (enableDebugTracing) { logCommitStopped(); @@ -2286,6 +2378,7 @@ export function flushPassiveEffects(): boolean { const priority = lowerEventPriority(DefaultEventPriority, renderPriority); const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(priority); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 7a02471993612..86ad6216d1286 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -15,6 +15,11 @@ import type {StackCursor} from './ReactFiberStack.old'; import type {Flags} from './ReactFiberFlags'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; import type {EventPriority} from './ReactEventPriorities.old'; +import type { + PendingTransitionCallbacks, + TransitionObject, + Transitions, +} from './ReactFiberTracingMarkerComponent.old'; import { warnAboutDeprecatedLifecycles, @@ -32,6 +37,7 @@ import { enableStrictEffects, enableUpdaterTracking, enableCache, + enableTransitionTracing, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -138,6 +144,8 @@ import { getHighestPriorityLane, addFiberToLanesMap, movePendingFibersToMemoized, + addTransitionToLanesMap, + getTransitionsForLanes, } from './ReactFiberLane.old'; import { DiscreteEventPriority, @@ -231,6 +239,7 @@ import { isLegacyActEnvironment, isConcurrentActEnvironment, } from './ReactFiberAct.old'; +import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old'; const ceil = Math.ceil; @@ -313,6 +322,51 @@ let workInProgressRootRenderTargetTime: number = Infinity; // suspense heuristics and opt out of rendering more content. const RENDER_TIMEOUT_MS = 500; +let workInProgressTransitions: Transitions | null = null; +export function getWorkInProgressTransitions() { + return workInProgressTransitions; +} + +let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null; + +export function addTransitionStartCallbackToPendingTransition( + transition: TransitionObject, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: [], + transitionComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.transitionStart === null) { + currentPendingTransitionCallbacks.transitionStart = []; + } + + currentPendingTransitionCallbacks.transitionStart.push(transition); + } +} + +export function addTransitionCompleteCallbackToPendingTransition( + transition: TransitionObject, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionComplete: [], + }; + } + + if (currentPendingTransitionCallbacks.transitionComplete === null) { + currentPendingTransitionCallbacks.transitionComplete = []; + } + + currentPendingTransitionCallbacks.transitionComplete.push(transition); + } +} + function resetRenderTimer() { workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS; } @@ -515,6 +569,17 @@ export function scheduleUpdateOnFiber( } } + if (enableTransitionTracing) { + const transition = ReactCurrentBatchConfig.transition; + if (transition !== null) { + if (transition.startTime === -1) { + transition.startTime = now(); + } + + addTransitionToLanesMap(root, transition, lane); + } + } + if (root.isDehydrated && root.tag !== LegacyRoot) { // This root's shell hasn't hydrated yet. Revert to client rendering. if (workInProgressRoot === root) { @@ -1244,6 +1309,7 @@ export function getExecutionContext(): ExecutionContext { export function deferredUpdates(fn: () => A): A { const previousPriority = getCurrentUpdatePriority(); const prevTransition = ReactCurrentBatchConfig.transition; + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(DefaultEventPriority); @@ -1318,6 +1384,7 @@ export function flushSync(fn) { const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(DiscreteEventPriority); @@ -1329,6 +1396,7 @@ export function flushSync(fn) { } finally { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + executionContext = prevExecutionContext; // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up @@ -1617,6 +1685,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } } + workInProgressTransitions = getTransitionsForLanes(root, lanes); prepareFreshStack(root, lanes); } @@ -1701,6 +1770,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } } + workInProgressTransitions = getTransitionsForLanes(root, lanes); resetRenderTimer(); prepareFreshStack(root, lanes); } @@ -1896,6 +1966,7 @@ function commitRoot(root: FiberRoot, recoverableErrors: null | Array) { // layout phases. Should be able to remove. const previousUpdateLanePriority = getCurrentUpdatePriority(); const prevTransition = ReactCurrentBatchConfig.transition; + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(DiscreteEventPriority); @@ -2237,6 +2308,27 @@ function commitRootImpl( // If layout work was scheduled, flush it now. flushSyncCallbacks(); + if (enableTransitionTracing) { + const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks; + const prevRootTransitionCallbacks = root.transitionCallbacks; + if ( + prevPendingTransitionCallbacks !== null && + prevRootTransitionCallbacks !== null + ) { + // TODO(luna) Refactor this code into the Host Config + const endTime = now(); + currentPendingTransitionCallbacks = null; + + scheduleCallback(IdleSchedulerPriority, () => + processTransitionCallbacks( + prevPendingTransitionCallbacks, + endTime, + prevRootTransitionCallbacks, + ), + ); + } + } + if (__DEV__) { if (enableDebugTracing) { logCommitStopped(); @@ -2286,6 +2378,7 @@ export function flushPassiveEffects(): boolean { const priority = lowerEventPriority(DefaultEventPriority, renderPriority); const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); + try { ReactCurrentBatchConfig.transition = null; setCurrentUpdatePriority(priority); diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index ed668d9126b6f..162ba457d5490 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -15,6 +15,7 @@ import type { MutableSourceGetSnapshotFn, MutableSourceVersion, MutableSource, + StartTransitionOptions, } from 'shared/ReactTypes'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import type {WorkTag} from './ReactWorkTags'; @@ -25,6 +26,7 @@ import type {RootTag} from './ReactRootTags'; import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig'; import type {Wakeable} from 'shared/ReactTypes'; import type {Cache} from './ReactFiberCacheComponent.old'; +import type {Transitions} from './ReactFiberTracingMarkerComponent.new'; // Unwind Circular: moved from ReactFiberHooks.old export type HookType = @@ -320,6 +322,7 @@ export type TransitionTracingCallbacks = { // The following fields are only used in transition tracing in Profile builds type TransitionTracingOnlyFiberRootProperties = {| transitionCallbacks: null | TransitionTracingCallbacks, + transitionLanes: Array, |}; // Exported FiberRoot type includes all properties, @@ -369,7 +372,10 @@ export type Dispatcher = {| ): void, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void, useDeferredValue(value: T): T, - useTransition(): [boolean, (() => void) => void], + useTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, + ], useMutableSource( source: MutableSource, getSnapshot: MutableSourceGetSnapshotFn, diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js new file mode 100644 index 0000000000000..0b07a7f778cba --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -0,0 +1,101 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +let React; +let ReactNoop; +let Scheduler; +let act; + +let useState; +let startTransition; + +describe('ReactInteractionTracing', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + + act = require('jest-react').act; + + useState = React.useState; + startTransition = React.startTransition; + }); + + function Text({text}) { + Scheduler.unstable_yieldValue(text); + return text; + } + + function advanceTimers(ms) { + // Note: This advances Jest's virtual time but not React's. Use + // ReactNoop.expire for that. + if (typeof ms !== 'number') { + throw new Error('Must specify ms'); + } + jest.advanceTimersByTime(ms); + // Wait until the end of the current tick + // We cannot use a timer since we're faking them + return Promise.resolve().then(() => {}); + } + + // @gate enableTransitionTracing + it('should correctly trace basic interaction', async () => { + const transitionCallbacks = { + onTransitionStart: (name, startTime) => { + Scheduler.unstable_yieldValue( + `onTransitionStart(${name}, ${startTime})`, + ); + }, + onTransitionComplete: (name, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onTransitionComplete(${name}, ${startTime}, ${endTime})`, + ); + }, + }; + + let navigateToPageTwo; + function App() { + const [navigate, setNavigate] = useState(false); + navigateToPageTwo = () => { + setNavigate(true); + }; + + return ( +
+ {navigate ? : } +
+ ); + } + + const root = ReactNoop.createRoot({transitionCallbacks}); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield(['Page One']); + + await act(async () => { + startTransition(() => navigateToPageTwo(), {name: 'page transition'}); + + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'onTransitionStart(page transition, 1000)', + 'onTransitionComplete(page transition, 1000, 2000)', + ]); + }); + }); + }); +}); diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 5997f1b02b902..c3ffa8cd6abd8 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -14,6 +14,7 @@ import type { MutableSourceGetSnapshotFn, MutableSourceSubscribeFn, ReactContext, + StartTransitionOptions, } from 'shared/ReactTypes'; import type {ResponseState} from './ReactServerFormatConfig'; @@ -505,7 +506,10 @@ function unsupportedStartTransition() { throw new Error('startTransition cannot be called during server rendering.'); } -function useTransition(): [boolean, (callback: () => void) => void] { +function useTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { resolveCurrentlyRenderingComponent(); return [false, unsupportedStartTransition]; } diff --git a/packages/react/src/ReactCurrentBatchConfig.js b/packages/react/src/ReactCurrentBatchConfig.js index bf368d6f0b6ef..91737680b4aa8 100644 --- a/packages/react/src/ReactCurrentBatchConfig.js +++ b/packages/react/src/ReactCurrentBatchConfig.js @@ -7,10 +7,10 @@ * @flow */ -import type {Transition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent.new'; +import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent.new'; type BatchConfig = { - transition: Transition | null, + transition: BatchConfigTransition | null, }; /** * Keeps track of the current batch's configuration such as how long an update diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 12a5350083f38..9dc7a98589e4e 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -13,6 +13,7 @@ import type { MutableSourceGetSnapshotFn, MutableSourceSubscribeFn, ReactContext, + StartTransitionOptions, } from 'shared/ReactTypes'; import ReactCurrentDispatcher from './ReactCurrentDispatcher'; @@ -158,7 +159,10 @@ export function useDebugValue( export const emptyObject = {}; -export function useTransition(): [boolean, (() => void) => void] { +export function useTransition(): [ + boolean, + (callback: () => void, options?: StartTransitionOptions) => void, +] { const dispatcher = resolveDispatcher(); return dispatcher.useTransition(); } diff --git a/packages/react/src/ReactStartTransition.js b/packages/react/src/ReactStartTransition.js index a7097f5b1c52b..634f5442645f7 100644 --- a/packages/react/src/ReactStartTransition.js +++ b/packages/react/src/ReactStartTransition.js @@ -6,10 +6,15 @@ * * @flow */ +import type {StartTransitionOptions} from 'shared/ReactTypes'; import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'; +import {enableTransitionTracing} from 'shared/ReactFeatureFlags'; -export function startTransition(scope: () => void) { +export function startTransition( + scope: () => void, + options?: StartTransitionOptions, +) { const prevTransition = ReactCurrentBatchConfig.transition; ReactCurrentBatchConfig.transition = {}; const currentTransition = ReactCurrentBatchConfig.transition; @@ -17,6 +22,14 @@ export function startTransition(scope: () => void) { if (__DEV__) { ReactCurrentBatchConfig.transition._updatedFibers = new Set(); } + + if (enableTransitionTracing) { + if (options !== undefined && options.name !== undefined) { + ReactCurrentBatchConfig.transition.name = options.name; + ReactCurrentBatchConfig.transition.startTime = -1; + } + } + try { scope(); } finally { diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 43f42bddb91d3..066d20552d6fc 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -171,3 +171,7 @@ export type OffscreenMode = | 'hidden' | 'unstable-defer-without-hiding' | 'visible'; + +export type StartTransitionOptions = { + name?: string, +}; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 055cbd5d1e973..36627a5d50d30 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -26,6 +26,7 @@ export const enableSyncDefaultUpdates = __VARIANT__; export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__; export const enableClientRenderFallbackOnHydrationMismatch = __VARIANT__; +export const enableTransitionTracing = __VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases.