@samitouri / QOS-React / commits / 4a1072194f

Memoize promise listeners to prevent exponential growth (#14429)

* Memoize promise listeners to prevent exponential growth Previously, React would attach a new listener every time a promise is thrown, regardless of whether the same listener was already attached during a previous render. Because React attempts to render every time a promise resolves, the number of listeners grows quickly. This was especially bad in synchronous mode because the renders that happen when the promise pings are not batched together. So if a single promise has multiple listeners for the same root, there will be multiple renders, which in turn results in more listeners being added to the remaining unresolved promises. This results in exponential growth in the number of listeners with respect to the number of IO-bound components in a single render. Fixes #14220 * Memoize on the root and Suspense fiber instead of on the promise * Add TODO to fix persistent mode tests

Andrew Clark committed Dec 14, 2018 at 11:03 UTC 4a1072194fcef2da1aae2510886c274736017fbd
14 files changed +220 -119
packages/react-reconciler/src/ReactFiberBeginWork.js
+1
@@ -1474,6 +1474,7 @@ function updateSuspenseComponent(
1474 );
1475 }
1476 }
1477 + workInProgress.stateNode = current.stateNode;
1478 }
1479
1480 workInProgress.memoizedState = nextState;
packages/react-reconciler/src/ReactFiberCommitWork.js
+29
@@ -20,7 +20,9 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
20 import type {CapturedValue, CapturedError} from './ReactCapturedValue';
21 import type {SuspenseState} from './ReactFiberSuspenseComponent';
22 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
23 +import type {Thenable} from './ReactFiberScheduler';
24
25 +import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
26 import {
27 enableHooks,
28 enableSchedulerTracing,
@@ -88,6 +90,7 @@ import {
90 import {
91 captureCommitPhaseError,
92 requestCurrentTime,
93 + retryTimedOutBoundary,
94 } from './ReactFiberScheduler';
95 import {
96 NoEffect as NoHookEffect,
@@ -106,6 +109,8 @@ if (__DEV__) {
109 didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
110 }
111
112 +const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
113 +
114 export function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
115 const source = errorInfo.source;
116 let stack = errorInfo.stack;
@@ -1180,6 +1185,30 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1185 if (primaryChildParent !== null) {
1186 hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
1187 }
1188 +
1189 + // If this boundary just timed out, then it will have a set of thenables.
1190 + // For each thenable, attach a listener so that when it resolves, React
1191 + // attempts to re-render the boundary in the primary (pre-timeout) state.
1192 + const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
1193 + if (thenables !== null) {
1194 + finishedWork.updateQueue = null;
1195 + let retryCache = finishedWork.stateNode;
1196 + if (retryCache === null) {
1197 + retryCache = finishedWork.stateNode = new PossiblyWeakSet();
1198 + }
1199 + thenables.forEach(thenable => {
1200 + // Memoize using the boundary fiber to prevent redundant listeners.
1201 + let retry = retryTimedOutBoundary.bind(null, finishedWork, thenable);
1202 + if (enableSchedulerTracing) {
1203 + retry = Schedule_tracing_wrap(retry);
1204 + }
1205 + if (!retryCache.has(thenable)) {
1206 + retryCache.add(thenable);
1207 + thenable.then(retry, retry);
1208 + }
1209 + });
1210 + }
1211 +
1212 return;
1213 }
1214 case IncompleteClassComponent: {
packages/react-reconciler/src/ReactFiberPendingPriority.js
+5 -3
@@ -62,6 +62,10 @@ export function markCommittedPriorityLevels(
62 return;
63 }
64
65 + if (earliestRemainingTime < root.latestPingedTime) {
66 + root.latestPingedTime = NoWork;
67 + }
68 +
69 // Let's see if the previous latest known pending level was just flushed.
70 const latestPendingTime = root.latestPendingTime;
71 if (latestPendingTime !== NoWork) {
@@ -209,10 +213,8 @@ export function markPingedPriorityLevel(
213 }
214
215 function clearPing(root, completedTime) {
212 - // TODO: Track whether the root was pinged during the render phase. If so,
213 - // we need to make sure we don't lose track of it.
216 const latestPingedTime = root.latestPingedTime;
215 - if (latestPingedTime !== NoWork && latestPingedTime >= completedTime) {
217 + if (latestPingedTime >= completedTime) {
218 root.latestPingedTime = NoWork;
219 }
220 }
packages/react-reconciler/src/ReactFiberRoot.js
+10
@@ -10,6 +10,7 @@
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
13 +import type {Thenable} from './ReactFiberScheduler';
14 import type {Interaction} from 'scheduler/src/Tracing';
15
16 import {noTimeout} from './ReactFiberHostConfig';
@@ -51,6 +52,11 @@ type BaseFiberRootProperties = {|
52 // be retried.
53 latestPingedTime: ExpirationTime,
54
55 + pingCache:
56 + | WeakMap<Thenable, Set<ExpirationTime>>
57 + | Map<Thenable, Set<ExpirationTime>>
58 + | null,
59 +
60 // If an error is thrown, and there are no more updates in the queue, we try
61 // rendering from the root one more time, synchronously, before handling
62 // the error.
@@ -121,6 +127,8 @@ export function createFiberRoot(
127 latestSuspendedTime: NoWork,
128 latestPingedTime: NoWork,
129
130 + pingCache: null,
131 +
132 didError: false,
133
134 pendingCommitExpirationTime: NoWork,
@@ -144,6 +152,8 @@ export function createFiberRoot(
152 containerInfo: containerInfo,
153 pendingChildren: null,
154
155 + pingCache: null,
156 +
157 earliestPendingTime: NoWork,
158 latestPendingTime: NoWork,
159 earliestSuspendedTime: NoWork,
packages/react-reconciler/src/ReactFiberScheduler.js
+48 -52
@@ -125,13 +125,8 @@ import {
125 computeAsyncExpiration,
126 computeInteractiveExpiration,
127 } from './ReactFiberExpirationTime';
128 -import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode';
129 -import {
130 - enqueueUpdate,
131 - resetCurrentlyProcessingQueue,
132 - ForceUpdate,
133 - createUpdate,
134 -} from './ReactUpdateQueue';
128 +import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
129 +import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
130 import {createCapturedValue} from './ReactCapturedValue';
131 import {
132 isContextProvider as isLegacyContextProvider,
@@ -1646,62 +1641,62 @@ function renderDidError() {
1641 nextRenderDidError = true;
1642 }
1643
1649 -function retrySuspendedRoot(
1644 +function pingSuspendedRoot(
1645 root: FiberRoot,
1651 - boundaryFiber: Fiber,
1652 - sourceFiber: Fiber,
1653 - suspendedTime: ExpirationTime,
1646 + thenable: Thenable,
1647 + pingTime: ExpirationTime,
1648 ) {
1655 - let retryTime;
1649 + // A promise that previously suspended React from committing has resolved.
1650 + // If React is still suspended, try again at the previous level (pingTime).
1651
1657 - if (isPriorityLevelSuspended(root, suspendedTime)) {
1658 - // Ping at the original level
1659 - retryTime = suspendedTime;
1652 + const pingCache = root.pingCache;
1653 + if (pingCache !== null) {
1654 + // The thenable resolved, so we no longer need to memoize, because it will
1655 + // never be thrown again.
1656 + pingCache.delete(thenable);
1657 + }
1658
1661 - markPingedPriorityLevel(root, retryTime);
1659 + if (nextRoot !== null && nextRenderExpirationTime === pingTime) {
1660 + // Received a ping at the same priority level at which we're currently
1661 + // rendering. Restart from the root.
1662 + nextRoot = null;
1663 } else {
1663 - // Suspense already timed out. Compute a new expiration time
1664 - const currentTime = requestCurrentTime();
1665 - retryTime = computeExpirationForFiber(currentTime, boundaryFiber);
1666 - markPendingPriorityLevel(root, retryTime);
1664 + // Confirm that the root is still suspended at this level. Otherwise exit.
1665 + if (isPriorityLevelSuspended(root, pingTime)) {
1666 + // Ping at the original level
1667 + markPingedPriorityLevel(root, pingTime);
1668 + const rootExpirationTime = root.expirationTime;
1669 + if (rootExpirationTime !== NoWork) {
1670 + requestWork(root, rootExpirationTime);
1671 + }
1672 + }
1673 }
1674 +}
1675
1669 - // TODO: If the suspense fiber has already rendered the primary children
1670 - // without suspending (that is, all of the promises have already resolved),
1671 - // we should not trigger another update here. One case this happens is when
1672 - // we are in sync mode and a single promise is thrown both on initial render
1673 - // and on update; we attach two .then(retrySuspendedRoot) callbacks and each
1674 - // one performs Sync work, rerendering the Suspense.
1676 +function retryTimedOutBoundary(boundaryFiber: Fiber, thenable: Thenable) {
1677 + // The boundary fiber (a Suspense component) previously timed out and was
1678 + // rendered in its fallback state. One of the promises that suspended it has
1679 + // resolved, which means at least part of the tree was likely unblocked. Try
1680 + // rendering again, at a new expiration time.
1681
1676 - if ((boundaryFiber.mode & ConcurrentMode) !== NoContext) {
1677 - if (root === nextRoot && nextRenderExpirationTime === suspendedTime) {
1678 - // Received a ping at the same priority level at which we're currently
1679 - // rendering. Restart from the root.
1680 - nextRoot = null;
1681 - }
1682 + const retryCache: WeakSet<Thenable> | Set<Thenable> | null =
1683 + boundaryFiber.stateNode;
1684 + if (retryCache !== null) {
1685 + // The thenable resolved, so we no longer need to memoize, because it will
1686 + // never be thrown again.
1687 + retryCache.delete(thenable);
1688 }
1689
1684 - scheduleWorkToRoot(boundaryFiber, retryTime);
1685 - if ((boundaryFiber.mode & ConcurrentMode) === NoContext) {
1686 - // Outside of concurrent mode, we must schedule an update on the source
1687 - // fiber, too, since it already committed in an inconsistent state and
1688 - // therefore does not have any pending work.
1689 - scheduleWorkToRoot(sourceFiber, retryTime);
1690 - const sourceTag = sourceFiber.tag;
1691 - if (sourceTag === ClassComponent && sourceFiber.stateNode !== null) {
1692 - // When we try rendering again, we should not reuse the current fiber,
1693 - // since it's known to be in an inconsistent state. Use a force updte to
1694 - // prevent a bail out.
1695 - const update = createUpdate(retryTime);
1696 - update.tag = ForceUpdate;
1697 - enqueueUpdate(sourceFiber, update);
1690 + const currentTime = requestCurrentTime();
1691 + const retryTime = computeExpirationForFiber(currentTime, boundaryFiber);
1692 + const root = scheduleWorkToRoot(boundaryFiber, retryTime);
1693 + if (root !== null) {
1694 + markPendingPriorityLevel(root, retryTime);
1695 + const rootExpirationTime = root.expirationTime;
1696 + if (rootExpirationTime !== NoWork) {
1697 + requestWork(root, rootExpirationTime);
1698 }
1699 }
1700 -
1701 - const rootExpirationTime = root.expirationTime;
1702 - if (rootExpirationTime !== NoWork) {
1703 - requestWork(root, rootExpirationTime);
1704 - }
1700 }
1701
1702 function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
@@ -2550,7 +2545,8 @@ export {
2545 onUncaughtError,
2546 renderDidSuspend,
2547 renderDidError,
2553 - retrySuspendedRoot,
2548 + pingSuspendedRoot,
2549 + retryTimedOutBoundary,
2550 markLegacyErrorBoundaryAsFailed,
2551 isAlreadyFailedLegacyErrorBoundary,
2552 scheduleWork,
packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+1 -4
@@ -14,10 +14,7 @@ export type SuspenseState = {|
14 timedOutAt: ExpirationTime,
15 |};
16
17 -export function shouldCaptureSuspense(
18 - current: Fiber | null,
19 - workInProgress: Fiber,
20 -): boolean {
17 +export function shouldCaptureSuspense(workInProgress: Fiber): boolean {
18 // In order to capture, the Suspense component must have a fallback prop.
19 if (workInProgress.memoizedProps.fallback === undefined) {
20 return false;
packages/react-reconciler/src/ReactFiberUnwindWork.js
+56 -25
@@ -43,6 +43,8 @@ import {
43 enqueueCapturedUpdate,
44 createUpdate,
45 CaptureUpdate,
46 + ForceUpdate,
47 + enqueueUpdate,
48 } from './ReactUpdateQueue';
49 import {logError} from './ReactFiberCommitWork';
50 import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
@@ -59,7 +61,7 @@ import {
61 onUncaughtError,
62 markLegacyErrorBoundaryAsFailed,
63 isAlreadyFailedLegacyErrorBoundary,
62 - retrySuspendedRoot,
64 + pingSuspendedRoot,
65 } from './ReactFiberScheduler';
66
67 import invariant from 'shared/invariant';
@@ -71,6 +73,8 @@ import {
73 } from './ReactFiberExpirationTime';
74 import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority';
75
76 +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
77 +
78 function createRootErrorUpdate(
79 fiber: Fiber,
80 errorInfo: CapturedValue<mixed>,
@@ -202,29 +206,18 @@ function throwException(
206 do {
207 if (
208 workInProgress.tag === SuspenseComponent &&
205 - shouldCaptureSuspense(workInProgress.alternate, workInProgress)
209 + shouldCaptureSuspense(workInProgress)
210 ) {
211 // Found the nearest boundary.
212
209 - // If the boundary is not in concurrent mode, we should not suspend, and
210 - // likewise, when the promise resolves, we should ping synchronously.
211 - const pingTime =
212 - (workInProgress.mode & ConcurrentMode) === NoEffect
213 - ? Sync
214 - : renderExpirationTime;
215 -
216 - // Attach a listener to the promise to "ping" the root and retry.
217 - let onResolveOrReject = retrySuspendedRoot.bind(
218 - null,
219 - root,
220 - workInProgress,
221 - sourceFiber,
222 - pingTime,
223 - );
224 - if (enableSchedulerTracing) {
225 - onResolveOrReject = Schedule_tracing_wrap(onResolveOrReject);
213 + // Stash the promise on the boundary fiber. If the boundary times out, we'll
214 + // attach another listener to flip the boundary back to its normal state.
215 + const thenables: Set<Thenable> = (workInProgress.updateQueue: any);
216 + if (thenables === null) {
217 + workInProgress.updateQueue = (new Set([thenable]): any);
218 + } else {
219 + thenables.add(thenable);
220 }
227 - thenable.then(onResolveOrReject, onResolveOrReject);
221
222 // If the boundary is outside of concurrent mode, we should *not*
223 // suspend the commit. Pretend as if the suspended component rendered
@@ -243,18 +236,25 @@ function throwException(
236 sourceFiber.effectTag &= ~(LifecycleEffectMask | Incomplete);
237
238 if (sourceFiber.tag === ClassComponent) {
246 - const current = sourceFiber.alternate;
247 - if (current === null) {
239 + const currentSourceFiber = sourceFiber.alternate;
240 + if (currentSourceFiber === null) {
241 // This is a new mount. Change the tag so it's not mistaken for a
242 // completed class component. For example, we should not call
243 // componentWillUnmount if it is deleted.
244 sourceFiber.tag = IncompleteClassComponent;
245 + } else {
246 + // When we try rendering again, we should not reuse the current fiber,
247 + // since it's known to be in an inconsistent state. Use a force updte to
248 + // prevent a bail out.
249 + const update = createUpdate(Sync);
250 + update.tag = ForceUpdate;
251 + enqueueUpdate(sourceFiber, update);
252 }
253 }
254
255 - // The source fiber did not complete. Mark it with the current
256 - // render priority to indicate that it still has pending work.
257 - sourceFiber.expirationTime = renderExpirationTime;
255 + // The source fiber did not complete. Mark it with Sync priority to
256 + // indicate that it still has pending work.
257 + sourceFiber.expirationTime = Sync;
258
259 // Exit without suspending.
260 return;
@@ -263,6 +263,37 @@ function throwException(
263 // Confirmed that the boundary is in a concurrent mode tree. Continue
264 // with the normal suspend path.
265
266 + // Attach a listener to the promise to "ping" the root and retry. But
267 + // only if one does not already exist for the current render expiration
268 + // time (which acts like a "thread ID" here).
269 + let pingCache = root.pingCache;
270 + let threadIDs;
271 + if (pingCache === null) {
272 + pingCache = root.pingCache = new PossiblyWeakMap();
273 + threadIDs = new Set();
274 + pingCache.set(thenable, threadIDs);
275 + } else {
276 + threadIDs = pingCache.get(thenable);
277 + if (threadIDs === undefined) {
278 + threadIDs = new Set();
279 + pingCache.set(thenable, threadIDs);
280 + }
281 + }
282 + if (!threadIDs.has(renderExpirationTime)) {
283 + // Memoize using the thread ID to prevent redundant listeners.
284 + threadIDs.add(renderExpirationTime);
285 + let ping = pingSuspendedRoot.bind(
286 + null,
287 + root,
288 + thenable,
289 + renderExpirationTime,
290 + );
291 + if (enableSchedulerTracing) {
292 + ping = Schedule_tracing_wrap(ping);
293 + }
294 + thenable.then(ping, ping);
295 + }
296 +
297 let absoluteTimeoutMs;
298 if (earliestTimeoutMs === -1) {
299 // If no explicit threshold is given, default to an abitrarily large
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+57 -14
@@ -485,11 +485,7 @@ describe('ReactSuspense', () => {
485 expect(root).toMatchRenderedOutput('Loading...');
486
487 jest.advanceTimersByTime(1000);
488 - expect(ReactTestRenderer).toHaveYielded([
489 - 'Promise resolved [B]',
490 - 'B',
491 - 'B',
492 - ]);
488 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [B]', 'B']);
489 expect(root).toMatchRenderedOutput('Stateful: 2B');
490 });
491
@@ -552,11 +548,7 @@ describe('ReactSuspense', () => {
548 expect(root).toMatchRenderedOutput('Loading...');
549
550 jest.advanceTimersByTime(1000);
555 - expect(ReactTestRenderer).toHaveYielded([
556 - 'Promise resolved [B]',
557 - 'B',
558 - 'B',
559 - ]);
551 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [B]', 'B']);
552 expect(root).toMatchRenderedOutput('Stateful: 2B');
553 });
554
@@ -736,10 +728,6 @@ describe('ReactSuspense', () => {
728 'Promise resolved [Child 2]',
729 'Child 2',
730 'Suspend! [Child 3]',
739 - // TODO: This suspends twice because there were two pings, once for each
740 - // time Child 2 suspended. This is only an issue in sync mode because
741 - // pings aren't batched.
742 - 'Suspend! [Child 3]',
731 ]);
732 jest.advanceTimersByTime(1000);
733 expect(ReactTestRenderer).toHaveYielded([
@@ -886,6 +874,61 @@ describe('ReactSuspense', () => {
874 jest.advanceTimersByTime(1000);
875 });
876
877 + it('memoizes promise listeners per thread ID to prevent redundant renders', () => {
878 + function App() {
879 + return (
880 + <Suspense fallback={<Text text="Loading..." />}>
881 + <AsyncText text="A" ms={1000} />
882 + <AsyncText text="B" ms={2000} />
883 + <AsyncText text="C" ms={3000} />
884 + </Suspense>
885 + );
886 + }
887 +
888 + const root = ReactTestRenderer.create(null);
889 +
890 + root.update(<App />);
891 +
892 + expect(ReactTestRenderer).toHaveYielded([
893 + 'Suspend! [A]',
894 + 'Suspend! [B]',
895 + 'Suspend! [C]',
896 + 'Loading...',
897 + ]);
898 +
899 + // Resolve A
900 + jest.advanceTimersByTime(1000);
901 +
902 + expect(ReactTestRenderer).toHaveYielded([
903 + 'Promise resolved [A]',
904 + 'A',
905 + // The promises for B and C have now been thrown twice
906 + 'Suspend! [B]',
907 + 'Suspend! [C]',
908 + ]);
909 +
910 + // Resolve B
911 + jest.advanceTimersByTime(1000);
912 +
913 + expect(ReactTestRenderer).toHaveYielded([
914 + 'Promise resolved [B]',
915 + // Even though the promise for B was thrown twice, we should only
916 + // re-render once.
917 + 'B',
918 + // The promise for C has now been thrown three times
919 + 'Suspend! [C]',
920 + ]);
921 +
922 + // Resolve C
923 + jest.advanceTimersByTime(1000);
924 + expect(ReactTestRenderer).toHaveYielded([
925 + 'Promise resolved [C]',
926 + // Even though the promise for C was thrown three times, we should only
927 + // re-render once.
928 + 'C',
929 + ]);
930 + });
931 +
932 it('#14162', () => {
933 const {lazy} = React;
934
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+7 -12
@@ -8,9 +8,12 @@
8 * @jest-environment node
9 */
10
11 -runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
12 - require('react-noop-renderer'),
13 -);
11 +// TODO: This does nothing since it was migrated from noop renderer to test
12 +// renderer! Switch back to noop renderer, or add persistent mode to test
13 +// renderer, or merge the two renderers into one somehow.
14 +// runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
15 +// require('react-noop-renderer'),
16 +// );
17 runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
18 require('react-noop-renderer/persistent'),
19 );
@@ -366,22 +369,14 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
369
370 jest.advanceTimersByTime(1000);
371
369 - // TODO Change expected onRender count to 4.
370 - // At the moment, every time we suspended while rendering will cause a commit.
371 - // This will probably change in the future, but that's why there are two new ones.
372 expect(root.toJSON()).toEqual(['Loaded', 'New']);
373 - expect(onRender).toHaveBeenCalledTimes(5);
373 + expect(onRender).toHaveBeenCalledTimes(4);
374
375 // When the suspending data is resolved and our final UI is rendered,
376 // the baseDuration should only include the 1ms re-rendering AsyncText,
377 // but the treeBaseDuration should include the full 9ms spent in the tree.
378 expect(onRender.mock.calls[3][2]).toBe(1);
379 expect(onRender.mock.calls[3][3]).toBe(9);
380 -
381 - // TODO Remove these assertions once this commit is gone.
382 - // For now, there was no actual work done during this commit; see above comment.
383 - expect(onRender.mock.calls[4][2]).toBe(0);
384 - expect(onRender.mock.calls[4][3]).toBe(9);
380 });
381
382 it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
packages/react/src/__tests__/ReactProfiler-test.internal.js
+2 -9
@@ -2548,22 +2548,15 @@ describe('Profiler', () => {
2548 await originalPromise;
2549 expect(renderer.toJSON()).toEqual(['loaded', 'updated']);
2550
2551 - // TODO: Bug. This *should* just be one render tied to both interactions.
2552 - expect(onRender).toHaveBeenCalledTimes(2);
2551 + expect(onRender).toHaveBeenCalledTimes(1);
2552 expect(onRender.mock.calls[0][6]).toMatchInteractions([
2553 initialRenderInteraction,
2554 ]);
2556 - expect(onRender.mock.calls[1][6]).toMatchInteractions([
2557 - highPriUpdateInteraction,
2558 - ]);
2555
2560 - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
2556 + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2557 expect(
2558 onInteractionScheduledWorkCompleted.mock.calls[0][0],
2559 ).toMatchInteraction(initialRenderInteraction);
2564 - expect(
2565 - onInteractionScheduledWorkCompleted.mock.calls[1][0],
2566 - ).toMatchInteraction(highPriUpdateInteraction);
2560 });
2561
2562 it('handles high-pri renderers between suspended and resolved (async) trees', async () => {
scripts/rollup/validate/eslintrc.cjs.js
+1
@@ -12,6 +12,7 @@ module.exports = {
12 Proxy: true,
13 Symbol: true,
14 WeakMap: true,
15 + WeakSet: true,
16 Uint16Array: true,
17 // Vendor specific
18 MSApp: true,
scripts/rollup/validate/eslintrc.fb.js
+1
@@ -12,6 +12,7 @@ module.exports = {
12 Symbol: true,
13 Proxy: true,
14 WeakMap: true,
15 + WeakSet: true,
16 Uint16Array: true,
17 // Vendor specific
18 MSApp: true,
scripts/rollup/validate/eslintrc.rn.js
+1
@@ -12,6 +12,7 @@ module.exports = {
12 Symbol: true,
13 Proxy: true,
14 WeakMap: true,
15 + WeakSet: true,
16 // Vendor specific
17 MSApp: true,
18 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,
scripts/rollup/validate/eslintrc.umd.js
+1
@@ -11,6 +11,7 @@ module.exports = {
11 Symbol: true,
12 Proxy: true,
13 WeakMap: true,
14 + WeakSet: true,
15 Uint16Array: true,
16 // Vendor specific
17 MSApp: true,