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 &&