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 0ffec00bd3f8..b7159859060e 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 9b8de1df8018..4435fef43893 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 457088598e51..b037863aa40d 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 5ff683009668..75553555b928 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 769974e048d5..23e9d6070c9a 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 bb01e2b8a2a2..50ab1d3d9672 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 0d9de679a0f4..81a51eaff8eb 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 31e716935bc3..3f64a7d8f917 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 bd9e2488a884..d55cac9b26c7 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 452107c907b6..597f99633199 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 8360d2630dac..9f366c9ade88 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 3a4546008ad5..00dd694be4f5 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 56ebcd5cccac..1e561e49facb 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 15349ce2d68d..aad0c912c531 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 000000000000..aad0c912c531 --- /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 71bce2613251..a529454dcd37 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 7a0247199361..86ad6216d128 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 ed668d9126b6..162ba457d549 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 000000000000..0b07a7f778cb --- /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 5997f1b02b90..c3ffa8cd6abd 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 bf368d6f0b6e..91737680b4aa 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 12a5350083f3..9dc7a98589e4 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 a7097f5b1c52..634f5442645f 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 43f42bddb91d..066d20552d6f 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 055cbd5d1e97..36627a5d50d3 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.