@samitouri / QOS-React / commits / fc3777b1fe

Add Profiler component for collecting new render timing info (#12745)

Add a new component type, Profiler, that can be used to collect new render time metrics. Since this is a new, experimental API, it will be exported as React.unstable_Profiler initially. Most of the functionality for this component has been added behind a feature flag, enableProfileModeMetrics. When the feature flag is disabled, the component will just render its children with no additional behavior. When the flag is enabled, React will also collect timing information and pass it to the onRender function (as described below).

Brian Vaughn committed May 10, 2018 at 15:25 UTC fc3777b1fe295fd2661f1974f5587d214791f04b
31 files changed +1451 -53
packages/react-dom/src/server/ReactPartialRenderer.js
+2
@@ -34,6 +34,7 @@ import {
34 REACT_CALL_TYPE,
35 REACT_RETURN_TYPE,
36 REACT_PORTAL_TYPE,
37 + REACT_PROFILER_TYPE,
38 REACT_PROVIDER_TYPE,
39 REACT_CONTEXT_TYPE,
40 } from 'shared/ReactSymbols';
@@ -811,6 +812,7 @@ class ReactDOMServerRenderer {
812 switch (elementType) {
813 case REACT_STRICT_MODE_TYPE:
814 case REACT_ASYNC_MODE_TYPE:
815 + case REACT_PROFILER_TYPE:
816 case REACT_FRAGMENT_TYPE: {
817 const nextChildren = toArray(
818 ((nextChild: any): ReactElement).props.children,
packages/react-is/src/ReactIs.js
+6
@@ -16,6 +16,7 @@ import {
16 REACT_FORWARD_REF_TYPE,
17 REACT_FRAGMENT_TYPE,
18 REACT_PORTAL_TYPE,
19 + REACT_PROFILER_TYPE,
20 REACT_PROVIDER_TYPE,
21 REACT_STRICT_MODE_TYPE,
22 } from 'shared/ReactSymbols';
@@ -32,6 +33,7 @@ export function typeOf(object: any) {
33 switch (type) {
34 case REACT_ASYNC_MODE_TYPE:
35 case REACT_FRAGMENT_TYPE:
36 + case REACT_PROFILER_TYPE:
37 case REACT_STRICT_MODE_TYPE:
38 return type;
39 default:
@@ -60,6 +62,7 @@ export const ContextProvider = REACT_PROVIDER_TYPE;
62 export const Element = REACT_ELEMENT_TYPE;
63 export const ForwardRef = REACT_FORWARD_REF_TYPE;
64 export const Fragment = REACT_FRAGMENT_TYPE;
65 +export const Profiler = REACT_PROFILER_TYPE;
66 export const Portal = REACT_PORTAL_TYPE;
67 export const StrictMode = REACT_STRICT_MODE_TYPE;
68
@@ -87,6 +90,9 @@ export function isForwardRef(object: any) {
90 export function isFragment(object: any) {
91 return typeOf(object) === REACT_FRAGMENT_TYPE;
92 }
93 +export function isProfiler(object: any) {
94 + return typeOf(object) === REACT_PROFILER_TYPE;
95 +}
96 export function isPortal(object: any) {
97 return typeOf(object) === REACT_PORTAL_TYPE;
98 }
packages/react-is/src/__tests__/ReactIs-test.js
+14
@@ -145,4 +145,18 @@ describe('ReactIs', () => {
145 expect(ReactIs.isStrictMode(<React.unstable_AsyncMode />)).toBe(false);
146 expect(ReactIs.isStrictMode(<div />)).toBe(false);
147 });
148 +
149 + it('should identify profile root', () => {
150 + expect(
151 + ReactIs.typeOf(<React.unstable_Profiler id="foo" onRender={jest.fn()} />),
152 + ).toBe(ReactIs.Profiler);
153 + expect(
154 + ReactIs.isProfiler(
155 + <React.unstable_Profiler id="foo" onRender={jest.fn()} />,
156 + ),
157 + ).toBe(true);
158 + expect(ReactIs.isProfiler({type: ReactIs.unstable_Profiler})).toBe(false);
159 + expect(ReactIs.isProfiler(<React.unstable_AsyncMode />)).toBe(false);
160 + expect(ReactIs.isProfiler(<div />)).toBe(false);
161 + });
162 });
packages/react-reconciler/src/ReactFiber.js
+53 -1
@@ -15,6 +15,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
15 import type {UpdateQueue} from './ReactUpdateQueue';
16
17 import invariant from 'fbjs/lib/invariant';
18 +import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
19 import {NoEffect} from 'shared/ReactTypeOfSideEffect';
20 import {
21 IndeterminateComponent,
@@ -30,17 +31,19 @@ import {
31 Mode,
32 ContextProvider,
33 ContextConsumer,
34 + Profiler,
35 } from 'shared/ReactTypeOfWork';
36 import getComponentName from 'shared/getComponentName';
37
38 import {NoWork} from './ReactFiberExpirationTime';
37 -import {NoContext, AsyncMode, StrictMode} from './ReactTypeOfMode';
39 +import {NoContext, AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode';
40 import {
41 REACT_FORWARD_REF_TYPE,
42 REACT_FRAGMENT_TYPE,
43 REACT_RETURN_TYPE,
44 REACT_CALL_TYPE,
45 REACT_STRICT_MODE_TYPE,
46 + REACT_PROFILER_TYPE,
47 REACT_PROVIDER_TYPE,
48 REACT_CONTEXT_TYPE,
49 REACT_ASYNC_MODE_TYPE,
@@ -150,6 +153,10 @@ export type Fiber = {|
153 // memory if we need to.
154 alternate: Fiber | null,
155
156 + // Profiling metrics
157 + selfBaseTime?: number,
158 + treeBaseTime?: number,
159 +
160 // Conceptual aliases
161 // workInProgress : Fiber -> alternate The alternate used for reuse happens
162 // to be the same as work in progress.
@@ -204,6 +211,11 @@ function FiberNode(
211
212 this.alternate = null;
213
214 + if (enableProfilerTimer) {
215 + this.selfBaseTime = 0;
216 + this.treeBaseTime = 0;
217 + }
218 +
219 if (__DEV__) {
220 this._debugID = debugCounter++;
221 this._debugSource = null;
@@ -298,6 +310,11 @@ export function createWorkInProgress(
310 workInProgress.index = current.index;
311 workInProgress.ref = current.ref;
312
313 + if (enableProfilerTimer) {
314 + workInProgress.selfBaseTime = current.selfBaseTime;
315 + workInProgress.treeBaseTime = current.treeBaseTime;
316 + }
317 +
318 return workInProgress;
319 }
320
@@ -343,6 +360,8 @@ export function createFiberFromElement(
360 fiberTag = Mode;
361 mode |= StrictMode;
362 break;
363 + case REACT_PROFILER_TYPE:
364 + return createFiberFromProfiler(pendingProps, mode, expirationTime, key);
365 case REACT_CALL_TYPE:
366 fiberTag = CallComponent;
367 break;
@@ -440,6 +459,35 @@ export function createFiberFromFragment(
459 return fiber;
460 }
461
462 +export function createFiberFromProfiler(
463 + pendingProps: any,
464 + mode: TypeOfMode,
465 + expirationTime: ExpirationTime,
466 + key: null | string,
467 +): Fiber {
468 + if (__DEV__) {
469 + if (
470 + typeof pendingProps.id !== 'string' ||
471 + typeof pendingProps.onRender !== 'function'
472 + ) {
473 + invariant(
474 + false,
475 + 'Profiler must specify an "id" string and "onRender" function as props',
476 + );
477 + }
478 + }
479 +
480 + const fiber = createFiber(Profiler, pendingProps, key, mode | ProfileMode);
481 + fiber.type = REACT_PROFILER_TYPE;
482 + fiber.expirationTime = expirationTime;
483 + fiber.stateNode = {
484 + duration: 0,
485 + startTime: 0,
486 + };
487 +
488 + return fiber;
489 +}
490 +
491 export function createFiberFromText(
492 content: string,
493 mode: TypeOfMode,
@@ -509,6 +557,10 @@ export function assignFiberPropertiesInDEV(
557 target.lastEffect = source.lastEffect;
558 target.expirationTime = source.expirationTime;
559 target.alternate = source.alternate;
560 + if (enableProfilerTimer) {
561 + target.selfBaseTime = source.selfBaseTime;
562 + target.treeBaseTime = source.treeBaseTime;
563 + }
564 target._debugID = source._debugID;
565 target._debugSource = source._debugSource;
566 target._debugOwner = source._debugOwner;
packages/react-reconciler/src/ReactFiberBeginWork.js
+50
@@ -16,6 +16,7 @@ import type {NewContext} from './ReactFiberNewContext';
16 import type {HydrationContext} from './ReactFiberHydrationContext';
17 import type {FiberRoot} from './ReactFiberRoot';
18 import type {ExpirationTime} from './ReactFiberExpirationTime';
19 +import type {ProfilerTimer} from './ReactProfilerTimer';
20 import checkPropTypes from 'prop-types/checkPropTypes';
21
22 import {
@@ -34,6 +35,7 @@ import {
35 Mode,
36 ContextProvider,
37 ContextConsumer,
38 + Profiler,
39 } from 'shared/ReactTypeOfWork';
40 import {
41 NoEffect,
@@ -42,12 +44,14 @@ import {
44 ContentReset,
45 Ref,
46 DidCapture,
47 + Update,
48 } from 'shared/ReactTypeOfSideEffect';
49 import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
50 import {
51 enableGetDerivedStateFromCatch,
52 debugRenderPhaseSideEffects,
53 debugRenderPhaseSideEffectsForStrictMode,
54 + enableProfilerTimer,
55 } from 'shared/ReactFeatureFlags';
56 import invariant from 'fbjs/lib/invariant';
57 import getComponentName from 'shared/getComponentName';
@@ -88,6 +92,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
92 hydrationContext: HydrationContext<C, CX>,
93 scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
94 computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
95 + profilerTimer: ProfilerTimer,
96 ) {
97 const {shouldSetTextContent, shouldDeprioritizeSubtree} = config;
98
@@ -95,6 +100,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
100
101 const {pushProvider} = newContext;
102
103 + const {
104 + markActualRenderTimeStarted,
105 + stopBaseRenderTimerIfRunning,
106 + } = profilerTimer;
107 +
108 const {
109 getMaskedContext,
110 getUnmaskedContext,
@@ -215,6 +225,25 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
225 return workInProgress.child;
226 }
227
228 + function updateProfiler(current, workInProgress) {
229 + const nextProps = workInProgress.pendingProps;
230 + if (enableProfilerTimer) {
231 + // Start render timer here and push start time onto queue
232 + markActualRenderTimeStarted(workInProgress);
233 +
234 + // Let the "complete" phase know to stop the timer,
235 + // And the scheduler to record the measured time.
236 + workInProgress.effectTag |= Update;
237 + }
238 + if (workInProgress.memoizedProps === nextProps) {
239 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
240 + }
241 + const nextChildren = nextProps.children;
242 + reconcileChildren(current, workInProgress, nextChildren);
243 + memoizeProps(workInProgress, nextProps);
244 + return workInProgress.child;
245 + }
246 +
247 function markRef(current: Fiber | null, workInProgress: Fiber) {
248 const ref = workInProgress.ref;
249 if (
@@ -344,6 +373,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
373 // the new API.
374 // TODO: Warn in a future release.
375 nextChildren = null;
376 +
377 + if (enableProfilerTimer) {
378 + stopBaseRenderTimerIfRunning();
379 + }
380 } else {
381 if (__DEV__) {
382 ReactDebugCurrentFiber.setCurrentPhase('render');
@@ -1054,6 +1087,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1087 ): Fiber | null {
1088 cancelWorkTimer(workInProgress);
1089
1090 + if (enableProfilerTimer) {
1091 + // Don't update "base" render times for bailouts.
1092 + stopBaseRenderTimerIfRunning();
1093 + }
1094 +
1095 // TODO: We should ideally be able to bail out early if the children have no
1096 // more work to do. However, since we don't have a separation of this
1097 // Fiber's priority and its children yet - we don't know without doing lots
@@ -1075,6 +1113,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1113 function bailoutOnLowPriority(current, workInProgress) {
1114 cancelWorkTimer(workInProgress);
1115
1116 + if (enableProfilerTimer) {
1117 + // Don't update "base" render times for bailouts.
1118 + stopBaseRenderTimerIfRunning();
1119 + }
1120 +
1121 // TODO: Handle HostComponent tags here as well and call pushHostContext()?
1122 // See PR 8590 discussion for context
1123 switch (workInProgress.tag) {
@@ -1093,6 +1136,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1136 case ContextProvider:
1137 pushProvider(workInProgress);
1138 break;
1139 + case Profiler:
1140 + if (enableProfilerTimer) {
1141 + markActualRenderTimeStarted(workInProgress);
1142 + }
1143 + break;
1144 }
1145 // TODO: What if this is currently in progress?
1146 // How can that happen? How is this not being cloned?
@@ -1173,6 +1221,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1221 return updateFragment(current, workInProgress);
1222 case Mode:
1223 return updateMode(current, workInProgress);
1224 + case Profiler:
1225 + return updateProfiler(current, workInProgress);
1226 case ContextProvider:
1227 return updateContextProvider(
1228 current,
packages/react-reconciler/src/ReactFiberCommitWork.js
+24 -2
@@ -17,6 +17,7 @@ import {
17 enableMutatingReconciler,
18 enableNoopReconciler,
19 enablePersistentReconciler,
20 + enableProfilerTimer,
21 } from 'shared/ReactFeatureFlags';
22 import {
23 ClassComponent,
@@ -25,13 +26,14 @@ import {
26 HostText,
27 HostPortal,
28 CallComponent,
29 + Profiler,
30 } from 'shared/ReactTypeOfWork';
31 import ReactErrorUtils from 'shared/ReactErrorUtils';
32 import {
31 - Placement,
32 - Update,
33 ContentReset,
34 + Placement,
35 Snapshot,
36 + Update,
37 } from 'shared/ReactTypeOfSideEffect';
38 import {commitUpdateQueue} from './ReactUpdateQueue';
39 import invariant from 'fbjs/lib/invariant';
@@ -308,6 +310,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
310 // We have no life-cycles associated with portals.
311 return;
312 }
313 + case Profiler: {
314 + // We have no life-cycles associated with Profiler.
315 + return;
316 + }
317 default: {
318 invariant(
319 false,
@@ -814,6 +820,22 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
820 case HostRoot: {
821 return;
822 }
823 + case Profiler: {
824 + if (enableProfilerTimer) {
825 + const onRender = finishedWork.memoizedProps.onRender;
826 + onRender(
827 + finishedWork.memoizedProps.id,
828 + current === null ? 'mount' : 'update',
829 + finishedWork.stateNode.duration,
830 + finishedWork.treeBaseTime,
831 + );
832 +
833 + // Reset actualTime after successful commit.
834 + // By default, we append to this time to account for errors and pauses.
835 + finishedWork.stateNode.duration = 0;
836 + }
837 + return;
838 + }
839 default: {
840 invariant(
841 false,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+11
@@ -15,11 +15,13 @@ import type {LegacyContext} from './ReactFiberContext';
15 import type {NewContext} from './ReactFiberNewContext';
16 import type {HydrationContext} from './ReactFiberHydrationContext';
17 import type {FiberRoot} from './ReactFiberRoot';
18 +import type {ProfilerTimer} from './ReactProfilerTimer';
19
20 import {
21 enableMutatingReconciler,
22 enablePersistentReconciler,
23 enableNoopReconciler,
24 + enableProfilerTimer,
25 } from 'shared/ReactFeatureFlags';
26 import {
27 IndeterminateComponent,
@@ -37,6 +39,7 @@ import {
39 ForwardRef,
40 Fragment,
41 Mode,
42 + Profiler,
43 } from 'shared/ReactTypeOfWork';
44 import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect';
45 import invariant from 'fbjs/lib/invariant';
@@ -49,6 +52,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
52 legacyContext: LegacyContext,
53 newContext: NewContext,
54 hydrationContext: HydrationContext<C, CX>,
55 + profilerTimer: ProfilerTimer,
56 ) {
57 const {
58 createInstance,
@@ -67,6 +71,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
71 popHostContainer,
72 } = hostContext;
73
74 + const {recordElapsedActualRenderTime} = profilerTimer;
75 +
76 const {
77 popContextProvider: popLegacyContextProvider,
78 popTopLevelContextObject: popTopLevelLegacyContextObject,
@@ -591,6 +597,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
597 return null;
598 case Mode:
599 return null;
600 + case Profiler:
601 + if (enableProfilerTimer) {
602 + recordElapsedActualRenderTime(workInProgress);
603 + }
604 + return null;
605 case HostPortal:
606 popHostContainer(workInProgress);
607 updateHostContainer(workInProgress);
packages/react-reconciler/src/ReactFiberScheduler.js
+99 -18
@@ -40,10 +40,12 @@ import {
40 HostPortal,
41 } from 'shared/ReactTypeOfWork';
42 import {
43 + enableProfilerTimer,
44 enableUserTimingAPI,
44 - warnAboutDeprecatedLifecycles,
45 replayFailedUnitOfWorkWithInvokeGuardedCallback,
46 + warnAboutDeprecatedLifecycles,
47 } from 'shared/ReactFeatureFlags';
48 +import {createProfilerTimer} from './ReactProfilerTimer';
49 import getComponentName from 'shared/getComponentName';
50 import invariant from 'fbjs/lib/invariant';
51 import warning from 'fbjs/lib/warning';
@@ -85,7 +87,7 @@ import {
87 expirationTimeToMs,
88 computeExpirationBucket,
89 } from './ReactFiberExpirationTime';
88 -import {AsyncMode} from './ReactTypeOfMode';
90 +import {AsyncMode, ProfileMode} from './ReactTypeOfMode';
91 import ReactFiberLegacyContext from './ReactFiberContext';
92 import ReactFiberNewContext from './ReactFiberNewContext';
93 import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
@@ -158,10 +160,18 @@ if (__DEV__) {
160 export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
161 config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
162 ) {
163 + const {
164 + now,
165 + scheduleDeferredCallback,
166 + cancelDeferredCallback,
167 + prepareForCommit,
168 + resetAfterCommit,
169 + } = config;
170 const stack = ReactFiberStack();
171 const hostContext = ReactFiberHostContext(config, stack);
172 const legacyContext = ReactFiberLegacyContext(stack);
173 const newContext = ReactFiberNewContext(stack);
174 + const profilerTimer = createProfilerTimer(now);
175 const {popHostContext, popHostContainer} = hostContext;
176 const {
177 popTopLevelContextObject: popTopLevelLegacyContextObject,
@@ -179,6 +189,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
189 hydrationContext,
190 scheduleWork,
191 computeExpirationForFiber,
192 + profilerTimer,
193 );
194 const {completeWork} = ReactFiberCompleteWork(
195 config,
@@ -186,6 +197,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
197 legacyContext,
198 newContext,
199 hydrationContext,
200 + profilerTimer,
201 );
202 const {
203 throwException,
@@ -194,6 +206,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
206 createRootErrorUpdate,
207 createClassErrorUpdate,
208 } = ReactFiberUnwindWork(
209 + config,
210 hostContext,
211 legacyContext,
212 newContext,
@@ -201,6 +214,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
214 markLegacyErrorBoundaryAsFailed,
215 isAlreadyFailedLegacyErrorBoundary,
216 onUncaughtError,
217 + profilerTimer,
218 );
219 const {
220 commitBeforeMutationLifeCycles,
@@ -219,13 +233,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
233 markLegacyErrorBoundaryAsFailed,
234 recalculateCurrentTime,
235 );
236 +
237 const {
223 - now,
224 - scheduleDeferredCallback,
225 - cancelDeferredCallback,
226 - prepareForCommit,
227 - resetAfterCommit,
228 - } = config;
238 + checkActualRenderTimeStackEmpty,
239 + pauseActualRenderTimerIfRunning,
240 + recordElapsedBaseRenderTimeIfRunning,
241 + resetActualRenderTimer,
242 + resumeActualRenderTimerIfPaused,
243 + startBaseRenderTimer,
244 + stopBaseRenderTimerIfRunning,
245 + } = profilerTimer;
246
247 // Represents the current time in ms.
248 const originalStartTimeMs = now();
@@ -305,6 +322,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
322 originalReplayError = null;
323 if (hasCaughtError()) {
324 clearCaughtError();
325 +
326 + if (enableProfilerTimer) {
327 + // Stop "base" render timer again (after the re-thrown error).
328 + stopBaseRenderTimerIfRunning();
329 + }
330 } else {
331 // If the begin phase did not fail the second time, set this pointer
332 // back to the original value.
@@ -645,6 +667,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
667 }
668 }
669
670 + if (enableProfilerTimer) {
671 + if (__DEV__) {
672 + checkActualRenderTimeStackEmpty();
673 + }
674 + resetActualRenderTimer();
675 + }
676 +
677 isCommitting = false;
678 isWorking = false;
679 stopCommitLifeCyclesTimer();
@@ -690,17 +719,36 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
719 // TODO: Calls need to visit stateNode
720
721 // Bubble up the earliest expiration time.
693 - let child = workInProgress.child;
694 - while (child !== null) {
695 - if (
696 - child.expirationTime !== NoWork &&
697 - (newExpirationTime === NoWork ||
698 - newExpirationTime > child.expirationTime)
699 - ) {
700 - newExpirationTime = child.expirationTime;
722 + // (And "base" render timers if that feature flag is enabled)
723 + if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
724 + let treeBaseTime = workInProgress.selfBaseTime;
725 + let child = workInProgress.child;
726 + while (child !== null) {
727 + treeBaseTime += child.treeBaseTime;
728 + if (
729 + child.expirationTime !== NoWork &&
730 + (newExpirationTime === NoWork ||
731 + newExpirationTime > child.expirationTime)
732 + ) {
733 + newExpirationTime = child.expirationTime;
734 + }
735 + child = child.sibling;
736 + }
737 + workInProgress.treeBaseTime = treeBaseTime;
738 + } else {
739 + let child = workInProgress.child;
740 + while (child !== null) {
741 + if (
742 + child.expirationTime !== NoWork &&
743 + (newExpirationTime === NoWork ||
744 + newExpirationTime > child.expirationTime)
745 + ) {
746 + newExpirationTime = child.expirationTime;
747 + }
748 + child = child.sibling;
749 }
702 - child = child.sibling;
750 }
751 +
752 workInProgress.expirationTime = newExpirationTime;
753 }
754
@@ -875,7 +923,19 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
923 workInProgress,
924 );
925 }
878 - let next = beginWork(current, workInProgress, nextRenderExpirationTime);
926 +
927 + let next;
928 + if (enableProfilerTimer) {
929 + startBaseRenderTimer();
930 + next = beginWork(current, workInProgress, nextRenderExpirationTime);
931 +
932 + // Update "base" time if the render wasn't bailed out on.
933 + recordElapsedBaseRenderTimeIfRunning(workInProgress);
934 + stopBaseRenderTimerIfRunning();
935 + } else {
936 + next = beginWork(current, workInProgress, nextRenderExpirationTime);
937 + }
938 +
939 if (__DEV__) {
940 ReactDebugCurrentFiber.resetCurrentFiber();
941 if (isReplayingFailedUnitOfWork) {
@@ -911,6 +971,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
971 while (nextUnitOfWork !== null && !shouldYield()) {
972 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
973 }
974 +
975 + if (enableProfilerTimer) {
976 + // If we didn't finish, pause the "actual" render timer.
977 + // We'll restart it when we resume work.
978 + pauseActualRenderTimerIfRunning();
979 + }
980 }
981 }
982
@@ -953,6 +1019,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1019 try {
1020 workLoop(isAsync);
1021 } catch (thrownValue) {
1022 + if (enableProfilerTimer) {
1023 + // Stop "base" render timer in the event of an error.
1024 + stopBaseRenderTimerIfRunning();
1025 + }
1026 +
1027 if (nextUnitOfWork === null) {
1028 // This is a fatal error.
1029 didFatal = true;
@@ -1516,6 +1587,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1587 // the deadline.
1588 findHighestPriorityRoot();
1589
1590 + if (enableProfilerTimer) {
1591 + resumeActualRenderTimerIfPaused();
1592 + }
1593 +
1594 if (enableUserTimingAPI && deadline !== null) {
1595 const didExpire = nextFlushedExpirationTime < recalculateCurrentTime();
1596 const timeout = expirationTimeToMs(nextFlushedExpirationTime);
@@ -1661,6 +1736,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1736 // There's no time left. Mark this root as complete. We'll come
1737 // back and commit it later.
1738 root.finishedWork = finishedWork;
1739 +
1740 + if (enableProfilerTimer) {
1741 + // If we didn't finish, pause the "actual" render timer.
1742 + // We'll restart it when we resume work.
1743 + pauseActualRenderTimerIfRunning();
1744 + }
1745 }
1746 }
1747 }
packages/react-reconciler/src/ReactFiberUnwindWork.js
+22 -4
@@ -7,12 +7,14 @@
7 * @flow
8 */
9
10 +import type {HostConfig} from 'react-reconciler';
11 import type {Fiber} from './ReactFiber';
12 import type {ExpirationTime} from './ReactFiberExpirationTime';
13 import type {HostContext} from './ReactFiberHostContext';
14 import type {LegacyContext} from './ReactFiberContext';
15 import type {NewContext} from './ReactFiberNewContext';
16 import type {CapturedValue} from './ReactCapturedValue';
17 +import type {ProfilerTimer} from './ReactProfilerTimer';
18 import type {Update} from './ReactUpdateQueue';
19
20 import {createCapturedValue} from './ReactCapturedValue';
@@ -29,17 +31,21 @@ import {
31 HostComponent,
32 HostPortal,
33 ContextProvider,
34 + Profiler,
35 } from 'shared/ReactTypeOfWork';
36 import {
34 - NoEffect,
37 DidCapture,
38 Incomplete,
39 + NoEffect,
40 ShouldCapture,
41 } from 'shared/ReactTypeOfSideEffect';
42 +import {
43 + enableGetDerivedStateFromCatch,
44 + enableProfilerTimer,
45 +} from 'shared/ReactFeatureFlags';
46
40 -import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags';
41 -
42 -export default function<C, CX>(
47 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
48 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
49 hostContext: HostContext<C, CX>,
50 legacyContext: LegacyContext,
51 newContext: NewContext,
@@ -51,6 +57,7 @@ export default function<C, CX>(
57 markLegacyErrorBoundaryAsFailed: (instance: mixed) => void,
58 isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean,
59 onUncaughtError: (error: mixed) => void,
60 + profilerTimer: ProfilerTimer,
61 ) {
62 const {popHostContainer, popHostContext} = hostContext;
63 const {
@@ -58,6 +65,10 @@ export default function<C, CX>(
65 popTopLevelContextObject: popTopLevelLegacyContextObject,
66 } = legacyContext;
67 const {popProvider} = newContext;
68 + const {
69 + resumeActualRenderTimerIfPaused,
70 + recordElapsedActualRenderTime,
71 + } = profilerTimer;
72
73 function createRootErrorUpdate(
74 fiber: Fiber,
@@ -236,6 +247,13 @@ export default function<C, CX>(
247 case ContextProvider:
248 popProvider(interruptedWork);
249 break;
250 + case Profiler:
251 + if (enableProfilerTimer) {
252 + // Resume in case we're picking up on work that was paused.
253 + resumeActualRenderTimerIfPaused();
254 + recordElapsedActualRenderTime(interruptedWork);
255 + }
256 + break;
257 default:
258 break;
259 }
packages/react-reconciler/src/ReactProfilerTimer.js new
+144
@@ -0,0 +1,144 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Fiber} from './ReactFiber';
11 +
12 +import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
13 +
14 +import warning from 'fbjs/lib/warning';
15 +
16 +/**
17 + * The "actual" render time is total time required to render the descendants of a Profiler component.
18 + * This time is stored as a stack, since Profilers can be nested.
19 + * This time is started during the "begin" phase and stopped during the "complete" phase.
20 + * It is paused (and accumulated) in the event of an interruption or an aborted render.
21 + */
22 +
23 +export type ProfilerTimer = {
24 + checkActualRenderTimeStackEmpty(): void,
25 + markActualRenderTimeStarted(fiber: Fiber): void,
26 + pauseActualRenderTimerIfRunning(): void,
27 + recordElapsedActualRenderTime(fiber: Fiber): void,
28 + resetActualRenderTimer(): void,
29 + resumeActualRenderTimerIfPaused(): void,
30 + recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void,
31 + startBaseRenderTimer(): void,
32 + stopBaseRenderTimerIfRunning(): void,
33 +};
34 +
35 +export function createProfilerTimer(now: () => number): ProfilerTimer {
36 + let fiberStack: Array<Fiber | null>;
37 +
38 + if (__DEV__) {
39 + fiberStack = [];
40 + }
41 +
42 + let timerPausedAt: number = 0;
43 + let totalElapsedPauseTime: number = 0;
44 +
45 + function checkActualRenderTimeStackEmpty(): void {
46 + if (__DEV__) {
47 + warning(
48 + fiberStack.length === 0,
49 + 'Expected an empty stack. Something was not reset properly.',
50 + );
51 + }
52 + }
53 +
54 + function markActualRenderTimeStarted(fiber: Fiber): void {
55 + if (__DEV__) {
56 + fiberStack.push(fiber);
57 + }
58 + fiber.stateNode.startTime = now() - totalElapsedPauseTime;
59 + }
60 +
61 + function pauseActualRenderTimerIfRunning(): void {
62 + if (timerPausedAt === 0) {
63 + timerPausedAt = now();
64 + }
65 + }
66 +
67 + function recordElapsedActualRenderTime(fiber: Fiber): void {
68 + if (__DEV__) {
69 + warning(fiber === fiberStack.pop(), 'Unexpected Fiber popped.');
70 + }
71 + fiber.stateNode.duration +=
72 + now() - totalElapsedPauseTime - fiber.stateNode.startTime;
73 + }
74 +
75 + function resetActualRenderTimer(): void {
76 + totalElapsedPauseTime = 0;
77 + }
78 +
79 + function resumeActualRenderTimerIfPaused(): void {
80 + if (timerPausedAt > 0) {
81 + totalElapsedPauseTime += now() - timerPausedAt;
82 + timerPausedAt = 0;
83 + }
84 + }
85 +
86 + /**
87 + * The "base" render time is the duration of the “begin” phase of work for a particular fiber.
88 + * This time is measured and stored on each fiber.
89 + * The time for all sibling fibers are accumulated and stored on their parent during the "complete" phase.
90 + * If a fiber bails out (sCU false) then its "base" timer is cancelled and the fiber is not updated.
91 + */
92 +
93 + let baseStartTime: number = -1;
94 +
95 + function recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void {
96 + if (baseStartTime !== -1) {
97 + fiber.selfBaseTime = now() - baseStartTime;
98 + }
99 + }
100 +
101 + function startBaseRenderTimer(): void {
102 + if (__DEV__) {
103 + if (baseStartTime !== -1) {
104 + warning(
105 + false,
106 + 'Cannot start base timer that is already running. ' +
107 + 'This error is likely caused by a bug in React. ' +
108 + 'Please file an issue.',
109 + );
110 + }
111 + }
112 + baseStartTime = now();
113 + }
114 +
115 + function stopBaseRenderTimerIfRunning(): void {
116 + baseStartTime = -1;
117 + }
118 +
119 + if (enableProfilerTimer) {
120 + return {
121 + checkActualRenderTimeStackEmpty,
122 + markActualRenderTimeStarted,
123 + pauseActualRenderTimerIfRunning,
124 + recordElapsedActualRenderTime,
125 + resetActualRenderTimer,
126 + resumeActualRenderTimerIfPaused,
127 + recordElapsedBaseRenderTimeIfRunning,
128 + startBaseRenderTimer,
129 + stopBaseRenderTimerIfRunning,
130 + };
131 + } else {
132 + return {
133 + checkActualRenderTimeStackEmpty(): void {},
134 + markActualRenderTimeStarted(fiber: Fiber): void {},
135 + pauseActualRenderTimerIfRunning(): void {},
136 + recordElapsedActualRenderTime(fiber: Fiber): void {},
137 + resetActualRenderTimer(): void {},
138 + resumeActualRenderTimerIfPaused(): void {},
139 + recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void {},
140 + startBaseRenderTimer(): void {},
141 + stopBaseRenderTimerIfRunning(): void {},
142 + };
143 + }
144 +}
packages/react-reconciler/src/ReactTypeOfMode.js
+4 -3
@@ -9,6 +9,7 @@
9
10 export type TypeOfMode = number;
11
12 -export const NoContext = 0b00;
13 -export const AsyncMode = 0b01;
14 -export const StrictMode = 0b10;
12 +export const NoContext = 0b000;
13 +export const AsyncMode = 0b001;
14 +export const StrictMode = 0b010;
15 +export const ProfileMode = 0b100;
packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js
+10 -8
@@ -187,15 +187,17 @@ describe('ReactDebugFiberPerf', () => {
187 expect(getFlameChart()).toMatchSnapshot();
188 });
189
190 - it('does not include StrictMode or AsyncMode components in measurements', () => {
190 + it('does not include AsyncMode, StrictMode, or Profiler components in measurements', () => {
191 ReactNoop.render(
192 - <React.StrictMode>
193 - <Parent>
194 - <React.unstable_AsyncMode>
195 - <Child />
196 - </React.unstable_AsyncMode>
197 - </Parent>
198 - </React.StrictMode>,
192 + <React.unstable_Profiler id="test" onRender={jest.fn()}>
193 + <React.StrictMode>
194 + <Parent>
195 + <React.unstable_AsyncMode>
196 + <Child />
197 + </React.unstable_AsyncMode>
198 + </Parent>
199 + </React.StrictMode>
200 + </React.unstable_Profiler>,
201 );
202 addComment('Mount');
203 ReactNoop.flush();
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+4 -3
@@ -91,13 +91,14 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc
91 "
92 `;
93
94 -exports[`ReactDebugFiberPerf does not include StrictMode or AsyncMode components in measurements 1`] = `
94 +exports[`ReactDebugFiberPerf does not include AsyncMode, StrictMode, or Profiler components in measurements 1`] = `
95 "⚛ (Waiting for async callback... will force flush in 5230 ms)
96
97 // Mount
98 ⚛ (React Tree Reconciliation: Completed Root)
99 - ⚛ Parent [mount]
100 - ⚛ Child [mount]
99 + ⚛ Profiler(test) [mount]
100 + ⚛ Parent [mount]
101 + ⚛ Child [mount]
102
103 ⚛ (Committing Changes)
104 ⚛ (Committing Snapshot Effects: 0 Total)
packages/react-test-renderer/src/ReactTestRenderer.js
+16 -5
@@ -27,6 +27,7 @@ import {
27 ContextProvider,
28 Mode,
29 ForwardRef,
30 + Profiler,
31 } from 'shared/ReactTypeOfWork';
32 import invariant from 'fbjs/lib/invariant';
33
@@ -119,7 +120,7 @@ function removeChild(
120 }
121
122 // Current virtual time
122 -let currentTime: number = 0;
123 +let nowImplementation = () => 0;
124 let scheduledCallback: ((deadline: Deadline) => mixed) | null = null;
125 let yieldedValues: Array<mixed> | null = null;
126
@@ -221,9 +222,9 @@ const TestRenderer = ReactFiberReconciler({
222
223 getPublicInstance,
224
224 - now(): number {
225 - return currentTime;
226 - },
225 + // This approach enables `now` to be mocked by tests,
226 + // Even after the reconciler has initialized and read host config values.
227 + now: () => nowImplementation(),
228
229 mutation: {
230 commitUpdate(
@@ -383,6 +384,7 @@ function toTree(node: ?Fiber) {
384 case ContextProvider:
385 case ContextConsumer:
386 case Mode:
387 + case Profiler:
388 case ForwardRef:
389 return childrenToTree(node.child);
390 default:
@@ -486,6 +488,7 @@ class ReactTestInstance {
488 case ContextProvider:
489 case ContextConsumer:
490 case Mode:
491 + case Profiler:
492 descend = true;
493 break;
494 default:
@@ -737,7 +740,11 @@ const ReactTestRendererFiber = {
740 }
741 return TestRenderer.getPublicRootInstance(root);
742 },
740 - unstable_flushSync: TestRenderer.flushSync,
743 + unstable_flushSync(fn: Function) {
744 + yieldedValues = [];
745 + TestRenderer.flushSync(fn);
746 + return yieldedValues;
747 + },
748 };
749
750 Object.defineProperty(
@@ -761,6 +768,10 @@ const ReactTestRendererFiber = {
768 /* eslint-disable camelcase */
769 unstable_batchedUpdates: batchedUpdates,
770 /* eslint-enable camelcase */
771 +
772 + unstable_setNowImplementation(implementation: () => number): void {
773 + nowImplementation = implementation;
774 + },
775 };
776
777 export default ReactTestRendererFiber;
packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js
+26
@@ -230,6 +230,32 @@ describe('ReactShallowRenderer', () => {
230 ]);
231 });
232
233 + it('should handle Profiler', () => {
234 + class SomeComponent extends React.Component {
235 + render() {
236 + return (
237 + <React.unstable_Profiler id="test" onRender={jest.fn()}>
238 + <div>
239 + <span className="child1" />
240 + <span className="child2" />
241 + </div>
242 + </React.unstable_Profiler>
243 + );
244 + }
245 + }
246 +
247 + const shallowRenderer = createRenderer();
248 + const result = shallowRenderer.render(<SomeComponent />);
249 +
250 + expect(result.type).toBe(React.unstable_Profiler);
251 + expect(result.props.children).toEqual(
252 + <div>
253 + <span className="child1" />
254 + <span className="child2" />
255 + </div>,
256 + );
257 + });
258 +
259 it('should enable shouldComponentUpdate to prevent a re-render', () => {
260 let renderCounter = 0;
261 class SimpleComponent extends React.Component {
packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js
+3 -1
@@ -37,7 +37,9 @@ describe('ReactTestRendererTraversal', () => {
37 <View void="void" />
38 <View void="void" />
39 </ExampleNull>
40 - <ExampleForwardRef qux="qux" />
40 + <React.unstable_Profiler id="test" onRender={() => {}}>
41 + <ExampleForwardRef qux="qux" />
42 + </React.unstable_Profiler>
43 </View>
44 </View>
45 );
packages/react/src/React.js
+3 -1
@@ -8,9 +8,10 @@
8 import assign from 'object-assign';
9 import ReactVersion from 'shared/ReactVersion';
10 import {
11 + REACT_ASYNC_MODE_TYPE,
12 REACT_FRAGMENT_TYPE,
13 + REACT_PROFILER_TYPE,
14 REACT_STRICT_MODE_TYPE,
13 - REACT_ASYNC_MODE_TYPE,
15 } from 'shared/ReactSymbols';
16
17 import {Component, PureComponent} from './ReactBaseClasses';
@@ -51,6 +52,7 @@ const React = {
52 Fragment: REACT_FRAGMENT_TYPE,
53 StrictMode: REACT_STRICT_MODE_TYPE,
54 unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
55 + unstable_Profiler: REACT_PROFILER_TYPE,
56
57 createElement: __DEV__ ? createElementWithValidation : createElement,
58 cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
packages/react/src/__tests__/ReactProfiler-test.internal.js new
+862
@@ -0,0 +1,862 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactTestRenderer;
15 +
16 +function loadModules({
17 + enableProfilerTimer = true,
18 + replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
19 +} = {}) {
20 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
21 + ReactFeatureFlags.debugRenderPhaseSideEffects = false;
22 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
23 + ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
24 + ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
25 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
26 + React = require('react');
27 + ReactTestRenderer = require('react-test-renderer');
28 +}
29 +
30 +describe('Profiler', () => {
31 + describe('works in profiling and non-profiling bundles', () => {
32 + [true, false].forEach(flagEnabled => {
33 + describe(`enableProfilerTimer ${
34 + flagEnabled ? 'enabled' : 'disabled'
35 + }`, () => {
36 + beforeEach(() => {
37 + jest.resetModules();
38 +
39 + loadModules({enableProfilerTimer: flagEnabled});
40 + });
41 +
42 + // This will throw in production too,
43 + // But the test is only interested in verifying the DEV error message.
44 + if (__DEV__) {
45 + it('should warn if required params are missing', () => {
46 + expect(() => {
47 + ReactTestRenderer.create(<React.unstable_Profiler />);
48 + }).toThrow(
49 + 'Profiler must specify an "id" string and "onRender" function as props',
50 + );
51 + });
52 + }
53 +
54 + it('should support an empty Profiler (with no children)', () => {
55 + // As root
56 + expect(
57 + ReactTestRenderer.create(
58 + <React.unstable_Profiler id="label" onRender={jest.fn()} />,
59 + ).toJSON(),
60 + ).toMatchSnapshot();
61 +
62 + // As non-root
63 + expect(
64 + ReactTestRenderer.create(
65 + <div>
66 + <React.unstable_Profiler id="label" onRender={jest.fn()} />
67 + </div>,
68 + ).toJSON(),
69 + ).toMatchSnapshot();
70 + });
71 +
72 + it('should render children', () => {
73 + const FunctionalComponent = ({label}) => <span>{label}</span>;
74 + const renderer = ReactTestRenderer.create(
75 + <div>
76 + <span>outside span</span>
77 + <React.unstable_Profiler id="label" onRender={jest.fn()}>
78 + <span>inside span</span>
79 + <FunctionalComponent label="functional component" />
80 + </React.unstable_Profiler>
81 + </div>,
82 + );
83 + expect(renderer.toJSON()).toMatchSnapshot();
84 + });
85 +
86 + it('should support nested Profilers', () => {
87 + const FunctionalComponent = ({label}) => <div>{label}</div>;
88 + class ClassComponent extends React.Component {
89 + render() {
90 + return <block>{this.props.label}</block>;
91 + }
92 + }
93 + const renderer = ReactTestRenderer.create(
94 + <React.unstable_Profiler id="outer" onRender={jest.fn()}>
95 + <FunctionalComponent label="outer functional component" />
96 + <React.unstable_Profiler id="inner" onRender={jest.fn()}>
97 + <ClassComponent label="inner class component" />
98 + <span>inner span</span>
99 + </React.unstable_Profiler>
100 + </React.unstable_Profiler>,
101 + );
102 + expect(renderer.toJSON()).toMatchSnapshot();
103 + });
104 + });
105 + });
106 + });
107 +
108 + describe('onRender callback', () => {
109 + let AdvanceTime;
110 + let advanceTimeBy;
111 +
112 + const mockNowForTests = () => {
113 + let currentTime = 0;
114 + ReactTestRenderer.unstable_setNowImplementation(() => currentTime);
115 + advanceTimeBy = amount => {
116 + currentTime += amount;
117 + };
118 + };
119 +
120 + beforeEach(() => {
121 + jest.resetModules();
122 +
123 + loadModules();
124 + mockNowForTests();
125 +
126 + AdvanceTime = class extends React.Component {
127 + static defaultProps = {
128 + byAmount: 10,
129 + shouldComponentUpdate: true,
130 + };
131 + shouldComponentUpdate(nextProps) {
132 + return nextProps.shouldComponentUpdate;
133 + }
134 + render() {
135 + // Simulate time passing when this component is rendered
136 + advanceTimeBy(this.props.byAmount);
137 + return this.props.children || null;
138 + }
139 + };
140 + });
141 +
142 + it('is not invoked until the commit phase', () => {
143 + const callback = jest.fn();
144 +
145 + const Yield = ({value}) => {
146 + renderer.unstable_yield(value);
147 + return null;
148 + };
149 +
150 + const renderer = ReactTestRenderer.create(
151 + <React.unstable_Profiler id="test" onRender={callback}>
152 + <Yield value="first" />
153 + <Yield value="last" />
154 + </React.unstable_Profiler>,
155 + {
156 + unstable_isAsync: true,
157 + },
158 + );
159 +
160 + // Times are logged until a render is committed.
161 + renderer.unstable_flushThrough(['first']);
162 + expect(callback).toHaveBeenCalledTimes(0);
163 + expect(renderer.unstable_flushAll()).toEqual(['last']);
164 + expect(callback).toHaveBeenCalledTimes(1);
165 + });
166 +
167 + it('logs render times for both mount and update', () => {
168 + const callback = jest.fn();
169 +
170 + const renderer = ReactTestRenderer.create(
171 + <React.unstable_Profiler id="test" onRender={callback}>
172 + <AdvanceTime />
173 + </React.unstable_Profiler>,
174 + );
175 +
176 + expect(callback).toHaveBeenCalledTimes(1);
177 +
178 + let [call] = callback.mock.calls;
179 +
180 + expect(call).toHaveLength(4);
181 + expect(call[0]).toBe('test');
182 + expect(call[1]).toBe('mount');
183 + expect(call[2]).toBe(10); // "actual" time
184 + expect(call[3]).toBe(10); // "base" time
185 +
186 + callback.mockReset();
187 +
188 + renderer.update(
189 + <React.unstable_Profiler id="test" onRender={callback}>
190 + <AdvanceTime />
191 + </React.unstable_Profiler>,
192 + );
193 +
194 + expect(callback).toHaveBeenCalledTimes(1);
195 +
196 + [call] = callback.mock.calls;
197 +
198 + expect(call).toHaveLength(4);
199 + expect(call[0]).toBe('test');
200 + expect(call[1]).toBe('update');
201 + expect(call[2]).toBe(10); // "actual" time
202 + expect(call[3]).toBe(10); // "base" time
203 + });
204 +
205 + it('includes render times of nested Profilers in their parent times', () => {
206 + const callback = jest.fn();
207 +
208 + ReactTestRenderer.create(
209 + <React.Fragment>
210 + <React.unstable_Profiler id="parent" onRender={callback}>
211 + <AdvanceTime byAmount={10}>
212 + <React.unstable_Profiler id="child" onRender={callback}>
213 + <AdvanceTime byAmount={20} />
214 + </React.unstable_Profiler>
215 + </AdvanceTime>
216 + </React.unstable_Profiler>
217 + </React.Fragment>,
218 + );
219 +
220 + expect(callback).toHaveBeenCalledTimes(2);
221 +
222 + // Callbacks bubble (reverse order).
223 + const [childCall, parentCall] = callback.mock.calls;
224 + expect(childCall[0]).toBe('child');
225 + expect(parentCall[0]).toBe('parent');
226 +
227 + // Parent times should include child times
228 + expect(childCall[2]).toBe(20); // "actual" time
229 + expect(childCall[3]).toBe(20); // "base" time
230 + expect(parentCall[2]).toBe(30); // "actual" time
231 + expect(parentCall[3]).toBe(30); // "base" time
232 + });
233 +
234 + it('tracks sibling Profilers separately', () => {
235 + const callback = jest.fn();
236 +
237 + ReactTestRenderer.create(
238 + <React.Fragment>
239 + <React.unstable_Profiler id="first" onRender={callback}>
240 + <AdvanceTime byAmount={20} />
241 + </React.unstable_Profiler>
242 + <React.unstable_Profiler id="second" onRender={callback}>
243 + <AdvanceTime byAmount={5} />
244 + </React.unstable_Profiler>
245 + </React.Fragment>,
246 + );
247 +
248 + expect(callback).toHaveBeenCalledTimes(2);
249 +
250 + const [firstCall, secondCall] = callback.mock.calls;
251 + expect(firstCall[0]).toBe('first');
252 + expect(secondCall[0]).toBe('second');
253 +
254 + // Parent times should include child times
255 + expect(firstCall[2]).toBe(20); // "actual" time
256 + expect(firstCall[3]).toBe(20); // "base" time
257 + expect(secondCall[2]).toBe(5); // "actual" time
258 + expect(secondCall[3]).toBe(5); // "base" time
259 + });
260 +
261 + it('does not include time spent outside of profile root', () => {
262 + const callback = jest.fn();
263 +
264 + ReactTestRenderer.create(
265 + <React.Fragment>
266 + <AdvanceTime byAmount={20} />
267 + <React.unstable_Profiler id="test" onRender={callback}>
268 + <AdvanceTime byAmount={5} />
269 + </React.unstable_Profiler>
270 + <AdvanceTime byAmount={20} />
271 + </React.Fragment>,
272 + );
273 +
274 + expect(callback).toHaveBeenCalledTimes(1);
275 +
276 + const [call] = callback.mock.calls;
277 + expect(call[0]).toBe('test');
278 + expect(call[2]).toBe(5); // "actual" time
279 + expect(call[3]).toBe(5); // "base" time
280 + });
281 +
282 + it('is not called when blocked by sCU false', () => {
283 + const callback = jest.fn();
284 +
285 + let instance;
286 + class Updater extends React.Component {
287 + state = {};
288 + render() {
289 + instance = this;
290 + return this.props.children;
291 + }
292 + }
293 +
294 + class Pure extends React.PureComponent {
295 + render() {
296 + return this.props.children;
297 + }
298 + }
299 +
300 + const renderer = ReactTestRenderer.create(
301 + <React.unstable_Profiler id="outer" onRender={callback}>
302 + <Updater>
303 + <React.unstable_Profiler id="middle" onRender={callback}>
304 + <Pure>
305 + <React.unstable_Profiler id="inner" onRender={callback}>
306 + <div />
307 + </React.unstable_Profiler>
308 + </Pure>
309 + </React.unstable_Profiler>
310 + </Updater>
311 + </React.unstable_Profiler>,
312 + );
313 +
314 + // All profile callbacks are called for initial render
315 + expect(callback).toHaveBeenCalledTimes(3);
316 +
317 + callback.mockReset();
318 +
319 + renderer.unstable_flushSync(() => {
320 + instance.setState({
321 + count: 1,
322 + });
323 + });
324 +
325 + // Only call profile updates for paths that have re-rendered
326 + // Since "inner" is beneath a pure compoent, it isn't called
327 + expect(callback).toHaveBeenCalledTimes(2);
328 + expect(callback.mock.calls[0][0]).toBe('middle');
329 + expect(callback.mock.calls[1][0]).toBe('outer');
330 + });
331 +
332 + it('decreases "actual" time but not "base" time when sCU prevents an update', () => {
333 + const callback = jest.fn();
334 +
335 + const renderer = ReactTestRenderer.create(
336 + <React.unstable_Profiler id="test" onRender={callback}>
337 + <AdvanceTime>
338 + <AdvanceTime shouldComponentUpdate={false} />
339 + </AdvanceTime>
340 + </React.unstable_Profiler>,
341 + );
342 +
343 + expect(callback).toHaveBeenCalledTimes(1);
344 +
345 + renderer.update(
346 + <React.unstable_Profiler id="test" onRender={callback}>
347 + <AdvanceTime>
348 + <AdvanceTime shouldComponentUpdate={false} />
349 + </AdvanceTime>
350 + </React.unstable_Profiler>,
351 + );
352 +
353 + expect(callback).toHaveBeenCalledTimes(2);
354 +
355 + const [mountCall, updateCall] = callback.mock.calls;
356 +
357 + expect(mountCall[1]).toBe('mount');
358 + expect(mountCall[2]).toBe(20); // "actual" time
359 + expect(mountCall[3]).toBe(20); // "base" time
360 +
361 + expect(updateCall[1]).toBe('update');
362 + expect(updateCall[2]).toBe(10); // "actual" time
363 + expect(updateCall[3]).toBe(20); // "base" time
364 + });
365 +
366 + it('includes time spent in render phase lifecycles', () => {
367 + class WithLifecycles extends React.Component {
368 + state = {};
369 + static getDerivedStateFromProps() {
370 + advanceTimeBy(3);
371 + return null;
372 + }
373 + shouldComponentUpdate() {
374 + advanceTimeBy(7);
375 + return true;
376 + }
377 + render() {
378 + advanceTimeBy(5);
379 + return null;
380 + }
381 + }
382 +
383 + const callback = jest.fn();
384 +
385 + const renderer = ReactTestRenderer.create(
386 + <React.unstable_Profiler id="test" onRender={callback}>
387 + <WithLifecycles />
388 + </React.unstable_Profiler>,
389 + );
390 +
391 + renderer.update(
392 + <React.unstable_Profiler id="test" onRender={callback}>
393 + <WithLifecycles />
394 + </React.unstable_Profiler>,
395 + );
396 +
397 + expect(callback).toHaveBeenCalledTimes(2);
398 +
399 + const [mountCall, updateCall] = callback.mock.calls;
400 +
401 + expect(mountCall[1]).toBe('mount');
402 + expect(mountCall[2]).toBe(8); // "actual" time
403 + expect(mountCall[3]).toBe(8); // "base" time
404 +
405 + expect(updateCall[1]).toBe('update');
406 + expect(updateCall[2]).toBe(15); // "actual" time
407 + expect(updateCall[3]).toBe(15); // "base" time
408 + });
409 +
410 + describe('with regard to interruptions', () => {
411 + it('should accumulate "actual" time after a scheduling interruptions', () => {
412 + const callback = jest.fn();
413 +
414 + const Yield = ({renderTime}) => {
415 + advanceTimeBy(renderTime);
416 + renderer.unstable_yield('Yield:' + renderTime);
417 + return null;
418 + };
419 +
420 + // Render partially, but run out of time before completing.
421 + const renderer = ReactTestRenderer.create(
422 + <React.unstable_Profiler id="test" onRender={callback}>
423 + <Yield renderTime={2} />
424 + <Yield renderTime={3} />
425 + </React.unstable_Profiler>,
426 + {unstable_isAsync: true},
427 + );
428 + expect(renderer.unstable_flushThrough(['Yield:2'])).toEqual([
429 + 'Yield:2',
430 + ]);
431 + expect(callback).toHaveBeenCalledTimes(0);
432 +
433 + // Resume render for remaining children.
434 + expect(renderer.unstable_flushAll()).toEqual(['Yield:3']);
435 +
436 + // Verify that logged times include both durations above.
437 + expect(callback).toHaveBeenCalledTimes(1);
438 + expect(callback.mock.calls[0][2]).toBe(5); // "actual" time
439 + expect(callback.mock.calls[0][3]).toBe(5); // "base" time
440 + });
441 +
442 + it('should not include time between frames', () => {
443 + const callback = jest.fn();
444 +
445 + const Yield = ({renderTime}) => {
446 + advanceTimeBy(renderTime);
447 + renderer.unstable_yield('Yield:' + renderTime);
448 + return null;
449 + };
450 +
451 + // Render partially, but don't finish.
452 + // This partial render should take 5ms of simulated time.
453 + const renderer = ReactTestRenderer.create(
454 + <React.unstable_Profiler id="outer" onRender={callback}>
455 + <Yield renderTime={5} />
456 + <Yield renderTime={10} />
457 + <React.unstable_Profiler id="inner" onRender={callback}>
458 + <Yield renderTime={17} />
459 + </React.unstable_Profiler>
460 + </React.unstable_Profiler>,
461 + {unstable_isAsync: true},
462 + );
463 + expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([
464 + 'Yield:5',
465 + ]);
466 + expect(callback).toHaveBeenCalledTimes(0);
467 +
468 + // Simulate time moving forward while frame is paused.
469 + advanceTimeBy(50);
470 +
471 + // Flush the remaninig work,
472 + // Which should take an additional 10ms of simulated time.
473 + expect(renderer.unstable_flushAll()).toEqual(['Yield:10', 'Yield:17']);
474 + expect(callback).toHaveBeenCalledTimes(2);
475 +
476 + const [innerCall, outerCall] = callback.mock.calls;
477 +
478 + // Verify that the "actual" time includes all work times,
479 + // But not the time that elapsed between frames.
480 + expect(innerCall[0]).toBe('inner');
481 + expect(innerCall[2]).toBe(17); // "actual" time
482 + expect(innerCall[3]).toBe(17); // "base" time
483 + expect(outerCall[0]).toBe('outer');
484 + expect(outerCall[2]).toBe(32); // "actual" time
485 + expect(outerCall[3]).toBe(32); // "base" time
486 + });
487 +
488 + it('should report the expected times when a high-priority update replaces an in-progress initial render', () => {
489 + const callback = jest.fn();
490 +
491 + const Yield = ({renderTime}) => {
492 + advanceTimeBy(renderTime);
493 + renderer.unstable_yield('Yield:' + renderTime);
494 + return null;
495 + };
496 +
497 + // Render a partially update, but don't finish.
498 + // This partial render should take 10ms of simulated time.
499 + const renderer = ReactTestRenderer.create(
500 + <React.unstable_Profiler id="test" onRender={callback}>
501 + <Yield renderTime={10} />
502 + <Yield renderTime={20} />
503 + </React.unstable_Profiler>,
504 + {unstable_isAsync: true},
505 + );
506 + expect(renderer.unstable_flushThrough(['first'])).toEqual(['Yield:10']);
507 + expect(callback).toHaveBeenCalledTimes(0);
508 +
509 + // Simulate time moving forward while frame is paused.
510 + advanceTimeBy(100);
511 +
512 + // Interrupt with higher priority work.
513 + // The interrupted work simulates an additional 5ms of time.
514 + expect(
515 + renderer.unstable_flushSync(() => {
516 + renderer.update(
517 + <React.unstable_Profiler id="test" onRender={callback}>
518 + <Yield renderTime={5} />
519 + </React.unstable_Profiler>,
520 + );
521 + }),
522 + ).toEqual(['Yield:5']);
523 +
524 + // The initial work was thrown away in this case,
525 + // So the "actual" and "base" times should only include the final rendered tree times.
526 + expect(callback).toHaveBeenCalledTimes(1);
527 + let call = callback.mock.calls[0];
528 + expect(call[2]).toBe(5); // "actual" time
529 + expect(call[3]).toBe(5); // "base" time
530 +
531 + callback.mockReset();
532 +
533 + // Verify no more unexpected callbacks from low priority work
534 + expect(renderer.unstable_flushAll()).toEqual([]);
535 + expect(callback).toHaveBeenCalledTimes(0);
536 + });
537 +
538 + it('should report the expected times when a high-priority update replaces a low-priority update', () => {
539 + const callback = jest.fn();
540 +
541 + const Yield = ({renderTime}) => {
542 + advanceTimeBy(renderTime);
543 + renderer.unstable_yield('Yield:' + renderTime);
544 + return null;
545 + };
546 +
547 + const renderer = ReactTestRenderer.create(
548 + <React.unstable_Profiler id="test" onRender={callback}>
549 + <Yield renderTime={6} />
550 + <Yield renderTime={15} />
551 + </React.unstable_Profiler>,
552 + {unstable_isAsync: true},
553 + );
554 +
555 + // Render everything initially.
556 + // This should take 21 seconds of "actual" and "base" time.
557 + expect(renderer.unstable_flushAll()).toEqual(['Yield:6', 'Yield:15']);
558 + expect(callback).toHaveBeenCalledTimes(1);
559 + let call = callback.mock.calls[0];
560 + expect(call[2]).toBe(21); // "actual" time
561 + expect(call[3]).toBe(21); // "base" time
562 +
563 + callback.mockReset();
564 +
565 + // Render a partially update, but don't finish.
566 + // This partial render should take 3ms of simulated time.
567 + renderer.update(
568 + <React.unstable_Profiler id="test" onRender={callback}>
569 + <Yield renderTime={3} />
570 + <Yield renderTime={5} />
571 + <Yield renderTime={9} />
572 + </React.unstable_Profiler>,
573 + );
574 + expect(renderer.unstable_flushThrough(['Yield:3'])).toEqual([
575 + 'Yield:3',
576 + ]);
577 + expect(callback).toHaveBeenCalledTimes(0);
578 +
579 + // Simulate time moving forward while frame is paused.
580 + advanceTimeBy(100);
581 +
582 + // Render another 5ms of simulated time.
583 + expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([
584 + 'Yield:5',
585 + ]);
586 + expect(callback).toHaveBeenCalledTimes(0);
587 +
588 + // Simulate time moving forward while frame is paused.
589 + advanceTimeBy(100);
590 +
591 + // Interrupt with higher priority work.
592 + // The interrupted work simulates an additional 11ms of time.
593 + expect(
594 + renderer.unstable_flushSync(() => {
595 + renderer.update(
596 + <React.unstable_Profiler id="test" onRender={callback}>
597 + <Yield renderTime={11} />
598 + </React.unstable_Profiler>,
599 + );
600 + }),
601 + ).toEqual(['Yield:11']);
602 +
603 + // Verify that the "actual" time includes all three durations above.
604 + // And the "base" time includes only the final rendered tree times.
605 + expect(callback).toHaveBeenCalledTimes(1);
606 + call = callback.mock.calls[0];
607 + expect(call[2]).toBe(19); // "actual" time
608 + expect(call[3]).toBe(11); // "base" time
609 +
610 + // Verify no more unexpected callbacks from low priority work
611 + expect(renderer.unstable_flushAll()).toEqual([]);
612 + expect(callback).toHaveBeenCalledTimes(1);
613 + });
614 +
615 + it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
616 + const callback = jest.fn();
617 +
618 + const Yield = ({renderTime}) => {
619 + advanceTimeBy(renderTime);
620 + renderer.unstable_yield('Yield:' + renderTime);
621 + return null;
622 + };
623 +
624 + let first;
625 + class FirstComponent extends React.Component {
626 + state = {renderTime: 1};
627 + render() {
628 + first = this;
629 + advanceTimeBy(this.state.renderTime);
630 + renderer.unstable_yield('FirstComponent:' + this.state.renderTime);
631 + return <Yield renderTime={4} />;
632 + }
633 + }
634 + let second;
635 + class SecondComponent extends React.Component {
636 + state = {renderTime: 2};
637 + render() {
638 + second = this;
639 + advanceTimeBy(this.state.renderTime);
640 + renderer.unstable_yield('SecondComponent:' + this.state.renderTime);
641 + return <Yield renderTime={7} />;
642 + }
643 + }
644 +
645 + const renderer = ReactTestRenderer.create(
646 + <React.unstable_Profiler id="test" onRender={callback} foo="1">
647 + <FirstComponent />
648 + <SecondComponent />
649 + </React.unstable_Profiler>,
650 + {unstable_isAsync: true},
651 + );
652 +
653 + // Render everything initially.
654 + // This simulates a total of 14ms of "actual" render time.
655 + // The "base" render time is also 14ms for the initial render.
656 + expect(renderer.unstable_flushAll()).toEqual([
657 + 'FirstComponent:1',
658 + 'Yield:4',
659 + 'SecondComponent:2',
660 + 'Yield:7',
661 + ]);
662 + expect(callback).toHaveBeenCalledTimes(1);
663 + let call = callback.mock.calls[0];
664 + expect(call[2]).toBe(14); // "actual" time
665 + expect(call[3]).toBe(14); // "base" time
666 +
667 + callback.mockClear();
668 +
669 + // Render a partially update, but don't finish.
670 + // This partial render will take 10ms of "actual" render time.
671 + first.setState({renderTime: 10});
672 + expect(renderer.unstable_flushThrough(['FirstComponent:10'])).toEqual([
673 + 'FirstComponent:10',
674 + ]);
675 + expect(callback).toHaveBeenCalledTimes(0);
676 +
677 + // Simulate time moving forward while frame is paused.
678 + advanceTimeBy(100);
679 +
680 + // Interrupt with higher priority work.
681 + // This simulates a total of 37ms of "actual" render time.
682 + expect(
683 + renderer.unstable_flushSync(() => second.setState({renderTime: 30})),
684 + ).toEqual(['SecondComponent:30', 'Yield:7']);
685 +
686 + // Verify that the "actual" time includes time spent in the both renders so far (10ms and 37ms).
687 + // The "base" time should include the more recent times for the SecondComponent subtree,
688 + // As well as the original times for the FirstComponent subtree.
689 + expect(callback).toHaveBeenCalledTimes(1);
690 + call = callback.mock.calls[0];
691 + expect(call[2]).toBe(47); // "actual" time
692 + expect(call[3]).toBe(42); // "base" time
693 +
694 + callback.mockClear();
695 +
696 + // Resume the original low priority update, with rebased state.
697 + // This simulates a total of 14ms of "actual" render time,
698 + // And does not include the original (interrupted) 10ms.
699 + // The tree contains 42ms of "base" render time at this point,
700 + // Reflecting the most recent (longer) render durations.
701 + // TODO: This "actual" time should decrease by 10ms once the scheduler supports resuming.
702 + expect(renderer.unstable_flushAll()).toEqual([
703 + 'FirstComponent:10',
704 + 'Yield:4',
705 + ]);
706 + expect(callback).toHaveBeenCalledTimes(1);
707 + call = callback.mock.calls[0];
708 + expect(call[2]).toBe(14); // "actual" time
709 + expect(call[3]).toBe(51); // "base" time
710 + });
711 +
712 + [true, false].forEach(flagEnabled => {
713 + describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
714 + flagEnabled ? 'enabled' : 'disabled'
715 + }`, () => {
716 + beforeEach(() => {
717 + jest.resetModules();
718 +
719 + loadModules({
720 + replayFailedUnitOfWorkWithInvokeGuardedCallback: flagEnabled,
721 + });
722 + mockNowForTests();
723 + });
724 +
725 + it('should accumulate "actual" time after an error handled by componentDidCatch()', () => {
726 + const callback = jest.fn();
727 +
728 + const ThrowsError = () => {
729 + advanceTimeBy(10);
730 + throw Error('expected error');
731 + };
732 +
733 + class ErrorBoundary extends React.Component {
734 + state = {error: null};
735 + componentDidCatch(error) {
736 + this.setState({error});
737 + }
738 + render() {
739 + advanceTimeBy(2);
740 + return this.state.error === null ? (
741 + this.props.children
742 + ) : (
743 + <AdvanceTime byAmount={20} />
744 + );
745 + }
746 + }
747 +
748 + ReactTestRenderer.create(
749 + <React.unstable_Profiler id="test" onRender={callback}>
750 + <ErrorBoundary>
751 + <AdvanceTime byAmount={5} />
752 + <ThrowsError />
753 + </ErrorBoundary>
754 + </React.unstable_Profiler>,
755 + );
756 +
757 + expect(callback).toHaveBeenCalledTimes(2);
758 +
759 + // Callbacks bubble (reverse order).
760 + let [mountCall, updateCall] = callback.mock.calls;
761 +
762 + // The initial mount only includes the ErrorBoundary (which takes 2ms)
763 + // But it spends time rendering all of the failed subtree also.
764 + expect(mountCall[1]).toBe('mount');
765 + // "actual" time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
766 + // If replayFailedUnitOfWorkWithInvokeGuardedCallback is enbaled, ThrowsError is replayed.
767 + expect(mountCall[2]).toBe(flagEnabled && __DEV__ ? 27 : 17);
768 + // "base" time includes: 2 (ErrorBoundary)
769 + expect(mountCall[3]).toBe(2);
770 +
771 + // The update includes the ErrorBoundary and its fallback child
772 + expect(updateCall[1]).toBe('update');
773 + // "actual" time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
774 + expect(updateCall[2]).toBe(22);
775 + // "base" time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
776 + expect(updateCall[3]).toBe(22);
777 + });
778 +
779 + it('should accumulate "actual" time after an error handled by getDerivedStateFromCatch()', () => {
780 + const callback = jest.fn();
781 +
782 + const ThrowsError = () => {
783 + advanceTimeBy(10);
784 + throw Error('expected error');
785 + };
786 +
787 + class ErrorBoundary extends React.Component {
788 + state = {error: null};
789 + static getDerivedStateFromCatch(error) {
790 + return {error};
791 + }
792 + render() {
793 + advanceTimeBy(2);
794 + return this.state.error === null ? (
795 + this.props.children
796 + ) : (
797 + <AdvanceTime byAmount={20} />
798 + );
799 + }
800 + }
801 +
802 + ReactTestRenderer.create(
803 + <React.unstable_Profiler id="test" onRender={callback}>
804 + <ErrorBoundary>
805 + <AdvanceTime byAmount={5} />
806 + <ThrowsError />
807 + </ErrorBoundary>
808 + </React.unstable_Profiler>,
809 + );
810 +
811 + expect(callback).toHaveBeenCalledTimes(1);
812 +
813 + // Callbacks bubble (reverse order).
814 + let [mountCall] = callback.mock.calls;
815 +
816 + // The initial mount includes the ErrorBoundary's error state,
817 + // But i also spends "actual" time rendering UI that fails and isn't included.
818 + expect(mountCall[1]).toBe('mount');
819 + // "actual" time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
820 + // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
821 + // If replayFailedUnitOfWorkWithInvokeGuardedCallback is enbaled, ThrowsError is replayed.
822 + expect(mountCall[2]).toBe(flagEnabled && __DEV__ ? 49 : 39);
823 + // "base" time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
824 + expect(mountCall[3]).toBe(22);
825 + });
826 + });
827 + });
828 + });
829 +
830 + it('reflects the most recently rendered id value', () => {
831 + const callback = jest.fn();
832 +
833 + const renderer = ReactTestRenderer.create(
834 + <React.unstable_Profiler id="one" onRender={callback}>
835 + <AdvanceTime byAmount={2} />
836 + </React.unstable_Profiler>,
837 + );
838 +
839 + expect(callback).toHaveBeenCalledTimes(1);
840 +
841 + renderer.update(
842 + <React.unstable_Profiler id="two" onRender={callback}>
843 + <AdvanceTime byAmount={1} />
844 + </React.unstable_Profiler>,
845 + );
846 +
847 + expect(callback).toHaveBeenCalledTimes(2);
848 +
849 + const [mountCall, updateCall] = callback.mock.calls;
850 +
851 + expect(mountCall[0]).toBe('one');
852 + expect(mountCall[1]).toBe('mount');
853 + expect(mountCall[2]).toBe(2); // "actual" time
854 + expect(mountCall[3]).toBe(2); // "base" time
855 +
856 + expect(updateCall[0]).toBe('two');
857 + expect(updateCall[1]).toBe('update');
858 + expect(updateCall[2]).toBe(1); // "actual" time
859 + expect(updateCall[3]).toBe(1); // "base" time
860 + });
861 + });
862 +});
packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap new
+65
@@ -0,0 +1,65 @@
1 +// Jest Snapshot v1, https://goo.gl/fbAQLP
2 +
3 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should render children 1`] = `
4 +<div>
5 + <span>
6 + outside span
7 + </span>
8 + <span>
9 + inside span
10 + </span>
11 + <span>
12 + functional component
13 + </span>
14 +</div>
15 +`;
16 +
17 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support an empty Profiler (with no children) 1`] = `null`;
18 +
19 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support an empty Profiler (with no children) 2`] = `<div />`;
20 +
21 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support nested Profilers 1`] = `
22 +Array [
23 + <div>
24 + outer functional component
25 + </div>,
26 + <block>
27 + inner class component
28 + </block>,
29 + <span>
30 + inner span
31 + </span>,
32 +]
33 +`;
34 +
35 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should render children 1`] = `
36 +<div>
37 + <span>
38 + outside span
39 + </span>
40 + <span>
41 + inside span
42 + </span>
43 + <span>
44 + functional component
45 + </span>
46 +</div>
47 +`;
48 +
49 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support an empty Profiler (with no children) 1`] = `null`;
50 +
51 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support an empty Profiler (with no children) 2`] = `<div />`;
52 +
53 +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support nested Profilers 1`] = `
54 +Array [
55 + <div>
56 + outer functional component
57 + </div>,
58 + <block>
59 + inner class component
60 + </block>,
61 + <span>
62 + inner span
63 + </span>,
64 +]
65 +`;
packages/shared/ReactFeatureFlags.js
+3
@@ -37,6 +37,9 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
37 // Warn about deprecated, async-unsafe lifecycles; relates to RFC #6:
38 export const warnAboutDeprecatedLifecycles = false;
39
40 +// Gather advanced timing metrics for Profiler subtrees.
41 +export const enableProfilerTimer = false;
42 +
43 // Only used in www builds.
44 export function addUserTimingListener() {
45 invariant(false, 'Not implemented.');
packages/shared/ReactSymbols.js
+3
@@ -27,6 +27,9 @@ export const REACT_FRAGMENT_TYPE = hasSymbol
27 export const REACT_STRICT_MODE_TYPE = hasSymbol
28 ? Symbol.for('react.strict_mode')
29 : 0xeacc;
30 +export const REACT_PROFILER_TYPE = hasSymbol
31 + ? Symbol.for('react.profile_root')
32 + : 0xeacc;
33 export const REACT_PROVIDER_TYPE = hasSymbol
34 ? Symbol.for('react.provider')
35 : 0xeacd;
packages/shared/ReactTypeOfWork.js
+3 -1
@@ -22,7 +22,8 @@ export type TypeOfWork =
22 | 11
23 | 12
24 | 13
25 - | 14;
25 + | 14
26 + | 15;
27
28 export const IndeterminateComponent = 0; // Before we know whether it is functional or class
29 export const FunctionalComponent = 1;
@@ -39,3 +40,4 @@ export const Mode = 11;
40 export const ContextConsumer = 12;
41 export const ContextProvider = 13;
42 export const ForwardRef = 14;
43 +export const Profiler = 15;
packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
+1
@@ -18,6 +18,7 @@ export const enableUserTimingAPI = __DEV__;
18 export const enableGetDerivedStateFromCatch = false;
19 export const warnAboutDeprecatedLifecycles = false;
20 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
21 +export const enableProfilerTimer = __DEV__;
22
23 // React Fabric uses persistent reconciler.
24 export const enableMutatingReconciler = false;
packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
+1
@@ -18,6 +18,7 @@ export const enableUserTimingAPI = __DEV__;
18 export const enableGetDerivedStateFromCatch = false;
19 export const warnAboutDeprecatedLifecycles = false;
20 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
21 +export const enableProfilerTimer = false;
22
23 // React Fabric uses persistent reconciler.
24 export const enableMutatingReconciler = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -19,6 +19,7 @@ export const {
19 debugRenderPhaseSideEffectsForStrictMode,
20 warnAboutDeprecatedLifecycles,
21 replayFailedUnitOfWorkWithInvokeGuardedCallback,
22 + enableProfilerTimer,
23 } = require('ReactFeatureFlags');
24
25 // The rest of the flags are static for better dead code elimination.
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -21,6 +21,7 @@ export const enablePersistentReconciler = false;
21 export const enableUserTimingAPI = __DEV__;
22 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
23 export const warnAboutDeprecatedLifecycles = false;
24 +export const enableProfilerTimer = false;
25
26 // Only used in www builds.
27 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -18,6 +18,7 @@ export const enableUserTimingAPI = __DEV__;
18 export const enableGetDerivedStateFromCatch = false;
19 export const warnAboutDeprecatedLifecycles = false;
20 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
21 +export const enableProfilerTimer = false;
22
23 // react-reconciler/persistent entry point
24 // uses a persistent reconciler.
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -21,6 +21,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
21 export const enableMutatingReconciler = true;
22 export const enableNoopReconciler = false;
23 export const enablePersistentReconciler = false;
24 +export const enableProfilerTimer = false;
25
26 // Only used in www builds.
27 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -17,6 +17,7 @@ export const {
17 enableGetDerivedStateFromCatch,
18 replayFailedUnitOfWorkWithInvokeGuardedCallback,
19 warnAboutDeprecatedLifecycles,
20 + enableProfilerTimer,
21 } = require('ReactFeatureFlags');
22
23 // The rest of the flags are static for better dead code elimination.
packages/shared/getComponentName.js
+12 -3
@@ -10,11 +10,14 @@
10 import type {Fiber} from 'react-reconciler/src/ReactFiber';
11
12 import {
13 + REACT_ASYNC_MODE_TYPE,
14 REACT_CALL_TYPE,
15 + REACT_FORWARD_REF_TYPE,
16 REACT_FRAGMENT_TYPE,
17 REACT_RETURN_TYPE,
18 REACT_PORTAL_TYPE,
17 - REACT_FORWARD_REF_TYPE,
19 + REACT_PROFILER_TYPE,
20 + REACT_STRICT_MODE_TYPE,
21 } from 'shared/ReactSymbols';
22
23 function getComponentName(fiber: Fiber): string | null {
@@ -26,14 +29,20 @@ function getComponentName(fiber: Fiber): string | null {
29 return type;
30 }
31 switch (type) {
32 + case REACT_ASYNC_MODE_TYPE:
33 + return 'AsyncMode';
34 + case REACT_CALL_TYPE:
35 + return 'ReactCall';
36 case REACT_FRAGMENT_TYPE:
37 return 'ReactFragment';
38 case REACT_PORTAL_TYPE:
39 return 'ReactPortal';
33 - case REACT_CALL_TYPE:
34 - return 'ReactCall';
40 + case REACT_PROFILER_TYPE:
41 + return `Profiler(${fiber.pendingProps.id})`;
42 case REACT_RETURN_TYPE:
43 return 'ReactReturn';
44 + case REACT_STRICT_MODE_TYPE:
45 + return 'StrictMode';
46 }
47 if (typeof type === 'object' && type !== null) {
48 switch (type.$$typeof) {
packages/shared/isValidElementType.js
+5 -3
@@ -8,12 +8,13 @@
8 */
9
10 import {
11 - REACT_FRAGMENT_TYPE,
11 REACT_ASYNC_MODE_TYPE,
13 - REACT_STRICT_MODE_TYPE,
14 - REACT_PROVIDER_TYPE,
12 REACT_CONTEXT_TYPE,
13 REACT_FORWARD_REF_TYPE,
14 + REACT_FRAGMENT_TYPE,
15 + REACT_PROFILER_TYPE,
16 + REACT_PROVIDER_TYPE,
17 + REACT_STRICT_MODE_TYPE,
18 } from 'shared/ReactSymbols';
19
20 export default function isValidElementType(type: mixed) {
@@ -23,6 +24,7 @@ export default function isValidElementType(type: mixed) {
24 // Note: its typeof might be other than 'symbol' or 'number' if it's a polyfill.
25 type === REACT_FRAGMENT_TYPE ||
26 type === REACT_ASYNC_MODE_TYPE ||
27 + type === REACT_PROFILER_TYPE ||
28 type === REACT_STRICT_MODE_TYPE ||
29 (typeof type === 'object' &&
30 type !== null &&