@samitouri / QOS-React / commits / 6e4f7c7886

Profiler integration with interaction-tracking package (#13253)

* Updated suspense fixture to use new interaction-tracking API * Integrated Profiler API with interaction-tracking API (and added tests) * Pass interaction Set (rather than Array) to Profiler onRender callback * Removed some :any casts for enableInteractionTracking fields in FiberRoot type * Refactored threadID calculation into a helper method * Errors thrown by interaction tracking hooks use unhandledError to rethrow more safely. Reverted try/finally change to ReactTestRendererScheduling * Added a $FlowFixMe above the FiberRoot :any cast * Reduce overhead from calling work-started hook * Remove interaction-tracking wrap() references from unwind work in favor of managing suspense/interaction continuations in the scheduler * Moved the logic for calling work-started hook from performWorkOnRoot() to renderRoot() * Add interaction-tracking to bundle externals. Set feature flag to __PROFILE__ * Renamed the freezeInteractionCount flag and replaced one use-case with a method param * let -> const * Updated suspense fixture to handle recent API changes

Brian Vaughn committed Aug 28, 2018 at 18:58 UTC 6e4f7c788603dac7fccd227a4852c110b072fe16
14 files changed +2462 -945
fixtures/unstable-async/suspense/package.json
+1
@@ -10,6 +10,7 @@
10 "dependencies": {
11 "clipboard": "^1.7.1",
12 "github-fork-ribbon-css": "^0.2.1",
13 + "interaction-tracking": "../../../build/node_modules/interaction-tracking",
14 "react": "../../../build/node_modules/react",
15 "react-dom": "../../../build/node_modules/react-dom",
16 "react-draggable": "^3.0.5",
fixtures/unstable-async/suspense/src/components/App.js
+22 -11
@@ -1,3 +1,4 @@
1 +import {track, wrap} from 'interaction-tracking';
2 import React, {Placeholder, PureComponent} from 'react';
3 import {createResource} from 'simple-cache-provider';
4 import {cache} from '../cache';
@@ -27,21 +28,31 @@ export default class App extends PureComponent {
28 }
29
30 handleUserClick = id => {
30 - this.setState({
31 - currentId: id,
32 - });
33 - requestIdleCallback(() => {
34 - this.setState({
35 - showDetail: true,
36 - });
31 + track(`View ${id}`, performance.now(), () => {
32 + track(`View ${id} (high-pri)`, performance.now(), () =>
33 + this.setState({
34 + currentId: id,
35 + })
36 + );
37 + requestIdleCallback(
38 + wrap(() =>
39 + track(`View ${id} (low-pri)`, performance.now(), () =>
40 + this.setState({
41 + showDetail: true,
42 + })
43 + )
44 + )
45 + );
46 });
47 };
48
49 handleBackClick = () =>
41 - this.setState({
42 - currentId: null,
43 - showDetail: false,
44 - });
50 + track('View list', performance.now(), () =>
51 + this.setState({
52 + currentId: null,
53 + showDetail: false,
54 + })
55 + );
56
57 render() {
58 const {currentId, showDetail} = this.state;
fixtures/unstable-async/suspense/src/index.js
+8 -5
@@ -1,3 +1,4 @@
1 +import {track} from 'interaction-tracking';
2 import React, {Fragment, PureComponent} from 'react';
3 import {unstable_createRoot, render} from 'react-dom';
4 import {cache} from './cache';
@@ -64,11 +65,13 @@ class Debugger extends PureComponent {
65 }
66
67 handleReset = () => {
67 - cache.invalidate();
68 - this.setState(state => ({
69 - requests: {},
70 - }));
71 - handleReset();
68 + track('Clear cache', () => {
69 + cache.invalidate();
70 + this.setState(state => ({
71 + requests: {},
72 + }));
73 + handleReset();
74 + });
75 };
76
77 handleProgress = (url, progress, isPaused) => {
fixtures/unstable-async/suspense/yarn.lock
+3
@@ -3482,6 +3482,9 @@ inquirer@3.3.0, inquirer@^3.0.6:
3482 strip-ansi "^4.0.0"
3483 through "^2.3.6"
3484
3485 +interaction-tracking@../../../build/node_modules/interaction-tracking:
3486 + version "0.0.1"
3487 +
3488 internal-ip@1.2.0:
3489 version "1.2.0"
3490 resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
packages/react-reconciler/src/ReactFiber.js
+2 -1
@@ -17,6 +17,7 @@ import type {UpdateQueue} from './ReactUpdateQueue';
17 import type {ContextDependency} from './ReactFiberNewContext';
18
19 import invariant from 'shared/invariant';
20 +import warningWithoutStack from 'shared/warningWithoutStack';
21 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
22 import {NoEffect} from 'shared/ReactSideEffectTags';
23 import {
@@ -531,7 +532,7 @@ export function createFiberFromProfiler(
532 typeof pendingProps.id !== 'string' ||
533 typeof pendingProps.onRender !== 'function'
534 ) {
534 - invariant(
535 + warningWithoutStack(
536 false,
537 'Profiler must specify an "id" string and "onRender" function as props',
538 );
packages/react-reconciler/src/ReactFiberCommitWork.js
+31 -10
@@ -19,7 +19,11 @@ import type {FiberRoot} from './ReactFiberRoot';
19 import type {ExpirationTime} from './ReactFiberExpirationTime';
20 import type {CapturedValue, CapturedError} from './ReactCapturedValue';
21
22 -import {enableProfilerTimer, enableSuspense} from 'shared/ReactFeatureFlags';
22 +import {
23 + enableInteractionTracking,
24 + enableProfilerTimer,
25 + enableSuspense,
26 +} from 'shared/ReactFeatureFlags';
27 import {
28 ClassComponent,
29 ClassComponentLazy,
@@ -777,7 +781,11 @@ function commitDeletion(current: Fiber): void {
781 detachFiber(current);
782 }
783
780 -function commitWork(current: Fiber | null, finishedWork: Fiber): void {
784 +function commitWork(
785 + root: FiberRoot,
786 + current: Fiber | null,
787 + finishedWork: Fiber,
788 +): void {
789 if (!supportsMutation) {
790 commitContainer(finishedWork);
791 return;
@@ -836,14 +844,27 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
844 case Profiler: {
845 if (enableProfilerTimer) {
846 const onRender = finishedWork.memoizedProps.onRender;
839 - onRender(
840 - finishedWork.memoizedProps.id,
841 - current === null ? 'mount' : 'update',
842 - finishedWork.actualDuration,
843 - finishedWork.treeBaseDuration,
844 - finishedWork.actualStartTime,
845 - getCommitTime(),
846 - );
847 +
848 + if (enableInteractionTracking) {
849 + onRender(
850 + finishedWork.memoizedProps.id,
851 + current === null ? 'mount' : 'update',
852 + finishedWork.actualDuration,
853 + finishedWork.treeBaseDuration,
854 + finishedWork.actualStartTime,
855 + getCommitTime(),
856 + root.memoizedInteractions,
857 + );
858 + } else {
859 + onRender(
860 + finishedWork.memoizedProps.id,
861 + current === null ? 'mount' : 'update',
862 + finishedWork.actualDuration,
863 + finishedWork.treeBaseDuration,
864 + finishedWork.actualStartTime,
865 + getCommitTime(),
866 + );
867 + }
868 }
869 return;
870 }
packages/react-reconciler/src/ReactFiberRoot.js
+90 -27
@@ -10,11 +10,13 @@
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
13 +import type {Interaction} from 'interaction-tracking/src/InteractionTracking';
14
15 import {noTimeout} from './ReactFiberHostConfig';
15 -
16 import {createHostRootFiber} from './ReactFiber';
17 import {NoWork} from './ReactFiberExpirationTime';
18 +import {enableInteractionTracking} from 'shared/ReactFeatureFlags';
19 +import {getThreadID} from 'interaction-tracking';
20
21 // TODO: This should be lifted into the renderer.
22 export type Batch = {
@@ -24,7 +26,9 @@ export type Batch = {
26 _next: Batch | null,
27 };
28
27 -export type FiberRoot = {
29 +export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
30 +
31 +type BaseFiberRootProperties = {|
32 // Any additional information from the host associated with this root.
33 containerInfo: any,
34 // Used only by persistent updates.
@@ -73,6 +77,26 @@ export type FiberRoot = {
77 firstBatch: Batch | null,
78 // Linked-list of roots
79 nextScheduledRoot: FiberRoot | null,
80 +|};
81 +
82 +// The following attributes are only used by interaction tracking builds.
83 +// They enable interactions to be associated with their async work,
84 +// And expose interaction metadata to the React DevTools Profiler plugin.
85 +// Note that these attributes are only defined when the enableInteractionTracking flag is enabled.
86 +type ProfilingOnlyFiberRootProperties = {|
87 + interactionThreadID: number,
88 + memoizedInteractions: Set<Interaction>,
89 + pendingInteractionMap: PendingInteractionMap,
90 +|};
91 +
92 +// Exported FiberRoot type includes all properties,
93 +// To avoid requiring potentially error-prone :any casts throughout the project.
94 +// Profiling properties are only safe to access in profiling builds (when enableInteractionTracking is true).
95 +// The types are defined separately within this file to ensure they stay in sync.
96 +// (We don't have to use an inline :any cast when enableInteractionTracking is disabled.)
97 +export type FiberRoot = {
98 + ...BaseFiberRootProperties,
99 + ...ProfilingOnlyFiberRootProperties,
100 };
101
102 export function createFiberRoot(
@@ -83,30 +107,69 @@ export function createFiberRoot(
107 // Cyclic construction. This cheats the type system right now because
108 // stateNode is any.
109 const uninitializedFiber = createHostRootFiber(isAsync);
86 - const root = {
87 - current: uninitializedFiber,
88 - containerInfo: containerInfo,
89 - pendingChildren: null,
90 -
91 - earliestPendingTime: NoWork,
92 - latestPendingTime: NoWork,
93 - earliestSuspendedTime: NoWork,
94 - latestSuspendedTime: NoWork,
95 - latestPingedTime: NoWork,
96 -
97 - didError: false,
98 -
99 - pendingCommitExpirationTime: NoWork,
100 - finishedWork: null,
101 - timeoutHandle: noTimeout,
102 - context: null,
103 - pendingContext: null,
104 - hydrate,
105 - nextExpirationTimeToWorkOn: NoWork,
106 - expirationTime: NoWork,
107 - firstBatch: null,
108 - nextScheduledRoot: null,
109 - };
110 +
111 + let root;
112 + if (enableInteractionTracking) {
113 + root = ({
114 + current: uninitializedFiber,
115 + containerInfo: containerInfo,
116 + pendingChildren: null,
117 +
118 + earliestPendingTime: NoWork,
119 + latestPendingTime: NoWork,
120 + earliestSuspendedTime: NoWork,
121 + latestSuspendedTime: NoWork,
122 + latestPingedTime: NoWork,
123 +
124 + didError: false,
125 +
126 + pendingCommitExpirationTime: NoWork,
127 + finishedWork: null,
128 + timeoutHandle: noTimeout,
129 + context: null,
130 + pendingContext: null,
131 + hydrate,
132 + nextExpirationTimeToWorkOn: NoWork,
133 + expirationTime: NoWork,
134 + firstBatch: null,
135 + nextScheduledRoot: null,
136 +
137 + interactionThreadID: getThreadID(),
138 + memoizedInteractions: new Set(),
139 + pendingInteractionMap: new Map(),
140 + }: FiberRoot);
141 + } else {
142 + root = ({
143 + current: uninitializedFiber,
144 + containerInfo: containerInfo,
145 + pendingChildren: null,
146 +
147 + earliestPendingTime: NoWork,
148 + latestPendingTime: NoWork,
149 + earliestSuspendedTime: NoWork,
150 + latestSuspendedTime: NoWork,
151 + latestPingedTime: NoWork,
152 +
153 + didError: false,
154 +
155 + pendingCommitExpirationTime: NoWork,
156 + finishedWork: null,
157 + timeoutHandle: noTimeout,
158 + context: null,
159 + pendingContext: null,
160 + hydrate,
161 + nextExpirationTimeToWorkOn: NoWork,
162 + expirationTime: NoWork,
163 + firstBatch: null,
164 + nextScheduledRoot: null,
165 + }: BaseFiberRootProperties);
166 + }
167 +
168 uninitializedFiber.stateNode = root;
111 - return root;
169 +
170 + // The reason for the way the Flow types are structured in this file,
171 + // Is to avoid needing :any casts everywhere interaction-tracking fields are used.
172 + // Unfortunately that requires an :any cast for non-interaction-tracking capable builds.
173 + // $FlowFixMe Remove this :any cast and replace it with something better.
174 + return ((root: any): FiberRoot);
175 }
packages/react-reconciler/src/ReactFiberScheduler.js
+230 -11
@@ -8,9 +8,11 @@
8 */
9
10 import type {Fiber} from './ReactFiber';
11 -import type {FiberRoot, Batch} from './ReactFiberRoot';
11 +import type {Batch, FiberRoot} from './ReactFiberRoot';
12 import type {ExpirationTime} from './ReactFiberExpirationTime';
13 +import type {Interaction} from 'interaction-tracking/src/InteractionTracking';
14
15 +import {__interactionsRef, __subscriberRef} from 'interaction-tracking';
16 import {
17 invokeGuardedCallback,
18 hasCaughtError,
@@ -42,6 +44,7 @@ import {
44 HostPortal,
45 } from 'shared/ReactWorkTags';
46 import {
47 + enableInteractionTracking,
48 enableProfilerTimer,
49 enableUserTimingAPI,
50 replayFailedUnitOfWorkWithInvokeGuardedCallback,
@@ -242,6 +245,10 @@ let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
245 // Used for performance tracking.
246 let interruptedBy: Fiber | null = null;
247
248 +// Do not decrement interaction counts in the event of suspense timeouts.
249 +// This would lead to prematurely calling the interaction-complete hook.
250 +let suspenseDidTimeout: boolean = false;
251 +
252 let stashedWorkInProgressProperties;
253 let replayUnitOfWork;
254 let isReplayingFailedUnitOfWork;
@@ -363,7 +370,7 @@ function resetStack() {
370 nextUnitOfWork = null;
371 }
372
366 -function commitAllHostEffects() {
373 +function commitAllHostEffects(root: FiberRoot) {
374 while (nextEffect !== null) {
375 if (__DEV__) {
376 ReactCurrentFiber.setCurrentFiber(nextEffect);
@@ -408,12 +415,12 @@ function commitAllHostEffects() {
415
416 // Update
417 const current = nextEffect.alternate;
411 - commitWork(current, nextEffect);
418 + commitWork(root, current, nextEffect);
419 break;
420 }
421 case Update: {
422 const current = nextEffect.alternate;
416 - commitWork(current, nextEffect);
423 + commitWork(root, current, nextEffect);
424 break;
425 }
426 case Deletion: {
@@ -545,6 +552,34 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
552 : updateExpirationTimeBeforeCommit;
553 markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit);
554
555 + let prevInteractions: Set<Interaction> = (null: any);
556 + let committedInteractions: Array<Interaction> = enableInteractionTracking
557 + ? []
558 + : (null: any);
559 + if (enableInteractionTracking) {
560 + // Restore any pending interactions at this point,
561 + // So that cascading work triggered during the render phase will be accounted for.
562 + prevInteractions = __interactionsRef.current;
563 + __interactionsRef.current = root.memoizedInteractions;
564 +
565 + // We are potentially finished with the current batch of interactions.
566 + // So we should clear them out of the pending interaction map.
567 + // We do this at the start of commit in case cascading work is scheduled by commit phase lifecycles.
568 + // In that event, interaction data may be added back into the pending map for a future commit.
569 + // We also store the interactions we are about to commit so that we can notify subscribers after we're done.
570 + // These are stored as an Array rather than a Set,
571 + // Because the same interaction may be pending for multiple expiration times,
572 + // In which case it's important that we decrement the count the right number of times after finishing.
573 + root.pendingInteractionMap.forEach(
574 + (scheduledInteractions, scheduledExpirationTime) => {
575 + if (scheduledExpirationTime <= committedExpirationTime) {
576 + committedInteractions.push(...Array.from(scheduledInteractions));
577 + root.pendingInteractionMap.delete(scheduledExpirationTime);
578 + }
579 + },
580 + );
581 + }
582 +
583 // Reset this to null before calling lifecycles
584 ReactCurrentOwner.current = null;
585
@@ -617,14 +652,14 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
652 let didError = false;
653 let error;
654 if (__DEV__) {
620 - invokeGuardedCallback(null, commitAllHostEffects, null);
655 + invokeGuardedCallback(null, commitAllHostEffects, null, root);
656 if (hasCaughtError()) {
657 didError = true;
658 error = clearCaughtError();
659 }
660 } else {
661 try {
627 - commitAllHostEffects();
662 + commitAllHostEffects(root);
663 } catch (e) {
664 didError = true;
665 error = e;
@@ -718,6 +753,53 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
753 legacyErrorBoundariesThatAlreadyFailed = null;
754 }
755 onCommit(root, earliestRemainingTimeAfterCommit);
756 +
757 + if (enableInteractionTracking) {
758 + __interactionsRef.current = prevInteractions;
759 +
760 + let subscriber;
761 +
762 + try {
763 + subscriber = __subscriberRef.current;
764 + if (subscriber !== null && root.memoizedInteractions.size > 0) {
765 + const threadID = computeThreadID(
766 + committedExpirationTime,
767 + root.interactionThreadID,
768 + );
769 + subscriber.onWorkStopped(root.memoizedInteractions, threadID);
770 + }
771 + } catch (error) {
772 + // It's not safe for commitRoot() to throw.
773 + // Store the error for now and we'll re-throw in finishRendering().
774 + if (!hasUnhandledError) {
775 + hasUnhandledError = true;
776 + unhandledError = error;
777 + }
778 + } finally {
779 + // Don't update interaction counts if we're frozen due to suspense.
780 + // In this case, we can skip the completed-work check entirely.
781 + if (!suspenseDidTimeout) {
782 + // Now that we're done, check the completed batch of interactions.
783 + // If no more work is outstanding for a given interaction,
784 + // We need to notify the subscribers that it's finished.
785 + committedInteractions.forEach(interaction => {
786 + interaction.__count--;
787 + if (subscriber !== null && interaction.__count === 0) {
788 + try {
789 + subscriber.onInteractionScheduledWorkCompleted(interaction);
790 + } catch (error) {
791 + // It's not safe for commitRoot() to throw.
792 + // Store the error for now and we'll re-throw in finishRendering().
793 + if (!hasUnhandledError) {
794 + hasUnhandledError = true;
795 + unhandledError = error;
796 + }
797 + }
798 + }
799 + });
800 + }
801 + }
802 + }
803 }
804
805 function resetChildExpirationTime(
@@ -1079,6 +1161,14 @@ function renderRoot(
1161
1162 const expirationTime = root.nextExpirationTimeToWorkOn;
1163
1164 + let prevInteractions: Set<Interaction> = (null: any);
1165 + if (enableInteractionTracking) {
1166 + // We're about to start new tracked work.
1167 + // Restore pending interactions so cascading work triggered during the render phase will be accounted for.
1168 + prevInteractions = __interactionsRef.current;
1169 + __interactionsRef.current = root.memoizedInteractions;
1170 + }
1171 +
1172 // Check if we're starting from a fresh stack, or if we're resuming from
1173 // previously yielded work.
1174 if (
@@ -1096,6 +1186,49 @@ function renderRoot(
1186 nextRenderExpirationTime,
1187 );
1188 root.pendingCommitExpirationTime = NoWork;
1189 +
1190 + if (enableInteractionTracking) {
1191 + // Determine which interactions this batch of work currently includes,
1192 + // So that we can accurately attribute time spent working on it,
1193 + // And so that cascading work triggered during the render phase will be associated with it.
1194 + const interactions: Set<Interaction> = new Set();
1195 + root.pendingInteractionMap.forEach(
1196 + (scheduledInteractions, scheduledExpirationTime) => {
1197 + if (scheduledExpirationTime <= expirationTime) {
1198 + scheduledInteractions.forEach(interaction =>
1199 + interactions.add(interaction),
1200 + );
1201 + }
1202 + },
1203 + );
1204 +
1205 + // Store the current set of interactions on the FiberRoot for a few reasons:
1206 + // We can re-use it in hot functions like renderRoot() without having to recalculate it.
1207 + // We will also use it in commitWork() to pass to any Profiler onRender() hooks.
1208 + // This also provides DevTools with a way to access it when the onCommitRoot() hook is called.
1209 + root.memoizedInteractions = interactions;
1210 +
1211 + if (interactions.size > 0) {
1212 + const subscriber = __subscriberRef.current;
1213 + if (subscriber !== null) {
1214 + const threadID = computeThreadID(
1215 + expirationTime,
1216 + root.interactionThreadID,
1217 + );
1218 + try {
1219 + subscriber.onWorkStarted(interactions, threadID);
1220 + } catch (error) {
1221 + // Work thrown by a interaction-tracking subscriber should be rethrown,
1222 + // But only once it's safe (to avoid leaveing the scheduler in an invalid state).
1223 + // Store the error for now and we'll re-throw in finishRendering().
1224 + if (!hasUnhandledError) {
1225 + hasUnhandledError = true;
1226 + unhandledError = error;
1227 + }
1228 + }
1229 + }
1230 + }
1231 + }
1232 }
1233
1234 let didFatal = false;
@@ -1159,6 +1292,11 @@ function renderRoot(
1292 break;
1293 } while (true);
1294
1295 + if (enableInteractionTracking) {
1296 + // Tracked work is done for now; restore the previous interactions.
1297 + __interactionsRef.current = prevInteractions;
1298 + }
1299 +
1300 // We're done performing work. Time to clean up.
1301 isWorking = false;
1302 ReactCurrentOwner.currentDispatcher = null;
@@ -1351,6 +1489,14 @@ function captureCommitPhaseError(fiber: Fiber, error: mixed) {
1489 return dispatch(fiber, error, Sync);
1490 }
1491
1492 +function computeThreadID(
1493 + expirationTime: ExpirationTime,
1494 + interactionThreadID: number,
1495 +): number {
1496 + // Interaction threads are unique per root and expiration time.
1497 + return expirationTime * 1000 + interactionThreadID;
1498 +}
1499 +
1500 // Creates a unique async expiration time.
1501 function computeUniqueAsyncExpiration(): ExpirationTime {
1502 const currentTime = requestCurrentTime();
@@ -1455,7 +1601,18 @@ function retrySuspendedRoot(
1601 scheduleWorkToRoot(fiber, retryTime);
1602 const rootExpirationTime = root.expirationTime;
1603 if (rootExpirationTime !== NoWork) {
1458 - requestWork(root, rootExpirationTime);
1604 + if (enableInteractionTracking) {
1605 + // Restore previous interactions so that new work is associated with them.
1606 + let prevInteractions = __interactionsRef.current;
1607 + __interactionsRef.current = root.memoizedInteractions;
1608 + // Because suspense timeouts do not decrement the interaction count,
1609 + // Continued suspense work should also not increment the count.
1610 + storeInteractionsForExpirationTime(root, rootExpirationTime, false);
1611 + requestWork(root, rootExpirationTime);
1612 + __interactionsRef.current = prevInteractions;
1613 + } else {
1614 + requestWork(root, rootExpirationTime);
1615 + }
1616 }
1617 }
1618 }
@@ -1510,6 +1667,49 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
1667 return null;
1668 }
1669
1670 +function storeInteractionsForExpirationTime(
1671 + root: FiberRoot,
1672 + expirationTime: ExpirationTime,
1673 + updateInteractionCounts: boolean,
1674 +): void {
1675 + if (!enableInteractionTracking) {
1676 + return;
1677 + }
1678 +
1679 + const interactions = __interactionsRef.current;
1680 + if (interactions.size > 0) {
1681 + const pendingInteractions = root.pendingInteractionMap.get(expirationTime);
1682 + if (pendingInteractions != null) {
1683 + interactions.forEach(interaction => {
1684 + if (updateInteractionCounts && !pendingInteractions.has(interaction)) {
1685 + // Update the pending async work count for previously unscheduled interaction.
1686 + interaction.__count++;
1687 + }
1688 +
1689 + pendingInteractions.add(interaction);
1690 + });
1691 + } else {
1692 + root.pendingInteractionMap.set(expirationTime, new Set(interactions));
1693 +
1694 + // Update the pending async work count for the current interactions.
1695 + if (updateInteractionCounts) {
1696 + interactions.forEach(interaction => {
1697 + interaction.__count++;
1698 + });
1699 + }
1700 + }
1701 +
1702 + const subscriber = __subscriberRef.current;
1703 + if (subscriber !== null) {
1704 + const threadID = computeThreadID(
1705 + expirationTime,
1706 + root.interactionThreadID,
1707 + );
1708 + subscriber.onWorkScheduled(interactions, threadID);
1709 + }
1710 + }
1711 +}
1712 +
1713 function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1714 recordScheduleUpdate();
1715
@@ -1531,6 +1731,10 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1731 return;
1732 }
1733
1734 + if (enableInteractionTracking) {
1735 + storeInteractionsForExpirationTime(root, expirationTime, true);
1736 + }
1737 +
1738 if (
1739 !isWorking &&
1740 nextRenderExpirationTime !== NoWork &&
@@ -1637,7 +1841,10 @@ function recomputeCurrentRendererTime() {
1841 currentRendererTime = msToExpirationTime(currentTimeMs);
1842 }
1843
1640 -function scheduleCallbackWithExpirationTime(expirationTime) {
1844 +function scheduleCallbackWithExpirationTime(
1845 + root: FiberRoot,
1846 + expirationTime: ExpirationTime,
1847 +) {
1848 if (callbackExpirationTime !== NoWork) {
1849 // A callback is already scheduled. Check its expiration time (timeout).
1850 if (expirationTime > callbackExpirationTime) {
@@ -1714,7 +1921,16 @@ function onTimeout(root, finishedWork, suspendedExpirationTime) {
1921 // because we're at the top of a timer event.
1922 recomputeCurrentRendererTime();
1923 currentSchedulerTime = currentRendererTime;
1717 - flushRoot(root, suspendedExpirationTime);
1924 +
1925 + if (enableInteractionTracking) {
1926 + // Don't update pending interaction counts for suspense timeouts,
1927 + // Because we know we still need to do more work in this case.
1928 + suspenseDidTimeout = true;
1929 + flushRoot(root, suspendedExpirationTime);
1930 + suspenseDidTimeout = false;
1931 + } else {
1932 + flushRoot(root, suspendedExpirationTime);
1933 + }
1934 }
1935 }
1936
@@ -1793,7 +2009,7 @@ function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
2009 if (expirationTime === Sync) {
2010 performSyncWork();
2011 } else {
1796 - scheduleCallbackWithExpirationTime(expirationTime);
2012 + scheduleCallbackWithExpirationTime(root, expirationTime);
2013 }
2014 }
2015
@@ -1955,7 +2171,10 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
2171 }
2172 // If there's work left over, schedule a new callback.
2173 if (nextFlushedExpirationTime !== NoWork) {
1958 - scheduleCallbackWithExpirationTime(nextFlushedExpirationTime);
2174 + scheduleCallbackWithExpirationTime(
2175 + ((nextFlushedRoot: any): FiberRoot),
2176 + nextFlushedExpirationTime,
2177 + );
2178 }
2179
2180 // Clean-up.
packages/react/src/__tests__/ReactProfiler-test.internal.js
+1938 -851
@@ -5,33 +5,72 @@
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 + * @jest-environment node
9 */
10
11 'use strict';
12
13 +let InteractionTracking;
14 +let InteractionTrackingSubscriptions;
15 let React;
16 let ReactFeatureFlags;
17 let ReactNoop;
18 let ReactTestRenderer;
19 +let advanceTimeBy;
20 +let mockNow;
21 +let AdvanceTime;
22
23 function loadModules({
24 enableProfilerTimer = true,
25 + enableSuspense = false,
26 + enableInteractionTracking = true,
27 replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
28 useNoopRenderer = false,
29 } = {}) {
30 + let currentTime = 0;
31 +
32 + mockNow = jest.fn().mockImplementation(() => currentTime);
33 +
34 + global.Date.now = mockNow;
35 +
36 ReactFeatureFlags = require('shared/ReactFeatureFlags');
37 ReactFeatureFlags.debugRenderPhaseSideEffects = false;
38 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
39 ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
40 ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
41 + ReactFeatureFlags.enableInteractionTracking = enableInteractionTracking;
42 + ReactFeatureFlags.enableSuspense = enableSuspense;
43 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
44 +
45 + InteractionTracking = require('interaction-tracking');
46 + InteractionTrackingSubscriptions = require('interaction-tracking/subscriptions');
47 React = require('react');
48
49 if (useNoopRenderer) {
50 ReactNoop = require('react-noop-renderer');
51 } else {
52 ReactTestRenderer = require('react-test-renderer');
53 + ReactTestRenderer.unstable_setNowImplementation(mockNow);
54 }
55 +
56 + advanceTimeBy = amount => {
57 + currentTime += amount;
58 + };
59 +
60 + AdvanceTime = class extends React.Component {
61 + static defaultProps = {
62 + byAmount: 10,
63 + shouldComponentUpdate: true,
64 + };
65 + shouldComponentUpdate(nextProps) {
66 + return nextProps.shouldComponentUpdate;
67 + }
68 + render() {
69 + // Simulate time passing when this component is rendered
70 + advanceTimeBy(this.props.byAmount);
71 + return this.props.children || null;
72 + }
73 + };
74 }
75
76 const mockDevToolsForTest = () => {
@@ -45,1009 +84,1016 @@ const mockDevToolsForTest = () => {
84
85 describe('Profiler', () => {
86 describe('works in profiling and non-profiling bundles', () => {
48 - [true, false].forEach(flagEnabled => {
49 - describe(`enableProfilerTimer ${
50 - flagEnabled ? 'enabled' : 'disabled'
51 - }`, () => {
52 - beforeEach(() => {
53 - jest.resetModules();
54 -
55 - loadModules({enableProfilerTimer: flagEnabled});
56 - });
87 + [true, false].forEach(enableInteractionTracking => {
88 + [true, false].forEach(enableProfilerTimer => {
89 + describe(`enableInteractionTracking:${
90 + enableInteractionTracking ? 'enabled' : 'disabled'
91 + } enableProfilerTimer:${
92 + enableProfilerTimer ? 'enabled' : 'disabled'
93 + }`, () => {
94 + beforeEach(() => {
95 + jest.resetModules();
96
58 - // This will throw in production too,
59 - // But the test is only interested in verifying the DEV error message.
60 - if (__DEV__ && flagEnabled) {
61 - it('should warn if required params are missing', () => {
62 - expect(() => {
63 - ReactTestRenderer.create(<React.unstable_Profiler />);
64 - }).toThrow(
65 - 'Profiler must specify an "id" string and "onRender" function as props',
66 - );
97 + loadModules({enableInteractionTracking, enableProfilerTimer});
98 + });
99 +
100 + // This will throw in production too,
101 + // But the test is only interested in verifying the DEV error message.
102 + if (__DEV__ && enableProfilerTimer) {
103 + it('should warn if required params are missing', () => {
104 + expect(() => {
105 + expect(() => {
106 + ReactTestRenderer.create(<React.unstable_Profiler />);
107 + }).toThrow('onRender is not a function');
108 + }).toWarnDev(
109 + 'Profiler must specify an "id" string and "onRender" function as props',
110 + {withoutStack: true},
111 + );
112 + });
113 + }
114 +
115 + it('should support an empty Profiler (with no children)', () => {
116 + // As root
117 + expect(
118 + ReactTestRenderer.create(
119 + <React.unstable_Profiler id="label" onRender={jest.fn()} />,
120 + ).toJSON(),
121 + ).toMatchSnapshot();
122 +
123 + // As non-root
124 + expect(
125 + ReactTestRenderer.create(
126 + <div>
127 + <React.unstable_Profiler id="label" onRender={jest.fn()} />
128 + </div>,
129 + ).toJSON(),
130 + ).toMatchSnapshot();
131 });
68 - }
132
70 - it('should support an empty Profiler (with no children)', () => {
71 - // As root
72 - expect(
73 - ReactTestRenderer.create(
74 - <React.unstable_Profiler id="label" onRender={jest.fn()} />,
75 - ).toJSON(),
76 - ).toMatchSnapshot();
77 -
78 - // As non-root
79 - expect(
80 - ReactTestRenderer.create(
133 + it('should render children', () => {
134 + const FunctionalComponent = ({label}) => <span>{label}</span>;
135 + const renderer = ReactTestRenderer.create(
136 <div>
82 - <React.unstable_Profiler id="label" onRender={jest.fn()} />
137 + <span>outside span</span>
138 + <React.unstable_Profiler id="label" onRender={jest.fn()}>
139 + <span>inside span</span>
140 + <FunctionalComponent label="functional component" />
141 + </React.unstable_Profiler>
142 </div>,
84 - ).toJSON(),
85 - ).toMatchSnapshot();
86 - });
87 -
88 - it('should render children', () => {
89 - const FunctionalComponent = ({label}) => <span>{label}</span>;
90 - const renderer = ReactTestRenderer.create(
91 - <div>
92 - <span>outside span</span>
93 - <React.unstable_Profiler id="label" onRender={jest.fn()}>
94 - <span>inside span</span>
95 - <FunctionalComponent label="functional component" />
96 - </React.unstable_Profiler>
97 - </div>,
98 - );
99 - expect(renderer.toJSON()).toMatchSnapshot();
100 - });
143 + );
144 + expect(renderer.toJSON()).toMatchSnapshot();
145 + });
146
102 - it('should support nested Profilers', () => {
103 - const FunctionalComponent = ({label}) => <div>{label}</div>;
104 - class ClassComponent extends React.Component {
105 - render() {
106 - return <block>{this.props.label}</block>;
147 + it('should support nested Profilers', () => {
148 + const FunctionalComponent = ({label}) => <div>{label}</div>;
149 + class ClassComponent extends React.Component {
150 + render() {
151 + return <block>{this.props.label}</block>;
152 + }
153 }
108 - }
109 - const renderer = ReactTestRenderer.create(
110 - <React.unstable_Profiler id="outer" onRender={jest.fn()}>
111 - <FunctionalComponent label="outer functional component" />
112 - <React.unstable_Profiler id="inner" onRender={jest.fn()}>
113 - <ClassComponent label="inner class component" />
114 - <span>inner span</span>
115 - </React.unstable_Profiler>
116 - </React.unstable_Profiler>,
117 - );
118 - expect(renderer.toJSON()).toMatchSnapshot();
154 + const renderer = ReactTestRenderer.create(
155 + <React.unstable_Profiler id="outer" onRender={jest.fn()}>
156 + <FunctionalComponent label="outer functional component" />
157 + <React.unstable_Profiler id="inner" onRender={jest.fn()}>
158 + <ClassComponent label="inner class component" />
159 + <span>inner span</span>
160 + </React.unstable_Profiler>
161 + </React.unstable_Profiler>,
162 + );
163 + expect(renderer.toJSON()).toMatchSnapshot();
164 + });
165 });
166 });
167 });
168 });
169
124 - describe('onRender callback', () => {
125 - let AdvanceTime;
126 - let advanceTimeBy;
127 - let mockNow;
128 -
129 - const mockNowForTests = () => {
130 - let currentTime = 0;
131 -
132 - mockNow = jest.fn().mockImplementation(() => currentTime);
133 -
134 - ReactTestRenderer.unstable_setNowImplementation(mockNow);
135 - advanceTimeBy = amount => {
136 - currentTime += amount;
137 - };
138 - };
170 + [true, false].forEach(enableInteractionTracking => {
171 + describe('onRender callback', () => {
172 + beforeEach(() => {
173 + jest.resetModules();
174
140 - beforeEach(() => {
141 - jest.resetModules();
175 + loadModules({enableInteractionTracking});
176 + });
177
143 - loadModules();
144 - mockNowForTests();
178 + it('is not invoked until the commit phase', () => {
179 + const callback = jest.fn();
180
146 - AdvanceTime = class extends React.Component {
147 - static defaultProps = {
148 - byAmount: 10,
149 - shouldComponentUpdate: true,
181 + const Yield = ({value}) => {
182 + ReactTestRenderer.unstable_yield(value);
183 + return null;
184 };
151 - shouldComponentUpdate(nextProps) {
152 - return nextProps.shouldComponentUpdate;
153 - }
154 - render() {
155 - // Simulate time passing when this component is rendered
156 - advanceTimeBy(this.props.byAmount);
157 - return this.props.children || null;
158 - }
159 - };
160 - });
185
162 - it('is not invoked until the commit phase', () => {
163 - const callback = jest.fn();
164 -
165 - const Yield = ({value}) => {
166 - ReactTestRenderer.unstable_yield(value);
167 - return null;
168 - };
186 + const renderer = ReactTestRenderer.create(
187 + <React.unstable_Profiler id="test" onRender={callback}>
188 + <Yield value="first" />
189 + <Yield value="last" />
190 + </React.unstable_Profiler>,
191 + {
192 + unstable_isAsync: true,
193 + },
194 + );
195
170 - const renderer = ReactTestRenderer.create(
171 - <React.unstable_Profiler id="test" onRender={callback}>
172 - <Yield value="first" />
173 - <Yield value="last" />
174 - </React.unstable_Profiler>,
175 - {
176 - unstable_isAsync: true,
177 - },
178 - );
196 + // Times are logged until a render is committed.
197 + expect(renderer).toFlushThrough(['first']);
198 + expect(callback).toHaveBeenCalledTimes(0);
199 + expect(renderer).toFlushAll(['last']);
200 + expect(callback).toHaveBeenCalledTimes(1);
201 + });
202
180 - // Times are logged until a render is committed.
181 - expect(renderer).toFlushThrough(['first']);
182 - expect(callback).toHaveBeenCalledTimes(0);
183 - expect(renderer).toFlushAll(['last']);
184 - expect(callback).toHaveBeenCalledTimes(1);
185 - });
203 + it('does not record times for components outside of Profiler tree', () => {
204 + ReactTestRenderer.create(
205 + <div>
206 + <AdvanceTime />
207 + <AdvanceTime />
208 + <AdvanceTime />
209 + <AdvanceTime />
210 + <AdvanceTime />
211 + </div>,
212 + );
213
187 - it('does not record times for components outside of Profiler tree', () => {
188 - ReactTestRenderer.create(
189 - <div>
190 - <AdvanceTime />
191 - <AdvanceTime />
192 - <AdvanceTime />
193 - <AdvanceTime />
194 - <AdvanceTime />
195 - </div>,
196 - );
214 + // Should be called two times:
215 + // 2. To compute the update expiration time
216 + // 3. To record the commit time
217 + // No additional calls from ProfilerTimer are expected.
218 + expect(mockNow).toHaveBeenCalledTimes(2);
219 + });
220
198 - // Should be called two times:
199 - // 2. To compute the update expiration time
200 - // 3. To record the commit time
201 - // No additional calls from ProfilerTimer are expected.
202 - expect(mockNow).toHaveBeenCalledTimes(2);
203 - });
221 + it('logs render times for both mount and update', () => {
222 + const callback = jest.fn();
223
205 - it('logs render times for both mount and update', () => {
206 - const callback = jest.fn();
224 + advanceTimeBy(5); // 0 -> 5
225
208 - advanceTimeBy(5); // 0 -> 5
226 + const renderer = ReactTestRenderer.create(
227 + <React.unstable_Profiler id="test" onRender={callback}>
228 + <AdvanceTime />
229 + </React.unstable_Profiler>,
230 + );
231
210 - const renderer = ReactTestRenderer.create(
211 - <React.unstable_Profiler id="test" onRender={callback}>
212 - <AdvanceTime />
213 - </React.unstable_Profiler>,
214 - );
232 + expect(callback).toHaveBeenCalledTimes(1);
233
216 - expect(callback).toHaveBeenCalledTimes(1);
234 + let [call] = callback.mock.calls;
235
218 - let [call] = callback.mock.calls;
236 + expect(call).toHaveLength(enableInteractionTracking ? 7 : 6);
237 + expect(call[0]).toBe('test');
238 + expect(call[1]).toBe('mount');
239 + expect(call[2]).toBe(10); // actual time
240 + expect(call[3]).toBe(10); // base time
241 + expect(call[4]).toBe(5); // start time
242 + expect(call[5]).toBe(15); // commit time
243 + expect(call[6]).toEqual(
244 + enableInteractionTracking ? new Set() : undefined,
245 + ); // interaction events
246
220 - expect(call).toHaveLength(6);
221 - expect(call[0]).toBe('test');
222 - expect(call[1]).toBe('mount');
223 - expect(call[2]).toBe(10); // actual time
224 - expect(call[3]).toBe(10); // base time
225 - expect(call[4]).toBe(5); // start time
226 - expect(call[5]).toBe(15); // commit time
247 + callback.mockReset();
248
228 - callback.mockReset();
249 + advanceTimeBy(20); // 15 -> 35
250
230 - advanceTimeBy(20); // 15 -> 35
251 + renderer.update(
252 + <React.unstable_Profiler id="test" onRender={callback}>
253 + <AdvanceTime />
254 + </React.unstable_Profiler>,
255 + );
256
232 - renderer.update(
233 - <React.unstable_Profiler id="test" onRender={callback}>
234 - <AdvanceTime />
235 - </React.unstable_Profiler>,
236 - );
257 + expect(callback).toHaveBeenCalledTimes(1);
258
238 - expect(callback).toHaveBeenCalledTimes(1);
259 + [call] = callback.mock.calls;
260
240 - [call] = callback.mock.calls;
261 + expect(call).toHaveLength(enableInteractionTracking ? 7 : 6);
262 + expect(call[0]).toBe('test');
263 + expect(call[1]).toBe('update');
264 + expect(call[2]).toBe(10); // actual time
265 + expect(call[3]).toBe(10); // base time
266 + expect(call[4]).toBe(35); // start time
267 + expect(call[5]).toBe(45); // commit time
268 + expect(call[6]).toEqual(
269 + enableInteractionTracking ? new Set() : undefined,
270 + ); // interaction events
271
242 - expect(call).toHaveLength(6);
243 - expect(call[0]).toBe('test');
244 - expect(call[1]).toBe('update');
245 - expect(call[2]).toBe(10); // actual time
246 - expect(call[3]).toBe(10); // base time
247 - expect(call[4]).toBe(35); // start time
248 - expect(call[5]).toBe(45); // commit time
272 + callback.mockReset();
273
250 - callback.mockReset();
274 + advanceTimeBy(20); // 45 -> 65
275
252 - advanceTimeBy(20); // 45 -> 65
276 + renderer.update(
277 + <React.unstable_Profiler id="test" onRender={callback}>
278 + <AdvanceTime byAmount={4} />
279 + </React.unstable_Profiler>,
280 + );
281
254 - renderer.update(
255 - <React.unstable_Profiler id="test" onRender={callback}>
256 - <AdvanceTime byAmount={4} />
257 - </React.unstable_Profiler>,
258 - );
282 + expect(callback).toHaveBeenCalledTimes(1);
283
260 - expect(callback).toHaveBeenCalledTimes(1);
284 + [call] = callback.mock.calls;
285 +
286 + expect(call).toHaveLength(enableInteractionTracking ? 7 : 6);
287 + expect(call[0]).toBe('test');
288 + expect(call[1]).toBe('update');
289 + expect(call[2]).toBe(4); // actual time
290 + expect(call[3]).toBe(4); // base time
291 + expect(call[4]).toBe(65); // start time
292 + expect(call[5]).toBe(69); // commit time
293 + expect(call[6]).toEqual(
294 + enableInteractionTracking ? new Set() : undefined,
295 + ); // interaction events
296 + });
297
262 - [call] = callback.mock.calls;
298 + it('includes render times of nested Profilers in their parent times', () => {
299 + const callback = jest.fn();
300
264 - expect(call).toHaveLength(6);
265 - expect(call[0]).toBe('test');
266 - expect(call[1]).toBe('update');
267 - expect(call[2]).toBe(4); // actual time
268 - expect(call[3]).toBe(4); // base time
269 - expect(call[4]).toBe(65); // start time
270 - expect(call[5]).toBe(69); // commit time
271 - });
301 + advanceTimeBy(5); // 0 -> 5
302
273 - it('includes render times of nested Profilers in their parent times', () => {
274 - const callback = jest.fn();
303 + ReactTestRenderer.create(
304 + <React.Fragment>
305 + <React.unstable_Profiler id="parent" onRender={callback}>
306 + <AdvanceTime byAmount={10}>
307 + <React.unstable_Profiler id="child" onRender={callback}>
308 + <AdvanceTime byAmount={20} />
309 + </React.unstable_Profiler>
310 + </AdvanceTime>
311 + </React.unstable_Profiler>
312 + </React.Fragment>,
313 + );
314
276 - advanceTimeBy(5); // 0 -> 5
315 + expect(callback).toHaveBeenCalledTimes(2);
316
278 - ReactTestRenderer.create(
279 - <React.Fragment>
280 - <React.unstable_Profiler id="parent" onRender={callback}>
281 - <AdvanceTime byAmount={10}>
282 - <React.unstable_Profiler id="child" onRender={callback}>
283 - <AdvanceTime byAmount={20} />
284 - </React.unstable_Profiler>
285 - </AdvanceTime>
286 - </React.unstable_Profiler>
287 - </React.Fragment>,
288 - );
317 + // Callbacks bubble (reverse order).
318 + const [childCall, parentCall] = callback.mock.calls;
319 + expect(childCall[0]).toBe('child');
320 + expect(parentCall[0]).toBe('parent');
321 +
322 + // Parent times should include child times
323 + expect(childCall[2]).toBe(20); // actual time
324 + expect(childCall[3]).toBe(20); // base time
325 + expect(childCall[4]).toBe(15); // start time
326 + expect(childCall[5]).toBe(35); // commit time
327 + expect(parentCall[2]).toBe(30); // actual time
328 + expect(parentCall[3]).toBe(30); // base time
329 + expect(parentCall[4]).toBe(5); // start time
330 + expect(parentCall[5]).toBe(35); // commit time
331 + });
332
290 - expect(callback).toHaveBeenCalledTimes(2);
291 -
292 - // Callbacks bubble (reverse order).
293 - const [childCall, parentCall] = callback.mock.calls;
294 - expect(childCall[0]).toBe('child');
295 - expect(parentCall[0]).toBe('parent');
296 -
297 - // Parent times should include child times
298 - expect(childCall[2]).toBe(20); // actual time
299 - expect(childCall[3]).toBe(20); // base time
300 - expect(childCall[4]).toBe(15); // start time
301 - expect(childCall[5]).toBe(35); // commit time
302 - expect(parentCall[2]).toBe(30); // actual time
303 - expect(parentCall[3]).toBe(30); // base time
304 - expect(parentCall[4]).toBe(5); // start time
305 - expect(parentCall[5]).toBe(35); // commit time
306 - });
333 + it('tracks sibling Profilers separately', () => {
334 + const callback = jest.fn();
335
308 - it('tracks sibling Profilers separately', () => {
309 - const callback = jest.fn();
336 + advanceTimeBy(5); // 0 -> 5
337
311 - advanceTimeBy(5); // 0 -> 5
338 + ReactTestRenderer.create(
339 + <React.Fragment>
340 + <React.unstable_Profiler id="first" onRender={callback}>
341 + <AdvanceTime byAmount={20} />
342 + </React.unstable_Profiler>
343 + <React.unstable_Profiler id="second" onRender={callback}>
344 + <AdvanceTime byAmount={5} />
345 + </React.unstable_Profiler>
346 + </React.Fragment>,
347 + );
348
313 - ReactTestRenderer.create(
314 - <React.Fragment>
315 - <React.unstable_Profiler id="first" onRender={callback}>
316 - <AdvanceTime byAmount={20} />
317 - </React.unstable_Profiler>
318 - <React.unstable_Profiler id="second" onRender={callback}>
319 - <AdvanceTime byAmount={5} />
320 - </React.unstable_Profiler>
321 - </React.Fragment>,
322 - );
349 + expect(callback).toHaveBeenCalledTimes(2);
350
324 - expect(callback).toHaveBeenCalledTimes(2);
325 -
326 - const [firstCall, secondCall] = callback.mock.calls;
327 - expect(firstCall[0]).toBe('first');
328 - expect(secondCall[0]).toBe('second');
329 -
330 - // Parent times should include child times
331 - expect(firstCall[2]).toBe(20); // actual time
332 - expect(firstCall[3]).toBe(20); // base time
333 - expect(firstCall[4]).toBe(5); // start time
334 - expect(firstCall[5]).toBe(30); // commit time
335 - expect(secondCall[2]).toBe(5); // actual time
336 - expect(secondCall[3]).toBe(5); // base time
337 - expect(secondCall[4]).toBe(25); // start time
338 - expect(secondCall[5]).toBe(30); // commit time
339 - });
351 + const [firstCall, secondCall] = callback.mock.calls;
352 + expect(firstCall[0]).toBe('first');
353 + expect(secondCall[0]).toBe('second');
354 +
355 + // Parent times should include child times
356 + expect(firstCall[2]).toBe(20); // actual time
357 + expect(firstCall[3]).toBe(20); // base time
358 + expect(firstCall[4]).toBe(5); // start time
359 + expect(firstCall[5]).toBe(30); // commit time
360 + expect(secondCall[2]).toBe(5); // actual time
361 + expect(secondCall[3]).toBe(5); // base time
362 + expect(secondCall[4]).toBe(25); // start time
363 + expect(secondCall[5]).toBe(30); // commit time
364 + });
365
341 - it('does not include time spent outside of profile root', () => {
342 - const callback = jest.fn();
366 + it('does not include time spent outside of profile root', () => {
367 + const callback = jest.fn();
368
344 - advanceTimeBy(5); // 0 -> 5
369 + advanceTimeBy(5); // 0 -> 5
370
346 - ReactTestRenderer.create(
347 - <React.Fragment>
348 - <AdvanceTime byAmount={20} />
349 - <React.unstable_Profiler id="test" onRender={callback}>
350 - <AdvanceTime byAmount={5} />
351 - </React.unstable_Profiler>
352 - <AdvanceTime byAmount={20} />
353 - </React.Fragment>,
354 - );
371 + ReactTestRenderer.create(
372 + <React.Fragment>
373 + <AdvanceTime byAmount={20} />
374 + <React.unstable_Profiler id="test" onRender={callback}>
375 + <AdvanceTime byAmount={5} />
376 + </React.unstable_Profiler>
377 + <AdvanceTime byAmount={20} />
378 + </React.Fragment>,
379 + );
380
356 - expect(callback).toHaveBeenCalledTimes(1);
381 + expect(callback).toHaveBeenCalledTimes(1);
382
358 - const [call] = callback.mock.calls;
359 - expect(call[0]).toBe('test');
360 - expect(call[2]).toBe(5); // actual time
361 - expect(call[3]).toBe(5); // base time
362 - expect(call[4]).toBe(25); // start time
363 - expect(call[5]).toBe(50); // commit time
364 - });
383 + const [call] = callback.mock.calls;
384 + expect(call[0]).toBe('test');
385 + expect(call[2]).toBe(5); // actual time
386 + expect(call[3]).toBe(5); // base time
387 + expect(call[4]).toBe(25); // start time
388 + expect(call[5]).toBe(50); // commit time
389 + });
390
366 - it('is not called when blocked by sCU false', () => {
367 - const callback = jest.fn();
391 + it('is not called when blocked by sCU false', () => {
392 + const callback = jest.fn();
393
369 - let instance;
370 - class Updater extends React.Component {
371 - state = {};
372 - render() {
373 - instance = this;
374 - return this.props.children;
394 + let instance;
395 + class Updater extends React.Component {
396 + state = {};
397 + render() {
398 + instance = this;
399 + return this.props.children;
400 + }
401 }
376 - }
402
378 - class Pure extends React.PureComponent {
379 - render() {
380 - return this.props.children;
403 + class Pure extends React.PureComponent {
404 + render() {
405 + return this.props.children;
406 + }
407 }
382 - }
408
384 - const renderer = ReactTestRenderer.create(
385 - <React.unstable_Profiler id="outer" onRender={callback}>
386 - <Updater>
387 - <React.unstable_Profiler id="middle" onRender={callback}>
388 - <Pure>
389 - <React.unstable_Profiler id="inner" onRender={callback}>
390 - <div />
391 - </React.unstable_Profiler>
392 - </Pure>
393 - </React.unstable_Profiler>
394 - </Updater>
395 - </React.unstable_Profiler>,
396 - );
409 + const renderer = ReactTestRenderer.create(
410 + <React.unstable_Profiler id="outer" onRender={callback}>
411 + <Updater>
412 + <React.unstable_Profiler id="middle" onRender={callback}>
413 + <Pure>
414 + <React.unstable_Profiler id="inner" onRender={callback}>
415 + <div />
416 + </React.unstable_Profiler>
417 + </Pure>
418 + </React.unstable_Profiler>
419 + </Updater>
420 + </React.unstable_Profiler>,
421 + );
422
398 - // All profile callbacks are called for initial render
399 - expect(callback).toHaveBeenCalledTimes(3);
423 + // All profile callbacks are called for initial render
424 + expect(callback).toHaveBeenCalledTimes(3);
425
401 - callback.mockReset();
426 + callback.mockReset();
427
403 - renderer.unstable_flushSync(() => {
404 - instance.setState({
405 - count: 1,
428 + renderer.unstable_flushSync(() => {
429 + instance.setState({
430 + count: 1,
431 + });
432 });
407 - });
433
409 - // Only call profile updates for paths that have re-rendered
410 - // Since "inner" is beneath a pure compoent, it isn't called
411 - expect(callback).toHaveBeenCalledTimes(2);
412 - expect(callback.mock.calls[0][0]).toBe('middle');
413 - expect(callback.mock.calls[1][0]).toBe('outer');
414 - });
434 + // Only call profile updates for paths that have re-rendered
435 + // Since "inner" is beneath a pure compoent, it isn't called
436 + expect(callback).toHaveBeenCalledTimes(2);
437 + expect(callback.mock.calls[0][0]).toBe('middle');
438 + expect(callback.mock.calls[1][0]).toBe('outer');
439 + });
440
416 - it('decreases actual time but not base time when sCU prevents an update', () => {
417 - const callback = jest.fn();
441 + it('decreases actual time but not base time when sCU prevents an update', () => {
442 + const callback = jest.fn();
443
419 - advanceTimeBy(5); // 0 -> 5
444 + advanceTimeBy(5); // 0 -> 5
445
421 - const renderer = ReactTestRenderer.create(
422 - <React.unstable_Profiler id="test" onRender={callback}>
423 - <AdvanceTime byAmount={10}>
424 - <AdvanceTime byAmount={13} shouldComponentUpdate={false} />
425 - </AdvanceTime>
426 - </React.unstable_Profiler>,
427 - );
446 + const renderer = ReactTestRenderer.create(
447 + <React.unstable_Profiler id="test" onRender={callback}>
448 + <AdvanceTime byAmount={10}>
449 + <AdvanceTime byAmount={13} shouldComponentUpdate={false} />
450 + </AdvanceTime>
451 + </React.unstable_Profiler>,
452 + );
453
429 - expect(callback).toHaveBeenCalledTimes(1);
454 + expect(callback).toHaveBeenCalledTimes(1);
455
431 - advanceTimeBy(30); // 28 -> 58
456 + advanceTimeBy(30); // 28 -> 58
457
433 - renderer.update(
434 - <React.unstable_Profiler id="test" onRender={callback}>
435 - <AdvanceTime byAmount={4}>
436 - <AdvanceTime byAmount={7} shouldComponentUpdate={false} />
437 - </AdvanceTime>
438 - </React.unstable_Profiler>,
439 - );
458 + renderer.update(
459 + <React.unstable_Profiler id="test" onRender={callback}>
460 + <AdvanceTime byAmount={4}>
461 + <AdvanceTime byAmount={7} shouldComponentUpdate={false} />
462 + </AdvanceTime>
463 + </React.unstable_Profiler>,
464 + );
465
441 - expect(callback).toHaveBeenCalledTimes(2);
466 + expect(callback).toHaveBeenCalledTimes(2);
467
443 - const [mountCall, updateCall] = callback.mock.calls;
468 + const [mountCall, updateCall] = callback.mock.calls;
469
445 - expect(mountCall[1]).toBe('mount');
446 - expect(mountCall[2]).toBe(23); // actual time
447 - expect(mountCall[3]).toBe(23); // base time
448 - expect(mountCall[4]).toBe(5); // start time
449 - expect(mountCall[5]).toBe(28); // commit time
470 + expect(mountCall[1]).toBe('mount');
471 + expect(mountCall[2]).toBe(23); // actual time
472 + expect(mountCall[3]).toBe(23); // base time
473 + expect(mountCall[4]).toBe(5); // start time
474 + expect(mountCall[5]).toBe(28); // commit time
475
451 - expect(updateCall[1]).toBe('update');
452 - expect(updateCall[2]).toBe(4); // actual time
453 - expect(updateCall[3]).toBe(17); // base time
454 - expect(updateCall[4]).toBe(58); // start time
455 - expect(updateCall[5]).toBe(62); // commit time
456 - });
476 + expect(updateCall[1]).toBe('update');
477 + expect(updateCall[2]).toBe(4); // actual time
478 + expect(updateCall[3]).toBe(17); // base time
479 + expect(updateCall[4]).toBe(58); // start time
480 + expect(updateCall[5]).toBe(62); // commit time
481 + });
482
458 - it('includes time spent in render phase lifecycles', () => {
459 - class WithLifecycles extends React.Component {
460 - state = {};
461 - static getDerivedStateFromProps() {
462 - advanceTimeBy(3);
463 - return null;
464 - }
465 - shouldComponentUpdate() {
466 - advanceTimeBy(7);
467 - return true;
468 - }
469 - render() {
470 - advanceTimeBy(5);
471 - return null;
483 + it('includes time spent in render phase lifecycles', () => {
484 + class WithLifecycles extends React.Component {
485 + state = {};
486 + static getDerivedStateFromProps() {
487 + advanceTimeBy(3);
488 + return null;
489 + }
490 + shouldComponentUpdate() {
491 + advanceTimeBy(7);
492 + return true;
493 + }
494 + render() {
495 + advanceTimeBy(5);
496 + return null;
497 + }
498 }
473 - }
474 -
475 - const callback = jest.fn();
499
477 - advanceTimeBy(5); // 0 -> 5
478 -
479 - const renderer = ReactTestRenderer.create(
480 - <React.unstable_Profiler id="test" onRender={callback}>
481 - <WithLifecycles />
482 - </React.unstable_Profiler>,
483 - );
484 -
485 - advanceTimeBy(15); // 13 -> 28
486 -
487 - renderer.update(
488 - <React.unstable_Profiler id="test" onRender={callback}>
489 - <WithLifecycles />
490 - </React.unstable_Profiler>,
491 - );
492 -
493 - expect(callback).toHaveBeenCalledTimes(2);
494 -
495 - const [mountCall, updateCall] = callback.mock.calls;
496 -
497 - expect(mountCall[1]).toBe('mount');
498 - expect(mountCall[2]).toBe(8); // actual time
499 - expect(mountCall[3]).toBe(8); // base time
500 - expect(mountCall[4]).toBe(5); // start time
501 - expect(mountCall[5]).toBe(13); // commit time
502 -
503 - expect(updateCall[1]).toBe('update');
504 - expect(updateCall[2]).toBe(15); // actual time
505 - expect(updateCall[3]).toBe(15); // base time
506 - expect(updateCall[4]).toBe(28); // start time
507 - expect(updateCall[5]).toBe(43); // commit time
508 - });
509 -
510 - describe('with regard to interruptions', () => {
511 - it('should accumulate actual time after a scheduling interruptions', () => {
500 const callback = jest.fn();
501
514 - const Yield = ({renderTime}) => {
515 - advanceTimeBy(renderTime);
516 - ReactTestRenderer.unstable_yield('Yield:' + renderTime);
517 - return null;
518 - };
519 -
502 advanceTimeBy(5); // 0 -> 5
503
522 - // Render partially, but run out of time before completing.
504 const renderer = ReactTestRenderer.create(
505 <React.unstable_Profiler id="test" onRender={callback}>
525 - <Yield renderTime={2} />
526 - <Yield renderTime={3} />
506 + <WithLifecycles />
507 </React.unstable_Profiler>,
528 - {unstable_isAsync: true},
508 );
530 - expect(renderer).toFlushThrough(['Yield:2']);
531 - expect(callback).toHaveBeenCalledTimes(0);
532 -
533 - // Resume render for remaining children.
534 - expect(renderer).toFlushAll(['Yield:3']);
535 -
536 - // Verify that logged times include both durations above.
537 - expect(callback).toHaveBeenCalledTimes(1);
538 - const [call] = callback.mock.calls;
539 - expect(call[2]).toBe(5); // actual time
540 - expect(call[3]).toBe(5); // base time
541 - expect(call[4]).toBe(5); // start time
542 - expect(call[5]).toBe(10); // commit time
543 - });
544 -
545 - it('should not include time between frames', () => {
546 - const callback = jest.fn();
547 -
548 - const Yield = ({renderTime}) => {
549 - advanceTimeBy(renderTime);
550 - ReactTestRenderer.unstable_yield('Yield:' + renderTime);
551 - return null;
552 - };
509
554 - advanceTimeBy(5); // 0 -> 5
510 + advanceTimeBy(15); // 13 -> 28
511
556 - // Render partially, but don't finish.
557 - // This partial render should take 5ms of simulated time.
558 - const renderer = ReactTestRenderer.create(
559 - <React.unstable_Profiler id="outer" onRender={callback}>
560 - <Yield renderTime={5} />
561 - <Yield renderTime={10} />
562 - <React.unstable_Profiler id="inner" onRender={callback}>
563 - <Yield renderTime={17} />
564 - </React.unstable_Profiler>
512 + renderer.update(
513 + <React.unstable_Profiler id="test" onRender={callback}>
514 + <WithLifecycles />
515 </React.unstable_Profiler>,
566 - {unstable_isAsync: true},
516 );
568 - expect(renderer).toFlushThrough(['Yield:5']);
569 - expect(callback).toHaveBeenCalledTimes(0);
517
571 - // Simulate time moving forward while frame is paused.
572 - advanceTimeBy(50); // 10 -> 60
573 -
574 - // Flush the remaninig work,
575 - // Which should take an additional 10ms of simulated time.
576 - expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
518 expect(callback).toHaveBeenCalledTimes(2);
519
579 - const [innerCall, outerCall] = callback.mock.calls;
580 -
581 - // Verify that the actual time includes all work times,
582 - // But not the time that elapsed between frames.
583 - expect(innerCall[0]).toBe('inner');
584 - expect(innerCall[2]).toBe(17); // actual time
585 - expect(innerCall[3]).toBe(17); // base time
586 - expect(innerCall[4]).toBe(70); // start time
587 - expect(innerCall[5]).toBe(87); // commit time
588 - expect(outerCall[0]).toBe('outer');
589 - expect(outerCall[2]).toBe(32); // actual time
590 - expect(outerCall[3]).toBe(32); // base time
591 - expect(outerCall[4]).toBe(5); // start time
592 - expect(outerCall[5]).toBe(87); // commit time
520 + const [mountCall, updateCall] = callback.mock.calls;
521 +
522 + expect(mountCall[1]).toBe('mount');
523 + expect(mountCall[2]).toBe(8); // actual time
524 + expect(mountCall[3]).toBe(8); // base time
525 + expect(mountCall[4]).toBe(5); // start time
526 + expect(mountCall[5]).toBe(13); // commit time
527 +
528 + expect(updateCall[1]).toBe('update');
529 + expect(updateCall[2]).toBe(15); // actual time
530 + expect(updateCall[3]).toBe(15); // base time
531 + expect(updateCall[4]).toBe(28); // start time
532 + expect(updateCall[5]).toBe(43); // commit time
533 });
534
595 - it('should report the expected times when a high-priority update replaces an in-progress initial render', () => {
596 - const callback = jest.fn();
535 + describe('with regard to interruptions', () => {
536 + it('should accumulate actual time after a scheduling interruptions', () => {
537 + const callback = jest.fn();
538
598 - const Yield = ({renderTime}) => {
599 - advanceTimeBy(renderTime);
600 - ReactTestRenderer.unstable_yield('Yield:' + renderTime);
601 - return null;
602 - };
539 + const Yield = ({renderTime}) => {
540 + advanceTimeBy(renderTime);
541 + ReactTestRenderer.unstable_yield('Yield:' + renderTime);
542 + return null;
543 + };
544
604 - advanceTimeBy(5); // 0 -> 5
545 + advanceTimeBy(5); // 0 -> 5
546
606 - // Render a partially update, but don't finish.
607 - // This partial render should take 10ms of simulated time.
608 - const renderer = ReactTestRenderer.create(
609 - <React.unstable_Profiler id="test" onRender={callback}>
610 - <Yield renderTime={10} />
611 - <Yield renderTime={20} />
612 - </React.unstable_Profiler>,
613 - {unstable_isAsync: true},
614 - );
615 - expect(renderer).toFlushThrough(['Yield:10']);
616 - expect(callback).toHaveBeenCalledTimes(0);
617 -
618 - // Simulate time moving forward while frame is paused.
619 - advanceTimeBy(100); // 15 -> 115
620 -
621 - // Interrupt with higher priority work.
622 - // The interrupted work simulates an additional 5ms of time.
623 - renderer.unstable_flushSync(() => {
624 - renderer.update(
547 + // Render partially, but run out of time before completing.
548 + const renderer = ReactTestRenderer.create(
549 <React.unstable_Profiler id="test" onRender={callback}>
626 - <Yield renderTime={5} />
550 + <Yield renderTime={2} />
551 + <Yield renderTime={3} />
552 </React.unstable_Profiler>,
553 + {unstable_isAsync: true},
554 );
555 + expect(renderer).toFlushThrough(['Yield:2']);
556 + expect(callback).toHaveBeenCalledTimes(0);
557 +
558 + // Resume render for remaining children.
559 + expect(renderer).toFlushAll(['Yield:3']);
560 +
561 + // Verify that logged times include both durations above.
562 + expect(callback).toHaveBeenCalledTimes(1);
563 + const [call] = callback.mock.calls;
564 + expect(call[2]).toBe(5); // actual time
565 + expect(call[3]).toBe(5); // base time
566 + expect(call[4]).toBe(5); // start time
567 + expect(call[5]).toBe(10); // commit time
568 });
630 - expect(ReactTestRenderer).toClearYields(['Yield:5']);
569
632 - // The initial work was thrown away in this case,
633 - // So the actual and base times should only include the final rendered tree times.
634 - expect(callback).toHaveBeenCalledTimes(1);
635 - let call = callback.mock.calls[0];
636 - expect(call[2]).toBe(5); // actual time
637 - expect(call[3]).toBe(5); // base time
638 - expect(call[4]).toBe(115); // start time
639 - expect(call[5]).toBe(120); // commit time
640 -
641 - callback.mockReset();
642 -
643 - // Verify no more unexpected callbacks from low priority work
644 - expect(renderer).toFlushAll([]);
645 - expect(callback).toHaveBeenCalledTimes(0);
646 - });
570 + it('should not include time between frames', () => {
571 + const callback = jest.fn();
572
648 - it('should report the expected times when a high-priority update replaces a low-priority update', () => {
649 - const callback = jest.fn();
573 + const Yield = ({renderTime}) => {
574 + advanceTimeBy(renderTime);
575 + ReactTestRenderer.unstable_yield('Yield:' + renderTime);
576 + return null;
577 + };
578
651 - const Yield = ({renderTime}) => {
652 - advanceTimeBy(renderTime);
653 - ReactTestRenderer.unstable_yield('Yield:' + renderTime);
654 - return null;
655 - };
579 + advanceTimeBy(5); // 0 -> 5
580
657 - advanceTimeBy(5); // 0 -> 5
658 -
659 - const renderer = ReactTestRenderer.create(
660 - <React.unstable_Profiler id="test" onRender={callback}>
661 - <Yield renderTime={6} />
662 - <Yield renderTime={15} />
663 - </React.unstable_Profiler>,
664 - {unstable_isAsync: true},
665 - );
666 -
667 - // Render everything initially.
668 - // This should take 21 seconds of actual and base time.
669 - expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
670 - expect(callback).toHaveBeenCalledTimes(1);
671 - let call = callback.mock.calls[0];
672 - expect(call[2]).toBe(21); // actual time
673 - expect(call[3]).toBe(21); // base time
674 - expect(call[4]).toBe(5); // start time
675 - expect(call[5]).toBe(26); // commit time
676 -
677 - callback.mockReset();
678 -
679 - advanceTimeBy(30); // 26 -> 56
680 -
681 - // Render a partially update, but don't finish.
682 - // This partial render should take 3ms of simulated time.
683 - renderer.update(
684 - <React.unstable_Profiler id="test" onRender={callback}>
685 - <Yield renderTime={3} />
686 - <Yield renderTime={5} />
687 - <Yield renderTime={9} />
688 - </React.unstable_Profiler>,
689 - );
690 - expect(renderer).toFlushThrough(['Yield:3']);
691 - expect(callback).toHaveBeenCalledTimes(0);
581 + // Render partially, but don't finish.
582 + // This partial render should take 5ms of simulated time.
583 + const renderer = ReactTestRenderer.create(
584 + <React.unstable_Profiler id="outer" onRender={callback}>
585 + <Yield renderTime={5} />
586 + <Yield renderTime={10} />
587 + <React.unstable_Profiler id="inner" onRender={callback}>
588 + <Yield renderTime={17} />
589 + </React.unstable_Profiler>
590 + </React.unstable_Profiler>,
591 + {unstable_isAsync: true},
592 + );
593 + expect(renderer).toFlushThrough(['Yield:5']);
594 + expect(callback).toHaveBeenCalledTimes(0);
595 +
596 + // Simulate time moving forward while frame is paused.
597 + advanceTimeBy(50); // 10 -> 60
598 +
599 + // Flush the remaninig work,
600 + // Which should take an additional 10ms of simulated time.
601 + expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
602 + expect(callback).toHaveBeenCalledTimes(2);
603 +
604 + const [innerCall, outerCall] = callback.mock.calls;
605 +
606 + // Verify that the actual time includes all work times,
607 + // But not the time that elapsed between frames.
608 + expect(innerCall[0]).toBe('inner');
609 + expect(innerCall[2]).toBe(17); // actual time
610 + expect(innerCall[3]).toBe(17); // base time
611 + expect(innerCall[4]).toBe(70); // start time
612 + expect(innerCall[5]).toBe(87); // commit time
613 + expect(outerCall[0]).toBe('outer');
614 + expect(outerCall[2]).toBe(32); // actual time
615 + expect(outerCall[3]).toBe(32); // base time
616 + expect(outerCall[4]).toBe(5); // start time
617 + expect(outerCall[5]).toBe(87); // commit time
618 + });
619
693 - // Simulate time moving forward while frame is paused.
694 - advanceTimeBy(100); // 59 -> 159
620 + it('should report the expected times when a high-pri update replaces a mount in-progress', () => {
621 + const callback = jest.fn();
622
696 - // Render another 5ms of simulated time.
697 - expect(renderer).toFlushThrough(['Yield:5']);
698 - expect(callback).toHaveBeenCalledTimes(0);
623 + const Yield = ({renderTime}) => {
624 + advanceTimeBy(renderTime);
625 + ReactTestRenderer.unstable_yield('Yield:' + renderTime);
626 + return null;
627 + };
628
700 - // Simulate time moving forward while frame is paused.
701 - advanceTimeBy(100); // 164 -> 264
629 + advanceTimeBy(5); // 0 -> 5
630
703 - // Interrupt with higher priority work.
704 - // The interrupted work simulates an additional 11ms of time.
705 - renderer.unstable_flushSync(() => {
706 - renderer.update(
631 + // Render a partially update, but don't finish.
632 + // This partial render should take 10ms of simulated time.
633 + const renderer = ReactTestRenderer.create(
634 <React.unstable_Profiler id="test" onRender={callback}>
708 - <Yield renderTime={11} />
635 + <Yield renderTime={10} />
636 + <Yield renderTime={20} />
637 </React.unstable_Profiler>,
638 + {unstable_isAsync: true},
639 );
711 - });
712 - expect(ReactTestRenderer).toClearYields(['Yield:11']);
640 + expect(renderer).toFlushThrough(['Yield:10']);
641 + expect(callback).toHaveBeenCalledTimes(0);
642
714 - // The actual time should include only the most recent render,
715 - // Because this lets us avoid a lot of commit phase reset complexity.
716 - // The base time includes only the final rendered tree times.
717 - expect(callback).toHaveBeenCalledTimes(1);
718 - call = callback.mock.calls[0];
719 - expect(call[2]).toBe(11); // actual time
720 - expect(call[3]).toBe(11); // base time
721 - expect(call[4]).toBe(264); // start time
722 - expect(call[5]).toBe(275); // commit time
723 -
724 - // Verify no more unexpected callbacks from low priority work
725 - expect(renderer).toFlushAll([]);
726 - expect(callback).toHaveBeenCalledTimes(1);
727 - });
728 -
729 - it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
730 - const callback = jest.fn();
643 + // Simulate time moving forward while frame is paused.
644 + advanceTimeBy(100); // 15 -> 115
645
732 - const Yield = ({renderTime}) => {
733 - advanceTimeBy(renderTime);
734 - ReactTestRenderer.unstable_yield('Yield:' + renderTime);
735 - return null;
736 - };
737 -
738 - let first;
739 - class FirstComponent extends React.Component {
740 - state = {renderTime: 1};
741 - render() {
742 - first = this;
743 - advanceTimeBy(this.state.renderTime);
744 - ReactTestRenderer.unstable_yield(
745 - 'FirstComponent:' + this.state.renderTime,
746 - );
747 - return <Yield renderTime={4} />;
748 - }
749 - }
750 - let second;
751 - class SecondComponent extends React.Component {
752 - state = {renderTime: 2};
753 - render() {
754 - second = this;
755 - advanceTimeBy(this.state.renderTime);
756 - ReactTestRenderer.unstable_yield(
757 - 'SecondComponent:' + this.state.renderTime,
646 + // Interrupt with higher priority work.
647 + // The interrupted work simulates an additional 5ms of time.
648 + renderer.unstable_flushSync(() => {
649 + renderer.update(
650 + <React.unstable_Profiler id="test" onRender={callback}>
651 + <Yield renderTime={5} />
652 + </React.unstable_Profiler>,
653 );
759 - return <Yield renderTime={7} />;
760 - }
761 - }
762 -
763 - advanceTimeBy(5); // 0 -> 5
764 -
765 - const renderer = ReactTestRenderer.create(
766 - <React.unstable_Profiler id="test" onRender={callback} foo="1">
767 - <FirstComponent />
768 - <SecondComponent />
769 - </React.unstable_Profiler>,
770 - {unstable_isAsync: true},
771 - );
772 -
773 - // Render everything initially.
774 - // This simulates a total of 14ms of actual render time.
775 - // The base render time is also 14ms for the initial render.
776 - expect(renderer).toFlushAll([
777 - 'FirstComponent:1',
778 - 'Yield:4',
779 - 'SecondComponent:2',
780 - 'Yield:7',
781 - ]);
782 - expect(callback).toHaveBeenCalledTimes(1);
783 - let call = callback.mock.calls[0];
784 - expect(call[2]).toBe(14); // actual time
785 - expect(call[3]).toBe(14); // base time
786 - expect(call[4]).toBe(5); // start time
787 - expect(call[5]).toBe(19); // commit time
788 -
789 - callback.mockClear();
790 -
791 - advanceTimeBy(100); // 19 -> 119
792 -
793 - // Render a partially update, but don't finish.
794 - // This partial render will take 10ms of actual render time.
795 - first.setState({renderTime: 10});
796 - expect(renderer).toFlushThrough(['FirstComponent:10']);
797 - expect(callback).toHaveBeenCalledTimes(0);
798 -
799 - // Simulate time moving forward while frame is paused.
800 - advanceTimeBy(100); // 129 -> 229
654 + });
655 + expect(ReactTestRenderer).toClearYields(['Yield:5']);
656 +
657 + // The initial work was thrown away in this case,
658 + // So the actual and base times should only include the final rendered tree times.
659 + expect(callback).toHaveBeenCalledTimes(1);
660 + let call = callback.mock.calls[0];
661 + expect(call[2]).toBe(5); // actual time
662 + expect(call[3]).toBe(5); // base time
663 + expect(call[4]).toBe(115); // start time
664 + expect(call[5]).toBe(120); // commit time
665 +
666 + callback.mockReset();
667 +
668 + // Verify no more unexpected callbacks from low priority work
669 + expect(renderer).toFlushAll([]);
670 + expect(callback).toHaveBeenCalledTimes(0);
671 + });
672
802 - // Interrupt with higher priority work.
803 - // This simulates a total of 37ms of actual render time.
804 - renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
805 - expect(ReactTestRenderer).toClearYields([
806 - 'SecondComponent:30',
807 - 'Yield:7',
808 - ]);
673 + it('should report the expected times when a high-priority update replaces a low-priority update', () => {
674 + const callback = jest.fn();
675
810 - // The actual time should include only the most recent render (37ms),
811 - // Because this greatly simplifies the commit phase logic.
812 - // The base time should include the more recent times for the SecondComponent subtree,
813 - // As well as the original times for the FirstComponent subtree.
814 - expect(callback).toHaveBeenCalledTimes(1);
815 - call = callback.mock.calls[0];
816 - expect(call[2]).toBe(37); // actual time
817 - expect(call[3]).toBe(42); // base time
818 - expect(call[4]).toBe(229); // start time
819 - expect(call[5]).toBe(266); // commit time
676 + const Yield = ({renderTime}) => {
677 + advanceTimeBy(renderTime);
678 + ReactTestRenderer.unstable_yield('Yield:' + renderTime);
679 + return null;
680 + };
681
821 - callback.mockClear();
682 + advanceTimeBy(5); // 0 -> 5
683
823 - // Simulate time moving forward while frame is paused.
824 - advanceTimeBy(100); // 266 -> 366
684 + const renderer = ReactTestRenderer.create(
685 + <React.unstable_Profiler id="test" onRender={callback}>
686 + <Yield renderTime={6} />
687 + <Yield renderTime={15} />
688 + </React.unstable_Profiler>,
689 + {unstable_isAsync: true},
690 + );
691
826 - // Resume the original low priority update, with rebased state.
827 - // This simulates a total of 14ms of actual render time,
828 - // And does not include the original (interrupted) 10ms.
829 - // The tree contains 42ms of base render time at this point,
830 - // Reflecting the most recent (longer) render durations.
831 - // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
832 - expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
833 - expect(callback).toHaveBeenCalledTimes(1);
834 - call = callback.mock.calls[0];
835 - expect(call[2]).toBe(14); // actual time
836 - expect(call[3]).toBe(51); // base time
837 - expect(call[4]).toBe(366); // start time
838 - expect(call[5]).toBe(380); // commit time
839 - });
692 + // Render everything initially.
693 + // This should take 21 seconds of actual and base time.
694 + expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
695 + expect(callback).toHaveBeenCalledTimes(1);
696 + let call = callback.mock.calls[0];
697 + expect(call[2]).toBe(21); // actual time
698 + expect(call[3]).toBe(21); // base time
699 + expect(call[4]).toBe(5); // start time
700 + expect(call[5]).toBe(26); // commit time
701
841 - [true, false].forEach(flagEnabled => {
842 - describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
843 - flagEnabled ? 'enabled' : 'disabled'
844 - }`, () => {
845 - beforeEach(() => {
846 - jest.resetModules();
702 + callback.mockReset();
703
848 - loadModules({
849 - replayFailedUnitOfWorkWithInvokeGuardedCallback: flagEnabled,
850 - });
851 - mockNowForTests();
852 - });
704 + advanceTimeBy(30); // 26 -> 56
705
854 - it('should accumulate actual time after an error handled by componentDidCatch()', () => {
855 - const callback = jest.fn();
706 + // Render a partially update, but don't finish.
707 + // This partial render should take 3ms of simulated time.
708 + renderer.update(
709 + <React.unstable_Profiler id="test" onRender={callback}>
710 + <Yield renderTime={3} />
711 + <Yield renderTime={5} />
712 + <Yield renderTime={9} />
713 + </React.unstable_Profiler>,
714 + );
715 + expect(renderer).toFlushThrough(['Yield:3']);
716 + expect(callback).toHaveBeenCalledTimes(0);
717
857 - const ThrowsError = () => {
858 - advanceTimeBy(3);
859 - throw Error('expected error');
860 - };
718 + // Simulate time moving forward while frame is paused.
719 + advanceTimeBy(100); // 59 -> 159
720
862 - class ErrorBoundary extends React.Component {
863 - state = {error: null};
864 - componentDidCatch(error) {
865 - this.setState({error});
866 - }
867 - render() {
868 - advanceTimeBy(2);
869 - return this.state.error === null ? (
870 - this.props.children
871 - ) : (
872 - <AdvanceTime byAmount={20} />
873 - );
874 - }
875 - }
721 + // Render another 5ms of simulated time.
722 + expect(renderer).toFlushThrough(['Yield:5']);
723 + expect(callback).toHaveBeenCalledTimes(0);
724
877 - advanceTimeBy(5); // 0 -> 5
725 + // Simulate time moving forward while frame is paused.
726 + advanceTimeBy(100); // 164 -> 264
727
879 - ReactTestRenderer.create(
728 + // Interrupt with higher priority work.
729 + // The interrupted work simulates an additional 11ms of time.
730 + renderer.unstable_flushSync(() => {
731 + renderer.update(
732 <React.unstable_Profiler id="test" onRender={callback}>
881 - <ErrorBoundary>
882 - <AdvanceTime byAmount={9} />
883 - <ThrowsError />
884 - </ErrorBoundary>
733 + <Yield renderTime={11} />
734 </React.unstable_Profiler>,
735 );
887 -
888 - expect(callback).toHaveBeenCalledTimes(2);
889 -
890 - // Callbacks bubble (reverse order).
891 - let [mountCall, updateCall] = callback.mock.calls;
892 -
893 - // The initial mount only includes the ErrorBoundary (which takes 2)
894 - // But it spends time rendering all of the failed subtree also.
895 - expect(mountCall[1]).toBe('mount');
896 - // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
897 - // We don't count the time spent in replaying the failed unit of work (ThrowsError)
898 - expect(mountCall[2]).toBe(14);
899 - // base time includes: 2 (ErrorBoundary)
900 - expect(mountCall[3]).toBe(2);
901 - // start time
902 - expect(mountCall[4]).toBe(5);
903 - // commit time: 5 initially + 14 of work
904 - // Add an additional 3 (ThrowsError) if we replaced the failed work
905 - expect(mountCall[5]).toBe(__DEV__ && flagEnabled ? 22 : 19);
906 -
907 - // The update includes the ErrorBoundary and its fallback child
908 - expect(updateCall[1]).toBe('update');
909 - // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
910 - expect(updateCall[2]).toBe(22);
911 - // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
912 - expect(updateCall[3]).toBe(22);
913 - // start time
914 - expect(updateCall[4]).toBe(__DEV__ && flagEnabled ? 22 : 19);
915 - // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
916 - // Add an additional 3 (ThrowsError) if we replaced the failed work
917 - expect(updateCall[5]).toBe(__DEV__ && flagEnabled ? 44 : 41);
736 });
737 + expect(ReactTestRenderer).toClearYields(['Yield:11']);
738 +
739 + // The actual time should include only the most recent render,
740 + // Because this lets us avoid a lot of commit phase reset complexity.
741 + // The base time includes only the final rendered tree times.
742 + expect(callback).toHaveBeenCalledTimes(1);
743 + call = callback.mock.calls[0];
744 + expect(call[2]).toBe(11); // actual time
745 + expect(call[3]).toBe(11); // base time
746 + expect(call[4]).toBe(264); // start time
747 + expect(call[5]).toBe(275); // commit time
748 +
749 + // Verify no more unexpected callbacks from low priority work
750 + expect(renderer).toFlushAll([]);
751 + expect(callback).toHaveBeenCalledTimes(1);
752 + });
753
920 - it('should accumulate actual time after an error handled by getDerivedStateFromCatch()', () => {
921 - const callback = jest.fn();
754 + it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
755 + const callback = jest.fn();
756
923 - const ThrowsError = () => {
924 - advanceTimeBy(10);
925 - throw Error('expected error');
926 - };
757 + const Yield = ({renderTime}) => {
758 + advanceTimeBy(renderTime);
759 + ReactTestRenderer.unstable_yield('Yield:' + renderTime);
760 + return null;
761 + };
762
928 - class ErrorBoundary extends React.Component {
929 - state = {error: null};
930 - static getDerivedStateFromCatch(error) {
931 - return {error};
932 - }
933 - render() {
934 - advanceTimeBy(2);
935 - return this.state.error === null ? (
936 - this.props.children
937 - ) : (
938 - <AdvanceTime byAmount={20} />
939 - );
940 - }
763 + let first;
764 + class FirstComponent extends React.Component {
765 + state = {renderTime: 1};
766 + render() {
767 + first = this;
768 + advanceTimeBy(this.state.renderTime);
769 + ReactTestRenderer.unstable_yield(
770 + 'FirstComponent:' + this.state.renderTime,
771 + );
772 + return <Yield renderTime={4} />;
773 + }
774 + }
775 + let second;
776 + class SecondComponent extends React.Component {
777 + state = {renderTime: 2};
778 + render() {
779 + second = this;
780 + advanceTimeBy(this.state.renderTime);
781 + ReactTestRenderer.unstable_yield(
782 + 'SecondComponent:' + this.state.renderTime,
783 + );
784 + return <Yield renderTime={7} />;
785 }
786 + }
787
943 - advanceTimeBy(5); // 0 -> 5
788 + advanceTimeBy(5); // 0 -> 5
789
945 - ReactTestRenderer.create(
946 - <React.unstable_Profiler id="test" onRender={callback}>
947 - <ErrorBoundary>
948 - <AdvanceTime byAmount={5} />
949 - <ThrowsError />
950 - </ErrorBoundary>
951 - </React.unstable_Profiler>,
952 - );
790 + const renderer = ReactTestRenderer.create(
791 + <React.unstable_Profiler id="test" onRender={callback}>
792 + <FirstComponent />
793 + <SecondComponent />
794 + </React.unstable_Profiler>,
795 + {unstable_isAsync: true},
796 + );
797
954 - expect(callback).toHaveBeenCalledTimes(1);
955 -
956 - // Callbacks bubble (reverse order).
957 - let [mountCall] = callback.mock.calls;
958 -
959 - // The initial mount includes the ErrorBoundary's error state,
960 - // But i also spends actual time rendering UI that fails and isn't included.
961 - expect(mountCall[1]).toBe('mount');
962 - // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
963 - // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
964 - // We don't count the time spent in replaying the failed unit of work (ThrowsError)
965 - expect(mountCall[2]).toBe(39);
966 - // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
967 - expect(mountCall[3]).toBe(22);
968 - // start time
969 - expect(mountCall[4]).toBe(5);
970 - // commit time
971 - expect(mountCall[5]).toBe(__DEV__ && flagEnabled ? 54 : 44);
972 - });
798 + // Render everything initially.
799 + // This simulates a total of 14ms of actual render time.
800 + // The base render time is also 14ms for the initial render.
801 + expect(renderer).toFlushAll([
802 + 'FirstComponent:1',
803 + 'Yield:4',
804 + 'SecondComponent:2',
805 + 'Yield:7',
806 + ]);
807 + expect(callback).toHaveBeenCalledTimes(1);
808 + let call = callback.mock.calls[0];
809 + expect(call[2]).toBe(14); // actual time
810 + expect(call[3]).toBe(14); // base time
811 + expect(call[4]).toBe(5); // start time
812 + expect(call[5]).toBe(19); // commit time
813 +
814 + callback.mockClear();
815 +
816 + advanceTimeBy(100); // 19 -> 119
817 +
818 + // Render a partially update, but don't finish.
819 + // This partial render will take 10ms of actual render time.
820 + first.setState({renderTime: 10});
821 + expect(renderer).toFlushThrough(['FirstComponent:10']);
822 + expect(callback).toHaveBeenCalledTimes(0);
823 +
824 + // Simulate time moving forward while frame is paused.
825 + advanceTimeBy(100); // 129 -> 229
826 +
827 + // Interrupt with higher priority work.
828 + // This simulates a total of 37ms of actual render time.
829 + renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
830 + expect(ReactTestRenderer).toClearYields([
831 + 'SecondComponent:30',
832 + 'Yield:7',
833 + ]);
834 +
835 + // The actual time should include only the most recent render (37ms),
836 + // Because this greatly simplifies the commit phase logic.
837 + // The base time should include the more recent times for the SecondComponent subtree,
838 + // As well as the original times for the FirstComponent subtree.
839 + expect(callback).toHaveBeenCalledTimes(1);
840 + call = callback.mock.calls[0];
841 + expect(call[2]).toBe(37); // actual time
842 + expect(call[3]).toBe(42); // base time
843 + expect(call[4]).toBe(229); // start time
844 + expect(call[5]).toBe(266); // commit time
845 +
846 + callback.mockClear();
847 +
848 + // Simulate time moving forward while frame is paused.
849 + advanceTimeBy(100); // 266 -> 366
850 +
851 + // Resume the original low priority update, with rebased state.
852 + // This simulates a total of 14ms of actual render time,
853 + // And does not include the original (interrupted) 10ms.
854 + // The tree contains 42ms of base render time at this point,
855 + // Reflecting the most recent (longer) render durations.
856 + // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
857 + expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
858 + expect(callback).toHaveBeenCalledTimes(1);
859 + call = callback.mock.calls[0];
860 + expect(call[2]).toBe(14); // actual time
861 + expect(call[3]).toBe(51); // base time
862 + expect(call[4]).toBe(366); // start time
863 + expect(call[5]).toBe(380); // commit time
864 + });
865
974 - it('should reset the fiber stack correct after a "complete" phase error', () => {
975 - jest.resetModules();
866 + [true, false].forEach(
867 + replayFailedUnitOfWorkWithInvokeGuardedCallback => {
868 + describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
869 + replayFailedUnitOfWorkWithInvokeGuardedCallback
870 + ? 'enabled'
871 + : 'disabled'
872 + }`, () => {
873 + beforeEach(() => {
874 + jest.resetModules();
875 +
876 + loadModules({
877 + replayFailedUnitOfWorkWithInvokeGuardedCallback,
878 + });
879 + });
880 +
881 + it('should accumulate actual time after an error handled by componentDidCatch()', () => {
882 + const callback = jest.fn();
883 +
884 + const ThrowsError = () => {
885 + advanceTimeBy(3);
886 + throw Error('expected error');
887 + };
888 +
889 + class ErrorBoundary extends React.Component {
890 + state = {error: null};
891 + componentDidCatch(error) {
892 + this.setState({error});
893 + }
894 + render() {
895 + advanceTimeBy(2);
896 + return this.state.error === null ? (
897 + this.props.children
898 + ) : (
899 + <AdvanceTime byAmount={20} />
900 + );
901 + }
902 + }
903 +
904 + advanceTimeBy(5); // 0 -> 5
905 +
906 + ReactTestRenderer.create(
907 + <React.unstable_Profiler id="test" onRender={callback}>
908 + <ErrorBoundary>
909 + <AdvanceTime byAmount={9} />
910 + <ThrowsError />
911 + </ErrorBoundary>
912 + </React.unstable_Profiler>,
913 + );
914
977 - loadModules({
978 - useNoopRenderer: true,
979 - replayFailedUnitOfWorkWithInvokeGuardedCallback: flagEnabled,
980 - });
915 + expect(callback).toHaveBeenCalledTimes(2);
916 +
917 + // Callbacks bubble (reverse order).
918 + let [mountCall, updateCall] = callback.mock.calls;
919 +
920 + // The initial mount only includes the ErrorBoundary (which takes 2)
921 + // But it spends time rendering all of the failed subtree also.
922 + expect(mountCall[1]).toBe('mount');
923 + // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
924 + // We don't count the time spent in replaying the failed unit of work (ThrowsError)
925 + expect(mountCall[2]).toBe(14);
926 + // base time includes: 2 (ErrorBoundary)
927 + // Since the tree is empty for the initial commit
928 + expect(mountCall[3]).toBe(2);
929 + // start time
930 + expect(mountCall[4]).toBe(5);
931 + // commit time: 5 initially + 14 of work
932 + // Add an additional 3 (ThrowsError) if we replayed the failed work
933 + expect(mountCall[5]).toBe(
934 + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
935 + ? 22
936 + : 19,
937 + );
938
982 - // Simulate a renderer error during the "complete" phase.
983 - // This mimics behavior like React Native's View/Text nesting validation.
984 - ReactNoop.render(
985 - <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
986 - <errorInCompletePhase>hi</errorInCompletePhase>
987 - </React.unstable_Profiler>,
988 - );
989 - expect(ReactNoop.flush).toThrow('Error in host config.');
990 -
991 - // A similar case we've seen caused by an invariant in ReactDOM.
992 - // It didn't reproduce without a host component inside.
993 - ReactNoop.render(
994 - <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
995 - <errorInCompletePhase>
996 - <span>hi</span>
997 - </errorInCompletePhase>
998 - </React.unstable_Profiler>,
999 - );
1000 - expect(ReactNoop.flush).toThrow('Error in host config.');
939 + // The update includes the ErrorBoundary and its fallback child
940 + expect(updateCall[1]).toBe('update');
941 + // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
942 + expect(updateCall[2]).toBe(22);
943 + // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
944 + expect(updateCall[3]).toBe(22);
945 + // start time
946 + expect(updateCall[4]).toBe(
947 + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
948 + ? 22
949 + : 19,
950 + );
951 + // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
952 + // Add an additional 3 (ThrowsError) if we replayed the failed work
953 + expect(updateCall[5]).toBe(
954 + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
955 + ? 44
956 + : 41,
957 + );
958 + });
959 +
960 + it('should accumulate actual time after an error handled by getDerivedStateFromCatch()', () => {
961 + const callback = jest.fn();
962 +
963 + const ThrowsError = () => {
964 + advanceTimeBy(10);
965 + throw Error('expected error');
966 + };
967 +
968 + class ErrorBoundary extends React.Component {
969 + state = {error: null};
970 + static getDerivedStateFromCatch(error) {
971 + return {error};
972 + }
973 + render() {
974 + advanceTimeBy(2);
975 + return this.state.error === null ? (
976 + this.props.children
977 + ) : (
978 + <AdvanceTime byAmount={20} />
979 + );
980 + }
981 + }
982 +
983 + advanceTimeBy(5); // 0 -> 5
984 +
985 + ReactTestRenderer.create(
986 + <React.unstable_Profiler id="test" onRender={callback}>
987 + <ErrorBoundary>
988 + <AdvanceTime byAmount={5} />
989 + <ThrowsError />
990 + </ErrorBoundary>
991 + </React.unstable_Profiler>,
992 + );
993
1002 - // So long as the profiler timer's fiber stack is reset correctly,
1003 - // Subsequent renders should not error.
1004 - ReactNoop.render(
1005 - <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1006 - <span>hi</span>
1007 - </React.unstable_Profiler>,
1008 - );
1009 - ReactNoop.flush();
1010 - });
1011 - });
994 + expect(callback).toHaveBeenCalledTimes(1);
995 +
996 + // Callbacks bubble (reverse order).
997 + let [mountCall] = callback.mock.calls;
998 +
999 + // The initial mount includes the ErrorBoundary's error state,
1000 + // But it also spends actual time rendering UI that fails and isn't included.
1001 + expect(mountCall[1]).toBe('mount');
1002 + // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
1003 + // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
1004 + // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1005 + expect(mountCall[2]).toBe(39);
1006 + // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1007 + expect(mountCall[3]).toBe(22);
1008 + // start time
1009 + expect(mountCall[4]).toBe(5);
1010 + // commit time
1011 + expect(mountCall[5]).toBe(
1012 + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1013 + ? 54
1014 + : 44,
1015 + );
1016 + });
1017 +
1018 + it('should reset the fiber stack correct after a "complete" phase error', () => {
1019 + jest.resetModules();
1020 +
1021 + loadModules({
1022 + useNoopRenderer: true,
1023 + replayFailedUnitOfWorkWithInvokeGuardedCallback,
1024 + });
1025 +
1026 + // Simulate a renderer error during the "complete" phase.
1027 + // This mimics behavior like React Native's View/Text nesting validation.
1028 + ReactNoop.render(
1029 + <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1030 + <errorInCompletePhase>hi</errorInCompletePhase>
1031 + </React.unstable_Profiler>,
1032 + );
1033 + expect(ReactNoop.flush).toThrow('Error in host config.');
1034 +
1035 + // A similar case we've seen caused by an invariant in ReactDOM.
1036 + // It didn't reproduce without a host component inside.
1037 + ReactNoop.render(
1038 + <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1039 + <errorInCompletePhase>
1040 + <span>hi</span>
1041 + </errorInCompletePhase>
1042 + </React.unstable_Profiler>,
1043 + );
1044 + expect(ReactNoop.flush).toThrow('Error in host config.');
1045 +
1046 + // So long as the profiler timer's fiber stack is reset correctly,
1047 + // Subsequent renders should not error.
1048 + ReactNoop.render(
1049 + <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1050 + <span>hi</span>
1051 + </React.unstable_Profiler>,
1052 + );
1053 + ReactNoop.flush();
1054 + });
1055 + });
1056 + },
1057 + );
1058 });
1013 - });
1059
1015 - it('reflects the most recently rendered id value', () => {
1016 - const callback = jest.fn();
1060 + it('reflects the most recently rendered id value', () => {
1061 + const callback = jest.fn();
1062
1018 - advanceTimeBy(5); // 0 -> 5
1063 + advanceTimeBy(5); // 0 -> 5
1064
1020 - const renderer = ReactTestRenderer.create(
1021 - <React.unstable_Profiler id="one" onRender={callback}>
1022 - <AdvanceTime byAmount={2} />
1023 - </React.unstable_Profiler>,
1024 - );
1065 + const renderer = ReactTestRenderer.create(
1066 + <React.unstable_Profiler id="one" onRender={callback}>
1067 + <AdvanceTime byAmount={2} />
1068 + </React.unstable_Profiler>,
1069 + );
1070
1026 - expect(callback).toHaveBeenCalledTimes(1);
1071 + expect(callback).toHaveBeenCalledTimes(1);
1072
1028 - advanceTimeBy(20); // 7 -> 27
1073 + advanceTimeBy(20); // 7 -> 27
1074
1030 - renderer.update(
1031 - <React.unstable_Profiler id="two" onRender={callback}>
1032 - <AdvanceTime byAmount={1} />
1033 - </React.unstable_Profiler>,
1034 - );
1075 + renderer.update(
1076 + <React.unstable_Profiler id="two" onRender={callback}>
1077 + <AdvanceTime byAmount={1} />
1078 + </React.unstable_Profiler>,
1079 + );
1080
1036 - expect(callback).toHaveBeenCalledTimes(2);
1081 + expect(callback).toHaveBeenCalledTimes(2);
1082
1038 - const [mountCall, updateCall] = callback.mock.calls;
1083 + const [mountCall, updateCall] = callback.mock.calls;
1084
1040 - expect(mountCall[0]).toBe('one');
1041 - expect(mountCall[1]).toBe('mount');
1042 - expect(mountCall[2]).toBe(2); // actual time
1043 - expect(mountCall[3]).toBe(2); // base time
1044 - expect(mountCall[4]).toBe(5); // start time
1085 + expect(mountCall[0]).toBe('one');
1086 + expect(mountCall[1]).toBe('mount');
1087 + expect(mountCall[2]).toBe(2); // actual time
1088 + expect(mountCall[3]).toBe(2); // base time
1089 + expect(mountCall[4]).toBe(5); // start time
1090
1046 - expect(updateCall[0]).toBe('two');
1047 - expect(updateCall[1]).toBe('update');
1048 - expect(updateCall[2]).toBe(1); // actual time
1049 - expect(updateCall[3]).toBe(1); // base time
1050 - expect(updateCall[4]).toBe(27); // start time
1091 + expect(updateCall[0]).toBe('two');
1092 + expect(updateCall[1]).toBe('update');
1093 + expect(updateCall[2]).toBe(1); // actual time
1094 + expect(updateCall[3]).toBe(1); // base time
1095 + expect(updateCall[4]).toBe(27); // start time
1096 + });
1097 });
1098 });
1099
@@ -1110,4 +1156,1045 @@ describe('Profiler', () => {
1156
1157 expect(ReactNoop.getRoot('two').current.actualDuration).toBe(14);
1158 });
1159 +
1160 + describe('interaction tracking', () => {
1161 + let onInteractionScheduledWorkCompleted;
1162 + let onInteractionTracked;
1163 + let onWorkCanceled;
1164 + let onWorkScheduled;
1165 + let onWorkStarted;
1166 + let onWorkStopped;
1167 + let throwInOnInteractionScheduledWorkCompleted;
1168 + let throwInOnWorkScheduled;
1169 + let throwInOnWorkStarted;
1170 + let throwInOnWorkStopped;
1171 +
1172 + const getWorkForReactThreads = mockFn =>
1173 + mockFn.mock.calls.filter(([interactions, threadID]) => threadID > 0);
1174 +
1175 + beforeEach(() => {
1176 + jest.resetModules();
1177 +
1178 + loadModules({
1179 + enableInteractionTracking: true,
1180 + });
1181 +
1182 + throwInOnInteractionScheduledWorkCompleted = false;
1183 + throwInOnWorkScheduled = false;
1184 + throwInOnWorkStarted = false;
1185 + throwInOnWorkStopped = false;
1186 +
1187 + onInteractionScheduledWorkCompleted = jest.fn(() => {
1188 + if (throwInOnInteractionScheduledWorkCompleted) {
1189 + throw Error('Expected error onInteractionScheduledWorkCompleted');
1190 + }
1191 + });
1192 + onInteractionTracked = jest.fn();
1193 + onWorkCanceled = jest.fn();
1194 + onWorkScheduled = jest.fn(() => {
1195 + if (throwInOnWorkScheduled) {
1196 + throw Error('Expected error onWorkScheduled');
1197 + }
1198 + });
1199 + onWorkStarted = jest.fn(() => {
1200 + if (throwInOnWorkStarted) {
1201 + throw Error('Expected error onWorkStarted');
1202 + }
1203 + });
1204 + onWorkStopped = jest.fn(() => {
1205 + if (throwInOnWorkStopped) {
1206 + throw Error('Expected error onWorkStopped');
1207 + }
1208 + });
1209 +
1210 + // Verify interaction subscriber methods are called as expected.
1211 + InteractionTrackingSubscriptions.subscribe({
1212 + onInteractionScheduledWorkCompleted,
1213 + onInteractionTracked,
1214 + onWorkCanceled,
1215 + onWorkScheduled,
1216 + onWorkStarted,
1217 + onWorkStopped,
1218 + });
1219 + });
1220 +
1221 + describe('error handling', () => {
1222 + it('should cover errors thrown in onWorkScheduled', () => {
1223 + function Component({children}) {
1224 + ReactTestRenderer.unstable_yield('Component:' + children);
1225 + return children;
1226 + }
1227 +
1228 + let renderer;
1229 +
1230 + // Errors that happen inside of a subscriber should throw,
1231 + throwInOnWorkScheduled = true;
1232 + expect(() => {
1233 + InteractionTracking.track('event', mockNow(), () => {
1234 + renderer = ReactTestRenderer.create(<Component>fail</Component>, {
1235 + unstable_isAsync: true,
1236 + });
1237 + });
1238 + }).toThrow('Expected error onWorkScheduled');
1239 + throwInOnWorkScheduled = false;
1240 + expect(onWorkScheduled).toHaveBeenCalled();
1241 +
1242 + // But should not leave React in a broken state for subsequent renders.
1243 + renderer = ReactTestRenderer.create(<Component>succeed</Component>, {
1244 + unstable_isAsync: true,
1245 + });
1246 + expect(renderer).toFlushAll(['Component:succeed']);
1247 + const tree = renderer.toTree();
1248 + expect(tree.type).toBe(Component);
1249 + expect(tree.props.children).toBe('succeed');
1250 + });
1251 +
1252 + it('should cover errors thrown in onWorkStarted', () => {
1253 + function Component({children}) {
1254 + ReactTestRenderer.unstable_yield('Component:' + children);
1255 + return children;
1256 + }
1257 +
1258 + let renderer;
1259 + InteractionTracking.track('event', mockNow(), () => {
1260 + renderer = ReactTestRenderer.create(<Component>text</Component>, {
1261 + unstable_isAsync: true,
1262 + });
1263 + });
1264 + onWorkStarted.mockClear();
1265 +
1266 + // Errors that happen inside of a subscriber should throw,
1267 + throwInOnWorkStarted = true;
1268 + expect(() => {
1269 + expect(renderer).toFlushAll(['Component:text']);
1270 + }).toThrow('Expected error onWorkStarted');
1271 + throwInOnWorkStarted = false;
1272 + expect(onWorkStarted).toHaveBeenCalled();
1273 +
1274 + // But the React work should have still been processed.
1275 + expect(renderer).toFlushAll([]);
1276 + const tree = renderer.toTree();
1277 + expect(tree.type).toBe(Component);
1278 + expect(tree.props.children).toBe('text');
1279 + });
1280 +
1281 + it('should cover errors thrown in onWorkStopped', () => {
1282 + function Component({children}) {
1283 + ReactTestRenderer.unstable_yield('Component:' + children);
1284 + return children;
1285 + }
1286 +
1287 + let renderer;
1288 + InteractionTracking.track('event', mockNow(), () => {
1289 + renderer = ReactTestRenderer.create(<Component>text</Component>, {
1290 + unstable_isAsync: true,
1291 + });
1292 + });
1293 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1294 +
1295 + // Errors that happen in an on-stopped callback,
1296 + throwInOnWorkStopped = true;
1297 + expect(() => {
1298 + renderer.unstable_flushAll(['Component:text']);
1299 + }).toThrow('Expected error onWorkStopped');
1300 + throwInOnWorkStopped = false;
1301 + expect(onWorkStopped).toHaveBeenCalledTimes(2);
1302 +
1303 + // Should still commit the update,
1304 + const tree = renderer.toTree();
1305 + expect(tree.type).toBe(Component);
1306 + expect(tree.props.children).toBe('text');
1307 +
1308 + // And still call onInteractionScheduledWorkCompleted if the interaction is finished.
1309 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1310 + });
1311 +
1312 + it('should cover errors thrown in onInteractionScheduledWorkCompleted', () => {
1313 + function Component({children}) {
1314 + ReactTestRenderer.unstable_yield('Component:' + children);
1315 + return children;
1316 + }
1317 +
1318 + const eventOne = {
1319 + id: 0,
1320 + name: 'event one',
1321 + timestamp: mockNow(),
1322 + };
1323 + const eventTwo = {
1324 + id: 1,
1325 + name: 'event two',
1326 + timestamp: mockNow(),
1327 + };
1328 +
1329 + let renderer;
1330 + InteractionTracking.track(eventOne.name, mockNow(), () => {
1331 + InteractionTracking.track(eventTwo.name, mockNow(), () => {
1332 + renderer = ReactTestRenderer.create(<Component>text</Component>, {
1333 + unstable_isAsync: true,
1334 + });
1335 + });
1336 + });
1337 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1338 +
1339 + throwInOnInteractionScheduledWorkCompleted = true;
1340 + expect(() => {
1341 + renderer.unstable_flushAll(['Component:text']);
1342 + }).toThrow('Expected error onInteractionScheduledWorkCompleted');
1343 +
1344 + // Even though an error is thrown for one completed interaction,
1345 + // The completed callback should be called for all completed interactions.
1346 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1347 + });
1348 + });
1349 +
1350 + it('should associate tracked events with their subsequent commits', () => {
1351 + let instance = null;
1352 +
1353 + const Yield = ({duration = 10, value}) => {
1354 + advanceTimeBy(duration);
1355 + ReactTestRenderer.unstable_yield(value);
1356 + return null;
1357 + };
1358 +
1359 + class Example extends React.Component {
1360 + state = {
1361 + count: 0,
1362 + };
1363 + render() {
1364 + instance = this;
1365 + return (
1366 + <React.Fragment>
1367 + <Yield value="first" />
1368 + {this.state.count}
1369 + <Yield value="last" />
1370 + </React.Fragment>
1371 + );
1372 + }
1373 + }
1374 +
1375 + advanceTimeBy(1);
1376 +
1377 + const interactionCreation = {
1378 + id: 0,
1379 + name: 'creation event',
1380 + timestamp: mockNow(),
1381 + };
1382 +
1383 + const onRender = jest.fn();
1384 + let renderer;
1385 + InteractionTracking.track(interactionCreation.name, mockNow(), () => {
1386 + renderer = ReactTestRenderer.create(
1387 + <React.unstable_Profiler id="test-profiler" onRender={onRender}>
1388 + <Example />
1389 + </React.unstable_Profiler>,
1390 + {
1391 + unstable_isAsync: true,
1392 + },
1393 + );
1394 + });
1395 +
1396 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1397 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1398 + interactionCreation,
1399 + );
1400 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1401 +
1402 + // The interaction-tracking package will notify of work started for the default thread,
1403 + // But React shouldn't notify until it's been flushed.
1404 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1405 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1406 +
1407 + // Work may have been scheduled multiple times.
1408 + // We only care that the subscriber was notified at least once.
1409 + // As for the thread ID- the actual value isn't important, only that there was one.
1410 + expect(onWorkScheduled).toHaveBeenCalled();
1411 + expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1412 + interactionCreation,
1413 + ]);
1414 + expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
1415 +
1416 + // Mount
1417 + renderer.unstable_flushAll(['first', 'last']);
1418 + expect(onRender).toHaveBeenCalledTimes(1);
1419 + let call = onRender.mock.calls[0];
1420 + expect(call[0]).toEqual('test-profiler');
1421 + expect(call[5]).toEqual(mockNow());
1422 + if (ReactFeatureFlags.enableInteractionTracking) {
1423 + expect(call[6]).toMatchInteractions([interactionCreation]);
1424 + }
1425 +
1426 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1427 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1428 + expect(
1429 + onInteractionScheduledWorkCompleted,
1430 + ).toHaveBeenLastNotifiedOfInteraction(interactionCreation);
1431 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1432 + expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
1433 + interactionCreation,
1434 + ]);
1435 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
1436 + expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
1437 + interactionCreation,
1438 + ]);
1439 +
1440 + onRender.mockClear();
1441 + onWorkScheduled.mockClear();
1442 + onWorkStarted.mockClear();
1443 + onWorkStopped.mockClear();
1444 +
1445 + advanceTimeBy(3);
1446 +
1447 + let didRunCallback = false;
1448 +
1449 + const interactionOne = {
1450 + id: 1,
1451 + name: 'initial event',
1452 + timestamp: mockNow(),
1453 + };
1454 + InteractionTracking.track(interactionOne.name, mockNow(), () => {
1455 + instance.setState({count: 1});
1456 +
1457 + // Update state again to verify our tracked interaction isn't registered twice
1458 + instance.setState({count: 2});
1459 +
1460 + // The interaction-tracking package will notify of work started for the default thread,
1461 + // But React shouldn't notify until it's been flushed.
1462 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1463 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1464 +
1465 + // Work may have been scheduled multiple times.
1466 + // We only care that the subscriber was notified at least once.
1467 + // As for the thread ID- the actual value isn't important, only that there was one.
1468 + expect(onWorkScheduled).toHaveBeenCalled();
1469 + expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1470 + interactionOne,
1471 + ]);
1472 + expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
1473 +
1474 + expect(renderer).toFlushThrough(['first']);
1475 + expect(onRender).not.toHaveBeenCalled();
1476 +
1477 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1478 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1479 + interactionOne,
1480 + );
1481 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1482 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1483 + expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions(
1484 + [interactionOne],
1485 + );
1486 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1487 +
1488 + renderer.unstable_flushAll(['last']);
1489 + expect(onRender).toHaveBeenCalledTimes(1);
1490 +
1491 + call = onRender.mock.calls[0];
1492 + expect(call[0]).toEqual('test-profiler');
1493 + expect(call[5]).toEqual(mockNow());
1494 + if (ReactFeatureFlags.enableInteractionTracking) {
1495 + expect(call[6]).toMatchInteractions([interactionOne]);
1496 + }
1497 +
1498 + didRunCallback = true;
1499 +
1500 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1501 + expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions(
1502 + [interactionOne],
1503 + );
1504 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
1505 + expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions(
1506 + [interactionOne],
1507 + );
1508 + });
1509 +
1510 + expect(didRunCallback).toBe(true);
1511 +
1512 + onRender.mockClear();
1513 + onWorkScheduled.mockClear();
1514 + onWorkStarted.mockClear();
1515 + onWorkStopped.mockClear();
1516 +
1517 + advanceTimeBy(17);
1518 +
1519 + // Verify that updating state again does not re-log our interaction.
1520 + instance.setState({count: 3});
1521 + renderer.unstable_flushAll(['first', 'last']);
1522 +
1523 + expect(onRender).toHaveBeenCalledTimes(1);
1524 + call = onRender.mock.calls[0];
1525 + expect(call[0]).toEqual('test-profiler');
1526 + expect(call[5]).toEqual(mockNow());
1527 + if (ReactFeatureFlags.enableInteractionTracking) {
1528 + expect(call[6]).toMatchInteractions([]);
1529 + }
1530 +
1531 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1532 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1533 + expect(
1534 + onInteractionScheduledWorkCompleted,
1535 + ).toHaveBeenLastNotifiedOfInteraction(interactionOne);
1536 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1537 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1538 +
1539 + onRender.mockClear();
1540 +
1541 + advanceTimeBy(3);
1542 +
1543 + // Verify that root updates are also associated with tracked events.
1544 + const interactionTwo = {
1545 + id: 2,
1546 + name: 'root update event',
1547 + timestamp: mockNow(),
1548 + };
1549 + InteractionTracking.track(interactionTwo.name, mockNow(), () => {
1550 + renderer.update(
1551 + <React.unstable_Profiler id="test-profiler" onRender={onRender}>
1552 + <Example />
1553 + </React.unstable_Profiler>,
1554 + );
1555 + });
1556 +
1557 + expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1558 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1559 + interactionTwo,
1560 + );
1561 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1562 +
1563 + // The interaction-tracking package will notify of work started for the default thread,
1564 + // But React shouldn't notify until it's been flushed.
1565 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1566 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1567 +
1568 + // Work may have been scheduled multiple times.
1569 + // We only care that the subscriber was notified at least once.
1570 + // As for the thread ID- the actual value isn't important, only that there was one.
1571 + expect(onWorkScheduled).toHaveBeenCalled();
1572 + expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1573 + interactionTwo,
1574 + ]);
1575 + expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
1576 +
1577 + renderer.unstable_flushAll(['first', 'last']);
1578 +
1579 + expect(onRender).toHaveBeenCalledTimes(1);
1580 + call = onRender.mock.calls[0];
1581 + expect(call[0]).toEqual('test-profiler');
1582 + expect(call[5]).toEqual(mockNow());
1583 + if (ReactFeatureFlags.enableInteractionTracking) {
1584 + expect(call[6]).toMatchInteractions([interactionTwo]);
1585 + }
1586 +
1587 + expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1588 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3);
1589 + expect(
1590 + onInteractionScheduledWorkCompleted,
1591 + ).toHaveBeenLastNotifiedOfInteraction(interactionTwo);
1592 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1593 + expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
1594 + interactionTwo,
1595 + ]);
1596 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
1597 + expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
1598 + interactionTwo,
1599 + ]);
1600 + });
1601 +
1602 + it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
1603 + const onRender = jest.fn();
1604 +
1605 + let first;
1606 + class FirstComponent extends React.Component {
1607 + state = {count: 0};
1608 + render() {
1609 + first = this;
1610 + ReactTestRenderer.unstable_yield('FirstComponent');
1611 + return null;
1612 + }
1613 + }
1614 + let second;
1615 + class SecondComponent extends React.Component {
1616 + state = {count: 0};
1617 + render() {
1618 + second = this;
1619 + ReactTestRenderer.unstable_yield('SecondComponent');
1620 + return null;
1621 + }
1622 + }
1623 +
1624 + advanceTimeBy(5);
1625 +
1626 + const renderer = ReactTestRenderer.create(
1627 + <React.unstable_Profiler id="test" onRender={onRender}>
1628 + <FirstComponent />
1629 + <SecondComponent />
1630 + </React.unstable_Profiler>,
1631 + {unstable_isAsync: true},
1632 + );
1633 +
1634 + // Initial mount.
1635 + renderer.unstable_flushAll(['FirstComponent', 'SecondComponent']);
1636 +
1637 + expect(onInteractionTracked).not.toHaveBeenCalled();
1638 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1639 +
1640 + onRender.mockClear();
1641 +
1642 + advanceTimeBy(100);
1643 +
1644 + const interactionLowPri = {
1645 + id: 0,
1646 + name: 'lowPri',
1647 + timestamp: mockNow(),
1648 + };
1649 +
1650 + InteractionTracking.track(interactionLowPri.name, mockNow(), () => {
1651 + // Render a partially update, but don't finish.
1652 + first.setState({count: 1});
1653 +
1654 + expect(onWorkScheduled).toHaveBeenCalled();
1655 + expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1656 + interactionLowPri,
1657 + ]);
1658 +
1659 + expect(renderer).toFlushThrough(['FirstComponent']);
1660 + expect(onRender).not.toHaveBeenCalled();
1661 +
1662 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1663 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1664 + interactionLowPri,
1665 + );
1666 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1667 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1668 + expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions(
1669 + [interactionLowPri],
1670 + );
1671 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1672 +
1673 + advanceTimeBy(100);
1674 +
1675 + const interactionHighPri = {
1676 + id: 1,
1677 + name: 'highPri',
1678 + timestamp: mockNow(),
1679 + };
1680 +
1681 + // Interrupt with higher priority work.
1682 + // This simulates a total of 37ms of actual render time.
1683 + renderer.unstable_flushSync(() => {
1684 + InteractionTracking.track(interactionHighPri.name, mockNow(), () => {
1685 + second.setState({count: 1});
1686 +
1687 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1688 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1689 + interactionHighPri,
1690 + );
1691 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1692 +
1693 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1694 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1695 + });
1696 + });
1697 + expect(ReactTestRenderer).toClearYields(['SecondComponent']);
1698 +
1699 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1700 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1701 + expect(
1702 + onInteractionScheduledWorkCompleted,
1703 + ).toHaveBeenLastNotifiedOfInteraction(interactionHighPri);
1704 +
1705 + // Verify the high priority update was associated with the high priority event.
1706 + expect(onRender).toHaveBeenCalledTimes(1);
1707 + let call = onRender.mock.calls[0];
1708 + expect(call[0]).toEqual('test');
1709 + expect(call[5]).toEqual(mockNow());
1710 + expect(call[6]).toMatchInteractions(
1711 + ReactFeatureFlags.enableInteractionTracking
1712 + ? [interactionLowPri, interactionHighPri]
1713 + : [],
1714 + );
1715 +
1716 + onRender.mockClear();
1717 +
1718 + advanceTimeBy(100);
1719 +
1720 + // Resume the original low priority update, with rebased state.
1721 + // Verify the low priority update was retained.
1722 + renderer.unstable_flushAll(['FirstComponent']);
1723 + expect(onRender).toHaveBeenCalledTimes(1);
1724 + call = onRender.mock.calls[0];
1725 + expect(call[0]).toEqual('test');
1726 + expect(call[5]).toEqual(mockNow());
1727 + expect(call[6]).toMatchInteractions(
1728 + ReactFeatureFlags.enableInteractionTracking
1729 + ? [interactionLowPri]
1730 + : [],
1731 + );
1732 +
1733 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1734 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1735 +
1736 + // Work might be started multiple times before being completed.
1737 + // This is okay; it's part of the interaction-tracking subscriber contract.
1738 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(3);
1739 + expect(getWorkForReactThreads(onWorkStarted)[1][0]).toMatchInteractions(
1740 + [interactionLowPri, interactionHighPri],
1741 + );
1742 + expect(getWorkForReactThreads(onWorkStarted)[2][0]).toMatchInteractions(
1743 + [interactionLowPri],
1744 + );
1745 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1746 + expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions(
1747 + [interactionLowPri, interactionHighPri],
1748 + );
1749 + expect(getWorkForReactThreads(onWorkStopped)[1][0]).toMatchInteractions(
1750 + [interactionLowPri],
1751 + );
1752 + });
1753 +
1754 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1755 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1756 + expect(
1757 + onInteractionScheduledWorkCompleted,
1758 + ).toHaveBeenLastNotifiedOfInteraction(interactionLowPri);
1759 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(3);
1760 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1761 + });
1762 +
1763 + it('should track work spawned by a commit phase lifecycle and setState callback', () => {
1764 + let instance;
1765 + class Example extends React.Component {
1766 + state = {
1767 + count: 0,
1768 + };
1769 + componentDidMount() {
1770 + advanceTimeBy(10); // Advance timer to keep commits separate
1771 + this.setState({count: 1}); // Intentional cascading update
1772 + }
1773 + componentDidUpdate(prevProps, prevState) {
1774 + if (this.state.count === 2 && prevState.count === 1) {
1775 + advanceTimeBy(10); // Advance timer to keep commits separate
1776 + this.setState({count: 3}); // Intentional cascading update
1777 + }
1778 + }
1779 + render() {
1780 + instance = this;
1781 + ReactTestRenderer.unstable_yield('Example:' + this.state.count);
1782 + return null;
1783 + }
1784 + }
1785 +
1786 + const interactionOne = {
1787 + id: 0,
1788 + name: 'componentDidMount test',
1789 + timestamp: mockNow(),
1790 + };
1791 +
1792 + // Initial mount.
1793 + const onRender = jest.fn();
1794 + let firstCommitTime = mockNow();
1795 + let renderer;
1796 + InteractionTracking.track(interactionOne.name, mockNow(), () => {
1797 + renderer = ReactTestRenderer.create(
1798 + <React.unstable_Profiler id="test" onRender={onRender}>
1799 + <Example />
1800 + </React.unstable_Profiler>,
1801 + {unstable_isAsync: true},
1802 + );
1803 + });
1804 +
1805 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1806 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1807 + interactionOne,
1808 + );
1809 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1810 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1811 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1812 +
1813 + renderer.unstable_flushAll(['Example:0', 'Example:1']);
1814 +
1815 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1816 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1817 + expect(
1818 + onInteractionScheduledWorkCompleted,
1819 + ).toHaveBeenLastNotifiedOfInteraction(interactionOne);
1820 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(2);
1821 + expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
1822 + interactionOne,
1823 + ]);
1824 + expect(getWorkForReactThreads(onWorkStarted)[1][0]).toMatchInteractions([
1825 + interactionOne,
1826 + ]);
1827 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1828 + expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
1829 + interactionOne,
1830 + ]);
1831 + expect(getWorkForReactThreads(onWorkStopped)[1][0]).toMatchInteractions([
1832 + interactionOne,
1833 + ]);
1834 +
1835 + expect(onRender).toHaveBeenCalledTimes(2);
1836 + let call = onRender.mock.calls[0];
1837 + expect(call[0]).toEqual('test');
1838 + expect(call[5]).toEqual(firstCommitTime);
1839 + expect(call[6]).toMatchInteractions(
1840 + ReactFeatureFlags.enableInteractionTracking ? [interactionOne] : [],
1841 + );
1842 + call = onRender.mock.calls[1];
1843 + expect(call[0]).toEqual('test');
1844 + expect(call[5]).toEqual(mockNow());
1845 + expect(call[6]).toMatchInteractions(
1846 + ReactFeatureFlags.enableInteractionTracking ? [interactionOne] : [],
1847 + );
1848 +
1849 + onRender.mockClear();
1850 +
1851 + const interactionTwo = {
1852 + id: 1,
1853 + name: 'componentDidUpdate test',
1854 + timestamp: mockNow(),
1855 + };
1856 +
1857 + // Cause an tracked, async update
1858 + InteractionTracking.track(interactionTwo.name, mockNow(), () => {
1859 + instance.setState({count: 2});
1860 + });
1861 + expect(onRender).not.toHaveBeenCalled();
1862 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1863 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1864 + interactionTwo,
1865 + );
1866 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1867 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(2);
1868 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1869 +
1870 + advanceTimeBy(5);
1871 +
1872 + // Flush async work (outside of tracked scope)
1873 + // This will cause an intentional cascading update from did-update
1874 + firstCommitTime = mockNow();
1875 + renderer.unstable_flushAll(['Example:2', 'Example:3']);
1876 +
1877 + expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1878 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1879 + expect(
1880 + onInteractionScheduledWorkCompleted,
1881 + ).toHaveBeenLastNotifiedOfInteraction(interactionTwo);
1882 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(4);
1883 + expect(getWorkForReactThreads(onWorkStarted)[2][0]).toMatchInteractions([
1884 + interactionTwo,
1885 + ]);
1886 + expect(getWorkForReactThreads(onWorkStarted)[3][0]).toMatchInteractions([
1887 + interactionTwo,
1888 + ]);
1889 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(4);
1890 + expect(getWorkForReactThreads(onWorkStopped)[2][0]).toMatchInteractions([
1891 + interactionTwo,
1892 + ]);
1893 + expect(getWorkForReactThreads(onWorkStopped)[3][0]).toMatchInteractions([
1894 + interactionTwo,
1895 + ]);
1896 +
1897 + // Verify the cascading commit is associated with the origin event
1898 + expect(onRender).toHaveBeenCalledTimes(2);
1899 + call = onRender.mock.calls[0];
1900 + expect(call[0]).toEqual('test');
1901 + expect(call[5]).toEqual(firstCommitTime);
1902 + expect(call[6]).toMatchInteractions(
1903 + ReactFeatureFlags.enableInteractionTracking ? [interactionTwo] : [],
1904 + );
1905 + call = onRender.mock.calls[1];
1906 + expect(call[0]).toEqual('test');
1907 + expect(call[5]).toEqual(mockNow());
1908 + expect(call[6]).toMatchInteractions(
1909 + ReactFeatureFlags.enableInteractionTracking ? [interactionTwo] : [],
1910 + );
1911 +
1912 + onRender.mockClear();
1913 +
1914 + const interactionThree = {
1915 + id: 2,
1916 + name: 'setState callback test',
1917 + timestamp: mockNow(),
1918 + };
1919 +
1920 + // Cause a cascading update from the setState callback
1921 + function callback() {
1922 + instance.setState({count: 6});
1923 + }
1924 + InteractionTracking.track(interactionThree.name, mockNow(), () => {
1925 + instance.setState({count: 5}, callback);
1926 + });
1927 + expect(onRender).not.toHaveBeenCalled();
1928 +
1929 + expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1930 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1931 + interactionThree,
1932 + );
1933 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1934 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(4);
1935 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(4);
1936 +
1937 + // Flush async work (outside of tracked scope)
1938 + // This will cause an intentional cascading update from the setState callback
1939 + firstCommitTime = mockNow();
1940 + renderer.unstable_flushAll(['Example:5', 'Example:6']);
1941 +
1942 + expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1943 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3);
1944 + expect(
1945 + onInteractionScheduledWorkCompleted,
1946 + ).toHaveBeenLastNotifiedOfInteraction(interactionThree);
1947 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(6);
1948 + expect(getWorkForReactThreads(onWorkStarted)[4][0]).toMatchInteractions([
1949 + interactionThree,
1950 + ]);
1951 + expect(getWorkForReactThreads(onWorkStarted)[5][0]).toMatchInteractions([
1952 + interactionThree,
1953 + ]);
1954 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(6);
1955 + expect(getWorkForReactThreads(onWorkStopped)[4][0]).toMatchInteractions([
1956 + interactionThree,
1957 + ]);
1958 + expect(getWorkForReactThreads(onWorkStopped)[5][0]).toMatchInteractions([
1959 + interactionThree,
1960 + ]);
1961 +
1962 + // Verify the cascading commit is associated with the origin event
1963 + expect(onRender).toHaveBeenCalledTimes(2);
1964 + call = onRender.mock.calls[0];
1965 + expect(call[0]).toEqual('test');
1966 + expect(call[5]).toEqual(firstCommitTime);
1967 + expect(call[6]).toMatchInteractions(
1968 + ReactFeatureFlags.enableInteractionTracking ? [interactionThree] : [],
1969 + );
1970 + call = onRender.mock.calls[1];
1971 + expect(call[0]).toEqual('test');
1972 + expect(call[5]).toEqual(mockNow());
1973 + expect(call[6]).toMatchInteractions(
1974 + ReactFeatureFlags.enableInteractionTracking ? [interactionThree] : [],
1975 + );
1976 + });
1977 +
1978 + it('should track interactions associated with a parent component state update', () => {
1979 + const onRender = jest.fn();
1980 + let parentInstance = null;
1981 +
1982 + class Child extends React.Component {
1983 + render() {
1984 + ReactTestRenderer.unstable_yield('Child:', this.props.count);
1985 + return null;
1986 + }
1987 + }
1988 +
1989 + class Parent extends React.Component {
1990 + state = {
1991 + count: 0,
1992 + };
1993 + render() {
1994 + parentInstance = this;
1995 + return (
1996 + <React.unstable_Profiler id="test-profiler" onRender={onRender}>
1997 + <Child count={this.state.count} />
1998 + </React.unstable_Profiler>
1999 + );
2000 + }
2001 + }
2002 +
2003 + advanceTimeBy(1);
2004 +
2005 + const renderer = ReactTestRenderer.create(<Parent />, {
2006 + unstable_isAsync: true,
2007 + });
2008 + renderer.unstable_flushAll(['Child:0']);
2009 + onRender.mockClear();
2010 +
2011 + const interaction = {
2012 + id: 0,
2013 + name: 'parent interaction',
2014 + timestamp: mockNow(),
2015 + };
2016 +
2017 + InteractionTracking.track(interaction.name, mockNow(), () => {
2018 + parentInstance.setState({count: 1});
2019 + });
2020 +
2021 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2022 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
2023 + interaction,
2024 + );
2025 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2026 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
2027 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
2028 +
2029 + expect(onRender).not.toHaveBeenCalled();
2030 + renderer.unstable_flushAll(['Child:1']);
2031 + expect(onRender).toHaveBeenCalledTimes(1);
2032 + let call = onRender.mock.calls[0];
2033 + expect(call[0]).toEqual('test-profiler');
2034 + expect(call[6]).toMatchInteractions(
2035 + ReactFeatureFlags.enableInteractionTracking ? [interaction] : [],
2036 + );
2037 +
2038 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2039 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2040 + expect(
2041 + onInteractionScheduledWorkCompleted,
2042 + ).toHaveBeenLastNotifiedOfInteraction(interaction);
2043 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
2044 + expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
2045 + interaction,
2046 + ]);
2047 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
2048 + expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
2049 + interaction,
2050 + ]);
2051 + });
2052 +
2053 + it('tracks both the temporary placeholder and the finished render for an interaction', async () => {
2054 + jest.resetModules();
2055 +
2056 + loadModules({
2057 + useNoopRenderer: true,
2058 + enableSuspense: true,
2059 + enableInteractionTracking: true,
2060 + });
2061 +
2062 + // Re-register since we've reloaded modules
2063 + InteractionTrackingSubscriptions.subscribe({
2064 + onInteractionScheduledWorkCompleted,
2065 + onInteractionTracked,
2066 + onWorkCanceled,
2067 + onWorkScheduled,
2068 + onWorkStarted,
2069 + onWorkStopped,
2070 + });
2071 +
2072 + function awaitableAdvanceTimers(ms) {
2073 + jest.advanceTimersByTime(ms);
2074 + // Wait until the end of the current tick
2075 + return new Promise(resolve => {
2076 + setImmediate(resolve);
2077 + });
2078 + }
2079 +
2080 + const SimpleCacheProvider = require('simple-cache-provider');
2081 + let cache;
2082 + function invalidateCache() {
2083 + cache = SimpleCacheProvider.createCache(invalidateCache);
2084 + }
2085 + invalidateCache();
2086 + const TextResource = SimpleCacheProvider.createResource(
2087 + ([text, ms = 0]) => {
2088 + return new Promise((resolve, reject) => {
2089 + setTimeout(() => {
2090 + ReactNoop.yield(`Promise resolved [${text}]`);
2091 + resolve(text);
2092 + }, ms);
2093 + });
2094 + },
2095 + ([text, ms]) => text,
2096 + );
2097 +
2098 + function Text(props) {
2099 + ReactNoop.yield(props.text);
2100 + return <span prop={props.text} />;
2101 + }
2102 +
2103 + function span(prop) {
2104 + return {type: 'span', children: [], prop};
2105 + }
2106 +
2107 + function AsyncText(props) {
2108 + const text = props.text;
2109 + try {
2110 + TextResource.read(cache, [props.text, props.ms]);
2111 + ReactNoop.yield(text);
2112 + return <span prop={text} />;
2113 + } catch (promise) {
2114 + if (typeof promise.then === 'function') {
2115 + ReactNoop.yield(`Suspend! [${text}]`);
2116 + } else {
2117 + ReactNoop.yield(`Error! [${text}]`);
2118 + }
2119 + throw promise;
2120 + }
2121 + }
2122 +
2123 + const interaction = {
2124 + id: 0,
2125 + name: 'initial render',
2126 + timestamp: mockNow(),
2127 + };
2128 +
2129 + const onRender = jest.fn();
2130 + InteractionTracking.track(interaction.name, mockNow(), () => {
2131 + ReactNoop.render(
2132 + <React.unstable_Profiler id="test-profiler" onRender={onRender}>
2133 + <React.Placeholder fallback={<Text text="Loading..." />}>
2134 + <AsyncText text="Async" ms={20000} />
2135 + </React.Placeholder>
2136 + <Text text="Sync" />
2137 + </React.unstable_Profiler>,
2138 + );
2139 + });
2140 +
2141 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2142 + expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
2143 + interaction,
2144 + );
2145 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2146 + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
2147 + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
2148 +
2149 + expect(ReactNoop.flush()).toEqual([
2150 + 'Suspend! [Async]',
2151 + 'Loading...',
2152 + 'Sync',
2153 + ]);
2154 + // The update hasn't expired yet, so we commit nothing.
2155 + expect(ReactNoop.getChildren()).toEqual([]);
2156 + expect(onRender).not.toHaveBeenCalled();
2157 +
2158 + // Advance both React's virtual time and Jest's timers by enough to expire
2159 + // the update, but not by enough to flush the suspending promise.
2160 + ReactNoop.expire(10000);
2161 + await awaitableAdvanceTimers(10000);
2162 + // No additional rendering work is required, since we already prepared
2163 + // the placeholder.
2164 + expect(ReactNoop.flushExpired()).toEqual([]);
2165 + // Should have committed the placeholder.
2166 + expect(ReactNoop.getChildren()).toEqual([
2167 + span('Loading...'),
2168 + span('Sync'),
2169 + ]);
2170 + expect(onRender).toHaveBeenCalledTimes(1);
2171 +
2172 + let call = onRender.mock.calls[0];
2173 + expect(call[0]).toEqual('test-profiler');
2174 + expect(call[6]).toMatchInteractions(
2175 + ReactFeatureFlags.enableInteractionTracking ? [interaction] : [],
2176 + );
2177 +
2178 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2179 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2180 +
2181 + // Once the promise resolves, we render the suspended view
2182 + await awaitableAdvanceTimers(10000);
2183 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
2184 + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
2185 + expect(onRender).toHaveBeenCalledTimes(2);
2186 +
2187 + call = onRender.mock.calls[1];
2188 + expect(call[0]).toEqual('test-profiler');
2189 + expect(call[6]).toMatchInteractions(
2190 + ReactFeatureFlags.enableInteractionTracking ? [interaction] : [],
2191 + );
2192 +
2193 + expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2194 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2195 + expect(
2196 + onInteractionScheduledWorkCompleted,
2197 + ).toHaveBeenLastNotifiedOfInteraction(interaction);
2198 + });
2199 + });
2200 });
packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
+53 -14
@@ -5,6 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 + * @jest-environment node
9 */
10
11 'use strict';
@@ -13,22 +14,12 @@ describe('ReactProfiler DevTools integration', () => {
14 let React;
15 let ReactFeatureFlags;
16 let ReactTestRenderer;
17 + let InteractionTracking;
18 let AdvanceTime;
19 let advanceTimeBy;
20 let hook;
21 let mockNow;
22
21 - const mockNowForTests = () => {
22 - let currentTime = 0;
23 -
24 - mockNow = jest.fn().mockImplementation(() => currentTime);
25 -
26 - ReactTestRenderer.unstable_setNowImplementation(mockNow);
27 - advanceTimeBy = amount => {
28 - currentTime += amount;
29 - };
30 - };
31 -
23 beforeEach(() => {
24 global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
25 inject: () => {},
@@ -39,12 +30,23 @@ describe('ReactProfiler DevTools integration', () => {
30
31 jest.resetModules();
32
33 + let currentTime = 0;
34 +
35 + mockNow = jest.fn().mockImplementation(() => currentTime);
36 +
37 + global.Date.now = mockNow;
38 +
39 ReactFeatureFlags = require('shared/ReactFeatureFlags');
40 ReactFeatureFlags.enableProfilerTimer = true;
41 + ReactFeatureFlags.enableInteractionTracking = true;
42 React = require('react');
43 ReactTestRenderer = require('react-test-renderer');
44 + InteractionTracking = require('interaction-tracking');
45
47 - mockNowForTests();
46 + ReactTestRenderer.unstable_setNowImplementation(mockNow);
47 + advanceTimeBy = amount => {
48 + currentTime += amount;
49 + };
50
51 AdvanceTime = class extends React.Component {
52 static defaultProps = {
@@ -85,7 +87,15 @@ describe('ReactProfiler DevTools integration', () => {
87 // The time spent in App (above the Profiler) won't be included in the durations,
88 // But needs to be accounted for in the offset times.
89 expect(onRender).toHaveBeenCalledTimes(1);
88 - expect(onRender).toHaveBeenCalledWith('Profiler', 'mount', 10, 10, 2, 12);
90 + expect(onRender).toHaveBeenCalledWith(
91 + 'Profiler',
92 + 'mount',
93 + 10,
94 + 10,
95 + 2,
96 + 12,
97 + new Set(),
98 + );
99 onRender.mockClear();
100
101 // Measure unobservable timing required by the DevTools profiler.
@@ -102,7 +112,15 @@ describe('ReactProfiler DevTools integration', () => {
112 // The time spent in App (above the Profiler) won't be included in the durations,
113 // But needs to be accounted for in the offset times.
114 expect(onRender).toHaveBeenCalledTimes(1);
105 - expect(onRender).toHaveBeenCalledWith('Profiler', 'update', 6, 13, 14, 20);
115 + expect(onRender).toHaveBeenCalledWith(
116 + 'Profiler',
117 + 'update',
118 + 6,
119 + 13,
120 + 14,
121 + 20,
122 + new Set(),
123 + );
124
125 // Measure unobservable timing required by the DevTools profiler.
126 // At this point, the base time should include both:
@@ -149,4 +167,25 @@ describe('ReactProfiler DevTools integration', () => {
167 rendered.root.findByType('div')._currentFiber().treeBaseDuration,
168 ).toBe(7);
169 });
170 +
171 + it('should store tracked interactions on the HostNode so DevTools can access them', () => {
172 + // Render without an interaction
173 + const rendered = ReactTestRenderer.create(<div />);
174 +
175 + const root = rendered.root._currentFiber().return;
176 + expect(root.stateNode.memoizedInteractions).toContainNoInteractions();
177 +
178 + advanceTimeBy(10);
179 +
180 + const eventTime = mockNow();
181 +
182 + // Render with an interaction
183 + InteractionTracking.track('some event', eventTime, () => {
184 + rendered.update(<div />);
185 + });
186 +
187 + expect(root.stateNode.memoizedInteractions).toMatchInteractions([
188 + {name: 'some event', timestamp: eventTime},
189 + ]);
190 + });
191 });
packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap
+72 -8
@@ -1,6 +1,6 @@
1 // Jest Snapshot v1, https://goo.gl/fbAQLP
2
3 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should render children 1`] = `
3 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:disabled should render children 1`] = `
4 <div>
5 <span>
6 outside span
@@ -14,11 +14,11 @@ exports[`Profiler works in profiling and non-profiling bundles enableProfilerTim
14 </div>
15 `;
16
17 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support an empty Profiler (with no children) 1`] = `null`;
17 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`;
18
19 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support an empty Profiler (with no children) 2`] = `<div />`;
19 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `<div />`;
20
21 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support nested Profilers 1`] = `
21 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:disabled should support nested Profilers 1`] = `
22 Array [
23 <div>
24 outer functional component
@@ -32,7 +32,7 @@ Array [
32 ]
33 `;
34
35 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should render children 1`] = `
35 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:enabled should render children 1`] = `
36 <div>
37 <span>
38 outside span
@@ -46,11 +46,75 @@ exports[`Profiler works in profiling and non-profiling bundles enableProfilerTim
46 </div>
47 `;
48
49 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support an empty Profiler (with no children) 1`] = `null`;
49 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`;
50
51 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support an empty Profiler (with no children) 2`] = `<div />`;
51 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `<div />`;
52
53 -exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support nested Profilers 1`] = `
53 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:disabled enableProfilerTimer:enabled should support nested Profilers 1`] = `
54 +Array [
55 + <div>
56 + outer functional component
57 + </div>,
58 + <block>
59 + inner class component
60 + </block>,
61 + <span>
62 + inner span
63 + </span>,
64 +]
65 +`;
66 +
67 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:disabled should render children 1`] = `
68 +<div>
69 + <span>
70 + outside span
71 + </span>
72 + <span>
73 + inside span
74 + </span>
75 + <span>
76 + functional component
77 + </span>
78 +</div>
79 +`;
80 +
81 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`;
82 +
83 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `<div />`;
84 +
85 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:disabled should support nested Profilers 1`] = `
86 +Array [
87 + <div>
88 + outer functional component
89 + </div>,
90 + <block>
91 + inner class component
92 + </block>,
93 + <span>
94 + inner span
95 + </span>,
96 +]
97 +`;
98 +
99 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:enabled should render children 1`] = `
100 +<div>
101 + <span>
102 + outside span
103 + </span>
104 + <span>
105 + inside span
106 + </span>
107 + <span>
108 + functional component
109 + </span>
110 +</div>
111 +`;
112 +
113 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`;
114 +
115 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `<div />`;
116 +
117 +exports[`Profiler works in profiling and non-profiling bundles enableInteractionTracking:enabled enableProfilerTimer:enabled should support nested Profilers 1`] = `
118 Array [
119 <div>
120 outer functional component
packages/shared/ReactFeatureFlags.js
+1 -1
@@ -40,7 +40,7 @@ export const warnAboutLegacyContextAPI = false;
40 export const enableProfilerTimer = __PROFILE__;
41
42 // Track which interactions trigger each commit.
43 -export const enableInteractionTracking = false;
43 +export const enableInteractionTracking = __PROFILE__;
44
45 // Only used in www builds.
46 export function addUserTimingListener() {
scripts/rollup/bundles.js
+10 -6
@@ -85,7 +85,7 @@ const bundles = [
85 moduleType: RENDERER,
86 entry: 'react-dom',
87 global: 'ReactDOM',
88 - externals: ['react'],
88 + externals: ['interaction-tracking', 'react'],
89 },
90
91 //******* Test Utils *******/
@@ -129,7 +129,7 @@ const bundles = [
129 moduleType: NON_FIBER_RENDERER,
130 entry: 'react-dom/server.browser',
131 global: 'ReactDOMServer',
132 - externals: ['react'],
132 + externals: ['interaction-tracking', 'react'],
133 },
134
135 {
@@ -137,7 +137,7 @@ const bundles = [
137 bundleTypes: [NODE_DEV, NODE_PROD],
138 moduleType: NON_FIBER_RENDERER,
139 entry: 'react-dom/server.node',
140 - externals: ['react', 'stream'],
140 + externals: ['interaction-tracking', 'react', 'stream'],
141 },
142
143 /******* React ART *******/
@@ -179,6 +179,7 @@ const bundles = [
179 'deepDiffer',
180 'deepFreezeAndThrowOnMutationInDev',
181 'flattenStyle',
182 + 'interaction-tracking',
183 'ReactNativeViewConfigRegistry',
184 ],
185 },
@@ -199,6 +200,7 @@ const bundles = [
200 'deepDiffer',
201 'deepFreezeAndThrowOnMutationInDev',
202 'flattenStyle',
203 + 'interaction-tracking',
204 'ReactNativeViewConfigRegistry',
205 ],
206 },
@@ -221,6 +223,7 @@ const bundles = [
223 'deepDiffer',
224 'deepFreezeAndThrowOnMutationInDev',
225 'flattenStyle',
226 + 'interaction-tracking',
227 'ReactNativeViewConfigRegistry',
228 ],
229 },
@@ -242,6 +245,7 @@ const bundles = [
245 'deepDiffer',
246 'deepFreezeAndThrowOnMutationInDev',
247 'flattenStyle',
248 + 'interaction-tracking',
249 'ReactNativeViewConfigRegistry',
250 ],
251 },
@@ -253,7 +257,7 @@ const bundles = [
257 moduleType: RENDERER,
258 entry: 'react-test-renderer',
259 global: 'ReactTestRenderer',
256 - externals: ['react'],
260 + externals: ['interaction-tracking', 'react'],
261 },
262
263 {
@@ -318,7 +322,7 @@ const bundles = [
322 moduleType: RECONCILER,
323 entry: 'react-reconciler',
324 global: 'ReactReconciler',
321 - externals: ['react'],
325 + externals: ['interaction-tracking', 'react'],
326 },
327
328 /******* React Persistent Reconciler *******/
@@ -328,7 +332,7 @@ const bundles = [
332 moduleType: RECONCILER,
333 entry: 'react-reconciler/persistent',
334 global: 'ReactPersistentReconciler',
331 - externals: ['react'],
335 + externals: ['interaction-tracking', 'react'],
336 },
337
338 /******* Reflection *******/
scripts/rollup/modules.js
+1
@@ -14,6 +14,7 @@ const HAS_NO_SIDE_EFFECTS_ON_IMPORT = false;
14 const importSideEffects = Object.freeze({
15 'prop-types/checkPropTypes': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
16 deepFreezeAndThrowOnMutationInDev: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
17 + 'interaction-tracking': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
18 });
19
20 // Bundles exporting globals that other modules rely on.