From 42f15b324f50d0fd98322c21646ac3013e30344a Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Thu, 24 Feb 2022 17:28:18 -0500 Subject: [PATCH] [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 --- .../react-debug-tools/src/ReactDebugHooks.js | 6 +- .../src/ReactFiberBeginWork.new.js | 10 +- .../src/ReactFiberBeginWork.old.js | 10 +- .../src/ReactFiberCommitWork.new.js | 50 +++++++-- .../src/ReactFiberCommitWork.old.js | 50 +++++++-- .../src/ReactFiberCompleteWork.new.js | 11 +- .../src/ReactFiberCompleteWork.old.js | 11 +- .../src/ReactFiberHooks.new.js | 28 ++++- .../src/ReactFiberHooks.old.js | 28 ++++- .../src/ReactFiberLane.new.js | 77 +++++++++++++ .../src/ReactFiberLane.old.js | 77 +++++++++++++ .../src/ReactFiberRoot.new.js | 19 +++- .../src/ReactFiberRoot.old.js | 19 +++- .../ReactFiberTracingMarkerComponent.new.js | 64 +++++++++++ .../ReactFiberTracingMarkerComponent.old.js | 77 +++++++++++++ .../src/ReactFiberWorkLoop.new.js | 93 ++++++++++++++++ .../src/ReactFiberWorkLoop.old.js | 93 ++++++++++++++++ .../src/ReactInternalTypes.js | 8 +- .../__tests__/ReactTransitionTracing-test.js | 101 ++++++++++++++++++ packages/react-server/src/ReactFizzHooks.js | 6 +- packages/react/src/ReactCurrentBatchConfig.js | 4 +- packages/react/src/ReactHooks.js | 6 +- packages/react/src/ReactStartTransition.js | 15 ++- packages/shared/ReactTypes.js | 4 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + 25 files changed, 831 insertions(+), 37 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js create mode 100644 packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js 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.