Implemented Profiler onCommit() and onPostCommit() hooks (#17910)
* Implemented Profiler onCommit() and onPostCommit() hooks * Added enableProfilerCommitHooks feature flag for commit hooks * Moved onCommit and onPassiveCommit behind separate feature flag
Brian Vaughn committed
Mar 5, 2020 at 11:02 UTC
024a764310e64ef38963df16970364b9942a60cd
15 files changed
+2795
-1060
packages/react-reconciler/src/ReactFiber.js
+9
-7
@@ -826,13 +826,8 @@ function createFiberFromProfiler(
826
key: null | string,
827
): Fiber {
828
if (__DEV__) {
829
- if (
830
- typeof pendingProps.id !== 'string' ||
831
- typeof pendingProps.onRender !== 'function'
832
- ) {
833
- console.error(
834
- 'Profiler must specify an "id" string and "onRender" function as props',
835
- );
829
+ if (typeof pendingProps.id !== 'string') {
830
+ console.error('Profiler must specify an "id" as a prop');
831
}
832
}
833
@@ -842,6 +837,13 @@ function createFiberFromProfiler(
837
fiber.type = REACT_PROFILER_TYPE;
838
fiber.expirationTime = expirationTime;
839
840
+ if (enableProfilerTimer) {
841
+ fiber.stateNode = {
842
+ effectDuration: 0,
843
+ passiveEffectDuration: 0,
844
+ };
845
+ }
846
+
847
return fiber;
848
}
849
packages/react-reconciler/src/ReactFiberBeginWork.js
+12
@@ -580,6 +580,12 @@ function updateProfiler(
580
) {
581
if (enableProfilerTimer) {
582
workInProgress.effectTag |= Update;
583
+
584
+ // Reset effect durations for the next eventual effect phase.
585
+ // These are reset during render to allow the DevTools commit hook a chance to read them,
586
+ const stateNode = workInProgress.stateNode;
587
+ stateNode.effectDuration = 0;
588
+ stateNode.passiveEffectDuration = 0;
589
}
590
const nextProps = workInProgress.pendingProps;
591
const nextChildren = nextProps.children;
@@ -2944,6 +2950,12 @@ function beginWork(
2950
if (hasChildWork) {
2951
workInProgress.effectTag |= Update;
2952
}
2953
+
2954
+ // Reset effect durations for the next eventual effect phase.
2955
+ // These are reset during render to allow the DevTools commit hook a chance to read them,
2956
+ const stateNode = workInProgress.stateNode;
2957
+ stateNode.effectDuration = 0;
2958
+ stateNode.passiveEffectDuration = 0;
2959
}
2960
break;
2961
case SuspenseComponent: {
packages/react-reconciler/src/ReactFiberCommitWork.js
+256
-19
@@ -29,6 +29,7 @@ import {
29
deferPassiveEffectCleanupDuringUnmount,
30
enableSchedulerTracing,
31
enableProfilerTimer,
32
+ enableProfilerCommitHooks,
33
enableSuspenseServerRenderer,
34
enableDeprecatedFlareAPI,
35
enableFundamentalAPI,
@@ -76,7 +77,14 @@ import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
77
import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
78
import {logCapturedError} from './ReactFiberErrorLogger';
79
import {resolveDefaultProps} from './ReactFiberLazyComponent';
79
-import {getCommitTime} from './ReactProfilerTimer';
80
+import {
81
+ getCommitTime,
82
+ recordLayoutEffectDuration,
83
+ recordPassiveEffectDuration,
84
+ startLayoutEffectTimer,
85
+ startPassiveEffectTimer,
86
+} from './ReactProfilerTimer';
87
+import {ProfileMode} from './ReactTypeOfMode';
88
import {commitUpdateQueue} from './ReactUpdateQueue';
89
import {
90
getPublicInstance,
@@ -113,6 +121,7 @@ import {
121
markCommitTimeOfFallback,
122
enqueuePendingPassiveHookEffectMount,
123
enqueuePendingPassiveHookEffectUnmount,
124
+ enqueuePendingPassiveProfilerEffect,
125
} from './ReactFiberWorkLoop';
126
import {
127
NoEffect as NoHookEffect,
@@ -175,7 +184,20 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
184
startPhaseTimer(current, 'componentWillUnmount');
185
instance.props = current.memoizedProps;
186
instance.state = current.memoizedState;
178
- instance.componentWillUnmount();
187
+ if (
188
+ enableProfilerTimer &&
189
+ enableProfilerCommitHooks &&
190
+ current.mode & ProfileMode
191
+ ) {
192
+ try {
193
+ startLayoutEffectTimer();
194
+ instance.componentWillUnmount();
195
+ } finally {
196
+ recordLayoutEffectDuration(current);
197
+ }
198
+ } else {
199
+ instance.componentWillUnmount();
200
+ }
201
stopPhaseTimer();
202
};
203
@@ -430,8 +452,31 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void {
452
// TODO (#17945) We should call all passive destroy functions (for all fibers)
453
// before calling any create functions. The current approach only serializes
454
// these for a single fiber.
433
- commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
434
- commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
455
+ if (
456
+ enableProfilerTimer &&
457
+ enableProfilerCommitHooks &&
458
+ finishedWork.mode & ProfileMode
459
+ ) {
460
+ try {
461
+ startPassiveEffectTimer();
462
+ commitHookEffectListUnmount(
463
+ HookPassive | HookHasEffect,
464
+ finishedWork,
465
+ );
466
+ commitHookEffectListMount(
467
+ HookPassive | HookHasEffect,
468
+ finishedWork,
469
+ );
470
+ } finally {
471
+ recordPassiveEffectDuration(finishedWork);
472
+ }
473
+ } else {
474
+ commitHookEffectListUnmount(
475
+ HookPassive | HookHasEffect,
476
+ finishedWork,
477
+ );
478
+ commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
479
+ }
480
break;
481
}
482
default:
@@ -440,6 +485,61 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void {
485
}
486
}
487
488
+export function commitPassiveEffectDurations(
489
+ finishedRoot: FiberRoot,
490
+ finishedWork: Fiber,
491
+): void {
492
+ if (enableProfilerTimer && enableProfilerCommitHooks) {
493
+ // Only Profilers with work in their subtree will have an Update effect scheduled.
494
+ if ((finishedWork.effectTag & Update) !== NoEffect) {
495
+ switch (finishedWork.tag) {
496
+ case Profiler: {
497
+ const {passiveEffectDuration} = finishedWork.stateNode;
498
+ const {id, onPostCommit} = finishedWork.memoizedProps;
499
+
500
+ // This value will still reflect the previous commit phase.
501
+ // It does not get reset until the start of the next commit phase.
502
+ const commitTime = getCommitTime();
503
+
504
+ if (typeof onPostCommit === 'function') {
505
+ if (enableSchedulerTracing) {
506
+ onPostCommit(
507
+ id,
508
+ finishedWork.alternate === null ? 'mount' : 'update',
509
+ passiveEffectDuration,
510
+ commitTime,
511
+ finishedRoot.memoizedInteractions,
512
+ );
513
+ } else {
514
+ onPostCommit(
515
+ id,
516
+ finishedWork.alternate === null ? 'mount' : 'update',
517
+ passiveEffectDuration,
518
+ commitTime,
519
+ );
520
+ }
521
+ }
522
+
523
+ // Bubble times to the next nearest ancestor Profiler.
524
+ // After we process that Profiler, we'll bubble further up.
525
+ let parentFiber = finishedWork.return;
526
+ while (parentFiber !== null) {
527
+ if (parentFiber.tag === Profiler) {
528
+ const parentStateNode = parentFiber.stateNode;
529
+ parentStateNode.passiveEffectDuration += passiveEffectDuration;
530
+ break;
531
+ }
532
+ parentFiber = parentFiber.return;
533
+ }
534
+ break;
535
+ }
536
+ default:
537
+ break;
538
+ }
539
+ }
540
+ }
541
+}
542
+
543
function commitLifeCycles(
544
finishedRoot: FiberRoot,
545
current: Fiber | null,
@@ -455,7 +555,20 @@ function commitLifeCycles(
555
// This is done to prevent sibling component effects from interfering with each other,
556
// e.g. a destroy function in one component should never override a ref set
557
// by a create function in another component during the same commit.
458
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
558
+ if (
559
+ enableProfilerTimer &&
560
+ enableProfilerCommitHooks &&
561
+ finishedWork.mode & ProfileMode
562
+ ) {
563
+ try {
564
+ startLayoutEffectTimer();
565
+ commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
566
+ } finally {
567
+ recordLayoutEffectDuration(finishedWork);
568
+ }
569
+ } else {
570
+ commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
571
+ }
572
573
if (runAllPassiveEffectDestroysBeforeCreates) {
574
schedulePassiveEffects(finishedWork);
@@ -497,7 +610,20 @@ function commitLifeCycles(
610
}
611
}
612
}
500
- instance.componentDidMount();
613
+ if (
614
+ enableProfilerTimer &&
615
+ enableProfilerCommitHooks &&
616
+ finishedWork.mode & ProfileMode
617
+ ) {
618
+ try {
619
+ startLayoutEffectTimer();
620
+ instance.componentDidMount();
621
+ } finally {
622
+ recordLayoutEffectDuration(finishedWork);
623
+ }
624
+ } else {
625
+ instance.componentDidMount();
626
+ }
627
stopPhaseTimer();
628
} else {
629
const prevProps =
@@ -536,11 +662,28 @@ function commitLifeCycles(
662
}
663
}
664
}
539
- instance.componentDidUpdate(
540
- prevProps,
541
- prevState,
542
- instance.__reactInternalSnapshotBeforeUpdate,
543
- );
665
+ if (
666
+ enableProfilerTimer &&
667
+ enableProfilerCommitHooks &&
668
+ finishedWork.mode & ProfileMode
669
+ ) {
670
+ try {
671
+ startLayoutEffectTimer();
672
+ instance.componentDidUpdate(
673
+ prevProps,
674
+ prevState,
675
+ instance.__reactInternalSnapshotBeforeUpdate,
676
+ );
677
+ } finally {
678
+ recordLayoutEffectDuration(finishedWork);
679
+ }
680
+ } else {
681
+ instance.componentDidUpdate(
682
+ prevProps,
683
+ prevState,
684
+ instance.__reactInternalSnapshotBeforeUpdate,
685
+ );
686
+ }
687
stopPhaseTimer();
688
}
689
}
@@ -623,7 +766,10 @@ function commitLifeCycles(
766
}
767
case Profiler: {
768
if (enableProfilerTimer) {
626
- const onRender = finishedWork.memoizedProps.onRender;
769
+ const {onCommit, onRender} = finishedWork.memoizedProps;
770
+ const {effectDuration} = finishedWork.stateNode;
771
+
772
+ const commitTime = getCommitTime();
773
774
if (typeof onRender === 'function') {
775
if (enableSchedulerTracing) {
@@ -633,7 +779,7 @@ function commitLifeCycles(
779
finishedWork.actualDuration,
780
finishedWork.treeBaseDuration,
781
finishedWork.actualStartTime,
636
- getCommitTime(),
782
+ commitTime,
783
finishedRoot.memoizedInteractions,
784
);
785
} else {
@@ -643,10 +789,48 @@ function commitLifeCycles(
789
finishedWork.actualDuration,
790
finishedWork.treeBaseDuration,
791
finishedWork.actualStartTime,
646
- getCommitTime(),
792
+ commitTime,
793
);
794
}
795
}
796
+
797
+ if (enableProfilerCommitHooks) {
798
+ if (typeof onCommit === 'function') {
799
+ if (enableSchedulerTracing) {
800
+ onCommit(
801
+ finishedWork.memoizedProps.id,
802
+ current === null ? 'mount' : 'update',
803
+ effectDuration,
804
+ commitTime,
805
+ finishedRoot.memoizedInteractions,
806
+ );
807
+ } else {
808
+ onCommit(
809
+ finishedWork.memoizedProps.id,
810
+ current === null ? 'mount' : 'update',
811
+ effectDuration,
812
+ commitTime,
813
+ );
814
+ }
815
+ }
816
+
817
+ // Schedule a passive effect for this Profiler to call onPostCommit hooks.
818
+ // This effect should be scheduled even if there is no onPostCommit callback for this Profiler,
819
+ // because the effect is also where times bubble to parent Profilers.
820
+ enqueuePendingPassiveProfilerEffect(finishedWork);
821
+
822
+ // Propagate layout effect durations to the next nearest Profiler ancestor.
823
+ // Do not reset these values until the next render so DevTools has a chance to read them first.
824
+ let parentFiber = finishedWork.return;
825
+ while (parentFiber !== null) {
826
+ if (parentFiber.tag === Profiler) {
827
+ const parentStateNode = parentFiber.stateNode;
828
+ parentStateNode.effectDuration += effectDuration;
829
+ break;
830
+ }
831
+ parentFiber = parentFiber.return;
832
+ }
833
+ }
834
}
835
return;
836
}
@@ -797,7 +981,17 @@ function commitUnmount(
981
if ((tag & HookPassive) !== NoHookEffect) {
982
enqueuePendingPassiveHookEffectUnmount(current, effect);
983
} else {
800
- safelyCallDestroy(current, destroy);
984
+ if (
985
+ enableProfilerTimer &&
986
+ enableProfilerCommitHooks &&
987
+ current.mode & ProfileMode
988
+ ) {
989
+ startLayoutEffectTimer();
990
+ safelyCallDestroy(current, destroy);
991
+ recordLayoutEffectDuration(current);
992
+ } else {
993
+ safelyCallDestroy(current, destroy);
994
+ }
995
}
996
}
997
effect = effect.next;
@@ -822,9 +1016,23 @@ function commitUnmount(
1016
runWithPriority(priorityLevel, () => {
1017
let effect = firstEffect;
1018
do {
825
- const destroy = effect.destroy;
1019
+ const {destroy, tag} = effect;
1020
if (destroy !== undefined) {
827
- safelyCallDestroy(current, destroy);
1021
+ if (
1022
+ enableProfilerTimer &&
1023
+ enableProfilerCommitHooks &&
1024
+ current.mode & ProfileMode
1025
+ ) {
1026
+ if ((tag & HookPassive) !== NoHookEffect) {
1027
+ safelyCallDestroy(current, destroy);
1028
+ } else {
1029
+ startLayoutEffectTimer();
1030
+ safelyCallDestroy(current, destroy);
1031
+ recordLayoutEffectDuration(current);
1032
+ }
1033
+ } else {
1034
+ safelyCallDestroy(current, destroy);
1035
+ }
1036
}
1037
effect = effect.next;
1038
} while (effect !== firstEffect);
@@ -1366,7 +1574,23 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1574
// This prevents sibling component effects from interfering with each other,
1575
// e.g. a destroy function in one component should never override a ref set
1576
// by a create function in another component during the same commit.
1369
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1577
+ if (
1578
+ enableProfilerTimer &&
1579
+ enableProfilerCommitHooks &&
1580
+ finishedWork.mode & ProfileMode
1581
+ ) {
1582
+ try {
1583
+ startLayoutEffectTimer();
1584
+ commitHookEffectListUnmount(
1585
+ HookLayout | HookHasEffect,
1586
+ finishedWork,
1587
+ );
1588
+ } finally {
1589
+ recordLayoutEffectDuration(finishedWork);
1590
+ }
1591
+ } else {
1592
+ commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1593
+ }
1594
return;
1595
}
1596
case Profiler: {
@@ -1409,7 +1633,20 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1633
// This prevents sibling component effects from interfering with each other,
1634
// e.g. a destroy function in one component should never override a ref set
1635
// by a create function in another component during the same commit.
1412
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1636
+ if (
1637
+ enableProfilerTimer &&
1638
+ enableProfilerCommitHooks &&
1639
+ finishedWork.mode & ProfileMode
1640
+ ) {
1641
+ try {
1642
+ startLayoutEffectTimer();
1643
+ commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1644
+ } finally {
1645
+ recordLayoutEffectDuration(finishedWork);
1646
+ }
1647
+ } else {
1648
+ commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1649
+ }
1650
return;
1651
}
1652
case ClassComponent: {
packages/react-reconciler/src/ReactFiberWorkLoop.js
+79
-5
@@ -24,6 +24,7 @@ import {
24
enableSuspenseServerRenderer,
25
replayFailedUnitOfWorkWithInvokeGuardedCallback,
26
enableProfilerTimer,
27
+ enableProfilerCommitHooks,
28
enableSchedulerTracing,
29
warnAboutUnmockedScheduler,
30
flushSuspenseFallbacksInTests,
@@ -139,6 +140,7 @@ import {
140
commitDeletion,
141
commitDetachRef,
142
commitAttachRef,
143
+ commitPassiveEffectDurations,
144
commitResetTextContent,
145
} from './ReactFiberCommitWork';
146
import {enqueueUpdate} from './ReactUpdateQueue';
@@ -148,6 +150,8 @@ import {createCapturedValue} from './ReactCapturedValue';
150
151
import {
152
recordCommitTime,
153
+ recordPassiveEffectDuration,
154
+ startPassiveEffectTimer,
155
startProfilerTimer,
156
stopProfilerTimerIfRunningAndRecordDelta,
157
} from './ReactProfilerTimer';
@@ -261,6 +265,7 @@ let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority;
265
let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
266
let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
267
let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
268
+let pendingPassiveProfilerEffects: Array<Fiber> = [];
269
270
let rootsWithPendingDiscreteUpdates: Map<
271
FiberRoot,
@@ -2209,6 +2214,19 @@ export function flushPassiveEffects() {
2214
}
2215
}
2216
2217
+export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void {
2218
+ if (enableProfilerTimer && enableProfilerCommitHooks) {
2219
+ pendingPassiveProfilerEffects.push(fiber);
2220
+ if (!rootDoesHavePassiveEffects) {
2221
+ rootDoesHavePassiveEffects = true;
2222
+ scheduleCallback(NormalPriority, () => {
2223
+ flushPassiveEffects();
2224
+ return null;
2225
+ });
2226
+ }
2227
+ }
2228
+}
2229
+
2230
export function enqueuePendingPassiveHookEffectMount(
2231
fiber: Fiber,
2232
effect: HookEffect,
@@ -2250,6 +2268,7 @@ function flushPassiveEffectsImpl() {
2268
if (rootWithPendingPassiveEffects === null) {
2269
return false;
2270
}
2271
+
2272
const root = rootWithPendingPassiveEffects;
2273
const expirationTime = pendingPassiveEffectsExpirationTime;
2274
rootWithPendingPassiveEffects = null;
@@ -2282,7 +2301,17 @@ function flushPassiveEffectsImpl() {
2301
if (typeof destroy === 'function') {
2302
if (__DEV__) {
2303
setCurrentDebugFiberInDEV(fiber);
2285
- invokeGuardedCallback(null, destroy, null);
2304
+ if (
2305
+ enableProfilerTimer &&
2306
+ enableProfilerCommitHooks &&
2307
+ fiber.mode & ProfileMode
2308
+ ) {
2309
+ startPassiveEffectTimer();
2310
+ invokeGuardedCallback(null, destroy, null);
2311
+ recordPassiveEffectDuration(fiber);
2312
+ } else {
2313
+ invokeGuardedCallback(null, destroy, null);
2314
+ }
2315
if (hasCaughtError()) {
2316
invariant(fiber !== null, 'Should be working on an effect.');
2317
const error = clearCaughtError();
@@ -2291,7 +2320,20 @@ function flushPassiveEffectsImpl() {
2320
resetCurrentDebugFiberInDEV();
2321
} else {
2322
try {
2294
- destroy();
2323
+ if (
2324
+ enableProfilerTimer &&
2325
+ enableProfilerCommitHooks &&
2326
+ fiber.mode & ProfileMode
2327
+ ) {
2328
+ try {
2329
+ startPassiveEffectTimer();
2330
+ destroy();
2331
+ } finally {
2332
+ recordPassiveEffectDuration(fiber);
2333
+ }
2334
+ } else {
2335
+ destroy();
2336
+ }
2337
} catch (error) {
2338
invariant(fiber !== null, 'Should be working on an effect.');
2339
captureCommitPhaseError(fiber, error);
@@ -2299,7 +2341,6 @@ function flushPassiveEffectsImpl() {
2341
}
2342
}
2343
}
2302
-
2344
// Second pass: Create new passive effects.
2345
let mountEffects = pendingPassiveHookEffectsMount;
2346
pendingPassiveHookEffectsMount = [];
@@ -2308,7 +2349,17 @@ function flushPassiveEffectsImpl() {
2349
const fiber = ((mountEffects[i + 1]: any): Fiber);
2350
if (__DEV__) {
2351
setCurrentDebugFiberInDEV(fiber);
2311
- invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
2352
+ if (
2353
+ enableProfilerTimer &&
2354
+ enableProfilerCommitHooks &&
2355
+ fiber.mode & ProfileMode
2356
+ ) {
2357
+ startPassiveEffectTimer();
2358
+ invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
2359
+ recordPassiveEffectDuration(fiber);
2360
+ } else {
2361
+ invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
2362
+ }
2363
if (hasCaughtError()) {
2364
invariant(fiber !== null, 'Should be working on an effect.');
2365
const error = clearCaughtError();
@@ -2318,7 +2369,20 @@ function flushPassiveEffectsImpl() {
2369
} else {
2370
try {
2371
const create = effect.create;
2321
- effect.destroy = create();
2372
+ if (
2373
+ enableProfilerTimer &&
2374
+ enableProfilerCommitHooks &&
2375
+ fiber.mode & ProfileMode
2376
+ ) {
2377
+ try {
2378
+ startPassiveEffectTimer();
2379
+ effect.destroy = create();
2380
+ } finally {
2381
+ recordPassiveEffectDuration(fiber);
2382
+ }
2383
+ } else {
2384
+ effect.destroy = create();
2385
+ }
2386
} catch (error) {
2387
invariant(fiber !== null, 'Should be working on an effect.');
2388
captureCommitPhaseError(fiber, error);
@@ -2348,6 +2412,7 @@ function flushPassiveEffectsImpl() {
2412
captureCommitPhaseError(effect, error);
2413
}
2414
}
2415
+
2416
const nextNextEffect = effect.nextEffect;
2417
// Remove nextEffect pointer to assist GC
2418
effect.nextEffect = null;
@@ -2355,6 +2420,15 @@ function flushPassiveEffectsImpl() {
2420
}
2421
}
2422
2423
+ if (enableProfilerTimer && enableProfilerCommitHooks) {
2424
+ let profilerEffects = pendingPassiveProfilerEffects;
2425
+ pendingPassiveProfilerEffects = [];
2426
+ for (let i = 0; i < profilerEffects.length; i++) {
2427
+ const fiber = ((profilerEffects[i]: any): Fiber);
2428
+ commitPassiveEffectDurations(root, fiber);
2429
+ }
2430
+ }
2431
+
2432
if (enableSchedulerTracing) {
2433
popInteractions(((prevInteractions: any): Set<Interaction>));
2434
finishPendingInteractions(root, expirationTime);
packages/react-reconciler/src/ReactProfilerTimer.js
+76
-1
@@ -9,7 +9,11 @@
9
10
import type {Fiber} from './ReactFiber';
11
12
-import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
12
+import {
13
+ enableProfilerTimer,
14
+ enableProfilerCommitHooks,
15
+} from 'shared/ReactFeatureFlags';
16
+import {Profiler} from 'shared/ReactWorkTags';
17
18
// Intentionally not named imports because Rollup would use dynamic dispatch for
19
// CommonJS interop named imports.
@@ -27,7 +31,9 @@ export type ProfilerTimer = {
31
};
32
33
let commitTime: number = 0;
34
+let layoutEffectStartTime: number = -1;
35
let profilerStartTime: number = -1;
36
+let passiveEffectStartTime: number = -1;
37
38
function getCommitTime(): number {
39
return commitTime;
@@ -77,9 +83,78 @@ function stopProfilerTimerIfRunningAndRecordDelta(
83
}
84
}
85
86
+function recordLayoutEffectDuration(fiber: Fiber): void {
87
+ if (!enableProfilerTimer || !enableProfilerCommitHooks) {
88
+ return;
89
+ }
90
+
91
+ if (layoutEffectStartTime >= 0) {
92
+ const elapsedTime = now() - layoutEffectStartTime;
93
+
94
+ layoutEffectStartTime = -1;
95
+
96
+ // Store duration on the next nearest Profiler ancestor.
97
+ let parentFiber = fiber.return;
98
+ while (parentFiber !== null) {
99
+ if (parentFiber.tag === Profiler) {
100
+ const parentStateNode = parentFiber.stateNode;
101
+ parentStateNode.effectDuration += elapsedTime;
102
+ break;
103
+ }
104
+ parentFiber = parentFiber.return;
105
+ }
106
+ }
107
+}
108
+
109
+function recordPassiveEffectDuration(fiber: Fiber): void {
110
+ if (!enableProfilerTimer || !enableProfilerCommitHooks) {
111
+ return;
112
+ }
113
+
114
+ if (passiveEffectStartTime >= 0) {
115
+ const elapsedTime = now() - passiveEffectStartTime;
116
+
117
+ passiveEffectStartTime = -1;
118
+
119
+ // Store duration on the next nearest Profiler ancestor.
120
+ let parentFiber = fiber.return;
121
+ while (parentFiber !== null) {
122
+ if (parentFiber.tag === Profiler) {
123
+ const parentStateNode = parentFiber.stateNode;
124
+ if (parentStateNode !== null) {
125
+ // Detached fibers have their state node cleared out.
126
+ // In this case, the return pointer is also cleared out,
127
+ // so we won't be able to report the time spent in this Profiler's subtree.
128
+ parentStateNode.passiveEffectDuration += elapsedTime;
129
+ }
130
+ break;
131
+ }
132
+ parentFiber = parentFiber.return;
133
+ }
134
+ }
135
+}
136
+
137
+function startLayoutEffectTimer(): void {
138
+ if (!enableProfilerTimer || !enableProfilerCommitHooks) {
139
+ return;
140
+ }
141
+ layoutEffectStartTime = now();
142
+}
143
+
144
+function startPassiveEffectTimer(): void {
145
+ if (!enableProfilerTimer || !enableProfilerCommitHooks) {
146
+ return;
147
+ }
148
+ passiveEffectStartTime = now();
149
+}
150
+
151
export {
152
getCommitTime,
153
recordCommitTime,
154
+ recordLayoutEffectDuration,
155
+ recordPassiveEffectDuration,
156
+ startLayoutEffectTimer,
157
+ startPassiveEffectTimer,
158
startProfilerTimer,
159
stopProfilerTimerIfRunning,
160
stopProfilerTimerIfRunningAndRecordDelta,
packages/react/src/__tests__/ReactProfiler-test.internal.js
+2352
-1028
@@ -24,14 +24,19 @@ let TextResource;
24
let resourcePromise;
25
26
function loadModules({
27
+ deferPassiveEffectCleanupDuringUnmount = false,
28
enableProfilerTimer = true,
29
+ enableProfilerCommitHooks = true,
30
enableSchedulerTracing = true,
31
replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
32
useNoopRenderer = false,
33
} = {}) {
34
ReactFeatureFlags = require('shared/ReactFeatureFlags');
35
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
36
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = deferPassiveEffectCleanupDuringUnmount;
37
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates = deferPassiveEffectCleanupDuringUnmount;
38
ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
39
+ ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks;
40
ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing;
41
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
42
@@ -120,10 +125,9 @@ describe('Profiler', () => {
125
it('should warn if required params are missing', () => {
126
expect(() => {
127
ReactTestRenderer.create(<React.Profiler />);
123
- }).toErrorDev(
124
- 'Profiler must specify an "id" string and "onRender" function as props',
125
- {withoutStack: true},
126
- );
128
+ }).toErrorDev('Profiler must specify an "id" as a prop', {
129
+ withoutStack: true,
130
+ });
131
});
132
}
133
@@ -182,1084 +186,2282 @@ describe('Profiler', () => {
186
});
187
});
188
185
- [true, false].forEach(enableSchedulerTracing => {
186
- describe('onRender callback', () => {
187
- beforeEach(() => {
188
- jest.resetModules();
189
+ [true, false].forEach(deferPassiveEffectCleanupDuringUnmount => {
190
+ [true, false].forEach(enableSchedulerTracing => {
191
+ describe(`onRender enableSchedulerTracing:${
192
+ enableSchedulerTracing ? 'enabled' : 'disabled'
193
+ } deferPassiveEffectCleanupDuringUnmount:${
194
+ deferPassiveEffectCleanupDuringUnmount ? 'enabled' : 'disabled'
195
+ }`, () => {
196
+ beforeEach(() => {
197
+ jest.resetModules();
198
+
199
+ loadModules({
200
+ deferPassiveEffectCleanupDuringUnmount,
201
+ enableSchedulerTracing,
202
+ });
203
+ });
204
190
- loadModules({enableSchedulerTracing});
191
- });
205
+ it('should handle errors thrown', () => {
206
+ const callback = jest.fn(id => {
207
+ if (id === 'throw') {
208
+ throw Error('expected');
209
+ }
210
+ });
211
193
- it('should handle errors thrown', () => {
194
- const callback = jest.fn(id => {
195
- if (id === 'throw') {
196
- throw Error('expected');
212
+ let didMount = false;
213
+ class ClassComponent extends React.Component {
214
+ componentDidMount() {
215
+ didMount = true;
216
+ }
217
+ render() {
218
+ return this.props.children;
219
+ }
220
}
221
+
222
+ // Errors thrown from onRender should not break the commit phase,
223
+ // Or prevent other lifecycles from being called.
224
+ expect(() =>
225
+ ReactTestRenderer.create(
226
+ <ClassComponent>
227
+ <React.Profiler id="do-not-throw" onRender={callback}>
228
+ <React.Profiler id="throw" onRender={callback}>
229
+ <div />
230
+ </React.Profiler>
231
+ </React.Profiler>
232
+ </ClassComponent>,
233
+ ),
234
+ ).toThrow('expected');
235
+ expect(didMount).toBe(true);
236
+ expect(callback).toHaveBeenCalledTimes(2);
237
});
238
200
- let didMount = false;
201
- class ClassComponent extends React.Component {
202
- componentDidMount() {
203
- didMount = true;
204
- }
205
- render() {
206
- return this.props.children;
207
- }
208
- }
239
+ it('is not invoked until the commit phase', () => {
240
+ const callback = jest.fn();
241
210
- // Errors thrown from onRender should not break the commit phase,
211
- // Or prevent other lifecycles from being called.
212
- expect(() =>
213
- ReactTestRenderer.create(
214
- <ClassComponent>
215
- <React.Profiler id="do-not-throw" onRender={callback}>
216
- <React.Profiler id="throw" onRender={callback}>
217
- <div />
218
- </React.Profiler>
219
- </React.Profiler>
220
- </ClassComponent>,
221
- ),
222
- ).toThrow('expected');
223
- expect(didMount).toBe(true);
224
- expect(callback).toHaveBeenCalledTimes(2);
225
- });
242
+ const Yield = ({value}) => {
243
+ Scheduler.unstable_yieldValue(value);
244
+ return null;
245
+ };
246
227
- it('is not invoked until the commit phase', () => {
228
- const callback = jest.fn();
247
+ ReactTestRenderer.create(
248
+ <React.Profiler id="test" onRender={callback}>
249
+ <Yield value="first" />
250
+ <Yield value="last" />
251
+ </React.Profiler>,
252
+ {
253
+ unstable_isConcurrent: true,
254
+ },
255
+ );
256
230
- const Yield = ({value}) => {
231
- Scheduler.unstable_yieldValue(value);
232
- return null;
233
- };
257
+ // Times are logged until a render is committed.
258
+ expect(Scheduler).toFlushAndYieldThrough(['first']);
259
+ expect(callback).toHaveBeenCalledTimes(0);
260
+ expect(Scheduler).toFlushAndYield(['last']);
261
+ expect(callback).toHaveBeenCalledTimes(1);
262
+ });
263
235
- ReactTestRenderer.create(
236
- <React.Profiler id="test" onRender={callback}>
237
- <Yield value="first" />
238
- <Yield value="last" />
239
- </React.Profiler>,
240
- {
241
- unstable_isConcurrent: true,
242
- },
243
- );
264
+ it('does not record times for components outside of Profiler tree', () => {
265
+ // Mock the Scheduler module so we can track how many times the current
266
+ // time is read
267
+ jest.mock('scheduler', obj => {
268
+ const ActualScheduler = require.requireActual(
269
+ 'scheduler/unstable_mock',
270
+ );
271
+ return {
272
+ ...ActualScheduler,
273
+ unstable_now: function mockUnstableNow() {
274
+ ActualScheduler.unstable_yieldValue('read current time');
275
+ return ActualScheduler.unstable_now();
276
+ },
277
+ };
278
+ });
279
245
- // Times are logged until a render is committed.
246
- expect(Scheduler).toFlushAndYieldThrough(['first']);
247
- expect(callback).toHaveBeenCalledTimes(0);
248
- expect(Scheduler).toFlushAndYield(['last']);
249
- expect(callback).toHaveBeenCalledTimes(1);
250
- });
280
+ jest.resetModules();
281
252
- it('does not record times for components outside of Profiler tree', () => {
253
- // Mock the Scheduler module so we can track how many times the current
254
- // time is read
255
- jest.mock('scheduler', obj => {
256
- const ActualScheduler = require.requireActual(
257
- 'scheduler/unstable_mock',
258
- );
259
- return {
260
- ...ActualScheduler,
261
- unstable_now: function mockUnstableNow() {
262
- ActualScheduler.unstable_yieldValue('read current time');
263
- return ActualScheduler.unstable_now();
264
- },
265
- };
266
- });
282
+ loadModules({enableSchedulerTracing});
283
268
- jest.resetModules();
284
+ // Clear yields in case the current time is read during initialization.
285
+ Scheduler.unstable_clearYields();
286
270
- loadModules({enableSchedulerTracing});
287
+ ReactTestRenderer.create(
288
+ <div>
289
+ <AdvanceTime />
290
+ <AdvanceTime />
291
+ <AdvanceTime />
292
+ <AdvanceTime />
293
+ <AdvanceTime />
294
+ </div>,
295
+ );
296
272
- // Clear yields in case the current time is read during initialization.
273
- Scheduler.unstable_clearYields();
297
+ // Should be called two times:
298
+ // 2. To compute the update expiration time
299
+ // 3. To record the commit time
300
+ // No additional calls from ProfilerTimer are expected.
301
+ expect(Scheduler).toHaveYielded([
302
+ 'read current time',
303
+ 'read current time',
304
+ ]);
305
275
- ReactTestRenderer.create(
276
- <div>
277
- <AdvanceTime />
278
- <AdvanceTime />
279
- <AdvanceTime />
280
- <AdvanceTime />
281
- <AdvanceTime />
282
- </div>,
283
- );
306
+ // Restore original mock
307
+ jest.mock('scheduler', () =>
308
+ require.requireActual('scheduler/unstable_mock'),
309
+ );
310
+ });
311
285
- // Should be called two times:
286
- // 2. To compute the update expiration time
287
- // 3. To record the commit time
288
- // No additional calls from ProfilerTimer are expected.
289
- expect(Scheduler).toHaveYielded([
290
- 'read current time',
291
- 'read current time',
292
- ]);
312
+ it('does not report work done on a sibling', () => {
313
+ const callback = jest.fn();
314
294
- // Restore original mock
295
- jest.mock('scheduler', () =>
296
- require.requireActual('scheduler/unstable_mock'),
297
- );
298
- });
315
+ const DoesNotUpdate = React.memo(
316
+ function DoesNotUpdateInner() {
317
+ Scheduler.unstable_advanceTime(10);
318
+ return null;
319
+ },
320
+ () => true,
321
+ );
322
300
- it('does not report work done on a sibling', () => {
301
- const callback = jest.fn();
323
+ let updateProfilerSibling;
324
303
- const DoesNotUpdate = React.memo(
304
- function DoesNotUpdateInner() {
305
- Scheduler.unstable_advanceTime(10);
325
+ function ProfilerSibling() {
326
+ const [count, setCount] = React.useState(0);
327
+ updateProfilerSibling = () => setCount(count + 1);
328
return null;
307
- },
308
- () => true,
309
- );
310
-
311
- let updateProfilerSibling;
329
+ }
330
313
- function ProfilerSibling() {
314
- const [count, setCount] = React.useState(0);
315
- updateProfilerSibling = () => setCount(count + 1);
316
- return null;
317
- }
331
+ function App() {
332
+ return (
333
+ <React.Fragment>
334
+ <React.Profiler id="test" onRender={callback}>
335
+ <DoesNotUpdate />
336
+ </React.Profiler>
337
+ <ProfilerSibling />
338
+ </React.Fragment>
339
+ );
340
+ }
341
319
- function App() {
320
- return (
321
- <React.Fragment>
322
- <React.Profiler id="test" onRender={callback}>
323
- <DoesNotUpdate />
324
- </React.Profiler>
325
- <ProfilerSibling />
326
- </React.Fragment>
327
- );
328
- }
342
+ const renderer = ReactTestRenderer.create(<App />);
343
330
- const renderer = ReactTestRenderer.create(<App />);
344
+ expect(callback).toHaveBeenCalledTimes(1);
345
332
- expect(callback).toHaveBeenCalledTimes(1);
346
+ let call = callback.mock.calls[0];
347
334
- let call = callback.mock.calls[0];
348
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
349
+ expect(call[0]).toBe('test');
350
+ expect(call[1]).toBe('mount');
351
+ expect(call[2]).toBe(10); // actual time
352
+ expect(call[3]).toBe(10); // base time
353
+ expect(call[4]).toBe(0); // start time
354
+ expect(call[5]).toBe(10); // commit time
355
+ expect(call[6]).toEqual(
356
+ enableSchedulerTracing ? new Set() : undefined,
357
+ ); // interaction events
358
336
- expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
337
- expect(call[0]).toBe('test');
338
- expect(call[1]).toBe('mount');
339
- expect(call[2]).toBe(10); // actual time
340
- expect(call[3]).toBe(10); // base time
341
- expect(call[4]).toBe(0); // start time
342
- expect(call[5]).toBe(10); // commit time
343
- expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
359
+ callback.mockReset();
360
345
- callback.mockReset();
361
+ Scheduler.unstable_advanceTime(20); // 10 -> 30
362
347
- Scheduler.unstable_advanceTime(20); // 10 -> 30
363
+ // Updating a parent should report a re-render,
364
+ // since React technically did a little bit of work between the Profiler and the bailed out subtree.
365
+ renderer.update(<App />);
366
349
- // Updating a parent should report a re-render,
350
- // since React technically did a little bit of work between the Profiler and the bailed out subtree.
351
- renderer.update(<App />);
367
+ expect(callback).toHaveBeenCalledTimes(1);
368
353
- expect(callback).toHaveBeenCalledTimes(1);
369
+ call = callback.mock.calls[0];
370
355
- call = callback.mock.calls[0];
371
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
372
+ expect(call[0]).toBe('test');
373
+ expect(call[1]).toBe('update');
374
+ expect(call[2]).toBe(0); // actual time
375
+ expect(call[3]).toBe(10); // base time
376
+ expect(call[4]).toBe(30); // start time
377
+ expect(call[5]).toBe(30); // commit time
378
+ expect(call[6]).toEqual(
379
+ enableSchedulerTracing ? new Set() : undefined,
380
+ ); // interaction events
381
357
- expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
358
- expect(call[0]).toBe('test');
359
- expect(call[1]).toBe('update');
360
- expect(call[2]).toBe(0); // actual time
361
- expect(call[3]).toBe(10); // base time
362
- expect(call[4]).toBe(30); // start time
363
- expect(call[5]).toBe(30); // commit time
364
- expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
382
+ callback.mockReset();
383
366
- callback.mockReset();
384
+ Scheduler.unstable_advanceTime(20); // 30 -> 50
385
368
- Scheduler.unstable_advanceTime(20); // 30 -> 50
386
+ // Updating a sibling should not report a re-render.
387
+ ReactTestRenderer.act(updateProfilerSibling);
388
370
- // Updating a sibling should not report a re-render.
371
- ReactTestRenderer.act(updateProfilerSibling);
389
+ expect(callback).not.toHaveBeenCalled();
390
+ });
391
373
- expect(callback).not.toHaveBeenCalled();
374
- });
392
+ it('logs render times for both mount and update', () => {
393
+ const callback = jest.fn();
394
376
- it('logs render times for both mount and update', () => {
377
- const callback = jest.fn();
395
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
396
379
- Scheduler.unstable_advanceTime(5); // 0 -> 5
397
+ const renderer = ReactTestRenderer.create(
398
+ <React.Profiler id="test" onRender={callback}>
399
+ <AdvanceTime />
400
+ </React.Profiler>,
401
+ );
402
381
- const renderer = ReactTestRenderer.create(
382
- <React.Profiler id="test" onRender={callback}>
383
- <AdvanceTime />
384
- </React.Profiler>,
385
- );
403
+ expect(callback).toHaveBeenCalledTimes(1);
404
387
- expect(callback).toHaveBeenCalledTimes(1);
405
+ let [call] = callback.mock.calls;
406
389
- let [call] = callback.mock.calls;
407
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
408
+ expect(call[0]).toBe('test');
409
+ expect(call[1]).toBe('mount');
410
+ expect(call[2]).toBe(10); // actual time
411
+ expect(call[3]).toBe(10); // base time
412
+ expect(call[4]).toBe(5); // start time
413
+ expect(call[5]).toBe(15); // commit time
414
+ expect(call[6]).toEqual(
415
+ enableSchedulerTracing ? new Set() : undefined,
416
+ ); // interaction events
417
391
- expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
392
- expect(call[0]).toBe('test');
393
- expect(call[1]).toBe('mount');
394
- expect(call[2]).toBe(10); // actual time
395
- expect(call[3]).toBe(10); // base time
396
- expect(call[4]).toBe(5); // start time
397
- expect(call[5]).toBe(15); // commit time
398
- expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
418
+ callback.mockReset();
419
400
- callback.mockReset();
420
+ Scheduler.unstable_advanceTime(20); // 15 -> 35
421
402
- Scheduler.unstable_advanceTime(20); // 15 -> 35
422
+ renderer.update(
423
+ <React.Profiler id="test" onRender={callback}>
424
+ <AdvanceTime />
425
+ </React.Profiler>,
426
+ );
427
404
- renderer.update(
405
- <React.Profiler id="test" onRender={callback}>
406
- <AdvanceTime />
407
- </React.Profiler>,
408
- );
428
+ expect(callback).toHaveBeenCalledTimes(1);
429
410
- expect(callback).toHaveBeenCalledTimes(1);
430
+ [call] = callback.mock.calls;
431
412
- [call] = callback.mock.calls;
432
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
433
+ expect(call[0]).toBe('test');
434
+ expect(call[1]).toBe('update');
435
+ expect(call[2]).toBe(10); // actual time
436
+ expect(call[3]).toBe(10); // base time
437
+ expect(call[4]).toBe(35); // start time
438
+ expect(call[5]).toBe(45); // commit time
439
+ expect(call[6]).toEqual(
440
+ enableSchedulerTracing ? new Set() : undefined,
441
+ ); // interaction events
442
414
- expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
415
- expect(call[0]).toBe('test');
416
- expect(call[1]).toBe('update');
417
- expect(call[2]).toBe(10); // actual time
418
- expect(call[3]).toBe(10); // base time
419
- expect(call[4]).toBe(35); // start time
420
- expect(call[5]).toBe(45); // commit time
421
- expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
443
+ callback.mockReset();
444
423
- callback.mockReset();
445
+ Scheduler.unstable_advanceTime(20); // 45 -> 65
446
425
- Scheduler.unstable_advanceTime(20); // 45 -> 65
447
+ renderer.update(
448
+ <React.Profiler id="test" onRender={callback}>
449
+ <AdvanceTime byAmount={4} />
450
+ </React.Profiler>,
451
+ );
452
427
- renderer.update(
428
- <React.Profiler id="test" onRender={callback}>
429
- <AdvanceTime byAmount={4} />
430
- </React.Profiler>,
431
- );
453
+ expect(callback).toHaveBeenCalledTimes(1);
454
433
- expect(callback).toHaveBeenCalledTimes(1);
455
+ [call] = callback.mock.calls;
456
+
457
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
458
+ expect(call[0]).toBe('test');
459
+ expect(call[1]).toBe('update');
460
+ expect(call[2]).toBe(4); // actual time
461
+ expect(call[3]).toBe(4); // base time
462
+ expect(call[4]).toBe(65); // start time
463
+ expect(call[5]).toBe(69); // commit time
464
+ expect(call[6]).toEqual(
465
+ enableSchedulerTracing ? new Set() : undefined,
466
+ ); // interaction events
467
+ });
468
435
- [call] = callback.mock.calls;
469
+ it('includes render times of nested Profilers in their parent times', () => {
470
+ const callback = jest.fn();
471
437
- expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
438
- expect(call[0]).toBe('test');
439
- expect(call[1]).toBe('update');
440
- expect(call[2]).toBe(4); // actual time
441
- expect(call[3]).toBe(4); // base time
442
- expect(call[4]).toBe(65); // start time
443
- expect(call[5]).toBe(69); // commit time
444
- expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
445
- });
472
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
473
447
- it('includes render times of nested Profilers in their parent times', () => {
448
- const callback = jest.fn();
474
+ ReactTestRenderer.create(
475
+ <React.Fragment>
476
+ <React.Profiler id="parent" onRender={callback}>
477
+ <AdvanceTime byAmount={10}>
478
+ <React.Profiler id="child" onRender={callback}>
479
+ <AdvanceTime byAmount={20} />
480
+ </React.Profiler>
481
+ </AdvanceTime>
482
+ </React.Profiler>
483
+ </React.Fragment>,
484
+ );
485
450
- Scheduler.unstable_advanceTime(5); // 0 -> 5
486
+ expect(callback).toHaveBeenCalledTimes(2);
487
452
- ReactTestRenderer.create(
453
- <React.Fragment>
454
- <React.Profiler id="parent" onRender={callback}>
455
- <AdvanceTime byAmount={10}>
456
- <React.Profiler id="child" onRender={callback}>
457
- <AdvanceTime byAmount={20} />
458
- </React.Profiler>
459
- </AdvanceTime>
460
- </React.Profiler>
461
- </React.Fragment>,
462
- );
488
+ // Callbacks bubble (reverse order).
489
+ const [childCall, parentCall] = callback.mock.calls;
490
+ expect(childCall[0]).toBe('child');
491
+ expect(parentCall[0]).toBe('parent');
492
+
493
+ // Parent times should include child times
494
+ expect(childCall[2]).toBe(20); // actual time
495
+ expect(childCall[3]).toBe(20); // base time
496
+ expect(childCall[4]).toBe(15); // start time
497
+ expect(childCall[5]).toBe(35); // commit time
498
+ expect(parentCall[2]).toBe(30); // actual time
499
+ expect(parentCall[3]).toBe(30); // base time
500
+ expect(parentCall[4]).toBe(5); // start time
501
+ expect(parentCall[5]).toBe(35); // commit time
502
+ });
503
464
- expect(callback).toHaveBeenCalledTimes(2);
465
-
466
- // Callbacks bubble (reverse order).
467
- const [childCall, parentCall] = callback.mock.calls;
468
- expect(childCall[0]).toBe('child');
469
- expect(parentCall[0]).toBe('parent');
470
-
471
- // Parent times should include child times
472
- expect(childCall[2]).toBe(20); // actual time
473
- expect(childCall[3]).toBe(20); // base time
474
- expect(childCall[4]).toBe(15); // start time
475
- expect(childCall[5]).toBe(35); // commit time
476
- expect(parentCall[2]).toBe(30); // actual time
477
- expect(parentCall[3]).toBe(30); // base time
478
- expect(parentCall[4]).toBe(5); // start time
479
- expect(parentCall[5]).toBe(35); // commit time
480
- });
504
+ it('traces sibling Profilers separately', () => {
505
+ const callback = jest.fn();
506
482
- it('traces sibling Profilers separately', () => {
483
- const callback = jest.fn();
507
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
508
485
- Scheduler.unstable_advanceTime(5); // 0 -> 5
509
+ ReactTestRenderer.create(
510
+ <React.Fragment>
511
+ <React.Profiler id="first" onRender={callback}>
512
+ <AdvanceTime byAmount={20} />
513
+ </React.Profiler>
514
+ <React.Profiler id="second" onRender={callback}>
515
+ <AdvanceTime byAmount={5} />
516
+ </React.Profiler>
517
+ </React.Fragment>,
518
+ );
519
487
- ReactTestRenderer.create(
488
- <React.Fragment>
489
- <React.Profiler id="first" onRender={callback}>
490
- <AdvanceTime byAmount={20} />
491
- </React.Profiler>
492
- <React.Profiler id="second" onRender={callback}>
493
- <AdvanceTime byAmount={5} />
494
- </React.Profiler>
495
- </React.Fragment>,
496
- );
520
+ expect(callback).toHaveBeenCalledTimes(2);
521
498
- expect(callback).toHaveBeenCalledTimes(2);
499
-
500
- const [firstCall, secondCall] = callback.mock.calls;
501
- expect(firstCall[0]).toBe('first');
502
- expect(secondCall[0]).toBe('second');
503
-
504
- // Parent times should include child times
505
- expect(firstCall[2]).toBe(20); // actual time
506
- expect(firstCall[3]).toBe(20); // base time
507
- expect(firstCall[4]).toBe(5); // start time
508
- expect(firstCall[5]).toBe(30); // commit time
509
- expect(secondCall[2]).toBe(5); // actual time
510
- expect(secondCall[3]).toBe(5); // base time
511
- expect(secondCall[4]).toBe(25); // start time
512
- expect(secondCall[5]).toBe(30); // commit time
513
- });
522
+ const [firstCall, secondCall] = callback.mock.calls;
523
+ expect(firstCall[0]).toBe('first');
524
+ expect(secondCall[0]).toBe('second');
525
+
526
+ // Parent times should include child times
527
+ expect(firstCall[2]).toBe(20); // actual time
528
+ expect(firstCall[3]).toBe(20); // base time
529
+ expect(firstCall[4]).toBe(5); // start time
530
+ expect(firstCall[5]).toBe(30); // commit time
531
+ expect(secondCall[2]).toBe(5); // actual time
532
+ expect(secondCall[3]).toBe(5); // base time
533
+ expect(secondCall[4]).toBe(25); // start time
534
+ expect(secondCall[5]).toBe(30); // commit time
535
+ });
536
515
- it('does not include time spent outside of profile root', () => {
516
- const callback = jest.fn();
537
+ it('does not include time spent outside of profile root', () => {
538
+ const callback = jest.fn();
539
518
- Scheduler.unstable_advanceTime(5); // 0 -> 5
540
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
541
520
- ReactTestRenderer.create(
521
- <React.Fragment>
522
- <AdvanceTime byAmount={20} />
523
- <React.Profiler id="test" onRender={callback}>
524
- <AdvanceTime byAmount={5} />
525
- </React.Profiler>
526
- <AdvanceTime byAmount={20} />
527
- </React.Fragment>,
528
- );
542
+ ReactTestRenderer.create(
543
+ <React.Fragment>
544
+ <AdvanceTime byAmount={20} />
545
+ <React.Profiler id="test" onRender={callback}>
546
+ <AdvanceTime byAmount={5} />
547
+ </React.Profiler>
548
+ <AdvanceTime byAmount={20} />
549
+ </React.Fragment>,
550
+ );
551
530
- expect(callback).toHaveBeenCalledTimes(1);
552
+ expect(callback).toHaveBeenCalledTimes(1);
553
532
- const [call] = callback.mock.calls;
533
- expect(call[0]).toBe('test');
534
- expect(call[2]).toBe(5); // actual time
535
- expect(call[3]).toBe(5); // base time
536
- expect(call[4]).toBe(25); // start time
537
- expect(call[5]).toBe(50); // commit time
538
- });
554
+ const [call] = callback.mock.calls;
555
+ expect(call[0]).toBe('test');
556
+ expect(call[2]).toBe(5); // actual time
557
+ expect(call[3]).toBe(5); // base time
558
+ expect(call[4]).toBe(25); // start time
559
+ expect(call[5]).toBe(50); // commit time
560
+ });
561
540
- it('is not called when blocked by sCU false', () => {
541
- const callback = jest.fn();
562
+ it('is not called when blocked by sCU false', () => {
563
+ const callback = jest.fn();
564
543
- let instance;
544
- class Updater extends React.Component {
545
- state = {};
546
- render() {
547
- instance = this;
548
- return this.props.children;
565
+ let instance;
566
+ class Updater extends React.Component {
567
+ state = {};
568
+ render() {
569
+ instance = this;
570
+ return this.props.children;
571
+ }
572
}
550
- }
573
552
- const renderer = ReactTestRenderer.create(
553
- <React.Profiler id="outer" onRender={callback}>
554
- <Updater>
555
- <React.Profiler id="inner" onRender={callback}>
556
- <div />
557
- </React.Profiler>
558
- </Updater>
559
- </React.Profiler>,
560
- );
574
+ const renderer = ReactTestRenderer.create(
575
+ <React.Profiler id="outer" onRender={callback}>
576
+ <Updater>
577
+ <React.Profiler id="inner" onRender={callback}>
578
+ <div />
579
+ </React.Profiler>
580
+ </Updater>
581
+ </React.Profiler>,
582
+ );
583
562
- // All profile callbacks are called for initial render
563
- expect(callback).toHaveBeenCalledTimes(2);
584
+ // All profile callbacks are called for initial render
585
+ expect(callback).toHaveBeenCalledTimes(2);
586
565
- callback.mockReset();
587
+ callback.mockReset();
588
567
- renderer.unstable_flushSync(() => {
568
- instance.setState({
569
- count: 1,
589
+ renderer.unstable_flushSync(() => {
590
+ instance.setState({
591
+ count: 1,
592
+ });
593
});
571
- });
594
573
- // Only call onRender for paths that have re-rendered.
574
- // Since the Updater's props didn't change,
575
- // React does not re-render its children.
576
- expect(callback).toHaveBeenCalledTimes(1);
577
- expect(callback.mock.calls[0][0]).toBe('outer');
578
- });
595
+ // Only call onRender for paths that have re-rendered.
596
+ // Since the Updater's props didn't change,
597
+ // React does not re-render its children.
598
+ expect(callback).toHaveBeenCalledTimes(1);
599
+ expect(callback.mock.calls[0][0]).toBe('outer');
600
+ });
601
580
- it('decreases actual time but not base time when sCU prevents an update', () => {
581
- const callback = jest.fn();
602
+ it('decreases actual time but not base time when sCU prevents an update', () => {
603
+ const callback = jest.fn();
604
583
- Scheduler.unstable_advanceTime(5); // 0 -> 5
605
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
606
585
- const renderer = ReactTestRenderer.create(
586
- <React.Profiler id="test" onRender={callback}>
587
- <AdvanceTime byAmount={10}>
588
- <AdvanceTime byAmount={13} shouldComponentUpdate={false} />
589
- </AdvanceTime>
590
- </React.Profiler>,
591
- );
607
+ const renderer = ReactTestRenderer.create(
608
+ <React.Profiler id="test" onRender={callback}>
609
+ <AdvanceTime byAmount={10}>
610
+ <AdvanceTime byAmount={13} shouldComponentUpdate={false} />
611
+ </AdvanceTime>
612
+ </React.Profiler>,
613
+ );
614
593
- expect(callback).toHaveBeenCalledTimes(1);
615
+ expect(callback).toHaveBeenCalledTimes(1);
616
595
- Scheduler.unstable_advanceTime(30); // 28 -> 58
617
+ Scheduler.unstable_advanceTime(30); // 28 -> 58
618
597
- renderer.update(
598
- <React.Profiler id="test" onRender={callback}>
599
- <AdvanceTime byAmount={4}>
600
- <AdvanceTime byAmount={7} shouldComponentUpdate={false} />
601
- </AdvanceTime>
602
- </React.Profiler>,
603
- );
619
+ renderer.update(
620
+ <React.Profiler id="test" onRender={callback}>
621
+ <AdvanceTime byAmount={4}>
622
+ <AdvanceTime byAmount={7} shouldComponentUpdate={false} />
623
+ </AdvanceTime>
624
+ </React.Profiler>,
625
+ );
626
605
- expect(callback).toHaveBeenCalledTimes(2);
627
+ expect(callback).toHaveBeenCalledTimes(2);
628
607
- const [mountCall, updateCall] = callback.mock.calls;
629
+ const [mountCall, updateCall] = callback.mock.calls;
630
609
- expect(mountCall[1]).toBe('mount');
610
- expect(mountCall[2]).toBe(23); // actual time
611
- expect(mountCall[3]).toBe(23); // base time
612
- expect(mountCall[4]).toBe(5); // start time
613
- expect(mountCall[5]).toBe(28); // commit time
631
+ expect(mountCall[1]).toBe('mount');
632
+ expect(mountCall[2]).toBe(23); // actual time
633
+ expect(mountCall[3]).toBe(23); // base time
634
+ expect(mountCall[4]).toBe(5); // start time
635
+ expect(mountCall[5]).toBe(28); // commit time
636
615
- expect(updateCall[1]).toBe('update');
616
- expect(updateCall[2]).toBe(4); // actual time
617
- expect(updateCall[3]).toBe(17); // base time
618
- expect(updateCall[4]).toBe(58); // start time
619
- expect(updateCall[5]).toBe(62); // commit time
620
- });
637
+ expect(updateCall[1]).toBe('update');
638
+ expect(updateCall[2]).toBe(4); // actual time
639
+ expect(updateCall[3]).toBe(17); // base time
640
+ expect(updateCall[4]).toBe(58); // start time
641
+ expect(updateCall[5]).toBe(62); // commit time
642
+ });
643
622
- it('includes time spent in render phase lifecycles', () => {
623
- class WithLifecycles extends React.Component {
624
- state = {};
625
- static getDerivedStateFromProps() {
626
- Scheduler.unstable_advanceTime(3);
627
- return null;
628
- }
629
- shouldComponentUpdate() {
630
- Scheduler.unstable_advanceTime(7);
631
- return true;
632
- }
633
- render() {
634
- Scheduler.unstable_advanceTime(5);
635
- return null;
644
+ it('includes time spent in render phase lifecycles', () => {
645
+ class WithLifecycles extends React.Component {
646
+ state = {};
647
+ static getDerivedStateFromProps() {
648
+ Scheduler.unstable_advanceTime(3);
649
+ return null;
650
+ }
651
+ shouldComponentUpdate() {
652
+ Scheduler.unstable_advanceTime(7);
653
+ return true;
654
+ }
655
+ render() {
656
+ Scheduler.unstable_advanceTime(5);
657
+ return null;
658
+ }
659
}
637
- }
638
-
639
- const callback = jest.fn();
640
-
641
- Scheduler.unstable_advanceTime(5); // 0 -> 5
660
643
- const renderer = ReactTestRenderer.create(
644
- <React.Profiler id="test" onRender={callback}>
645
- <WithLifecycles />
646
- </React.Profiler>,
647
- );
661
+ const callback = jest.fn();
662
649
- Scheduler.unstable_advanceTime(15); // 13 -> 28
663
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
664
651
- renderer.update(
652
- <React.Profiler id="test" onRender={callback}>
653
- <WithLifecycles />
654
- </React.Profiler>,
655
- );
665
+ const renderer = ReactTestRenderer.create(
666
+ <React.Profiler id="test" onRender={callback}>
667
+ <WithLifecycles />
668
+ </React.Profiler>,
669
+ );
670
657
- expect(callback).toHaveBeenCalledTimes(2);
671
+ Scheduler.unstable_advanceTime(15); // 13 -> 28
672
659
- const [mountCall, updateCall] = callback.mock.calls;
673
+ renderer.update(
674
+ <React.Profiler id="test" onRender={callback}>
675
+ <WithLifecycles />
676
+ </React.Profiler>,
677
+ );
678
661
- expect(mountCall[1]).toBe('mount');
662
- expect(mountCall[2]).toBe(8); // actual time
663
- expect(mountCall[3]).toBe(8); // base time
664
- expect(mountCall[4]).toBe(5); // start time
665
- expect(mountCall[5]).toBe(13); // commit time
679
+ expect(callback).toHaveBeenCalledTimes(2);
680
667
- expect(updateCall[1]).toBe('update');
668
- expect(updateCall[2]).toBe(15); // actual time
669
- expect(updateCall[3]).toBe(15); // base time
670
- expect(updateCall[4]).toBe(28); // start time
671
- expect(updateCall[5]).toBe(43); // commit time
672
- });
681
+ const [mountCall, updateCall] = callback.mock.calls;
682
674
- describe('with regard to interruptions', () => {
675
- it('should accumulate actual time after a scheduling interruptions', () => {
676
- const callback = jest.fn();
683
+ expect(mountCall[1]).toBe('mount');
684
+ expect(mountCall[2]).toBe(8); // actual time
685
+ expect(mountCall[3]).toBe(8); // base time
686
+ expect(mountCall[4]).toBe(5); // start time
687
+ expect(mountCall[5]).toBe(13); // commit time
688
678
- const Yield = ({renderTime}) => {
679
- Scheduler.unstable_advanceTime(renderTime);
680
- Scheduler.unstable_yieldValue('Yield:' + renderTime);
681
- return null;
682
- };
689
+ expect(updateCall[1]).toBe('update');
690
+ expect(updateCall[2]).toBe(15); // actual time
691
+ expect(updateCall[3]).toBe(15); // base time
692
+ expect(updateCall[4]).toBe(28); // start time
693
+ expect(updateCall[5]).toBe(43); // commit time
694
+ });
695
684
- Scheduler.unstable_advanceTime(5); // 0 -> 5
696
+ describe('with regard to interruptions', () => {
697
+ it('should accumulate actual time after a scheduling interruptions', () => {
698
+ const callback = jest.fn();
699
686
- // Render partially, but run out of time before completing.
687
- ReactTestRenderer.create(
688
- <React.Profiler id="test" onRender={callback}>
689
- <Yield renderTime={2} />
690
- <Yield renderTime={3} />
691
- </React.Profiler>,
692
- {unstable_isConcurrent: true},
693
- );
694
- expect(Scheduler).toFlushAndYieldThrough(['Yield:2']);
695
- expect(callback).toHaveBeenCalledTimes(0);
700
+ const Yield = ({renderTime}) => {
701
+ Scheduler.unstable_advanceTime(renderTime);
702
+ Scheduler.unstable_yieldValue('Yield:' + renderTime);
703
+ return null;
704
+ };
705
697
- // Resume render for remaining children.
698
- expect(Scheduler).toFlushAndYield(['Yield:3']);
706
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
707
700
- // Verify that logged times include both durations above.
701
- expect(callback).toHaveBeenCalledTimes(1);
702
- const [call] = callback.mock.calls;
703
- expect(call[2]).toBe(5); // actual time
704
- expect(call[3]).toBe(5); // base time
705
- expect(call[4]).toBe(5); // start time
706
- expect(call[5]).toBe(10); // commit time
708
+ // Render partially, but run out of time before completing.
709
+ ReactTestRenderer.create(
710
+ <React.Profiler id="test" onRender={callback}>
711
+ <Yield renderTime={2} />
712
+ <Yield renderTime={3} />
713
+ </React.Profiler>,
714
+ {unstable_isConcurrent: true},
715
+ );
716
+ expect(Scheduler).toFlushAndYieldThrough(['Yield:2']);
717
+ expect(callback).toHaveBeenCalledTimes(0);
718
+
719
+ // Resume render for remaining children.
720
+ expect(Scheduler).toFlushAndYield(['Yield:3']);
721
+
722
+ // Verify that logged times include both durations above.
723
+ expect(callback).toHaveBeenCalledTimes(1);
724
+ const [call] = callback.mock.calls;
725
+ expect(call[2]).toBe(5); // actual time
726
+ expect(call[3]).toBe(5); // base time
727
+ expect(call[4]).toBe(5); // start time
728
+ expect(call[5]).toBe(10); // commit time
729
+ });
730
+
731
+ it('should not include time between frames', () => {
732
+ const callback = jest.fn();
733
+
734
+ const Yield = ({renderTime}) => {
735
+ Scheduler.unstable_advanceTime(renderTime);
736
+ Scheduler.unstable_yieldValue('Yield:' + renderTime);
737
+ return null;
738
+ };
739
+
740
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
741
+
742
+ // Render partially, but don't finish.
743
+ // This partial render should take 5ms of simulated time.
744
+ ReactTestRenderer.create(
745
+ <React.Profiler id="outer" onRender={callback}>
746
+ <Yield renderTime={5} />
747
+ <Yield renderTime={10} />
748
+ <React.Profiler id="inner" onRender={callback}>
749
+ <Yield renderTime={17} />
750
+ </React.Profiler>
751
+ </React.Profiler>,
752
+ {unstable_isConcurrent: true},
753
+ );
754
+ expect(Scheduler).toFlushAndYieldThrough(['Yield:5']);
755
+ expect(callback).toHaveBeenCalledTimes(0);
756
+
757
+ // Simulate time moving forward while frame is paused.
758
+ Scheduler.unstable_advanceTime(50); // 10 -> 60
759
+
760
+ // Flush the remaining work,
761
+ // Which should take an additional 10ms of simulated time.
762
+ expect(Scheduler).toFlushAndYield(['Yield:10', 'Yield:17']);
763
+ expect(callback).toHaveBeenCalledTimes(2);
764
+
765
+ const [innerCall, outerCall] = callback.mock.calls;
766
+
767
+ // Verify that the actual time includes all work times,
768
+ // But not the time that elapsed between frames.
769
+ expect(innerCall[0]).toBe('inner');
770
+ expect(innerCall[2]).toBe(17); // actual time
771
+ expect(innerCall[3]).toBe(17); // base time
772
+ expect(innerCall[4]).toBe(70); // start time
773
+ expect(innerCall[5]).toBe(87); // commit time
774
+ expect(outerCall[0]).toBe('outer');
775
+ expect(outerCall[2]).toBe(32); // actual time
776
+ expect(outerCall[3]).toBe(32); // base time
777
+ expect(outerCall[4]).toBe(5); // start time
778
+ expect(outerCall[5]).toBe(87); // commit time
779
+ });
780
+
781
+ it('should report the expected times when a high-pri update replaces a mount in-progress', () => {
782
+ const callback = jest.fn();
783
+
784
+ const Yield = ({renderTime}) => {
785
+ Scheduler.unstable_advanceTime(renderTime);
786
+ Scheduler.unstable_yieldValue('Yield:' + renderTime);
787
+ return null;
788
+ };
789
+
790
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
791
+
792
+ // Render a partially update, but don't finish.
793
+ // This partial render should take 10ms of simulated time.
794
+ const renderer = ReactTestRenderer.create(
795
+ <React.Profiler id="test" onRender={callback}>
796
+ <Yield renderTime={10} />
797
+ <Yield renderTime={20} />
798
+ </React.Profiler>,
799
+ {unstable_isConcurrent: true},
800
+ );
801
+ expect(Scheduler).toFlushAndYieldThrough(['Yield:10']);
802
+ expect(callback).toHaveBeenCalledTimes(0);
803
+
804
+ // Simulate time moving forward while frame is paused.
805
+ Scheduler.unstable_advanceTime(100); // 15 -> 115
806
+
807
+ // Interrupt with higher priority work.
808
+ // The interrupted work simulates an additional 5ms of time.
809
+ renderer.unstable_flushSync(() => {
810
+ renderer.update(
811
+ <React.Profiler id="test" onRender={callback}>
812
+ <Yield renderTime={5} />
813
+ </React.Profiler>,
814
+ );
815
+ });
816
+ expect(Scheduler).toHaveYielded(['Yield:5']);
817
+
818
+ // The initial work was thrown away in this case,
819
+ // So the actual and base times should only include the final rendered tree times.
820
+ expect(callback).toHaveBeenCalledTimes(1);
821
+ let call = callback.mock.calls[0];
822
+ expect(call[2]).toBe(5); // actual time
823
+ expect(call[3]).toBe(5); // base time
824
+ expect(call[4]).toBe(115); // start time
825
+ expect(call[5]).toBe(120); // commit time
826
+
827
+ callback.mockReset();
828
+
829
+ // Verify no more unexpected callbacks from low priority work
830
+ expect(Scheduler).toFlushWithoutYielding();
831
+ expect(callback).toHaveBeenCalledTimes(0);
832
+ });
833
+
834
+ it('should report the expected times when a high-priority update replaces a low-priority update', () => {
835
+ const callback = jest.fn();
836
+
837
+ const Yield = ({renderTime}) => {
838
+ Scheduler.unstable_advanceTime(renderTime);
839
+ Scheduler.unstable_yieldValue('Yield:' + renderTime);
840
+ return null;
841
+ };
842
+
843
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
844
+
845
+ const renderer = ReactTestRenderer.create(
846
+ <React.Profiler id="test" onRender={callback}>
847
+ <Yield renderTime={6} />
848
+ <Yield renderTime={15} />
849
+ </React.Profiler>,
850
+ {unstable_isConcurrent: true},
851
+ );
852
+
853
+ // Render everything initially.
854
+ // This should take 21 seconds of actual and base time.
855
+ expect(Scheduler).toFlushAndYield(['Yield:6', 'Yield:15']);
856
+ expect(callback).toHaveBeenCalledTimes(1);
857
+ let call = callback.mock.calls[0];
858
+ expect(call[2]).toBe(21); // actual time
859
+ expect(call[3]).toBe(21); // base time
860
+ expect(call[4]).toBe(5); // start time
861
+ expect(call[5]).toBe(26); // commit time
862
+
863
+ callback.mockReset();
864
+
865
+ Scheduler.unstable_advanceTime(30); // 26 -> 56
866
+
867
+ // Render a partially update, but don't finish.
868
+ // This partial render should take 3ms of simulated time.
869
+ renderer.update(
870
+ <React.Profiler id="test" onRender={callback}>
871
+ <Yield renderTime={3} />
872
+ <Yield renderTime={5} />
873
+ <Yield renderTime={9} />
874
+ </React.Profiler>,
875
+ );
876
+ expect(Scheduler).toFlushAndYieldThrough(['Yield:3']);
877
+ expect(callback).toHaveBeenCalledTimes(0);
878
+
879
+ // Simulate time moving forward while frame is paused.
880
+ Scheduler.unstable_advanceTime(100); // 59 -> 159
881
+
882
+ // Render another 5ms of simulated time.
883
+ expect(Scheduler).toFlushAndYieldThrough(['Yield:5']);
884
+ expect(callback).toHaveBeenCalledTimes(0);
885
+
886
+ // Simulate time moving forward while frame is paused.
887
+ Scheduler.unstable_advanceTime(100); // 164 -> 264
888
+
889
+ // Interrupt with higher priority work.
890
+ // The interrupted work simulates an additional 11ms of time.
891
+ renderer.unstable_flushSync(() => {
892
+ renderer.update(
893
+ <React.Profiler id="test" onRender={callback}>
894
+ <Yield renderTime={11} />
895
+ </React.Profiler>,
896
+ );
897
+ });
898
+ expect(Scheduler).toHaveYielded(['Yield:11']);
899
+
900
+ // The actual time should include only the most recent render,
901
+ // Because this lets us avoid a lot of commit phase reset complexity.
902
+ // The base time includes only the final rendered tree times.
903
+ expect(callback).toHaveBeenCalledTimes(1);
904
+ call = callback.mock.calls[0];
905
+ expect(call[2]).toBe(11); // actual time
906
+ expect(call[3]).toBe(11); // base time
907
+ expect(call[4]).toBe(264); // start time
908
+ expect(call[5]).toBe(275); // commit time
909
+
910
+ // Verify no more unexpected callbacks from low priority work
911
+ expect(Scheduler).toFlushAndYield([]);
912
+ expect(callback).toHaveBeenCalledTimes(1);
913
+ });
914
+
915
+ it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
916
+ const callback = jest.fn();
917
+
918
+ const Yield = ({renderTime}) => {
919
+ Scheduler.unstable_advanceTime(renderTime);
920
+ Scheduler.unstable_yieldValue('Yield:' + renderTime);
921
+ return null;
922
+ };
923
+
924
+ let first;
925
+ class FirstComponent extends React.Component {
926
+ state = {renderTime: 1};
927
+ render() {
928
+ first = this;
929
+ Scheduler.unstable_advanceTime(this.state.renderTime);
930
+ Scheduler.unstable_yieldValue(
931
+ 'FirstComponent:' + this.state.renderTime,
932
+ );
933
+ return <Yield renderTime={4} />;
934
+ }
935
+ }
936
+ let second;
937
+ class SecondComponent extends React.Component {
938
+ state = {renderTime: 2};
939
+ render() {
940
+ second = this;
941
+ Scheduler.unstable_advanceTime(this.state.renderTime);
942
+ Scheduler.unstable_yieldValue(
943
+ 'SecondComponent:' + this.state.renderTime,
944
+ );
945
+ return <Yield renderTime={7} />;
946
+ }
947
+ }
948
+
949
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
950
+
951
+ const renderer = ReactTestRenderer.create(
952
+ <React.Profiler id="test" onRender={callback}>
953
+ <FirstComponent />
954
+ <SecondComponent />
955
+ </React.Profiler>,
956
+ {unstable_isConcurrent: true},
957
+ );
958
+
959
+ // Render everything initially.
960
+ // This simulates a total of 14ms of actual render time.
961
+ // The base render time is also 14ms for the initial render.
962
+ expect(Scheduler).toFlushAndYield([
963
+ 'FirstComponent:1',
964
+ 'Yield:4',
965
+ 'SecondComponent:2',
966
+ 'Yield:7',
967
+ ]);
968
+ expect(callback).toHaveBeenCalledTimes(1);
969
+ let call = callback.mock.calls[0];
970
+ expect(call[2]).toBe(14); // actual time
971
+ expect(call[3]).toBe(14); // base time
972
+ expect(call[4]).toBe(5); // start time
973
+ expect(call[5]).toBe(19); // commit time
974
+
975
+ callback.mockClear();
976
+
977
+ Scheduler.unstable_advanceTime(100); // 19 -> 119
978
+
979
+ // Render a partially update, but don't finish.
980
+ // This partial render will take 10ms of actual render time.
981
+ first.setState({renderTime: 10});
982
+ expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']);
983
+ expect(callback).toHaveBeenCalledTimes(0);
984
+
985
+ // Simulate time moving forward while frame is paused.
986
+ Scheduler.unstable_advanceTime(100); // 129 -> 229
987
+
988
+ // Interrupt with higher priority work.
989
+ // This simulates a total of 37ms of actual render time.
990
+ renderer.unstable_flushSync(() =>
991
+ second.setState({renderTime: 30}),
992
+ );
993
+ expect(Scheduler).toHaveYielded(['SecondComponent:30', 'Yield:7']);
994
+
995
+ // The actual time should include only the most recent render (37ms),
996
+ // Because this greatly simplifies the commit phase logic.
997
+ // The base time should include the more recent times for the SecondComponent subtree,
998
+ // As well as the original times for the FirstComponent subtree.
999
+ expect(callback).toHaveBeenCalledTimes(1);
1000
+ call = callback.mock.calls[0];
1001
+ expect(call[2]).toBe(37); // actual time
1002
+ expect(call[3]).toBe(42); // base time
1003
+ expect(call[4]).toBe(229); // start time
1004
+ expect(call[5]).toBe(266); // commit time
1005
+
1006
+ callback.mockClear();
1007
+
1008
+ // Simulate time moving forward while frame is paused.
1009
+ Scheduler.unstable_advanceTime(100); // 266 -> 366
1010
+
1011
+ // Resume the original low priority update, with rebased state.
1012
+ // This simulates a total of 14ms of actual render time,
1013
+ // And does not include the original (interrupted) 10ms.
1014
+ // The tree contains 42ms of base render time at this point,
1015
+ // Reflecting the most recent (longer) render durations.
1016
+ // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
1017
+ expect(Scheduler).toFlushAndYield(['FirstComponent:10', 'Yield:4']);
1018
+ expect(callback).toHaveBeenCalledTimes(1);
1019
+ call = callback.mock.calls[0];
1020
+ expect(call[2]).toBe(14); // actual time
1021
+ expect(call[3]).toBe(51); // base time
1022
+ expect(call[4]).toBe(366); // start time
1023
+ expect(call[5]).toBe(380); // commit time
1024
+ });
1025
+
1026
+ [true, false].forEach(
1027
+ replayFailedUnitOfWorkWithInvokeGuardedCallback => {
1028
+ describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
1029
+ replayFailedUnitOfWorkWithInvokeGuardedCallback
1030
+ ? 'enabled'
1031
+ : 'disabled'
1032
+ }`, () => {
1033
+ beforeEach(() => {
1034
+ jest.resetModules();
1035
+
1036
+ loadModules({
1037
+ replayFailedUnitOfWorkWithInvokeGuardedCallback,
1038
+ });
1039
+ });
1040
+
1041
+ it('should accumulate actual time after an error handled by componentDidCatch()', () => {
1042
+ const callback = jest.fn();
1043
+
1044
+ const ThrowsError = () => {
1045
+ Scheduler.unstable_advanceTime(3);
1046
+ throw Error('expected error');
1047
+ };
1048
+
1049
+ class ErrorBoundary extends React.Component {
1050
+ state = {error: null};
1051
+ componentDidCatch(error) {
1052
+ this.setState({error});
1053
+ }
1054
+ render() {
1055
+ Scheduler.unstable_advanceTime(2);
1056
+ return this.state.error === null ? (
1057
+ this.props.children
1058
+ ) : (
1059
+ <AdvanceTime byAmount={20} />
1060
+ );
1061
+ }
1062
+ }
1063
+
1064
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
1065
+
1066
+ ReactTestRenderer.create(
1067
+ <React.Profiler id="test" onRender={callback}>
1068
+ <ErrorBoundary>
1069
+ <AdvanceTime byAmount={9} />
1070
+ <ThrowsError />
1071
+ </ErrorBoundary>
1072
+ </React.Profiler>,
1073
+ );
1074
+
1075
+ expect(callback).toHaveBeenCalledTimes(2);
1076
+
1077
+ // Callbacks bubble (reverse order).
1078
+ let [mountCall, updateCall] = callback.mock.calls;
1079
+
1080
+ // The initial mount only includes the ErrorBoundary (which takes 2)
1081
+ // But it spends time rendering all of the failed subtree also.
1082
+ expect(mountCall[1]).toBe('mount');
1083
+ // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
1084
+ // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1085
+ expect(mountCall[2]).toBe(14);
1086
+ // base time includes: 2 (ErrorBoundary)
1087
+ // Since the tree is empty for the initial commit
1088
+ expect(mountCall[3]).toBe(2);
1089
+ // start time
1090
+ expect(mountCall[4]).toBe(5);
1091
+ // commit time: 5 initially + 14 of work
1092
+ // Add an additional 3 (ThrowsError) if we replayed the failed work
1093
+ expect(mountCall[5]).toBe(
1094
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1095
+ ? 22
1096
+ : 19,
1097
+ );
1098
+
1099
+ // The update includes the ErrorBoundary and its fallback child
1100
+ expect(updateCall[1]).toBe('update');
1101
+ // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1102
+ expect(updateCall[2]).toBe(22);
1103
+ // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1104
+ expect(updateCall[3]).toBe(22);
1105
+ // start time
1106
+ expect(updateCall[4]).toBe(
1107
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1108
+ ? 22
1109
+ : 19,
1110
+ );
1111
+ // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
1112
+ // Add an additional 3 (ThrowsError) if we replayed the failed work
1113
+ expect(updateCall[5]).toBe(
1114
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1115
+ ? 44
1116
+ : 41,
1117
+ );
1118
+ });
1119
+
1120
+ it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => {
1121
+ const callback = jest.fn();
1122
+
1123
+ const ThrowsError = () => {
1124
+ Scheduler.unstable_advanceTime(10);
1125
+ throw Error('expected error');
1126
+ };
1127
+
1128
+ class ErrorBoundary extends React.Component {
1129
+ state = {error: null};
1130
+ static getDerivedStateFromError(error) {
1131
+ return {error};
1132
+ }
1133
+ render() {
1134
+ Scheduler.unstable_advanceTime(2);
1135
+ return this.state.error === null ? (
1136
+ this.props.children
1137
+ ) : (
1138
+ <AdvanceTime byAmount={20} />
1139
+ );
1140
+ }
1141
+ }
1142
+
1143
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
1144
+
1145
+ ReactTestRenderer.create(
1146
+ <React.Profiler id="test" onRender={callback}>
1147
+ <ErrorBoundary>
1148
+ <AdvanceTime byAmount={5} />
1149
+ <ThrowsError />
1150
+ </ErrorBoundary>
1151
+ </React.Profiler>,
1152
+ );
1153
+
1154
+ expect(callback).toHaveBeenCalledTimes(1);
1155
+
1156
+ // Callbacks bubble (reverse order).
1157
+ let [mountCall] = callback.mock.calls;
1158
+
1159
+ // The initial mount includes the ErrorBoundary's error state,
1160
+ // But it also spends actual time rendering UI that fails and isn't included.
1161
+ expect(mountCall[1]).toBe('mount');
1162
+ // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
1163
+ // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
1164
+ // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1165
+ expect(mountCall[2]).toBe(39);
1166
+ // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1167
+ expect(mountCall[3]).toBe(22);
1168
+ // start time
1169
+ expect(mountCall[4]).toBe(5);
1170
+ // commit time
1171
+ expect(mountCall[5]).toBe(
1172
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1173
+ ? 54
1174
+ : 44,
1175
+ );
1176
+ });
1177
+
1178
+ it('should reset the fiber stack correct after a "complete" phase error', () => {
1179
+ jest.resetModules();
1180
+
1181
+ loadModules({
1182
+ useNoopRenderer: true,
1183
+ replayFailedUnitOfWorkWithInvokeGuardedCallback,
1184
+ });
1185
+
1186
+ // Simulate a renderer error during the "complete" phase.
1187
+ // This mimics behavior like React Native's View/Text nesting validation.
1188
+ ReactNoop.render(
1189
+ <React.Profiler id="profiler" onRender={jest.fn()}>
1190
+ <errorInCompletePhase>hi</errorInCompletePhase>
1191
+ </React.Profiler>,
1192
+ );
1193
+ expect(Scheduler).toFlushAndThrow('Error in host config.');
1194
+
1195
+ // A similar case we've seen caused by an invariant in ReactDOM.
1196
+ // It didn't reproduce without a host component inside.
1197
+ ReactNoop.render(
1198
+ <React.Profiler id="profiler" onRender={jest.fn()}>
1199
+ <errorInCompletePhase>
1200
+ <span>hi</span>
1201
+ </errorInCompletePhase>
1202
+ </React.Profiler>,
1203
+ );
1204
+ expect(Scheduler).toFlushAndThrow('Error in host config.');
1205
+
1206
+ // So long as the profiler timer's fiber stack is reset correctly,
1207
+ // Subsequent renders should not error.
1208
+ ReactNoop.render(
1209
+ <React.Profiler id="profiler" onRender={jest.fn()}>
1210
+ <span>hi</span>
1211
+ </React.Profiler>,
1212
+ );
1213
+ expect(Scheduler).toFlushWithoutYielding();
1214
+ });
1215
+ });
1216
+ },
1217
+ );
1218
});
1219
709
- it('should not include time between frames', () => {
1220
+ it('reflects the most recently rendered id value', () => {
1221
const callback = jest.fn();
1222
712
- const Yield = ({renderTime}) => {
713
- Scheduler.unstable_advanceTime(renderTime);
714
- Scheduler.unstable_yieldValue('Yield:' + renderTime);
1223
+ Scheduler.unstable_advanceTime(5); // 0 -> 5
1224
+
1225
+ const renderer = ReactTestRenderer.create(
1226
+ <React.Profiler id="one" onRender={callback}>
1227
+ <AdvanceTime byAmount={2} />
1228
+ </React.Profiler>,
1229
+ );
1230
+
1231
+ expect(callback).toHaveBeenCalledTimes(1);
1232
+
1233
+ Scheduler.unstable_advanceTime(20); // 7 -> 27
1234
+
1235
+ renderer.update(
1236
+ <React.Profiler id="two" onRender={callback}>
1237
+ <AdvanceTime byAmount={1} />
1238
+ </React.Profiler>,
1239
+ );
1240
+
1241
+ expect(callback).toHaveBeenCalledTimes(2);
1242
+
1243
+ const [mountCall, updateCall] = callback.mock.calls;
1244
+
1245
+ expect(mountCall[0]).toBe('one');
1246
+ expect(mountCall[1]).toBe('mount');
1247
+ expect(mountCall[2]).toBe(2); // actual time
1248
+ expect(mountCall[3]).toBe(2); // base time
1249
+ expect(mountCall[4]).toBe(5); // start time
1250
+
1251
+ expect(updateCall[0]).toBe('two');
1252
+ expect(updateCall[1]).toBe('update');
1253
+ expect(updateCall[2]).toBe(1); // actual time
1254
+ expect(updateCall[3]).toBe(1); // base time
1255
+ expect(updateCall[4]).toBe(27); // start time
1256
+ });
1257
+
1258
+ it('should not be called until after mutations', () => {
1259
+ let classComponentMounted = false;
1260
+ const callback = jest.fn(
1261
+ (
1262
+ id,
1263
+ phase,
1264
+ actualDuration,
1265
+ baseDuration,
1266
+ startTime,
1267
+ commitTime,
1268
+ ) => {
1269
+ // Don't call this hook until after mutations
1270
+ expect(classComponentMounted).toBe(true);
1271
+ // But the commit time should reflect pre-mutation
1272
+ expect(commitTime).toBe(2);
1273
+ },
1274
+ );
1275
+
1276
+ class ClassComponent extends React.Component {
1277
+ componentDidMount() {
1278
+ Scheduler.unstable_advanceTime(5);
1279
+ classComponentMounted = true;
1280
+ }
1281
+ render() {
1282
+ Scheduler.unstable_advanceTime(2);
1283
+ return null;
1284
+ }
1285
+ }
1286
+
1287
+ ReactTestRenderer.create(
1288
+ <React.Profiler id="test" onRender={callback}>
1289
+ <ClassComponent />
1290
+ </React.Profiler>,
1291
+ );
1292
+
1293
+ expect(callback).toHaveBeenCalledTimes(1);
1294
+ });
1295
+ });
1296
+
1297
+ describe(`onCommit enableSchedulerTracing:${
1298
+ enableSchedulerTracing ? 'enabled' : 'disabled'
1299
+ } deferPassiveEffectCleanupDuringUnmount:${
1300
+ deferPassiveEffectCleanupDuringUnmount ? 'enabled' : 'disabled'
1301
+ }`, () => {
1302
+ beforeEach(() => {
1303
+ jest.resetModules();
1304
+
1305
+ loadModules({
1306
+ deferPassiveEffectCleanupDuringUnmount,
1307
+ enableSchedulerTracing,
1308
+ });
1309
+ });
1310
+
1311
+ it('should report time spent in layout effects and commit lifecycles', () => {
1312
+ const callback = jest.fn();
1313
+
1314
+ const ComponetWithEffects = () => {
1315
+ React.useLayoutEffect(() => {
1316
+ Scheduler.unstable_advanceTime(10);
1317
+ return () => {
1318
+ Scheduler.unstable_advanceTime(100);
1319
+ };
1320
+ }, []);
1321
+ React.useLayoutEffect(() => {
1322
+ Scheduler.unstable_advanceTime(1000);
1323
+ return () => {
1324
+ Scheduler.unstable_advanceTime(10000);
1325
+ };
1326
+ });
1327
+ React.useEffect(() => {
1328
+ // This passive effect is here to verify that its time isn't reported.
1329
+ Scheduler.unstable_advanceTime(5);
1330
+ return () => {
1331
+ Scheduler.unstable_advanceTime(7);
1332
+ };
1333
+ });
1334
return null;
1335
};
1336
718
- Scheduler.unstable_advanceTime(5); // 0 -> 5
1337
+ class ComponentWithCommitHooks extends React.Component {
1338
+ componentDidMount() {
1339
+ Scheduler.unstable_advanceTime(100000);
1340
+ }
1341
+ componentDidUpdate() {
1342
+ Scheduler.unstable_advanceTime(1000000);
1343
+ }
1344
+ render() {
1345
+ return null;
1346
+ }
1347
+ }
1348
720
- // Render partially, but don't finish.
721
- // This partial render should take 5ms of simulated time.
722
- ReactTestRenderer.create(
723
- <React.Profiler id="outer" onRender={callback}>
724
- <Yield renderTime={5} />
725
- <Yield renderTime={10} />
726
- <React.Profiler id="inner" onRender={callback}>
727
- <Yield renderTime={17} />
728
- </React.Profiler>
1349
+ Scheduler.unstable_advanceTime(1);
1350
+
1351
+ const renderer = ReactTestRenderer.create(
1352
+ <React.Profiler id="mount-test" onCommit={callback}>
1353
+ <ComponetWithEffects />
1354
+ <ComponentWithCommitHooks />
1355
</React.Profiler>,
730
- {unstable_isConcurrent: true},
1356
);
732
- expect(Scheduler).toFlushAndYieldThrough(['Yield:5']);
733
- expect(callback).toHaveBeenCalledTimes(0);
1357
735
- // Simulate time moving forward while frame is paused.
736
- Scheduler.unstable_advanceTime(50); // 10 -> 60
1358
+ expect(callback).toHaveBeenCalledTimes(1);
1359
+
1360
+ let call = callback.mock.calls[0];
1361
+
1362
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1363
+ expect(call[0]).toBe('mount-test');
1364
+ expect(call[1]).toBe('mount');
1365
+ expect(call[2]).toBe(101010); // durations
1366
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
1367
+ expect(call[4]).toEqual(
1368
+ enableSchedulerTracing ? new Set() : undefined,
1369
+ ); // interaction events
1370
+
1371
+ Scheduler.unstable_advanceTime(1);
1372
+
1373
+ renderer.update(
1374
+ <React.Profiler id="update-test" onCommit={callback}>
1375
+ <ComponetWithEffects />
1376
+ <ComponentWithCommitHooks />
1377
+ </React.Profiler>,
1378
+ );
1379
+
1380
+ expect(callback).toHaveBeenCalledTimes(2);
1381
+
1382
+ call = callback.mock.calls[1];
1383
+
1384
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1385
+ expect(call[0]).toBe('update-test');
1386
+ expect(call[1]).toBe('update');
1387
+ expect(call[2]).toBe(1011000); // durations
1388
+ expect(call[3]).toBe(101017); // commit start time (before mutations or effects)
1389
+ expect(call[4]).toEqual(
1390
+ enableSchedulerTracing ? new Set() : undefined,
1391
+ ); // interaction events
1392
+
1393
+ Scheduler.unstable_advanceTime(1);
1394
+
1395
+ renderer.update(
1396
+ <React.Profiler id="unmount-test" onCommit={callback} />,
1397
+ );
1398
+
1399
+ expect(callback).toHaveBeenCalledTimes(3);
1400
+
1401
+ call = callback.mock.calls[2];
1402
+
1403
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1404
+ expect(call[0]).toBe('unmount-test');
1405
+ expect(call[1]).toBe('update');
1406
+ expect(call[2]).toBe(10100); // durations
1407
+ expect(call[3]).toBe(1112030); // commit start time (before mutations or effects)
1408
+ expect(call[4]).toEqual(
1409
+ enableSchedulerTracing ? new Set() : undefined,
1410
+ ); // interaction events
1411
+ });
1412
+
1413
+ it('should report time spent in layout effects and commit lifecycles with cascading renders', () => {
1414
+ const callback = jest.fn();
1415
+
1416
+ const ComponetWithEffects = ({shouldCascade}) => {
1417
+ const [didCascade, setDidCascade] = React.useState(false);
1418
+ React.useLayoutEffect(() => {
1419
+ if (shouldCascade && !didCascade) {
1420
+ setDidCascade(true);
1421
+ }
1422
+ Scheduler.unstable_advanceTime(didCascade ? 30 : 10);
1423
+ return () => {
1424
+ Scheduler.unstable_advanceTime(100);
1425
+ };
1426
+ }, [didCascade, shouldCascade]);
1427
+ return null;
1428
+ };
1429
+
1430
+ class ComponentWithCommitHooks extends React.Component {
1431
+ state = {
1432
+ didCascade: false,
1433
+ };
1434
+ componentDidMount() {
1435
+ Scheduler.unstable_advanceTime(1000);
1436
+ }
1437
+ componentDidUpdate() {
1438
+ Scheduler.unstable_advanceTime(10000);
1439
+ if (this.props.shouldCascade && !this.state.didCascade) {
1440
+ this.setState({didCascade: true});
1441
+ }
1442
+ }
1443
+ render() {
1444
+ return null;
1445
+ }
1446
+ }
1447
+
1448
+ Scheduler.unstable_advanceTime(1);
1449
+
1450
+ const renderer = ReactTestRenderer.create(
1451
+ <React.Profiler id="mount-test" onCommit={callback}>
1452
+ <ComponetWithEffects shouldCascade={true} />
1453
+ <ComponentWithCommitHooks />
1454
+ </React.Profiler>,
1455
+ );
1456
+
1457
+ expect(callback).toHaveBeenCalledTimes(2);
1458
+
1459
+ let call = callback.mock.calls[0];
1460
+
1461
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1462
+ expect(call[0]).toBe('mount-test');
1463
+ expect(call[1]).toBe('mount');
1464
+ expect(call[2]).toBe(1010); // durations
1465
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
1466
+ expect(call[4]).toEqual(
1467
+ enableSchedulerTracing ? new Set() : undefined,
1468
+ ); // interaction events
1469
+
1470
+ call = callback.mock.calls[1];
1471
+
1472
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1473
+ expect(call[0]).toBe('mount-test');
1474
+ expect(call[1]).toBe('update');
1475
+ expect(call[2]).toBe(130); // durations
1476
+ expect(call[3]).toBe(1011); // commit start time (before mutations or effects)
1477
+ expect(call[4]).toEqual(
1478
+ enableSchedulerTracing ? new Set() : undefined,
1479
+ ); // interaction events
1480
+
1481
+ Scheduler.unstable_advanceTime(1);
1482
+
1483
+ renderer.update(
1484
+ <React.Profiler id="update-test" onCommit={callback}>
1485
+ <ComponetWithEffects />
1486
+ <ComponentWithCommitHooks shouldCascade={true} />
1487
+ </React.Profiler>,
1488
+ );
1489
+
1490
+ expect(callback).toHaveBeenCalledTimes(4);
1491
+
1492
+ call = callback.mock.calls[2];
1493
+
1494
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1495
+ expect(call[0]).toBe('update-test');
1496
+ expect(call[1]).toBe('update');
1497
+ expect(call[2]).toBe(10130); // durations
1498
+ expect(call[3]).toBe(1142); // commit start time (before mutations or effects)
1499
+ expect(call[4]).toEqual(
1500
+ enableSchedulerTracing ? new Set() : undefined,
1501
+ ); // interaction events
1502
+
1503
+ call = callback.mock.calls[3];
1504
+
1505
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1506
+ expect(call[0]).toBe('update-test');
1507
+ expect(call[1]).toBe('update');
1508
+ expect(call[2]).toBe(10000); // durations
1509
+ expect(call[3]).toBe(11272); // commit start time (before mutations or effects)
1510
+ expect(call[4]).toEqual(
1511
+ enableSchedulerTracing ? new Set() : undefined,
1512
+ ); // interaction events
1513
+ });
1514
+
1515
+ it('should bubble time spent in layout effects to higher profilers', () => {
1516
+ const callback = jest.fn();
1517
+
1518
+ const ComponetWithEffects = ({
1519
+ cleanupDuration,
1520
+ duration,
1521
+ setCountRef,
1522
+ }) => {
1523
+ const setCount = React.useState(0)[1];
1524
+ if (setCountRef != null) {
1525
+ setCountRef.current = setCount;
1526
+ }
1527
+ React.useLayoutEffect(() => {
1528
+ Scheduler.unstable_advanceTime(duration);
1529
+ return () => {
1530
+ Scheduler.unstable_advanceTime(cleanupDuration);
1531
+ };
1532
+ });
1533
+ Scheduler.unstable_advanceTime(1);
1534
+ return null;
1535
+ };
1536
+
1537
+ const setCountRef = React.createRef(null);
1538
+
1539
+ let renderer = null;
1540
+ ReactTestRenderer.act(() => {
1541
+ renderer = ReactTestRenderer.create(
1542
+ <React.Profiler id="root-mount" onCommit={callback}>
1543
+ <React.Profiler id="a">
1544
+ <ComponetWithEffects
1545
+ duration={10}
1546
+ cleanupDuration={100}
1547
+ setCountRef={setCountRef}
1548
+ />
1549
+ </React.Profiler>
1550
+ <React.Profiler id="b">
1551
+ <ComponetWithEffects
1552
+ duration={1000}
1553
+ cleanupDuration={10000}
1554
+ />
1555
+ </React.Profiler>
1556
+ </React.Profiler>,
1557
+ );
1558
+ });
1559
+
1560
+ expect(callback).toHaveBeenCalledTimes(1);
1561
+
1562
+ let call = callback.mock.calls[0];
1563
+
1564
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1565
+ expect(call[0]).toBe('root-mount');
1566
+ expect(call[1]).toBe('mount');
1567
+ expect(call[2]).toBe(1010); // durations
1568
+ expect(call[3]).toBe(2); // commit start time (before mutations or effects)
1569
+ expect(call[4]).toEqual(
1570
+ enableSchedulerTracing ? new Set() : undefined,
1571
+ ); // interaction events
1572
+
1573
+ ReactTestRenderer.act(() => setCountRef.current(count => count + 1));
1574
+
1575
+ expect(callback).toHaveBeenCalledTimes(2);
1576
+
1577
+ call = callback.mock.calls[1];
1578
+
1579
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1580
+ expect(call[0]).toBe('root-mount');
1581
+ expect(call[1]).toBe('update');
1582
+ expect(call[2]).toBe(110); // durations
1583
+ expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
1584
+ expect(call[4]).toEqual(
1585
+ enableSchedulerTracing ? new Set() : undefined,
1586
+ ); // interaction events
1587
+
1588
+ ReactTestRenderer.act(() => {
1589
+ renderer.update(
1590
+ <React.Profiler id="root-update" onCommit={callback}>
1591
+ <React.Profiler id="b">
1592
+ <ComponetWithEffects
1593
+ duration={1000}
1594
+ cleanupDuration={10000}
1595
+ />
1596
+ </React.Profiler>
1597
+ </React.Profiler>,
1598
+ );
1599
+ });
1600
+
1601
+ expect(callback).toHaveBeenCalledTimes(3);
1602
+
1603
+ call = callback.mock.calls[2];
1604
+
1605
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1606
+ expect(call[0]).toBe('root-update');
1607
+ expect(call[1]).toBe('update');
1608
+ expect(call[2]).toBe(1100); // durations
1609
+ expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
1610
+ expect(call[4]).toEqual(
1611
+ enableSchedulerTracing ? new Set() : undefined,
1612
+ ); // interaction events
1613
+ });
1614
+
1615
+ it('should properly report time in layout effects even when there are errors', () => {
1616
+ const callback = jest.fn();
1617
+
1618
+ class ErrorBoundary extends React.Component {
1619
+ state = {error: null};
1620
+ static getDerivedStateFromError(error) {
1621
+ return {error};
1622
+ }
1623
+ render() {
1624
+ return this.state.error === null
1625
+ ? this.props.children
1626
+ : this.props.fallback;
1627
+ }
1628
+ }
1629
+
1630
+ const ComponetWithEffects = ({
1631
+ cleanupDuration,
1632
+ duration,
1633
+ effectDuration,
1634
+ shouldThrow,
1635
+ }) => {
1636
+ React.useLayoutEffect(() => {
1637
+ Scheduler.unstable_advanceTime(effectDuration);
1638
+ if (shouldThrow) {
1639
+ throw Error('expected');
1640
+ }
1641
+ return () => {
1642
+ Scheduler.unstable_advanceTime(cleanupDuration);
1643
+ };
1644
+ });
1645
+ Scheduler.unstable_advanceTime(duration);
1646
+ return null;
1647
+ };
1648
+
1649
+ Scheduler.unstable_advanceTime(1);
1650
+
1651
+ // Test an error that happens during an effect
1652
+
1653
+ ReactTestRenderer.act(() => {
1654
+ ReactTestRenderer.create(
1655
+ <React.Profiler id="root" onCommit={callback}>
1656
+ <ErrorBoundary
1657
+ fallback={
1658
+ <ComponetWithEffects
1659
+ duration={10000000}
1660
+ effectDuration={100000000}
1661
+ cleanupDuration={1000000000}
1662
+ />
1663
+ }>
1664
+ <ComponetWithEffects
1665
+ duration={10}
1666
+ effectDuration={100}
1667
+ cleanupDuration={1000}
1668
+ shouldThrow={true}
1669
+ />
1670
+ </ErrorBoundary>
1671
+ <ComponetWithEffects
1672
+ duration={10000}
1673
+ effectDuration={100000}
1674
+ cleanupDuration={1000000}
1675
+ />
1676
+ </React.Profiler>,
1677
+ );
1678
+ });
1679
+
1680
+ expect(callback).toHaveBeenCalledTimes(2);
1681
+
1682
+ let call = callback.mock.calls[0];
1683
+
1684
+ // Initial render (with error)
1685
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1686
+ expect(call[0]).toBe('root');
1687
+ expect(call[1]).toBe('mount');
1688
+ expect(call[2]).toBe(100100); // durations
1689
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
1690
+ expect(call[4]).toEqual(
1691
+ enableSchedulerTracing ? new Set() : undefined,
1692
+ ); // interaction events
1693
+
1694
+ call = callback.mock.calls[1];
1695
+
1696
+ // Cleanup render from error boundary
1697
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1698
+ expect(call[0]).toBe('root');
1699
+ expect(call[1]).toBe('update');
1700
+ expect(call[2]).toBe(100000000); // durations
1701
+ expect(call[3]).toBe(10110111); // commit start time (before mutations or effects)
1702
+ expect(call[4]).toEqual(
1703
+ enableSchedulerTracing ? new Set() : undefined,
1704
+ ); // interaction events
1705
+ });
1706
+
1707
+ it('should properly report time in layout effect cleanup functions even when there are errors', () => {
1708
+ const callback = jest.fn();
1709
+
1710
+ class ErrorBoundary extends React.Component {
1711
+ state = {error: null};
1712
+ static getDerivedStateFromError(error) {
1713
+ return {error};
1714
+ }
1715
+ render() {
1716
+ return this.state.error === null
1717
+ ? this.props.children
1718
+ : this.props.fallback;
1719
+ }
1720
+ }
1721
+
1722
+ const ComponetWithEffects = ({
1723
+ cleanupDuration,
1724
+ duration,
1725
+ effectDuration,
1726
+ shouldThrow = false,
1727
+ }) => {
1728
+ React.useLayoutEffect(() => {
1729
+ Scheduler.unstable_advanceTime(effectDuration);
1730
+ return () => {
1731
+ Scheduler.unstable_advanceTime(cleanupDuration);
1732
+ if (shouldThrow) {
1733
+ throw Error('expected');
1734
+ }
1735
+ };
1736
+ });
1737
+ Scheduler.unstable_advanceTime(duration);
1738
+ return null;
1739
+ };
1740
+
1741
+ Scheduler.unstable_advanceTime(1);
1742
+
1743
+ let renderer = null;
1744
+
1745
+ ReactTestRenderer.act(() => {
1746
+ renderer = ReactTestRenderer.create(
1747
+ <React.Profiler id="root" onCommit={callback}>
1748
+ <ErrorBoundary
1749
+ fallback={
1750
+ <ComponetWithEffects
1751
+ duration={10000000}
1752
+ effectDuration={100000000}
1753
+ cleanupDuration={1000000000}
1754
+ />
1755
+ }>
1756
+ <ComponetWithEffects
1757
+ duration={10}
1758
+ effectDuration={100}
1759
+ cleanupDuration={1000}
1760
+ shouldThrow={true}
1761
+ />
1762
+ </ErrorBoundary>
1763
+ <ComponetWithEffects
1764
+ duration={10000}
1765
+ effectDuration={100000}
1766
+ cleanupDuration={1000000}
1767
+ />
1768
+ </React.Profiler>,
1769
+ );
1770
+ });
1771
+
1772
+ expect(callback).toHaveBeenCalledTimes(1);
1773
+
1774
+ let call = callback.mock.calls[0];
1775
+
1776
+ // Initial render
1777
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1778
+ expect(call[0]).toBe('root');
1779
+ expect(call[1]).toBe('mount');
1780
+ expect(call[2]).toBe(100100); // durations
1781
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
1782
+ expect(call[4]).toEqual(
1783
+ enableSchedulerTracing ? new Set() : undefined,
1784
+ ); // interaction events
1785
+
1786
+ callback.mockClear();
1787
+
1788
+ // Test an error that happens during an cleanup function
1789
+
1790
+ ReactTestRenderer.act(() => {
1791
+ renderer.update(
1792
+ <React.Profiler id="root" onCommit={callback}>
1793
+ <ErrorBoundary
1794
+ fallback={
1795
+ <ComponetWithEffects
1796
+ duration={10000000}
1797
+ effectDuration={100000000}
1798
+ cleanupDuration={1000000000}
1799
+ />
1800
+ }>
1801
+ <ComponetWithEffects
1802
+ duration={10}
1803
+ effectDuration={100}
1804
+ cleanupDuration={1000}
1805
+ shouldThrow={false}
1806
+ />
1807
+ </ErrorBoundary>
1808
+ <ComponetWithEffects
1809
+ duration={10000}
1810
+ effectDuration={100000}
1811
+ cleanupDuration={1000000}
1812
+ />
1813
+ </React.Profiler>,
1814
+ );
1815
+ });
1816
+
1817
+ expect(callback).toHaveBeenCalledTimes(2);
1818
+
1819
+ call = callback.mock.calls[0];
1820
+
1821
+ // Update (that throws)
1822
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1823
+ expect(call[0]).toBe('root');
1824
+ expect(call[1]).toBe('update');
1825
+ expect(call[2]).toBe(1101100); // durations
1826
+ expect(call[3]).toBe(120121); // commit start time (before mutations or effects)
1827
+ expect(call[4]).toEqual(
1828
+ enableSchedulerTracing ? new Set() : undefined,
1829
+ ); // interaction events
1830
+
1831
+ call = callback.mock.calls[1];
1832
+
1833
+ // Cleanup render from error boundary
1834
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1835
+ expect(call[0]).toBe('root');
1836
+ expect(call[1]).toBe('update');
1837
+ expect(call[2]).toBe(100001000); // durations
1838
+ expect(call[3]).toBe(11221221); // commit start time (before mutations or effects)
1839
+ expect(call[4]).toEqual(
1840
+ enableSchedulerTracing ? new Set() : undefined,
1841
+ ); // interaction events
1842
+ });
1843
+
1844
+ if (enableSchedulerTracing) {
1845
+ it('should report interactions that were active', () => {
1846
+ const callback = jest.fn();
1847
+
1848
+ const ComponetWithEffects = () => {
1849
+ const [didMount, setDidMount] = React.useState(false);
1850
+ React.useLayoutEffect(() => {
1851
+ Scheduler.unstable_advanceTime(didMount ? 1000 : 100);
1852
+ if (!didMount) {
1853
+ setDidMount(true);
1854
+ }
1855
+ return () => {
1856
+ Scheduler.unstable_advanceTime(10000);
1857
+ };
1858
+ }, [didMount]);
1859
+ Scheduler.unstable_advanceTime(10);
1860
+ return null;
1861
+ };
1862
+
1863
+ const interaction = {
1864
+ id: 0,
1865
+ name: 'mount',
1866
+ timestamp: Scheduler.unstable_now(),
1867
+ };
1868
+
1869
+ Scheduler.unstable_advanceTime(1);
1870
+
1871
+ SchedulerTracing.unstable_trace(
1872
+ interaction.name,
1873
+ interaction.timestamp,
1874
+ () => {
1875
+ ReactTestRenderer.create(
1876
+ <React.Profiler id="root" onCommit={callback}>
1877
+ <ComponetWithEffects />
1878
+ </React.Profiler>,
1879
+ );
1880
+ },
1881
+ );
1882
+
1883
+ expect(callback).toHaveBeenCalledTimes(2);
1884
+
1885
+ let call = callback.mock.calls[0];
1886
+
1887
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1888
+ expect(call[0]).toBe('root');
1889
+ expect(call[1]).toBe('mount');
1890
+ expect(call[4]).toMatchInteractions([interaction]);
1891
+
1892
+ call = callback.mock.calls[1];
1893
+
1894
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1895
+ expect(call[0]).toBe('root');
1896
+ expect(call[1]).toBe('update');
1897
+ expect(call[4]).toMatchInteractions([interaction]);
1898
+ });
1899
+ }
1900
+ });
1901
+
1902
+ describe(`onPostCommit enableSchedulerTracing:${
1903
+ enableSchedulerTracing ? 'enabled' : 'disabled'
1904
+ } deferPassiveEffectCleanupDuringUnmount:${
1905
+ deferPassiveEffectCleanupDuringUnmount ? 'enabled' : 'disabled'
1906
+ }`, () => {
1907
+ beforeEach(() => {
1908
+ jest.resetModules();
1909
+
1910
+ loadModules({
1911
+ deferPassiveEffectCleanupDuringUnmount,
1912
+ enableSchedulerTracing,
1913
+ });
1914
+ });
1915
+
1916
+ it('should report time spent in passive effects', () => {
1917
+ const callback = jest.fn();
1918
+
1919
+ const ComponetWithEffects = () => {
1920
+ React.useLayoutEffect(() => {
1921
+ // This layout effect is here to verify that its time isn't reported.
1922
+ Scheduler.unstable_advanceTime(5);
1923
+ return () => {
1924
+ Scheduler.unstable_advanceTime(7);
1925
+ };
1926
+ });
1927
+ React.useEffect(() => {
1928
+ Scheduler.unstable_advanceTime(10);
1929
+ return () => {
1930
+ Scheduler.unstable_advanceTime(100);
1931
+ };
1932
+ }, []);
1933
+ React.useEffect(() => {
1934
+ Scheduler.unstable_advanceTime(1000);
1935
+ return () => {
1936
+ Scheduler.unstable_advanceTime(10000);
1937
+ };
1938
+ });
1939
+ return null;
1940
+ };
1941
+
1942
+ Scheduler.unstable_advanceTime(1);
1943
+
1944
+ let renderer;
1945
+ ReactTestRenderer.act(() => {
1946
+ renderer = ReactTestRenderer.create(
1947
+ <React.Profiler id="mount-test" onPostCommit={callback}>
1948
+ <ComponetWithEffects />
1949
+ </React.Profiler>,
1950
+ );
1951
+ });
1952
+ Scheduler.unstable_flushAll();
1953
+
1954
+ expect(callback).toHaveBeenCalledTimes(1);
1955
+
1956
+ let call = callback.mock.calls[0];
1957
+
1958
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1959
+ expect(call[0]).toBe('mount-test');
1960
+ expect(call[1]).toBe('mount');
1961
+ expect(call[2]).toBe(1010); // durations
1962
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
1963
+ expect(call[4]).toEqual(
1964
+ enableSchedulerTracing ? new Set() : undefined,
1965
+ ); // interaction events
1966
+
1967
+ Scheduler.unstable_advanceTime(1);
1968
+
1969
+ ReactTestRenderer.act(() => {
1970
+ renderer.update(
1971
+ <React.Profiler id="update-test" onPostCommit={callback}>
1972
+ <ComponetWithEffects />
1973
+ </React.Profiler>,
1974
+ );
1975
+ });
1976
+ Scheduler.unstable_flushAll();
1977
738
- // Flush the remaining work,
739
- // Which should take an additional 10ms of simulated time.
740
- expect(Scheduler).toFlushAndYield(['Yield:10', 'Yield:17']);
1978
expect(callback).toHaveBeenCalledTimes(2);
1979
743
- const [innerCall, outerCall] = callback.mock.calls;
744
-
745
- // Verify that the actual time includes all work times,
746
- // But not the time that elapsed between frames.
747
- expect(innerCall[0]).toBe('inner');
748
- expect(innerCall[2]).toBe(17); // actual time
749
- expect(innerCall[3]).toBe(17); // base time
750
- expect(innerCall[4]).toBe(70); // start time
751
- expect(innerCall[5]).toBe(87); // commit time
752
- expect(outerCall[0]).toBe('outer');
753
- expect(outerCall[2]).toBe(32); // actual time
754
- expect(outerCall[3]).toBe(32); // base time
755
- expect(outerCall[4]).toBe(5); // start time
756
- expect(outerCall[5]).toBe(87); // commit time
1980
+ call = callback.mock.calls[1];
1981
+
1982
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
1983
+ expect(call[0]).toBe('update-test');
1984
+ expect(call[1]).toBe('update');
1985
+ expect(call[2]).toBe(11000); // durations
1986
+ expect(call[3]).toBe(1017); // commit start time (before mutations or effects)
1987
+ expect(call[4]).toEqual(
1988
+ enableSchedulerTracing ? new Set() : undefined,
1989
+ ); // interaction events
1990
+
1991
+ Scheduler.unstable_advanceTime(1);
1992
+
1993
+ ReactTestRenderer.act(() => {
1994
+ renderer.update(
1995
+ <React.Profiler id="unmount-test" onPostCommit={callback} />,
1996
+ );
1997
+ });
1998
+ Scheduler.unstable_flushAll();
1999
+
2000
+ expect(callback).toHaveBeenCalledTimes(3);
2001
+
2002
+ call = callback.mock.calls[2];
2003
+
2004
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2005
+ expect(call[0]).toBe('unmount-test');
2006
+ expect(call[1]).toBe('update');
2007
+ // TODO (bvaughn) The duration reported below should be 10100, but is 0
2008
+ // by the time the passive effect is flushed its parent Fiber pointer is gone.
2009
+ // If we refactor to preserve the unmounted Fiber tree we could fix this.
2010
+ // The current implementation would require too much extra overhead to track this.
2011
+ expect(call[2]).toBe(0); // durations
2012
+ expect(call[3]).toBe(12030); // commit start time (before mutations or effects)
2013
+ expect(call[4]).toEqual(
2014
+ enableSchedulerTracing ? new Set() : undefined,
2015
+ ); // interaction events
2016
});
2017
759
- it('should report the expected times when a high-pri update replaces a mount in-progress', () => {
2018
+ it('should report time spent in passive effects with cascading renders', () => {
2019
const callback = jest.fn();
2020
762
- const Yield = ({renderTime}) => {
763
- Scheduler.unstable_advanceTime(renderTime);
764
- Scheduler.unstable_yieldValue('Yield:' + renderTime);
2021
+ const ComponetWithEffects = () => {
2022
+ const [didMount, setDidMount] = React.useState(false);
2023
+ React.useEffect(() => {
2024
+ if (!didMount) {
2025
+ setDidMount(true);
2026
+ }
2027
+ Scheduler.unstable_advanceTime(didMount ? 30 : 10);
2028
+ return () => {
2029
+ Scheduler.unstable_advanceTime(100);
2030
+ };
2031
+ }, [didMount]);
2032
return null;
2033
};
2034
768
- Scheduler.unstable_advanceTime(5); // 0 -> 5
769
-
770
- // Render a partially update, but don't finish.
771
- // This partial render should take 10ms of simulated time.
772
- const renderer = ReactTestRenderer.create(
773
- <React.Profiler id="test" onRender={callback}>
774
- <Yield renderTime={10} />
775
- <Yield renderTime={20} />
776
- </React.Profiler>,
777
- {unstable_isConcurrent: true},
778
- );
779
- expect(Scheduler).toFlushAndYieldThrough(['Yield:10']);
780
- expect(callback).toHaveBeenCalledTimes(0);
781
-
782
- // Simulate time moving forward while frame is paused.
783
- Scheduler.unstable_advanceTime(100); // 15 -> 115
2035
+ Scheduler.unstable_advanceTime(1);
2036
785
- // Interrupt with higher priority work.
786
- // The interrupted work simulates an additional 5ms of time.
787
- renderer.unstable_flushSync(() => {
788
- renderer.update(
789
- <React.Profiler id="test" onRender={callback}>
790
- <Yield renderTime={5} />
2037
+ ReactTestRenderer.act(() => {
2038
+ ReactTestRenderer.create(
2039
+ <React.Profiler id="mount-test" onPostCommit={callback}>
2040
+ <ComponetWithEffects />
2041
</React.Profiler>,
2042
);
2043
});
794
- expect(Scheduler).toHaveYielded(['Yield:5']);
2044
796
- // The initial work was thrown away in this case,
797
- // So the actual and base times should only include the final rendered tree times.
798
- expect(callback).toHaveBeenCalledTimes(1);
799
- let call = callback.mock.calls[0];
800
- expect(call[2]).toBe(5); // actual time
801
- expect(call[3]).toBe(5); // base time
802
- expect(call[4]).toBe(115); // start time
803
- expect(call[5]).toBe(120); // commit time
2045
+ expect(callback).toHaveBeenCalledTimes(2);
2046
805
- callback.mockReset();
2047
+ let call = callback.mock.calls[0];
2048
807
- // Verify no more unexpected callbacks from low priority work
808
- expect(Scheduler).toFlushWithoutYielding();
809
- expect(callback).toHaveBeenCalledTimes(0);
2049
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2050
+ expect(call[0]).toBe('mount-test');
2051
+ expect(call[1]).toBe('mount');
2052
+ expect(call[2]).toBe(10); // durations
2053
+ expect(call[3]).toBe(1); // commit start time (before mutations or effects)
2054
+ expect(call[4]).toEqual(
2055
+ enableSchedulerTracing ? new Set() : undefined,
2056
+ ); // interaction events
2057
+
2058
+ call = callback.mock.calls[1];
2059
+
2060
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2061
+ expect(call[0]).toBe('mount-test');
2062
+ expect(call[1]).toBe('update');
2063
+ expect(call[2]).toBe(130); // durations
2064
+ expect(call[3]).toBe(11); // commit start time (before mutations or effects)
2065
+ expect(call[4]).toEqual(
2066
+ enableSchedulerTracing ? new Set() : undefined,
2067
+ ); // interaction events
2068
});
2069
812
- it('should report the expected times when a high-priority update replaces a low-priority update', () => {
2070
+ it('should bubble time spent in effects to higher profilers', () => {
2071
const callback = jest.fn();
2072
815
- const Yield = ({renderTime}) => {
816
- Scheduler.unstable_advanceTime(renderTime);
817
- Scheduler.unstable_yieldValue('Yield:' + renderTime);
2073
+ const ComponetWithEffects = ({
2074
+ cleanupDuration,
2075
+ duration,
2076
+ setCountRef,
2077
+ }) => {
2078
+ const setCount = React.useState(0)[1];
2079
+ if (setCountRef != null) {
2080
+ setCountRef.current = setCount;
2081
+ }
2082
+ React.useEffect(() => {
2083
+ Scheduler.unstable_advanceTime(duration);
2084
+ return () => {
2085
+ Scheduler.unstable_advanceTime(cleanupDuration);
2086
+ };
2087
+ });
2088
+ Scheduler.unstable_advanceTime(1);
2089
return null;
2090
};
2091
821
- Scheduler.unstable_advanceTime(5); // 0 -> 5
2092
+ const setCountRef = React.createRef(null);
2093
823
- const renderer = ReactTestRenderer.create(
824
- <React.Profiler id="test" onRender={callback}>
825
- <Yield renderTime={6} />
826
- <Yield renderTime={15} />
827
- </React.Profiler>,
828
- {unstable_isConcurrent: true},
829
- );
2094
+ let renderer = null;
2095
+ ReactTestRenderer.act(() => {
2096
+ renderer = ReactTestRenderer.create(
2097
+ <React.Profiler id="root-mount" onPostCommit={callback}>
2098
+ <React.Profiler id="a">
2099
+ <ComponetWithEffects
2100
+ duration={10}
2101
+ cleanupDuration={100}
2102
+ setCountRef={setCountRef}
2103
+ />
2104
+ </React.Profiler>
2105
+ <React.Profiler id="b">
2106
+ <ComponetWithEffects
2107
+ duration={1000}
2108
+ cleanupDuration={10000}
2109
+ />
2110
+ </React.Profiler>
2111
+ </React.Profiler>,
2112
+ );
2113
+ });
2114
831
- // Render everything initially.
832
- // This should take 21 seconds of actual and base time.
833
- expect(Scheduler).toFlushAndYield(['Yield:6', 'Yield:15']);
2115
expect(callback).toHaveBeenCalledTimes(1);
835
- let call = callback.mock.calls[0];
836
- expect(call[2]).toBe(21); // actual time
837
- expect(call[3]).toBe(21); // base time
838
- expect(call[4]).toBe(5); // start time
839
- expect(call[5]).toBe(26); // commit time
2116
841
- callback.mockReset();
2117
+ let call = callback.mock.calls[0];
2118
843
- Scheduler.unstable_advanceTime(30); // 26 -> 56
2119
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2120
+ expect(call[0]).toBe('root-mount');
2121
+ expect(call[1]).toBe('mount');
2122
+ expect(call[2]).toBe(1010); // durations
2123
+ expect(call[3]).toBe(2); // commit start time (before mutations or effects)
2124
+ expect(call[4]).toEqual(
2125
+ enableSchedulerTracing ? new Set() : undefined,
2126
+ ); // interaction events
2127
845
- // Render a partially update, but don't finish.
846
- // This partial render should take 3ms of simulated time.
847
- renderer.update(
848
- <React.Profiler id="test" onRender={callback}>
849
- <Yield renderTime={3} />
850
- <Yield renderTime={5} />
851
- <Yield renderTime={9} />
852
- </React.Profiler>,
853
- );
854
- expect(Scheduler).toFlushAndYieldThrough(['Yield:3']);
855
- expect(callback).toHaveBeenCalledTimes(0);
2128
+ ReactTestRenderer.act(() => setCountRef.current(count => count + 1));
2129
857
- // Simulate time moving forward while frame is paused.
858
- Scheduler.unstable_advanceTime(100); // 59 -> 159
2130
+ expect(callback).toHaveBeenCalledTimes(2);
2131
860
- // Render another 5ms of simulated time.
861
- expect(Scheduler).toFlushAndYieldThrough(['Yield:5']);
862
- expect(callback).toHaveBeenCalledTimes(0);
2132
+ call = callback.mock.calls[1];
2133
864
- // Simulate time moving forward while frame is paused.
865
- Scheduler.unstable_advanceTime(100); // 164 -> 264
2134
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2135
+ expect(call[0]).toBe('root-mount');
2136
+ expect(call[1]).toBe('update');
2137
+ expect(call[2]).toBe(110); // durations
2138
+ expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
2139
+ expect(call[4]).toEqual(
2140
+ enableSchedulerTracing ? new Set() : undefined,
2141
+ ); // interaction events
2142
867
- // Interrupt with higher priority work.
868
- // The interrupted work simulates an additional 11ms of time.
869
- renderer.unstable_flushSync(() => {
2143
+ ReactTestRenderer.act(() => {
2144
renderer.update(
871
- <React.Profiler id="test" onRender={callback}>
872
- <Yield renderTime={11} />
2145
+ <React.Profiler id="root-update" onPostCommit={callback}>
2146
+ <React.Profiler id="b">
2147
+ <ComponetWithEffects
2148
+ duration={1000}
2149
+ cleanupDuration={10000}
2150
+ />
2151
+ </React.Profiler>
2152
</React.Profiler>,
2153
);
2154
});
876
- expect(Scheduler).toHaveYielded(['Yield:11']);
2155
878
- // The actual time should include only the most recent render,
879
- // Because this lets us avoid a lot of commit phase reset complexity.
880
- // The base time includes only the final rendered tree times.
881
- expect(callback).toHaveBeenCalledTimes(1);
882
- call = callback.mock.calls[0];
883
- expect(call[2]).toBe(11); // actual time
884
- expect(call[3]).toBe(11); // base time
885
- expect(call[4]).toBe(264); // start time
886
- expect(call[5]).toBe(275); // commit time
2156
+ expect(callback).toHaveBeenCalledTimes(3);
2157
888
- // Verify no more unexpected callbacks from low priority work
889
- expect(Scheduler).toFlushAndYield([]);
890
- expect(callback).toHaveBeenCalledTimes(1);
2158
+ call = callback.mock.calls[2];
2159
+
2160
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2161
+ expect(call[0]).toBe('root-update');
2162
+ expect(call[1]).toBe('update');
2163
+ expect(call[2]).toBe(1100); // durations
2164
+ expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
2165
+ expect(call[4]).toEqual(
2166
+ enableSchedulerTracing ? new Set() : undefined,
2167
+ ); // interaction events
2168
});
2169
893
- it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
2170
+ it('should properly report time in passive effects even when there are errors', () => {
2171
const callback = jest.fn();
2172
896
- const Yield = ({renderTime}) => {
897
- Scheduler.unstable_advanceTime(renderTime);
898
- Scheduler.unstable_yieldValue('Yield:' + renderTime);
899
- return null;
900
- };
901
-
902
- let first;
903
- class FirstComponent extends React.Component {
904
- state = {renderTime: 1};
905
- render() {
906
- first = this;
907
- Scheduler.unstable_advanceTime(this.state.renderTime);
908
- Scheduler.unstable_yieldValue(
909
- 'FirstComponent:' + this.state.renderTime,
910
- );
911
- return <Yield renderTime={4} />;
2173
+ class ErrorBoundary extends React.Component {
2174
+ state = {error: null};
2175
+ static getDerivedStateFromError(error) {
2176
+ return {error};
2177
}
913
- }
914
- let second;
915
- class SecondComponent extends React.Component {
916
- state = {renderTime: 2};
2178
render() {
918
- second = this;
919
- Scheduler.unstable_advanceTime(this.state.renderTime);
920
- Scheduler.unstable_yieldValue(
921
- 'SecondComponent:' + this.state.renderTime,
922
- );
923
- return <Yield renderTime={7} />;
2179
+ return this.state.error === null
2180
+ ? this.props.children
2181
+ : this.props.fallback;
2182
}
2183
}
2184
927
- Scheduler.unstable_advanceTime(5); // 0 -> 5
928
-
929
- const renderer = ReactTestRenderer.create(
930
- <React.Profiler id="test" onRender={callback}>
931
- <FirstComponent />
932
- <SecondComponent />
933
- </React.Profiler>,
934
- {unstable_isConcurrent: true},
935
- );
936
-
937
- // Render everything initially.
938
- // This simulates a total of 14ms of actual render time.
939
- // The base render time is also 14ms for the initial render.
940
- expect(Scheduler).toFlushAndYield([
941
- 'FirstComponent:1',
942
- 'Yield:4',
943
- 'SecondComponent:2',
944
- 'Yield:7',
945
- ]);
946
- expect(callback).toHaveBeenCalledTimes(1);
947
- let call = callback.mock.calls[0];
948
- expect(call[2]).toBe(14); // actual time
949
- expect(call[3]).toBe(14); // base time
950
- expect(call[4]).toBe(5); // start time
951
- expect(call[5]).toBe(19); // commit time
952
-
953
- callback.mockClear();
954
-
955
- Scheduler.unstable_advanceTime(100); // 19 -> 119
956
-
957
- // Render a partially update, but don't finish.
958
- // This partial render will take 10ms of actual render time.
959
- first.setState({renderTime: 10});
960
- expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']);
961
- expect(callback).toHaveBeenCalledTimes(0);
2185
+ const ComponetWithEffects = ({
2186
+ cleanupDuration,
2187
+ duration,
2188
+ effectDuration,
2189
+ shouldThrow,
2190
+ }) => {
2191
+ React.useEffect(() => {
2192
+ Scheduler.unstable_advanceTime(effectDuration);
2193
+ if (shouldThrow) {
2194
+ throw Error('expected');
2195
+ }
2196
+ return () => {
2197
+ Scheduler.unstable_advanceTime(cleanupDuration);
2198
+ };
2199
+ });
2200
+ Scheduler.unstable_advanceTime(duration);
2201
+ return null;
2202
+ };
2203
963
- // Simulate time moving forward while frame is paused.
964
- Scheduler.unstable_advanceTime(100); // 129 -> 229
2204
+ Scheduler.unstable_advanceTime(1);
2205
966
- // Interrupt with higher priority work.
967
- // This simulates a total of 37ms of actual render time.
968
- renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
969
- expect(Scheduler).toHaveYielded(['SecondComponent:30', 'Yield:7']);
2206
+ // Test an error that happens during an effect
2207
971
- // The actual time should include only the most recent render (37ms),
972
- // Because this greatly simplifies the commit phase logic.
973
- // The base time should include the more recent times for the SecondComponent subtree,
974
- // As well as the original times for the FirstComponent subtree.
975
- expect(callback).toHaveBeenCalledTimes(1);
976
- call = callback.mock.calls[0];
977
- expect(call[2]).toBe(37); // actual time
978
- expect(call[3]).toBe(42); // base time
979
- expect(call[4]).toBe(229); // start time
980
- expect(call[5]).toBe(266); // commit time
2208
+ ReactTestRenderer.act(() => {
2209
+ ReactTestRenderer.create(
2210
+ <React.Profiler id="root" onPostCommit={callback}>
2211
+ <ErrorBoundary
2212
+ fallback={
2213
+ <ComponetWithEffects
2214
+ duration={10000000}
2215
+ effectDuration={100000000}
2216
+ cleanupDuration={1000000000}
2217
+ />
2218
+ }>
2219
+ <ComponetWithEffects
2220
+ duration={10}
2221
+ effectDuration={100}
2222
+ cleanupDuration={1000}
2223
+ shouldThrow={true}
2224
+ />
2225
+ </ErrorBoundary>
2226
+ <ComponetWithEffects
2227
+ duration={10000}
2228
+ effectDuration={100000}
2229
+ cleanupDuration={1000000}
2230
+ />
2231
+ </React.Profiler>,
2232
+ );
2233
+ });
2234
982
- callback.mockClear();
2235
+ expect(callback).toHaveBeenCalledTimes(2);
2236
984
- // Simulate time moving forward while frame is paused.
985
- Scheduler.unstable_advanceTime(100); // 266 -> 366
2237
+ let call = callback.mock.calls[0];
2238
987
- // Resume the original low priority update, with rebased state.
988
- // This simulates a total of 14ms of actual render time,
989
- // And does not include the original (interrupted) 10ms.
990
- // The tree contains 42ms of base render time at this point,
991
- // Reflecting the most recent (longer) render durations.
992
- // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
993
- expect(Scheduler).toFlushAndYield(['FirstComponent:10', 'Yield:4']);
994
- expect(callback).toHaveBeenCalledTimes(1);
995
- call = callback.mock.calls[0];
996
- expect(call[2]).toBe(14); // actual time
997
- expect(call[3]).toBe(51); // base time
998
- expect(call[4]).toBe(366); // start time
999
- expect(call[5]).toBe(380); // commit time
2239
+ // Initial render (with error)
2240
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2241
+ expect(call[0]).toBe('root');
2242
+ expect(call[1]).toBe('mount');
2243
+ expect(call[2]).toBe(100100); // durations
2244
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
2245
+ expect(call[4]).toEqual(
2246
+ enableSchedulerTracing ? new Set() : undefined,
2247
+ ); // interaction events
2248
+
2249
+ call = callback.mock.calls[1];
2250
+
2251
+ // Cleanup render from error boundary
2252
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2253
+ expect(call[0]).toBe('root');
2254
+ expect(call[1]).toBe('update');
2255
+ expect(call[2]).toBe(100000000); // durations
2256
+ expect(call[3]).toBe(10110111); // commit start time (before mutations or effects)
2257
+ expect(call[4]).toEqual(
2258
+ enableSchedulerTracing ? new Set() : undefined,
2259
+ ); // interaction events
2260
});
2261
1002
- [true, false].forEach(
1003
- replayFailedUnitOfWorkWithInvokeGuardedCallback => {
1004
- describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
1005
- replayFailedUnitOfWorkWithInvokeGuardedCallback
1006
- ? 'enabled'
1007
- : 'disabled'
1008
- }`, () => {
1009
- beforeEach(() => {
1010
- jest.resetModules();
1011
-
1012
- loadModules({
1013
- replayFailedUnitOfWorkWithInvokeGuardedCallback,
1014
- });
1015
- });
1016
-
1017
- it('should accumulate actual time after an error handled by componentDidCatch()', () => {
1018
- const callback = jest.fn();
2262
+ it('should properly report time in passive effect cleanup functions even when there are errors', () => {
2263
+ const callback = jest.fn();
2264
1020
- const ThrowsError = () => {
1021
- Scheduler.unstable_advanceTime(3);
1022
- throw Error('expected error');
1023
- };
2265
+ class ErrorBoundary extends React.Component {
2266
+ state = {error: null};
2267
+ static getDerivedStateFromError(error) {
2268
+ return {error};
2269
+ }
2270
+ render() {
2271
+ return this.state.error === null
2272
+ ? this.props.children
2273
+ : this.props.fallback;
2274
+ }
2275
+ }
2276
1025
- class ErrorBoundary extends React.Component {
1026
- state = {error: null};
1027
- componentDidCatch(error) {
1028
- this.setState({error});
1029
- }
1030
- render() {
1031
- Scheduler.unstable_advanceTime(2);
1032
- return this.state.error === null ? (
1033
- this.props.children
1034
- ) : (
1035
- <AdvanceTime byAmount={20} />
1036
- );
1037
- }
2277
+ const ComponetWithEffects = ({
2278
+ cleanupDuration,
2279
+ duration,
2280
+ effectDuration,
2281
+ shouldThrow = false,
2282
+ id,
2283
+ }) => {
2284
+ React.useEffect(() => {
2285
+ Scheduler.unstable_advanceTime(effectDuration);
2286
+ return () => {
2287
+ Scheduler.unstable_advanceTime(cleanupDuration);
2288
+ if (shouldThrow) {
2289
+ throw Error('expected');
2290
}
2291
+ };
2292
+ });
2293
+ Scheduler.unstable_advanceTime(duration);
2294
+ return null;
2295
+ };
2296
1040
- Scheduler.unstable_advanceTime(5); // 0 -> 5
1041
-
1042
- ReactTestRenderer.create(
1043
- <React.Profiler id="test" onRender={callback}>
1044
- <ErrorBoundary>
1045
- <AdvanceTime byAmount={9} />
1046
- <ThrowsError />
1047
- </ErrorBoundary>
1048
- </React.Profiler>,
1049
- );
2297
+ Scheduler.unstable_advanceTime(1);
2298
1051
- expect(callback).toHaveBeenCalledTimes(2);
1052
-
1053
- // Callbacks bubble (reverse order).
1054
- let [mountCall, updateCall] = callback.mock.calls;
1055
-
1056
- // The initial mount only includes the ErrorBoundary (which takes 2)
1057
- // But it spends time rendering all of the failed subtree also.
1058
- expect(mountCall[1]).toBe('mount');
1059
- // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
1060
- // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1061
- expect(mountCall[2]).toBe(14);
1062
- // base time includes: 2 (ErrorBoundary)
1063
- // Since the tree is empty for the initial commit
1064
- expect(mountCall[3]).toBe(2);
1065
- // start time
1066
- expect(mountCall[4]).toBe(5);
1067
- // commit time: 5 initially + 14 of work
1068
- // Add an additional 3 (ThrowsError) if we replayed the failed work
1069
- expect(mountCall[5]).toBe(
1070
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1071
- ? 22
1072
- : 19,
1073
- );
2299
+ let renderer = null;
2300
1075
- // The update includes the ErrorBoundary and its fallback child
1076
- expect(updateCall[1]).toBe('update');
1077
- // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1078
- expect(updateCall[2]).toBe(22);
1079
- // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1080
- expect(updateCall[3]).toBe(22);
1081
- // start time
1082
- expect(updateCall[4]).toBe(
1083
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1084
- ? 22
1085
- : 19,
1086
- );
1087
- // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
1088
- // Add an additional 3 (ThrowsError) if we replayed the failed work
1089
- expect(updateCall[5]).toBe(
1090
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1091
- ? 44
1092
- : 41,
1093
- );
1094
- });
2301
+ ReactTestRenderer.act(() => {
2302
+ renderer = ReactTestRenderer.create(
2303
+ <React.Profiler id="root" onPostCommit={callback}>
2304
+ <ErrorBoundary
2305
+ fallback={
2306
+ <ComponetWithEffects
2307
+ duration={10000000}
2308
+ effectDuration={100000000}
2309
+ cleanupDuration={1000000000}
2310
+ />
2311
+ }>
2312
+ <ComponetWithEffects
2313
+ duration={10}
2314
+ effectDuration={100}
2315
+ cleanupDuration={1000}
2316
+ shouldThrow={true}
2317
+ />
2318
+ </ErrorBoundary>
2319
+ <ComponetWithEffects
2320
+ duration={10000}
2321
+ effectDuration={100000}
2322
+ cleanupDuration={1000000}
2323
+ />
2324
+ </React.Profiler>,
2325
+ );
2326
+ });
2327
1096
- it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => {
1097
- const callback = jest.fn();
2328
+ expect(callback).toHaveBeenCalledTimes(1);
2329
1099
- const ThrowsError = () => {
1100
- Scheduler.unstable_advanceTime(10);
1101
- throw Error('expected error');
1102
- };
2330
+ let call = callback.mock.calls[0];
2331
1104
- class ErrorBoundary extends React.Component {
1105
- state = {error: null};
1106
- static getDerivedStateFromError(error) {
1107
- return {error};
1108
- }
1109
- render() {
1110
- Scheduler.unstable_advanceTime(2);
1111
- return this.state.error === null ? (
1112
- this.props.children
1113
- ) : (
1114
- <AdvanceTime byAmount={20} />
1115
- );
1116
- }
1117
- }
2332
+ // Initial render
2333
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2334
+ expect(call[0]).toBe('root');
2335
+ expect(call[1]).toBe('mount');
2336
+ expect(call[2]).toBe(100100); // durations
2337
+ expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
2338
+ expect(call[4]).toEqual(
2339
+ enableSchedulerTracing ? new Set() : undefined,
2340
+ ); // interaction events
2341
1119
- Scheduler.unstable_advanceTime(5); // 0 -> 5
2342
+ callback.mockClear();
2343
1121
- ReactTestRenderer.create(
1122
- <React.Profiler id="test" onRender={callback}>
1123
- <ErrorBoundary>
1124
- <AdvanceTime byAmount={5} />
1125
- <ThrowsError />
1126
- </ErrorBoundary>
1127
- </React.Profiler>,
1128
- );
2344
+ // Test an error that happens during an cleanup function
2345
1130
- expect(callback).toHaveBeenCalledTimes(1);
1131
-
1132
- // Callbacks bubble (reverse order).
1133
- let [mountCall] = callback.mock.calls;
1134
-
1135
- // The initial mount includes the ErrorBoundary's error state,
1136
- // But it also spends actual time rendering UI that fails and isn't included.
1137
- expect(mountCall[1]).toBe('mount');
1138
- // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
1139
- // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
1140
- // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1141
- expect(mountCall[2]).toBe(39);
1142
- // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1143
- expect(mountCall[3]).toBe(22);
1144
- // start time
1145
- expect(mountCall[4]).toBe(5);
1146
- // commit time
1147
- expect(mountCall[5]).toBe(
1148
- __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1149
- ? 54
1150
- : 44,
1151
- );
1152
- });
2346
+ ReactTestRenderer.act(() => {
2347
+ renderer.update(
2348
+ <React.Profiler id="root" onPostCommit={callback}>
2349
+ <ErrorBoundary
2350
+ fallback={
2351
+ <ComponetWithEffects
2352
+ duration={10000000}
2353
+ effectDuration={100000000}
2354
+ cleanupDuration={1000000000}
2355
+ />
2356
+ }>
2357
+ <ComponetWithEffects
2358
+ duration={10}
2359
+ effectDuration={100}
2360
+ cleanupDuration={1000}
2361
+ shouldThrow={false}
2362
+ />
2363
+ </ErrorBoundary>
2364
+ <ComponetWithEffects
2365
+ duration={10000}
2366
+ effectDuration={100000}
2367
+ cleanupDuration={1000000}
2368
+ />
2369
+ </React.Profiler>,
2370
+ );
2371
+ });
2372
1154
- it('should reset the fiber stack correct after a "complete" phase error', () => {
1155
- jest.resetModules();
2373
+ expect(callback).toHaveBeenCalledTimes(2);
2374
1157
- loadModules({
1158
- useNoopRenderer: true,
1159
- replayFailedUnitOfWorkWithInvokeGuardedCallback,
1160
- });
2375
+ call = callback.mock.calls[0];
2376
1162
- // Simulate a renderer error during the "complete" phase.
1163
- // This mimics behavior like React Native's View/Text nesting validation.
1164
- ReactNoop.render(
1165
- <React.Profiler id="profiler" onRender={jest.fn()}>
1166
- <errorInCompletePhase>hi</errorInCompletePhase>
1167
- </React.Profiler>,
1168
- );
1169
- expect(Scheduler).toFlushAndThrow('Error in host config.');
2377
+ // Update (that throws)
2378
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2379
+ expect(call[0]).toBe('root');
2380
+ expect(call[1]).toBe('update');
2381
+ // The duration varies because the flushing behavior varies when this flag is on.
2382
+ // We continue flushing pending effects even if one throws.
2383
+ expect(call[2]).toBe(
2384
+ deferPassiveEffectCleanupDuringUnmount ? 1101100 : 1101000,
2385
+ ); // durations
2386
+ expect(call[3]).toBe(120121); // commit start time (before mutations or effects)
2387
+ expect(call[4]).toEqual(
2388
+ enableSchedulerTracing ? new Set() : undefined,
2389
+ ); // interaction events
2390
+
2391
+ call = callback.mock.calls[1];
2392
+
2393
+ // Cleanup render from error boundary
2394
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2395
+ expect(call[0]).toBe('root');
2396
+ expect(call[1]).toBe('update');
2397
+ expect(call[2]).toBe(100000000); // durations
2398
+ // The commit time varies because the above duration time varies
2399
+ expect(call[3]).toBe(
2400
+ deferPassiveEffectCleanupDuringUnmount ? 11221221 : 11221121,
2401
+ ); // commit start time (before mutations or effects)
2402
+ expect(call[4]).toEqual(
2403
+ enableSchedulerTracing ? new Set() : undefined,
2404
+ ); // interaction events
2405
+ });
2406
1171
- // A similar case we've seen caused by an invariant in ReactDOM.
1172
- // It didn't reproduce without a host component inside.
1173
- ReactNoop.render(
1174
- <React.Profiler id="profiler" onRender={jest.fn()}>
1175
- <errorInCompletePhase>
1176
- <span>hi</span>
1177
- </errorInCompletePhase>
1178
- </React.Profiler>,
1179
- );
1180
- expect(Scheduler).toFlushAndThrow('Error in host config.');
2407
+ if (enableSchedulerTracing) {
2408
+ it('should report interactions that were active', () => {
2409
+ const callback = jest.fn();
2410
1182
- // So long as the profiler timer's fiber stack is reset correctly,
1183
- // Subsequent renders should not error.
1184
- ReactNoop.render(
1185
- <React.Profiler id="profiler" onRender={jest.fn()}>
1186
- <span>hi</span>
1187
- </React.Profiler>,
1188
- );
1189
- expect(Scheduler).toFlushWithoutYielding();
1190
- });
2411
+ const ComponetWithEffects = () => {
2412
+ const [didMount, setDidMount] = React.useState(false);
2413
+ React.useEffect(() => {
2414
+ Scheduler.unstable_advanceTime(didMount ? 1000 : 100);
2415
+ if (!didMount) {
2416
+ setDidMount(true);
2417
+ }
2418
+ return () => {
2419
+ Scheduler.unstable_advanceTime(10000);
2420
+ };
2421
+ }, [didMount]);
2422
+ Scheduler.unstable_advanceTime(10);
2423
+ return null;
2424
+ };
2425
+
2426
+ const interaction = {
2427
+ id: 0,
2428
+ name: 'mount',
2429
+ timestamp: Scheduler.unstable_now(),
2430
+ };
2431
+
2432
+ Scheduler.unstable_advanceTime(1);
2433
+
2434
+ ReactTestRenderer.act(() => {
2435
+ SchedulerTracing.unstable_trace(
2436
+ interaction.name,
2437
+ interaction.timestamp,
2438
+ () => {
2439
+ ReactTestRenderer.create(
2440
+ <React.Profiler id="root" onPostCommit={callback}>
2441
+ <ComponetWithEffects />
2442
+ </React.Profiler>,
2443
+ );
2444
+ },
2445
+ );
2446
});
1192
- },
1193
- );
1194
- });
1195
-
1196
- it('reflects the most recently rendered id value', () => {
1197
- const callback = jest.fn();
1198
-
1199
- Scheduler.unstable_advanceTime(5); // 0 -> 5
1200
-
1201
- const renderer = ReactTestRenderer.create(
1202
- <React.Profiler id="one" onRender={callback}>
1203
- <AdvanceTime byAmount={2} />
1204
- </React.Profiler>,
1205
- );
1206
-
1207
- expect(callback).toHaveBeenCalledTimes(1);
2447
1209
- Scheduler.unstable_advanceTime(20); // 7 -> 27
2448
+ expect(callback).toHaveBeenCalledTimes(2);
2449
1211
- renderer.update(
1212
- <React.Profiler id="two" onRender={callback}>
1213
- <AdvanceTime byAmount={1} />
1214
- </React.Profiler>,
1215
- );
1216
-
1217
- expect(callback).toHaveBeenCalledTimes(2);
1218
-
1219
- const [mountCall, updateCall] = callback.mock.calls;
2450
+ let call = callback.mock.calls[0];
2451
1221
- expect(mountCall[0]).toBe('one');
1222
- expect(mountCall[1]).toBe('mount');
1223
- expect(mountCall[2]).toBe(2); // actual time
1224
- expect(mountCall[3]).toBe(2); // base time
1225
- expect(mountCall[4]).toBe(5); // start time
2452
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2453
+ expect(call[0]).toBe('root');
2454
+ expect(call[1]).toBe('mount');
2455
+ expect(call[4]).toMatchInteractions([interaction]);
2456
1227
- expect(updateCall[0]).toBe('two');
1228
- expect(updateCall[1]).toBe('update');
1229
- expect(updateCall[2]).toBe(1); // actual time
1230
- expect(updateCall[3]).toBe(1); // base time
1231
- expect(updateCall[4]).toBe(27); // start time
1232
- });
1233
-
1234
- it('should not be called until after mutations', () => {
1235
- let classComponentMounted = false;
1236
- const callback = jest.fn(
1237
- (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
1238
- // Don't call this hook until after mutations
1239
- expect(classComponentMounted).toBe(true);
1240
- // But the commit time should reflect pre-mutation
1241
- expect(commitTime).toBe(2);
1242
- },
1243
- );
2457
+ call = callback.mock.calls[1];
2458
1245
- class ClassComponent extends React.Component {
1246
- componentDidMount() {
1247
- Scheduler.unstable_advanceTime(5);
1248
- classComponentMounted = true;
1249
- }
1250
- render() {
1251
- Scheduler.unstable_advanceTime(2);
1252
- return null;
1253
- }
2459
+ expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4);
2460
+ expect(call[0]).toBe('root');
2461
+ expect(call[1]).toBe('update');
2462
+ expect(call[4]).toMatchInteractions([interaction]);
2463
+ });
2464
}
1255
-
1256
- ReactTestRenderer.create(
1257
- <React.Profiler id="test" onRender={callback}>
1258
- <ClassComponent />
1259
- </React.Profiler>,
1260
- );
1261
-
1262
- expect(callback).toHaveBeenCalledTimes(1);
2465
});
2466
});
2467
});
@@ -1540,14 +2742,16 @@ describe('Profiler', () => {
2742
timestamp: Scheduler.unstable_now(),
2743
};
2744
1543
- const onRender = jest.fn();
2745
+ const onPostCommit = jest.fn(() => {
2746
+ Scheduler.unstable_yieldValue('onPostCommit');
2747
+ });
2748
let renderer;
2749
SchedulerTracing.unstable_trace(
2750
interactionCreation.name,
2751
Scheduler.unstable_now(),
2752
() => {
2753
renderer = ReactTestRenderer.create(
1550
- <React.Profiler id="test-profiler" onRender={onRender}>
2754
+ <React.Profiler id="test-profiler" onPostCommit={onPostCommit}>
2755
<Example />
2756
</React.Profiler>,
2757
{
@@ -1578,13 +2782,13 @@ describe('Profiler', () => {
2782
expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
2783
2784
// Mount
1581
- expect(Scheduler).toFlushAndYield(['first', 'last']);
1582
- expect(onRender).toHaveBeenCalledTimes(1);
1583
- let call = onRender.mock.calls[0];
2785
+ expect(Scheduler).toFlushAndYield(['first', 'last', 'onPostCommit']);
2786
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
2787
+ let call = onPostCommit.mock.calls[0];
2788
expect(call[0]).toEqual('test-profiler');
1585
- expect(call[5]).toEqual(Scheduler.unstable_now());
2789
+ expect(call[3]).toEqual(Scheduler.unstable_now());
2790
if (ReactFeatureFlags.enableSchedulerTracing) {
1587
- expect(call[6]).toMatchInteractions([interactionCreation]);
2791
+ expect(call[4]).toMatchInteractions([interactionCreation]);
2792
}
2793
2794
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -1601,7 +2805,7 @@ describe('Profiler', () => {
2805
interactionCreation,
2806
]);
2807
1604
- onRender.mockClear();
2808
+ onPostCommit.mockClear();
2809
onWorkScheduled.mockClear();
2810
onWorkStarted.mockClear();
2811
onWorkStopped.mockClear();
@@ -1639,7 +2843,7 @@ describe('Profiler', () => {
2843
expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
2844
2845
expect(Scheduler).toFlushAndYieldThrough(['first']);
1642
- expect(onRender).not.toHaveBeenCalled();
2846
+ expect(onPostCommit).not.toHaveBeenCalled();
2847
2848
expect(onInteractionTraced).toHaveBeenCalledTimes(2);
2849
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
@@ -1652,14 +2856,14 @@ describe('Profiler', () => {
2856
).toMatchInteractions([interactionOne]);
2857
expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
2858
1655
- expect(Scheduler).toFlushAndYield(['last']);
1656
- expect(onRender).toHaveBeenCalledTimes(1);
2859
+ expect(Scheduler).toFlushAndYield(['last', 'onPostCommit']);
2860
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
2861
1658
- call = onRender.mock.calls[0];
2862
+ call = onPostCommit.mock.calls[0];
2863
expect(call[0]).toEqual('test-profiler');
1660
- expect(call[5]).toEqual(Scheduler.unstable_now());
2864
+ expect(call[3]).toEqual(Scheduler.unstable_now());
2865
if (ReactFeatureFlags.enableSchedulerTracing) {
1662
- expect(call[6]).toMatchInteractions([interactionOne]);
2866
+ expect(call[4]).toMatchInteractions([interactionOne]);
2867
}
2868
2869
didRunCallback = true;
@@ -1677,7 +2881,7 @@ describe('Profiler', () => {
2881
2882
expect(didRunCallback).toBe(true);
2883
1680
- onRender.mockClear();
2884
+ onPostCommit.mockClear();
2885
onWorkScheduled.mockClear();
2886
onWorkStarted.mockClear();
2887
onWorkStopped.mockClear();
@@ -1686,14 +2890,14 @@ describe('Profiler', () => {
2890
2891
// Verify that updating state again does not re-log our interaction.
2892
instance.setState({count: 3});
1689
- expect(Scheduler).toFlushAndYield(['first', 'last']);
2893
+ expect(Scheduler).toFlushAndYield(['first', 'last', 'onPostCommit']);
2894
1691
- expect(onRender).toHaveBeenCalledTimes(1);
1692
- call = onRender.mock.calls[0];
2895
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
2896
+ call = onPostCommit.mock.calls[0];
2897
expect(call[0]).toEqual('test-profiler');
1694
- expect(call[5]).toEqual(Scheduler.unstable_now());
2898
+ expect(call[3]).toEqual(Scheduler.unstable_now());
2899
if (ReactFeatureFlags.enableSchedulerTracing) {
1696
- expect(call[6]).toMatchInteractions([]);
2900
+ expect(call[4]).toMatchInteractions([]);
2901
}
2902
2903
expect(onInteractionTraced).toHaveBeenCalledTimes(2);
@@ -1704,7 +2908,7 @@ describe('Profiler', () => {
2908
expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
2909
expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
2910
1707
- onRender.mockClear();
2911
+ onPostCommit.mockClear();
2912
2913
Scheduler.unstable_advanceTime(3);
2914
@@ -1719,7 +2923,7 @@ describe('Profiler', () => {
2923
Scheduler.unstable_now(),
2924
() => {
2925
renderer.update(
1722
- <React.Profiler id="test-profiler" onRender={onRender}>
2926
+ <React.Profiler id="test-profiler" onPostCommit={onPostCommit}>
2927
<Example />
2928
</React.Profiler>,
2929
);
@@ -1746,14 +2950,14 @@ describe('Profiler', () => {
2950
]);
2951
expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
2952
1749
- expect(Scheduler).toFlushAndYield(['first', 'last']);
2953
+ expect(Scheduler).toFlushAndYield(['first', 'last', 'onPostCommit']);
2954
1751
- expect(onRender).toHaveBeenCalledTimes(1);
1752
- call = onRender.mock.calls[0];
2955
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
2956
+ call = onPostCommit.mock.calls[0];
2957
expect(call[0]).toEqual('test-profiler');
1754
- expect(call[5]).toEqual(Scheduler.unstable_now());
2958
+ expect(call[3]).toEqual(Scheduler.unstable_now());
2959
if (ReactFeatureFlags.enableSchedulerTracing) {
1756
- expect(call[6]).toMatchInteractions([interactionTwo]);
2960
+ expect(call[4]).toMatchInteractions([interactionTwo]);
2961
}
2962
2963
expect(onInteractionTraced).toHaveBeenCalledTimes(3);
@@ -1771,8 +2975,51 @@ describe('Profiler', () => {
2975
]);
2976
});
2977
2978
+ it('should not mark an interaction complete while passive effects are outstanding', () => {
2979
+ const onCommit = jest.fn();
2980
+ const onPostCommit = jest.fn(() => {
2981
+ Scheduler.unstable_yieldValue('onPostCommit');
2982
+ });
2983
+
2984
+ const ComponetWithEffects = () => {
2985
+ React.useEffect(() => {
2986
+ Scheduler.unstable_yieldValue('passive effect');
2987
+ });
2988
+ React.useLayoutEffect(() => {
2989
+ Scheduler.unstable_yieldValue('layout effect');
2990
+ });
2991
+ Scheduler.unstable_yieldValue('render');
2992
+ return null;
2993
+ };
2994
+
2995
+ SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => {
2996
+ ReactTestRenderer.create(
2997
+ <React.Profiler
2998
+ id="root"
2999
+ onCommit={onCommit}
3000
+ onPostCommit={onPostCommit}>
3001
+ <ComponetWithEffects />
3002
+ </React.Profiler>,
3003
+ );
3004
+ });
3005
+
3006
+ expect(Scheduler).toHaveYielded(['render', 'layout effect']);
3007
+
3008
+ expect(onCommit).toHaveBeenCalled();
3009
+ expect(onPostCommit).not.toHaveBeenCalled();
3010
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
3011
+
3012
+ expect(Scheduler).toFlushAndYield(['passive effect', 'onPostCommit']);
3013
+
3014
+ expect(onCommit).toHaveBeenCalled();
3015
+ expect(onPostCommit).toHaveBeenCalled();
3016
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
3017
+ });
3018
+
3019
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
1775
- const onRender = jest.fn();
3020
+ const onPostCommit = jest.fn(() => {
3021
+ Scheduler.unstable_yieldValue('onPostCommit');
3022
+ });
3023
3024
let first;
3025
class FirstComponent extends React.Component {
@@ -1796,7 +3043,7 @@ describe('Profiler', () => {
3043
Scheduler.unstable_advanceTime(5);
3044
3045
const renderer = ReactTestRenderer.create(
1799
- <React.Profiler id="test" onRender={onRender}>
3046
+ <React.Profiler id="test" onPostCommit={onPostCommit}>
3047
<FirstComponent />
3048
<SecondComponent />
3049
</React.Profiler>,
@@ -1804,12 +3051,16 @@ describe('Profiler', () => {
3051
);
3052
3053
// Initial mount.
1807
- expect(Scheduler).toFlushAndYield(['FirstComponent', 'SecondComponent']);
3054
+ expect(Scheduler).toFlushAndYield([
3055
+ 'FirstComponent',
3056
+ 'SecondComponent',
3057
+ 'onPostCommit',
3058
+ ]);
3059
3060
expect(onInteractionTraced).not.toHaveBeenCalled();
3061
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
3062
1812
- onRender.mockClear();
3063
+ onPostCommit.mockClear();
3064
3065
Scheduler.unstable_advanceTime(100);
3066
@@ -1832,7 +3083,7 @@ describe('Profiler', () => {
3083
]);
3084
3085
expect(Scheduler).toFlushAndYieldThrough(['FirstComponent']);
1835
- expect(onRender).not.toHaveBeenCalled();
3086
+ expect(onPostCommit).not.toHaveBeenCalled();
3087
3088
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
3089
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
@@ -1875,7 +3126,13 @@ describe('Profiler', () => {
3126
},
3127
);
3128
});
3129
+
3130
+ // Profiler tag causes passive effects to be scheduled,
3131
+ // so the interactions are still not completed.
3132
expect(Scheduler).toHaveYielded(['SecondComponent']);
3133
+ expect(onInteractionTraced).toHaveBeenCalledTimes(2);
3134
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
3135
+ expect(Scheduler).toFlushAndYieldThrough(['onPostCommit']);
3136
3137
expect(onInteractionTraced).toHaveBeenCalledTimes(2);
3138
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
@@ -1884,28 +3141,28 @@ describe('Profiler', () => {
3141
).toHaveBeenLastNotifiedOfInteraction(interactionHighPri);
3142
3143
// Verify the high priority update was associated with the high priority event.
1887
- expect(onRender).toHaveBeenCalledTimes(1);
1888
- let call = onRender.mock.calls[0];
3144
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
3145
+ let call = onPostCommit.mock.calls[0];
3146
expect(call[0]).toEqual('test');
1890
- expect(call[5]).toEqual(Scheduler.unstable_now());
1891
- expect(call[6]).toMatchInteractions(
3147
+ expect(call[3]).toEqual(Scheduler.unstable_now());
3148
+ expect(call[4]).toMatchInteractions(
3149
ReactFeatureFlags.enableSchedulerTracing
3150
? [interactionLowPri, interactionHighPri]
3151
: [],
3152
);
3153
1897
- onRender.mockClear();
3154
+ onPostCommit.mockClear();
3155
3156
Scheduler.unstable_advanceTime(100);
3157
3158
// Resume the original low priority update, with rebased state.
3159
// Verify the low priority update was retained.
1903
- expect(Scheduler).toFlushAndYield(['FirstComponent']);
1904
- expect(onRender).toHaveBeenCalledTimes(1);
1905
- call = onRender.mock.calls[0];
3160
+ expect(Scheduler).toFlushAndYield(['FirstComponent', 'onPostCommit']);
3161
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
3162
+ call = onPostCommit.mock.calls[0];
3163
expect(call[0]).toEqual('test');
1907
- expect(call[5]).toEqual(Scheduler.unstable_now());
1908
- expect(call[6]).toMatchInteractions(
3164
+ expect(call[3]).toEqual(Scheduler.unstable_now());
3165
+ expect(call[4]).toMatchInteractions(
3166
ReactFeatureFlags.enableSchedulerTracing ? [interactionLowPri] : [],
3167
);
3168
@@ -1970,14 +3227,16 @@ describe('Profiler', () => {
3227
};
3228
3229
// Initial mount.
1973
- const onRender = jest.fn();
3230
+ const onPostCommit = jest.fn(() => {
3231
+ Scheduler.unstable_yieldValue('onPostCommit');
3232
+ });
3233
let firstCommitTime = Scheduler.unstable_now();
3234
SchedulerTracing.unstable_trace(
3235
interactionOne.name,
3236
Scheduler.unstable_now(),
3237
() => {
3238
ReactTestRenderer.create(
1980
- <React.Profiler id="test" onRender={onRender}>
3239
+ <React.Profiler id="test" onPostCommit={onPostCommit}>
3240
<Example />
3241
</React.Profiler>,
3242
{unstable_isConcurrent: true},
@@ -1993,7 +3252,12 @@ describe('Profiler', () => {
3252
expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
3253
expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
3254
1996
- expect(Scheduler).toFlushAndYield(['Example:0', 'Example:1']);
3255
+ expect(Scheduler).toFlushAndYield([
3256
+ 'Example:0',
3257
+ 'onPostCommit',
3258
+ 'Example:1',
3259
+ 'onPostCommit',
3260
+ ]);
3261
3262
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
3263
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
@@ -2015,21 +3279,21 @@ describe('Profiler', () => {
3279
interactionOne,
3280
]);
3281
2018
- expect(onRender).toHaveBeenCalledTimes(2);
2019
- let call = onRender.mock.calls[0];
3282
+ expect(onPostCommit).toHaveBeenCalledTimes(2);
3283
+ let call = onPostCommit.mock.calls[0];
3284
expect(call[0]).toEqual('test');
2021
- expect(call[5]).toEqual(firstCommitTime);
2022
- expect(call[6]).toMatchInteractions(
3285
+ expect(call[3]).toEqual(firstCommitTime);
3286
+ expect(call[4]).toMatchInteractions(
3287
ReactFeatureFlags.enableSchedulerTracing ? [interactionOne] : [],
3288
);
2025
- call = onRender.mock.calls[1];
3289
+ call = onPostCommit.mock.calls[1];
3290
expect(call[0]).toEqual('test');
2027
- expect(call[5]).toEqual(Scheduler.unstable_now());
2028
- expect(call[6]).toMatchInteractions(
3291
+ expect(call[3]).toEqual(Scheduler.unstable_now());
3292
+ expect(call[4]).toMatchInteractions(
3293
ReactFeatureFlags.enableSchedulerTracing ? [interactionOne] : [],
3294
);
3295
2032
- onRender.mockClear();
3296
+ onPostCommit.mockClear();
3297
3298
const interactionTwo = {
3299
id: 1,
@@ -2045,7 +3309,7 @@ describe('Profiler', () => {
3309
instance.setState({count: 2});
3310
},
3311
);
2048
- expect(onRender).not.toHaveBeenCalled();
3312
+ expect(onPostCommit).not.toHaveBeenCalled();
3313
expect(onInteractionTraced).toHaveBeenCalledTimes(2);
3314
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
3315
interactionTwo,
@@ -2059,7 +3323,12 @@ describe('Profiler', () => {
3323
// Flush async work (outside of traced scope)
3324
// This will cause an intentional cascading update from did-update
3325
firstCommitTime = Scheduler.unstable_now();
2062
- expect(Scheduler).toFlushAndYield(['Example:2', 'Example:3']);
3326
+ expect(Scheduler).toFlushAndYield([
3327
+ 'Example:2',
3328
+ 'onPostCommit',
3329
+ 'Example:3',
3330
+ 'onPostCommit',
3331
+ ]);
3332
3333
expect(onInteractionTraced).toHaveBeenCalledTimes(2);
3334
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
@@ -2082,21 +3351,21 @@ describe('Profiler', () => {
3351
]);
3352
3353
// Verify the cascading commit is associated with the origin event
2085
- expect(onRender).toHaveBeenCalledTimes(2);
2086
- call = onRender.mock.calls[0];
3354
+ expect(onPostCommit).toHaveBeenCalledTimes(2);
3355
+ call = onPostCommit.mock.calls[0];
3356
expect(call[0]).toEqual('test');
2088
- expect(call[5]).toEqual(firstCommitTime);
2089
- expect(call[6]).toMatchInteractions(
3357
+ expect(call[3]).toEqual(firstCommitTime);
3358
+ expect(call[4]).toMatchInteractions(
3359
ReactFeatureFlags.enableSchedulerTracing ? [interactionTwo] : [],
3360
);
2092
- call = onRender.mock.calls[1];
3361
+ call = onPostCommit.mock.calls[1];
3362
expect(call[0]).toEqual('test');
2094
- expect(call[5]).toEqual(Scheduler.unstable_now());
2095
- expect(call[6]).toMatchInteractions(
3363
+ expect(call[3]).toEqual(Scheduler.unstable_now());
3364
+ expect(call[4]).toMatchInteractions(
3365
ReactFeatureFlags.enableSchedulerTracing ? [interactionTwo] : [],
3366
);
3367
2099
- onRender.mockClear();
3368
+ onPostCommit.mockClear();
3369
3370
const interactionThree = {
3371
id: 2,
@@ -2115,7 +3384,7 @@ describe('Profiler', () => {
3384
instance.setState({count: 5}, callback);
3385
},
3386
);
2118
- expect(onRender).not.toHaveBeenCalled();
3387
+ expect(onPostCommit).not.toHaveBeenCalled();
3388
3389
expect(onInteractionTraced).toHaveBeenCalledTimes(3);
3390
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
@@ -2128,7 +3397,12 @@ describe('Profiler', () => {
3397
// Flush async work (outside of traced scope)
3398
// This will cause an intentional cascading update from the setState callback
3399
firstCommitTime = Scheduler.unstable_now();
2131
- expect(Scheduler).toFlushAndYield(['Example:5', 'Example:6']);
3400
+ expect(Scheduler).toFlushAndYield([
3401
+ 'Example:5',
3402
+ 'onPostCommit',
3403
+ 'Example:6',
3404
+ 'onPostCommit',
3405
+ ]);
3406
3407
expect(onInteractionTraced).toHaveBeenCalledTimes(3);
3408
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3);
@@ -2151,23 +3425,25 @@ describe('Profiler', () => {
3425
]);
3426
3427
// Verify the cascading commit is associated with the origin event
2154
- expect(onRender).toHaveBeenCalledTimes(2);
2155
- call = onRender.mock.calls[0];
3428
+ expect(onPostCommit).toHaveBeenCalledTimes(2);
3429
+ call = onPostCommit.mock.calls[0];
3430
expect(call[0]).toEqual('test');
2157
- expect(call[5]).toEqual(firstCommitTime);
2158
- expect(call[6]).toMatchInteractions(
3431
+ expect(call[3]).toEqual(firstCommitTime);
3432
+ expect(call[4]).toMatchInteractions(
3433
ReactFeatureFlags.enableSchedulerTracing ? [interactionThree] : [],
3434
);
2161
- call = onRender.mock.calls[1];
3435
+ call = onPostCommit.mock.calls[1];
3436
expect(call[0]).toEqual('test');
2163
- expect(call[5]).toEqual(Scheduler.unstable_now());
2164
- expect(call[6]).toMatchInteractions(
3437
+ expect(call[3]).toEqual(Scheduler.unstable_now());
3438
+ expect(call[4]).toMatchInteractions(
3439
ReactFeatureFlags.enableSchedulerTracing ? [interactionThree] : [],
3440
);
3441
});
3442
3443
it('should trace interactions associated with a parent component state update', () => {
2170
- const onRender = jest.fn();
3444
+ const onPostCommit = jest.fn(() => {
3445
+ Scheduler.unstable_yieldValue('onPostCommit');
3446
+ });
3447
let parentInstance = null;
3448
3449
class Child extends React.Component {
@@ -2184,7 +3460,7 @@ describe('Profiler', () => {
3460
render() {
3461
parentInstance = this;
3462
return (
2187
- <React.Profiler id="test-profiler" onRender={onRender}>
3463
+ <React.Profiler id="test-profiler" onPostCommit={onPostCommit}>
3464
<Child count={this.state.count} />
3465
</React.Profiler>
3466
);
@@ -2196,8 +3472,8 @@ describe('Profiler', () => {
3472
ReactTestRenderer.create(<Parent />, {
3473
unstable_isConcurrent: true,
3474
});
2199
- expect(Scheduler).toFlushAndYield(['Child:0']);
2200
- onRender.mockClear();
3475
+ expect(Scheduler).toFlushAndYield(['Child:0', 'onPostCommit']);
3476
+ onPostCommit.mockClear();
3477
3478
const interaction = {
3479
id: 0,
@@ -2221,12 +3497,12 @@ describe('Profiler', () => {
3497
expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
3498
expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
3499
2224
- expect(onRender).not.toHaveBeenCalled();
2225
- expect(Scheduler).toFlushAndYield(['Child:1']);
2226
- expect(onRender).toHaveBeenCalledTimes(1);
2227
- let call = onRender.mock.calls[0];
3500
+ expect(onPostCommit).not.toHaveBeenCalled();
3501
+ expect(Scheduler).toFlushAndYield(['Child:1', 'onPostCommit']);
3502
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
3503
+ let call = onPostCommit.mock.calls[0];
3504
expect(call[0]).toEqual('test-profiler');
2229
- expect(call[6]).toMatchInteractions(
3505
+ expect(call[4]).toMatchInteractions(
3506
ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [],
3507
);
3508
@@ -2270,13 +3546,15 @@ describe('Profiler', () => {
3546
}
3547
}
3548
2273
- const onRender = jest.fn();
3549
+ const onPostCommit = jest.fn(() => {
3550
+ Scheduler.unstable_yieldValue('onPostCommit');
3551
+ });
3552
SchedulerTracing.unstable_trace(
3553
interaction.name,
3554
Scheduler.unstable_now(),
3555
() => {
3556
ReactNoop.render(
2279
- <React.Profiler id="test-profiler" onRender={onRender}>
3557
+ <React.Profiler id="test-profiler" onPostCommit={onPostCommit}>
3558
<React.Suspense fallback={<Text text="Loading..." />}>
3559
<AsyncText text="Async" ms={20000} />
3560
</React.Suspense>
@@ -2300,14 +3578,15 @@ describe('Profiler', () => {
3578
'Text [Loading...]',
3579
'Text [Sync]',
3580
'Monkey',
3581
+ 'onPostCommit',
3582
]);
3583
// Should have committed the placeholder.
3584
expect(ReactNoop.getChildrenAsJSX()).toEqual('Loading...Sync');
2306
- expect(onRender).toHaveBeenCalledTimes(1);
3585
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
3586
2308
- let call = onRender.mock.calls[0];
3587
+ let call = onPostCommit.mock.calls[0];
3588
expect(call[0]).toEqual('test-profiler');
2310
- expect(call[6]).toMatchInteractions(
3589
+ expect(call[4]).toMatchInteractions(
3590
ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [],
3591
);
3592
@@ -2316,19 +3595,22 @@ describe('Profiler', () => {
3595
3596
// An unrelated update in the middle shouldn't affect things...
3597
monkey.current.forceUpdate();
2319
- expect(Scheduler).toFlushAndYield(['Monkey']);
2320
- expect(onRender).toHaveBeenCalledTimes(2);
3598
+ expect(Scheduler).toFlushAndYield(['Monkey', 'onPostCommit']);
3599
+ expect(onPostCommit).toHaveBeenCalledTimes(2);
3600
3601
// Once the promise resolves, we render the suspended view
3602
await awaitableAdvanceTimers(20000);
3603
expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
2325
- expect(Scheduler).toFlushAndYield(['AsyncText [Async]']);
3604
+ expect(Scheduler).toFlushAndYield([
3605
+ 'AsyncText [Async]',
3606
+ 'onPostCommit',
3607
+ ]);
3608
expect(ReactNoop.getChildrenAsJSX()).toEqual('AsyncSync');
2327
- expect(onRender).toHaveBeenCalledTimes(3);
3609
+ expect(onPostCommit).toHaveBeenCalledTimes(3);
3610
2329
- call = onRender.mock.calls[2];
3611
+ call = onPostCommit.mock.calls[2];
3612
expect(call[0]).toEqual('test-profiler');
2331
- expect(call[6]).toMatchInteractions(
3613
+ expect(call[4]).toMatchInteractions(
3614
ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [],
3615
);
3616
@@ -2346,13 +3628,15 @@ describe('Profiler', () => {
3628
timestamp: Scheduler.unstable_now(),
3629
};
3630
2349
- const onRender = jest.fn();
3631
+ const onPostCommit = jest.fn(() =>
3632
+ Scheduler.unstable_yieldValue('onPostCommit'),
3633
+ );
3634
SchedulerTracing.unstable_trace(
3635
interaction.name,
3636
interaction.timestamp,
3637
() => {
3638
ReactTestRenderer.create(
2355
- <React.Profiler id="app" onRender={onRender}>
3639
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3640
<React.Suspense fallback={<Text text="loading" />}>
3641
<AsyncText text="loaded" ms={500} />
3642
</React.Suspense>
@@ -2369,7 +3653,11 @@ describe('Profiler', () => {
3653
await resourcePromise;
3654
3655
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2372
- expect(Scheduler).toFlushExpired(['AsyncText [loaded]']);
3656
+ expect(Scheduler).toFlushExpired([
3657
+ 'onPostCommit',
3658
+ 'AsyncText [loaded]',
3659
+ ]);
3660
+ expect(Scheduler).toFlushAndYield(['onPostCommit']);
3661
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
3662
expect(
3663
onInteractionScheduledWorkCompleted,
@@ -2390,6 +3678,7 @@ describe('Profiler', () => {
3678
}
3679
3680
render() {
3681
+ Scheduler.unstable_yieldValue('render');
3682
const {ms, text} = this.props;
3683
TextResource.read([text, ms]);
3684
return <span prop={text}>{this.state.hasMounted}</span>;
@@ -2402,13 +3691,15 @@ describe('Profiler', () => {
3691
timestamp: Scheduler.unstable_now(),
3692
};
3693
2405
- const onRender = jest.fn();
3694
+ const onPostCommit = jest.fn(() =>
3695
+ Scheduler.unstable_yieldValue('onPostCommit'),
3696
+ );
3697
SchedulerTracing.unstable_trace(
3698
interaction.name,
3699
interaction.timestamp,
3700
() => {
3701
ReactTestRenderer.create(
2411
- <React.Profiler id="app" onRender={onRender}>
3702
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3703
<React.Suspense fallback={<Text text="loading" />}>
3704
<AsyncComponentWithCascadingWork text="loaded" ms={500} />
3705
</React.Suspense>
@@ -2419,17 +3710,19 @@ describe('Profiler', () => {
3710
3711
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
3712
2422
- expect(Scheduler).toHaveYielded(['Text [loading]']);
3713
+ expect(Scheduler).toHaveYielded(['render', 'Text [loading]']);
3714
3715
jest.runAllTimers();
3716
await resourcePromise;
3717
3718
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2428
- expect(Scheduler).toFlushExpired([]);
3719
+ expect(Scheduler).toFlushExpired(['onPostCommit', 'render']);
3720
3721
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
3722
3723
wrappedCascadingFn();
3724
+ expect(Scheduler).toHaveYielded(['onPostCommit', 'render']);
3725
+ expect(Scheduler).toFlushAndYield(['onPostCommit']);
3726
3727
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
3728
expect(
@@ -2444,13 +3737,15 @@ describe('Profiler', () => {
3737
timestamp: Scheduler.unstable_now(),
3738
};
3739
2447
- const onRender = jest.fn();
3740
+ const onPostCommit = jest.fn(() => {
3741
+ Scheduler.unstable_yieldValue('onPostCommit');
3742
+ });
3743
SchedulerTracing.unstable_trace(
3744
interaction.name,
3745
interaction.timestamp,
3746
() => {
3747
ReactTestRenderer.create(
2453
- <React.Profiler id="app" onRender={onRender}>
3748
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3749
<React.Suspense fallback={<Text text="loading" />}>
3750
<AsyncText text="loaded" ms={500} />
3751
</React.Suspense>
@@ -2468,6 +3763,7 @@ describe('Profiler', () => {
3763
expect(Scheduler).toFlushAndYield([
3764
'Suspend [loaded]',
3765
'Text [loading]',
3766
+ 'onPostCommit',
3767
]);
3768
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
3769
@@ -2475,7 +3771,10 @@ describe('Profiler', () => {
3771
await awaitableAdvanceTimers(500);
3772
3773
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2478
- expect(Scheduler).toFlushAndYield(['AsyncText [loaded]']);
3774
+ expect(Scheduler).toFlushAndYield([
3775
+ 'AsyncText [loaded]',
3776
+ 'onPostCommit',
3777
+ ]);
3778
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
3779
expect(
3780
onInteractionScheduledWorkCompleted,
@@ -2489,13 +3788,15 @@ describe('Profiler', () => {
3788
timestamp: Scheduler.unstable_now(),
3789
};
3790
2492
- const onRender = jest.fn();
3791
+ const onPostCommit = jest.fn(() => {
3792
+ Scheduler.unstable_yieldValue('onPostCommit');
3793
+ });
3794
SchedulerTracing.unstable_trace(
3795
interaction.name,
3796
interaction.timestamp,
3797
() => {
3798
ReactTestRenderer.create(
2498
- <React.Profiler id="app" onRender={onRender}>
3799
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3800
<React.Suspense fallback={<Text text="loading" />}>
3801
<AsyncText text="loaded" ms={100} />
3802
</React.Suspense>
@@ -2507,6 +3808,7 @@ describe('Profiler', () => {
3808
expect(Scheduler).toFlushAndYield([
3809
'Suspend [loaded]',
3810
'Text [loading]',
3811
+ 'onPostCommit',
3812
]);
3813
3814
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
@@ -2514,7 +3816,10 @@ describe('Profiler', () => {
3816
jest.advanceTimersByTime(100);
3817
await resourcePromise;
3818
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2517
- expect(Scheduler).toFlushAndYield(['AsyncText [loaded]']);
3819
+ expect(Scheduler).toFlushAndYield([
3820
+ 'AsyncText [loaded]',
3821
+ 'onPostCommit',
3822
+ ]);
3823
3824
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
3825
expect(
@@ -2529,14 +3834,16 @@ describe('Profiler', () => {
3834
timestamp: Scheduler.unstable_now(),
3835
};
3836
2532
- const onRender = jest.fn();
3837
+ const onPostCommit = jest.fn(() =>
3838
+ Scheduler.unstable_yieldValue('onPostCommit'),
3839
+ );
3840
let renderer;
3841
SchedulerTracing.unstable_trace(
3842
initialRenderInteraction.name,
3843
initialRenderInteraction.timestamp,
3844
() => {
3845
renderer = ReactTestRenderer.create(
2539
- <React.Profiler id="app" onRender={onRender}>
3846
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3847
<React.Suspense fallback={<Text text="loading" />}>
3848
<AsyncText text="loaded" ms={100} />
3849
</React.Suspense>
@@ -2546,13 +3853,19 @@ describe('Profiler', () => {
3853
},
3854
);
3855
expect(renderer.toJSON()).toEqual(['loading', 'initial']);
3856
+ expect(Scheduler).toHaveYielded([
3857
+ 'Suspend [loaded]',
3858
+ 'Text [loading]',
3859
+ 'Text [initial]',
3860
+ ]);
3861
+ expect(Scheduler).toFlushAndYield(['onPostCommit']);
3862
3863
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2551
- expect(onRender).toHaveBeenCalledTimes(1);
2552
- expect(onRender.mock.calls[0][6]).toMatchInteractions([
3864
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
3865
+ expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([
3866
initialRenderInteraction,
3867
]);
2555
- onRender.mockClear();
3868
+ onPostCommit.mockClear();
3869
3870
const highPriUpdateInteraction = {
3871
id: 1,
@@ -2568,7 +3881,7 @@ describe('Profiler', () => {
3881
highPriUpdateInteraction.timestamp,
3882
() => {
3883
renderer.update(
2571
- <React.Profiler id="app" onRender={onRender}>
3884
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3885
<React.Suspense fallback={<Text text="loading" />}>
3886
<AsyncText text="loaded" ms={100} />
3887
</React.Suspense>
@@ -2578,21 +3891,19 @@ describe('Profiler', () => {
3891
},
3892
);
3893
});
3894
+ expect(renderer.toJSON()).toEqual(['loading', 'updated']);
3895
expect(Scheduler).toHaveYielded([
2582
- 'Suspend [loaded]',
2583
- 'Text [loading]',
2584
- 'Text [initial]',
3896
'Suspend [loaded]',
3897
'Text [loading]',
3898
'Text [updated]',
3899
]);
2589
- expect(renderer.toJSON()).toEqual(['loading', 'updated']);
3900
+ expect(Scheduler).toFlushAndYield(['onPostCommit']);
3901
2591
- expect(onRender).toHaveBeenCalledTimes(1);
2592
- expect(onRender.mock.calls[0][6]).toMatchInteractions([
3902
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
3903
+ expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([
3904
highPriUpdateInteraction,
3905
]);
2595
- onRender.mockClear();
3906
+ onPostCommit.mockClear();
3907
3908
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
3909
expect(
@@ -2607,9 +3918,10 @@ describe('Profiler', () => {
3918
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
3919
expect(Scheduler).toFlushExpired(['AsyncText [loaded]']);
3920
expect(renderer.toJSON()).toEqual(['loaded', 'updated']);
3921
+ expect(Scheduler).toFlushAndYield(['onPostCommit']);
3922
2611
- expect(onRender).toHaveBeenCalledTimes(1);
2612
- expect(onRender.mock.calls[0][6]).toMatchInteractions([
3923
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
3924
+ expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([
3925
initialRenderInteraction,
3926
]);
3927
@@ -2636,13 +3948,15 @@ describe('Profiler', () => {
3948
timestamp: Scheduler.unstable_now(),
3949
};
3950
2639
- const onRender = jest.fn();
3951
+ const onPostCommit = jest.fn(() => {
3952
+ Scheduler.unstable_yieldValue('onPostCommit');
3953
+ });
3954
SchedulerTracing.unstable_trace(
3955
initialRenderInteraction.name,
3956
initialRenderInteraction.timestamp,
3957
() => {
3958
renderer.update(
2645
- <React.Profiler id="app" onRender={onRender}>
3959
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3960
<React.Suspense fallback={<Text text="loading" />}>
3961
<AsyncText text="loaded" ms={100} />
3962
</React.Suspense>
@@ -2658,7 +3972,7 @@ describe('Profiler', () => {
3972
]);
3973
3974
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2661
- expect(onRender).not.toHaveBeenCalled();
3975
+ expect(onPostCommit).not.toHaveBeenCalled();
3976
3977
Scheduler.unstable_advanceTime(50);
3978
jest.advanceTimersByTime(50);
@@ -2677,7 +3991,7 @@ describe('Profiler', () => {
3991
highPriUpdateInteraction.timestamp,
3992
() => {
3993
renderer.update(
2680
- <React.Profiler id="app" onRender={onRender}>
3994
+ <React.Profiler id="app" onPostCommit={onPostCommit}>
3995
<React.Suspense fallback={<Text text="loading" />}>
3996
<AsyncText text="loaded" ms={100} />
3997
</React.Suspense>
@@ -2692,13 +4006,14 @@ describe('Profiler', () => {
4006
'Text [loading]',
4007
'Text [updated]',
4008
]);
4009
+ expect(Scheduler).toFlushAndYieldThrough(['onPostCommit']);
4010
expect(renderer.toJSON()).toEqual(['loading', 'updated']);
4011
2697
- expect(onRender).toHaveBeenCalledTimes(1);
2698
- expect(onRender.mock.calls[0][6]).toMatchInteractions([
4012
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
4013
+ expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([
4014
highPriUpdateInteraction,
4015
]);
2701
- onRender.mockClear();
4016
+ onPostCommit.mockClear();
4017
4018
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
4019
@@ -2706,11 +4021,14 @@ describe('Profiler', () => {
4021
jest.advanceTimersByTime(50);
4022
await originalPromise;
4023
expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']);
2709
- expect(Scheduler).toFlushAndYield(['AsyncText [loaded]']);
4024
+ expect(Scheduler).toFlushAndYield([
4025
+ 'AsyncText [loaded]',
4026
+ 'onPostCommit',
4027
+ ]);
4028
expect(renderer.toJSON()).toEqual(['loaded', 'updated']);
4029
2712
- expect(onRender).toHaveBeenCalledTimes(1);
2713
- expect(onRender.mock.calls[0][6]).toMatchInteractions([
4030
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
4031
+ expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([
4032
initialRenderInteraction,
4033
highPriUpdateInteraction,
4034
]);
@@ -2750,13 +4068,15 @@ describe('Profiler', () => {
4068
}
4069
};
4070
2753
- const onRender = jest.fn();
4071
+ const onPostCommit = jest.fn(() => {
4072
+ Scheduler.unstable_yieldValue('onPostCommit');
4073
+ });
4074
SchedulerTracing.unstable_trace(
4075
interaction.name,
4076
Scheduler.unstable_now(),
4077
() => {
4078
ReactNoop.render(
2759
- <React.Profiler id="test-profiler" onRender={onRender}>
4079
+ <React.Profiler id="test-profiler" onPostCommit={onPostCommit}>
4080
<React.Suspense fallback={<Text text="Loading..." />}>
4081
<AsyncText text="Async" ms={20000} />
4082
</React.Suspense>
@@ -2778,14 +4098,15 @@ describe('Profiler', () => {
4098
'Suspend [Async]',
4099
'Text [Loading...]',
4100
'Text [Sync]',
4101
+ 'onPostCommit',
4102
]);
4103
// Should have committed the placeholder.
4104
expect(ReactNoop.getChildrenAsJSX()).toEqual('Loading...Sync');
2784
- expect(onRender).toHaveBeenCalledTimes(1);
4105
+ expect(onPostCommit).toHaveBeenCalledTimes(1);
4106
2786
- let call = onRender.mock.calls[0];
4107
+ let call = onPostCommit.mock.calls[0];
4108
expect(call[0]).toEqual('test-profiler');
2788
- expect(call[6]).toMatchInteractions(
4109
+ expect(call[4]).toMatchInteractions(
4110
ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [],
4111
);
4112
@@ -2799,14 +4120,17 @@ describe('Profiler', () => {
4120
// Once the promise resolves, we render the suspended view
4121
await awaitableAdvanceTimers(20000);
4122
expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
2802
- expect(Scheduler).toFlushAndYield(['AsyncText [Async]']);
4123
+ expect(Scheduler).toFlushAndYield([
4124
+ 'AsyncText [Async]',
4125
+ 'onPostCommit',
4126
+ ]);
4127
expect(ReactNoop.getChildrenAsJSX()).toEqual('AsyncSync');
2804
- expect(onRender).toHaveBeenCalledTimes(2);
4128
+ expect(onPostCommit).toHaveBeenCalledTimes(2);
4129
4130
// No interactions should be associated with this update.
2807
- call = onRender.mock.calls[1];
4131
+ call = onPostCommit.mock.calls[1];
4132
expect(call[0]).toEqual('test-profiler');
2809
- expect(call[6]).toMatchInteractions([]);
4133
+ expect(call[4]).toMatchInteractions([]);
4134
});
4135
});
4136
});
packages/shared/ReactFeatureFlags.js
+3
@@ -23,6 +23,9 @@ export const warnAboutDeprecatedLifecycles = true;
23
// Gather advanced timing metrics for Profiler subtrees.
24
export const enableProfilerTimer = __PROFILE__;
25
26
+// Record durations for commit and passive effects phases.
27
+export const enableProfilerCommitHooks = false;
28
+
29
// Trace which interactions trigger each commit.
30
export const enableSchedulerTracing = __PROFILE__;
31
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -15,6 +15,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.native-fb';
15
// The rest of the flags are static for better dead code elimination.
16
export const enableUserTimingAPI = __DEV__;
17
export const enableProfilerTimer = __PROFILE__;
18
+export const enableProfilerCommitHooks = false;
19
export const enableSchedulerTracing = __PROFILE__;
20
export const enableSuspenseServerRenderer = false;
21
export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -17,6 +17,7 @@ export const enableUserTimingAPI = __DEV__;
17
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
18
export const warnAboutDeprecatedLifecycles = true;
19
export const enableProfilerTimer = __PROFILE__;
20
+export const enableProfilerCommitHooks = false;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -17,6 +17,7 @@ export const enableUserTimingAPI = __DEV__;
17
export const warnAboutDeprecatedLifecycles = true;
18
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
19
export const enableProfilerTimer = __PROFILE__;
20
+export const enableProfilerCommitHooks = false;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -17,6 +17,7 @@ export const enableUserTimingAPI = __DEV__;
17
export const warnAboutDeprecatedLifecycles = true;
18
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
19
export const enableProfilerTimer = __PROFILE__;
20
+export const enableProfilerCommitHooks = false;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -17,6 +17,7 @@ export const enableUserTimingAPI = __DEV__;
17
export const warnAboutDeprecatedLifecycles = true;
18
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
19
export const enableProfilerTimer = __PROFILE__;
20
+export const enableProfilerCommitHooks = false;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -17,6 +17,7 @@ export const enableUserTimingAPI = __DEV__;
17
export const warnAboutDeprecatedLifecycles = true;
18
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
19
export const enableProfilerTimer = __PROFILE__;
20
+export const enableProfilerCommitHooks = false;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -17,6 +17,7 @@ export const enableUserTimingAPI = false;
17
export const warnAboutDeprecatedLifecycles = true;
18
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
19
export const enableProfilerTimer = false;
20
+export const enableProfilerCommitHooks = false;
21
export const enableSchedulerTracing = false;
22
export const enableSuspenseServerRenderer = true;
23
export const enableSelectiveHydration = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -35,6 +35,7 @@ export const {
35
export let enableUserTimingAPI = __DEV__ && !__EXPERIMENTAL__;
36
37
export const enableProfilerTimer = __PROFILE__;
38
+export const enableProfilerCommitHooks = false;
39
export const enableSchedulerTracing = __PROFILE__;
40
export const enableSchedulerDebugging = true;
41