@samitouri / QOS-React / commits / 4fe6eec15b

Always batch updates of like priority within the same event (#13071)

Expiration times are computed by adding to the current time (the start time). However, if two updates are scheduled within the same event, we should treat their start times as simultaneous, even if the actual clock time has advanced between the first and second call. In other words, because expiration times determine how updates are batched, we want all updates of like priority that occur within the same event to receive the same expiration time. Otherwise we get tearing. We keep track of two separate times: the current "renderer" time and the current "scheduler" time. The renderer time can be updated whenever; it only exists to minimize the calls performance.now. But the scheduler time can only be updated if there's no pending work, or if we know for certain that we're not in the middle of an event.

Andrew Clark committed Jun 19, 2018 at 10:34 UTC 4fe6eec15bc69737910e5c6c749ebc6187d67be0
12 files changed +261 -99
packages/react-noop-renderer/src/createReactNoop.js
+6 -1
@@ -440,7 +440,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
440 expect(actual).toEqual(expected);
441 },
442
443 - expire(ms: number): void {
443 + expire(ms: number): Array<mixed> {
444 + ReactNoop.advanceTime(ms);
445 + return ReactNoop.flushExpired();
446 + },
447 +
448 + advanceTime(ms: number): void {
449 elapsedTimeInMs += ms;
450 },
451
packages/react-reconciler/src/ReactFiberClassComponent.js
+4 -4
@@ -46,7 +46,7 @@ import {
46 emptyContextObject,
47 } from './ReactFiberContext';
48 import {
49 - recalculateCurrentTime,
49 + requestCurrentTime,
50 computeExpirationForFiber,
51 scheduleWork,
52 } from './ReactFiberScheduler';
@@ -171,7 +171,7 @@ const classComponentUpdater = {
171 isMounted,
172 enqueueSetState(inst, payload, callback) {
173 const fiber = ReactInstanceMap.get(inst);
174 - const currentTime = recalculateCurrentTime();
174 + const currentTime = requestCurrentTime();
175 const expirationTime = computeExpirationForFiber(currentTime, fiber);
176
177 const update = createUpdate(expirationTime);
@@ -188,7 +188,7 @@ const classComponentUpdater = {
188 },
189 enqueueReplaceState(inst, payload, callback) {
190 const fiber = ReactInstanceMap.get(inst);
191 - const currentTime = recalculateCurrentTime();
191 + const currentTime = requestCurrentTime();
192 const expirationTime = computeExpirationForFiber(currentTime, fiber);
193
194 const update = createUpdate(expirationTime);
@@ -207,7 +207,7 @@ const classComponentUpdater = {
207 },
208 enqueueForceUpdate(inst, callback) {
209 const fiber = ReactInstanceMap.get(inst);
210 - const currentTime = recalculateCurrentTime();
210 + const currentTime = requestCurrentTime();
211 const expirationTime = computeExpirationForFiber(currentTime, fiber);
212
213 const update = createUpdate(expirationTime);
packages/react-reconciler/src/ReactFiberCommitWork.js
-1
@@ -224,7 +224,6 @@ function commitLifeCycles(
224 finishedRoot: FiberRoot,
225 current: Fiber | null,
226 finishedWork: Fiber,
227 - currentTime: ExpirationTime,
227 committedExpirationTime: ExpirationTime,
228 ): void {
229 switch (finishedWork.tag) {
packages/react-reconciler/src/ReactFiberPendingPriority.js
-1
@@ -45,7 +45,6 @@ export function markPendingPriorityLevel(
45
46 export function markCommittedPriorityLevels(
47 root: FiberRoot,
48 - currentTime: ExpirationTime,
48 earliestRemainingTime: ExpirationTime,
49 ): void {
50 root.didError = false;
packages/react-reconciler/src/ReactFiberReconciler.js
+2 -2
@@ -39,7 +39,7 @@ import {createFiberRoot} from './ReactFiberRoot';
39 import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook';
40 import {
41 computeUniqueAsyncExpiration,
42 - recalculateCurrentTime,
42 + requestCurrentTime,
43 computeExpirationForFiber,
44 scheduleWork,
45 requestWork,
@@ -208,7 +208,7 @@ export function updateContainer(
208 callback: ?Function,
209 ): ExpirationTime {
210 const current = container.current;
211 - const currentTime = recalculateCurrentTime();
211 + const currentTime = requestCurrentTime();
212 const expirationTime = computeExpirationForFiber(currentTime, current);
213 return updateContainerAtExpirationTime(
214 element,
packages/react-reconciler/src/ReactFiberScheduler.js
+76 -34
@@ -208,12 +208,7 @@ if (__DEV__) {
208 };
209 }
210
211 -// Represents the current time in ms.
212 -const originalStartTimeMs = now();
213 -let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0);
214 -let mostRecentCurrentTimeMs: number = originalStartTimeMs;
215 -
216 -// Used to ensure computeUniqueAsyncExpiration is monotonically increases.
211 +// Used to ensure computeUniqueAsyncExpiration is monotonically increasing.
212 let lastUniqueAsyncExpiration: number = 0;
213
214 // Represents the expiration time that incoming updates should use. (If this
@@ -432,7 +427,6 @@ function commitBeforeMutationLifecycles() {
427
428 function commitAllLifeCycles(
429 finishedRoot: FiberRoot,
435 - currentTime: ExpirationTime,
430 committedExpirationTime: ExpirationTime,
431 ) {
432 if (__DEV__) {
@@ -456,7 +450,6 @@ function commitAllLifeCycles(
450 finishedRoot,
451 current,
452 nextEffect,
459 - currentTime,
453 committedExpirationTime,
454 );
455 }
@@ -516,8 +509,7 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
509 // about to commit. This needs to happen before calling the lifecycles, since
510 // they may schedule additional updates.
511 const earliestRemainingTime = finishedWork.expirationTime;
519 - const currentTime = recalculateCurrentTime();
520 - markCommittedPriorityLevels(root, currentTime, earliestRemainingTime);
512 + markCommittedPriorityLevels(root, earliestRemainingTime);
513
514 // Reset this to null before calling lifecycles
515 ReactCurrentOwner.current = null;
@@ -642,7 +634,6 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
634 commitAllLifeCycles,
635 null,
636 root,
645 - currentTime,
637 committedExpirationTime,
638 );
639 if (hasCaughtError()) {
@@ -651,7 +642,7 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
642 }
643 } else {
644 try {
654 - commitAllLifeCycles(root, currentTime, committedExpirationTime);
645 + commitAllLifeCycles(root, committedExpirationTime);
646 } catch (e) {
647 didError = true;
648 error = e;
@@ -1076,7 +1067,6 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1067 sourceFiber,
1068 thrownValue,
1069 nextRenderExpirationTime,
1079 - mostRecentCurrentTimeMs,
1070 );
1071 nextUnitOfWork = completeUnitOfWork(sourceFiber);
1072 }
@@ -1192,7 +1182,7 @@ function captureCommitPhaseError(fiber: Fiber, error: mixed) {
1182 return dispatch(fiber, error, Sync);
1183 }
1184
1195 -function computeAsyncExpiration(currentTime: ExpirationTime) {
1185 +function computeAsyncExpiration(currentTime) {
1186 // Given the current clock time, returns an expiration time. We use rounding
1187 // to batch like updates together.
1188 // Should complete within ~5000ms. 5250ms max.
@@ -1201,7 +1191,7 @@ function computeAsyncExpiration(currentTime: ExpirationTime) {
1191 return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
1192 }
1193
1204 -function computeInteractiveExpiration(currentTime: ExpirationTime) {
1194 +function computeInteractiveExpiration(currentTime) {
1195 let expirationMs;
1196 // We intentionally set a higher expiration time for interactive updates in
1197 // dev than in production.
@@ -1224,7 +1214,7 @@ function computeInteractiveExpiration(currentTime: ExpirationTime) {
1214
1215 // Creates a unique async expiration time.
1216 function computeUniqueAsyncExpiration(): ExpirationTime {
1227 - const currentTime = recalculateCurrentTime();
1217 + const currentTime = requestCurrentTime();
1218 let result = computeAsyncExpiration(currentTime);
1219 if (result <= lastUniqueAsyncExpiration) {
1220 // Since we assume the current time monotonically increases, we only hit
@@ -1379,16 +1369,9 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1369 }
1370 }
1371
1382 -function recalculateCurrentTime(): ExpirationTime {
1383 - // Subtract initial time so it fits inside 32bits
1384 - mostRecentCurrentTimeMs = now() - originalStartTimeMs;
1385 - mostRecentCurrentTime = msToExpirationTime(mostRecentCurrentTimeMs);
1386 - return mostRecentCurrentTime;
1387 -}
1388 -
1372 function deferredUpdates<A>(fn: () => A): A {
1373 + const currentTime = requestCurrentTime();
1374 const previousExpirationContext = expirationContext;
1391 - const currentTime = recalculateCurrentTime();
1375 expirationContext = computeAsyncExpiration(currentTime);
1376 try {
1377 return fn();
@@ -1396,6 +1379,7 @@ function deferredUpdates<A>(fn: () => A): A {
1379 expirationContext = previousExpirationContext;
1380 }
1381 }
1382 +
1383 function syncUpdates<A, B, C0, D, R>(
1384 fn: (A, B, C0, D) => R,
1385 a: A,
@@ -1436,12 +1420,23 @@ let isBatchingInteractiveUpdates: boolean = false;
1420
1421 let completedBatches: Array<Batch> | null = null;
1422
1423 +let originalStartTimeMs: number = now();
1424 +let currentRendererTime: ExpirationTime = msToExpirationTime(
1425 + originalStartTimeMs,
1426 +);
1427 +let currentSchedulerTime: ExpirationTime = currentRendererTime;
1428 +
1429 // Use these to prevent an infinite loop of nested updates
1430 const NESTED_UPDATE_LIMIT = 1000;
1431 let nestedUpdateCount: number = 0;
1432
1433 const timeHeuristicForUnitOfWork = 1;
1434
1435 +function recomputeCurrentRendererTime() {
1436 + const currentTimeMs = now() - originalStartTimeMs;
1437 + currentRendererTime = msToExpirationTime(currentTimeMs);
1438 +}
1439 +
1440 function scheduleCallbackWithExpirationTime(expirationTime) {
1441 if (callbackExpirationTime !== NoWork) {
1442 // A callback is already scheduled. Check its expiration time (timeout).
@@ -1508,6 +1503,50 @@ function onCommit(root, expirationTime) {
1503 root.finishedWork = null;
1504 }
1505
1506 +function requestCurrentTime() {
1507 + // requestCurrentTime is called by the scheduler to compute an expiration
1508 + // time.
1509 + //
1510 + // Expiration times are computed by adding to the current time (the start
1511 + // time). However, if two updates are scheduled within the same event, we
1512 + // should treat their start times as simultaneous, even if the actual clock
1513 + // time has advanced between the first and second call.
1514 +
1515 + // In other words, because expiration times determine how updates are batched,
1516 + // we want all updates of like priority that occur within the same event to
1517 + // receive the same expiration time. Otherwise we get tearing.
1518 + //
1519 + // We keep track of two separate times: the current "renderer" time and the
1520 + // current "scheduler" time. The renderer time can be updated whenever; it
1521 + // only exists to minimize the calls performance.now.
1522 + //
1523 + // But the scheduler time can only be updated if there's no pending work, or
1524 + // if we know for certain that we're not in the middle of an event.
1525 +
1526 + if (isRendering) {
1527 + // We're already rendering. Return the most recently read time.
1528 + return currentSchedulerTime;
1529 + }
1530 + // Check if there's pending work.
1531 + findHighestPriorityRoot();
1532 + if (
1533 + nextFlushedExpirationTime === NoWork ||
1534 + nextFlushedExpirationTime === Never
1535 + ) {
1536 + // If there's no pending work, or if the pending work is offscreen, we can
1537 + // read the current time without risk of tearing.
1538 + recomputeCurrentRendererTime();
1539 + currentSchedulerTime = currentRendererTime;
1540 + return currentSchedulerTime;
1541 + }
1542 + // There's already pending work. We might be in the middle of a browser
1543 + // event. If we were to read the current time, it could cause multiple updates
1544 + // within the same event to receive different expiration times, leading to
1545 + // tearing. Return the last read time. During the next idle callback, the
1546 + // time will be updated.
1547 + return currentSchedulerTime;
1548 +}
1549 +
1550 // requestWork is called by the scheduler whenever a root receives an update.
1551 // It's up to the renderer to call renderRoot at some point in the future.
1552 function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -1661,24 +1700,27 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
1700 resumeActualRenderTimerIfPaused();
1701 }
1702
1664 - if (enableUserTimingAPI && deadline !== null) {
1665 - const didExpire = nextFlushedExpirationTime < recalculateCurrentTime();
1666 - const timeout = expirationTimeToMs(nextFlushedExpirationTime);
1667 - stopRequestCallbackTimer(didExpire, timeout);
1668 - }
1669 -
1703 if (deadline !== null) {
1704 + recomputeCurrentRendererTime();
1705 + currentSchedulerTime = currentRendererTime;
1706 +
1707 + if (enableUserTimingAPI) {
1708 + const didExpire = nextFlushedExpirationTime < currentRendererTime;
1709 + const timeout = expirationTimeToMs(nextFlushedExpirationTime);
1710 + stopRequestCallbackTimer(didExpire, timeout);
1711 + }
1712 +
1713 while (
1714 nextFlushedRoot !== null &&
1715 nextFlushedExpirationTime !== NoWork &&
1716 (minExpirationTime === NoWork ||
1717 minExpirationTime >= nextFlushedExpirationTime) &&
1676 - (!deadlineDidExpire ||
1677 - recalculateCurrentTime() >= nextFlushedExpirationTime)
1718 + (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
1719 ) {
1679 - recalculateCurrentTime();
1720 performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
1721 findHighestPriorityRoot();
1722 + recomputeCurrentRendererTime();
1723 + currentSchedulerTime = currentRendererTime;
1724 }
1725 } else {
1726 while (
@@ -1977,7 +2019,7 @@ function flushControlled(fn: () => mixed): void {
2019 }
2020
2021 export {
1980 - recalculateCurrentTime,
2022 + requestCurrentTime,
2023 computeExpirationForFiber,
2024 captureCommitPhaseError,
2025 onUncaughtError,
packages/react-reconciler/src/ReactFiberUnwindWork.js
+4 -3
@@ -60,7 +60,7 @@ import {
60 onUncaughtError,
61 markLegacyErrorBoundaryAsFailed,
62 isAlreadyFailedLegacyErrorBoundary,
63 - recalculateCurrentTime,
63 + requestCurrentTime,
64 computeExpirationForFiber,
65 scheduleWork,
66 retrySuspendedRoot,
@@ -132,7 +132,7 @@ function createClassErrorUpdate(
132 function schedulePing(finishedWork) {
133 // Once the promise resolves, we should try rendering the non-
134 // placeholder state again.
135 - const currentTime = recalculateCurrentTime();
135 + const currentTime = requestCurrentTime();
136 const expirationTime = computeExpirationForFiber(currentTime, finishedWork);
137 const recoveryUpdate = createUpdate(expirationTime);
138 enqueueUpdate(finishedWork, recoveryUpdate, expirationTime);
@@ -145,7 +145,6 @@ function throwException(
145 sourceFiber: Fiber,
146 value: mixed,
147 renderExpirationTime: ExpirationTime,
148 - currentTimeMs: number,
148 ) {
149 // The source fiber did not complete.
150 sourceFiber.effectTag |= Incomplete;
@@ -162,7 +161,9 @@ function throwException(
161 const thenable: Thenable = (value: any);
162
163 // TODO: Should use the earliest known expiration time
164 + const currentTime = requestCurrentTime();
165 const expirationTimeMs = expirationTimeToMs(renderExpirationTime);
166 + const currentTimeMs = expirationTimeToMs(currentTime);
167 const startTimeMs = expirationTimeMs - 5000;
168 let elapsedMs = currentTimeMs - startTimeMs;
169 if (elapsedMs < 0) {
packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js new
+162
@@ -0,0 +1,162 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @jest-environment node
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactNoop;
15 +
16 +describe('ReactExpiration', () => {
17 + beforeEach(() => {
18 + jest.resetModules();
19 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
20 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
21 + React = require('react');
22 + ReactNoop = require('react-noop-renderer');
23 + });
24 +
25 + function span(prop) {
26 + return {type: 'span', children: [], prop};
27 + }
28 +
29 + it('increases priority of updates as time progresses', () => {
30 + ReactNoop.render(<span prop="done" />);
31 +
32 + expect(ReactNoop.getChildren()).toEqual([]);
33 +
34 + // Nothing has expired yet because time hasn't advanced.
35 + ReactNoop.flushExpired();
36 + expect(ReactNoop.getChildren()).toEqual([]);
37 +
38 + // Advance time a bit, but not enough to expire the low pri update.
39 + ReactNoop.expire(4500);
40 + ReactNoop.flushExpired();
41 + expect(ReactNoop.getChildren()).toEqual([]);
42 +
43 + // Advance by another second. Now the update should expire and flush.
44 + ReactNoop.expire(1000);
45 + ReactNoop.flushExpired();
46 + expect(ReactNoop.getChildren()).toEqual([span('done')]);
47 + });
48 +
49 + it('two updates of like priority in the same event always flush within the same batch', () => {
50 + class Text extends React.Component {
51 + componentDidMount() {
52 + ReactNoop.yield(`${this.props.text} [commit]`);
53 + }
54 + componentDidUpdate() {
55 + ReactNoop.yield(`${this.props.text} [commit]`);
56 + }
57 + render() {
58 + ReactNoop.yield(`${this.props.text} [render]`);
59 + return <span prop={this.props.text} />;
60 + }
61 + }
62 +
63 + // First, show what happens for updates in two separate events.
64 + // Schedule an update.
65 + ReactNoop.render(<Text text="A" />);
66 + // Advance the timer and flush any work that expired. Flushing the expired
67 + // work signals to the renderer that the event has ended.
68 + ReactNoop.advanceTime(2000);
69 + // Don't advance time by enough to expire the first update.
70 + expect(ReactNoop.flushExpired()).toEqual([]);
71 + expect(ReactNoop.getChildren()).toEqual([]);
72 + // Schedule another update.
73 + ReactNoop.render(<Text text="B" />);
74 + // The updates should flush in separate batches, since sufficient time
75 + // passed in between them *and* they occurred in separate events.
76 + expect(ReactNoop.flush()).toEqual([
77 + 'A [render]',
78 + 'A [commit]',
79 + 'B [render]',
80 + 'B [commit]',
81 + ]);
82 + expect(ReactNoop.getChildren()).toEqual([span('B')]);
83 +
84 + // Now do the same thing again, except this time don't flush any work in
85 + // between the two updates.
86 + ReactNoop.render(<Text text="A" />);
87 + // Advance the timer, but don't flush the expired work. Because we still
88 + // haven't entered an idle callback, the scheduler must assume that we're
89 + // inside the same event.
90 + ReactNoop.advanceTime(2000);
91 + expect(ReactNoop.clearYields()).toEqual(null);
92 + expect(ReactNoop.getChildren()).toEqual([span('B')]);
93 + // Schedule another update.
94 + ReactNoop.render(<Text text="B" />);
95 + // The updates should flush in the same batch, since as far as the scheduler
96 + // knows, they may have occurred inside the same event.
97 + expect(ReactNoop.flush()).toEqual(['B [render]', 'B [commit]']);
98 + });
99 +
100 + it(
101 + 'two updates of like priority in the same event always flush within the ' +
102 + "same batch, even if there's a sync update in between",
103 + () => {
104 + class Text extends React.Component {
105 + componentDidMount() {
106 + ReactNoop.yield(`${this.props.text} [commit]`);
107 + }
108 + componentDidUpdate() {
109 + ReactNoop.yield(`${this.props.text} [commit]`);
110 + }
111 + render() {
112 + ReactNoop.yield(`${this.props.text} [render]`);
113 + return <span prop={this.props.text} />;
114 + }
115 + }
116 +
117 + // First, show what happens for updates in two separate events.
118 + // Schedule an update.
119 + ReactNoop.render(<Text text="A" />);
120 + // Advance the timer and flush any work that expired. Flushing the expired
121 + // work signals to the renderer that the event has ended.
122 + ReactNoop.advanceTime(2000);
123 + // Don't advance time by enough to expire the first update.
124 + expect(ReactNoop.flushExpired()).toEqual([]);
125 + expect(ReactNoop.getChildren()).toEqual([]);
126 + // Schedule another update.
127 + ReactNoop.render(<Text text="B" />);
128 + // The updates should flush in separate batches, since sufficient time
129 + // passed in between them *and* they occurred in separate events.
130 + expect(ReactNoop.flush()).toEqual([
131 + 'A [render]',
132 + 'A [commit]',
133 + 'B [render]',
134 + 'B [commit]',
135 + ]);
136 + expect(ReactNoop.getChildren()).toEqual([span('B')]);
137 +
138 + // Now do the same thing again, except this time don't flush any work in
139 + // between the two updates.
140 + ReactNoop.render(<Text text="A" />);
141 + // Advance the timer, but don't flush the expired work. Because we still
142 + // haven't entered an idle callback, the scheduler must assume that we're
143 + // inside the same event.
144 + ReactNoop.advanceTime(2000);
145 + expect(ReactNoop.clearYields()).toEqual(null);
146 + expect(ReactNoop.getChildren()).toEqual([span('B')]);
147 +
148 + // Perform some synchronous work. Again, the scheduler must assume we're
149 + // inside the same event.
150 + ReactNoop.flushSync(() => {
151 + ReactNoop.renderToRootWithID('1', 'second-root');
152 + });
153 +
154 + // Even though React flushed a sync update, it should not have updated the
155 + // current time. Schedule another update.
156 + ReactNoop.render(<Text text="B" />);
157 + // The updates should flush in the same batch, since as far as the scheduler
158 + // knows, they may have occurred inside the same event.
159 + expect(ReactNoop.flush()).toEqual(['B [render]', 'B [commit]']);
160 + },
161 + );
162 +});
packages/react-reconciler/src/__tests__/ReactExpiration-test.js deleted
-45
@@ -1,45 +0,0 @@
1 -/**
2 - * Copyright (c) 2013-present, Facebook, Inc.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @jest-environment node
8 - */
9 -
10 -'use strict';
11 -
12 -let React;
13 -let ReactNoop;
14 -
15 -describe('ReactExpiration', () => {
16 - beforeEach(() => {
17 - jest.resetModules();
18 - React = require('react');
19 - ReactNoop = require('react-noop-renderer');
20 - });
21 -
22 - function span(prop) {
23 - return {type: 'span', children: [], prop};
24 - }
25 -
26 - it('increases priority of updates as time progresses', () => {
27 - ReactNoop.render(<span prop="done" />);
28 -
29 - expect(ReactNoop.getChildren()).toEqual([]);
30 -
31 - // Nothing has expired yet because time hasn't advanced.
32 - ReactNoop.flushExpired();
33 - expect(ReactNoop.getChildren()).toEqual([]);
34 -
35 - // Advance time a bit, but not enough to expire the low pri update.
36 - ReactNoop.expire(4500);
37 - ReactNoop.flushExpired();
38 - expect(ReactNoop.getChildren()).toEqual([]);
39 -
40 - // Advance by another second. Now the update should expire and flush.
41 - ReactNoop.expire(1000);
42 - ReactNoop.flushExpired();
43 - expect(ReactNoop.getChildren()).toEqual([span('done')]);
44 - });
45 -});
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+2 -4
@@ -711,9 +711,8 @@ describe('ReactSuspense', () => {
711 );
712 expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
713 await advanceTimers(10000);
714 - ReactNoop.expire(10000);
714 expect(() => {
716 - expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
715 + ReactNoop.expire(10000);
716 }).toThrow(
717 'An update was suspended for longer than the timeout, but no fallback ' +
718 'UI was provided.',
@@ -727,8 +726,7 @@ describe('ReactSuspense', () => {
726 <AsyncText text="B" ms={100} />
727 </Fallback>,
728 );
730 - ReactNoop.expire(10000);
731 - expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Suspend! [B]']);
729 + expect(ReactNoop.expire(10000)).toEqual(['Suspend! [A]', 'Suspend! [B]']);
730 expect(ReactNoop.getChildren()).toEqual([]);
731
732 await advanceTimers(100);
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+2
@@ -406,6 +406,8 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
406 exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
407 "⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread
408
409 +⚛ (React Tree Reconciliation: Yielded)
410 +
411 ⚛ (React Tree Reconciliation: Completed Root)
412 ⚛ Foo [mount]
413
packages/react/src/__tests__/ReactProfiler-test.internal.js
+3 -4
@@ -177,11 +177,10 @@ describe('Profiler', () => {
177 </div>,
178 );
179
180 - // Should only be called three times:
181 - // Twice for normal expiration time purposes,
182 - // And once to capture the commit time.
180 + // Should be called twice: once to compute the update expiration time,
181 + // and once to record the commit time.
182 // No additional calls from ProfilerTimer are expected.
184 - expect(mockNow).toHaveBeenCalledTimes(3);
183 + expect(mockNow).toHaveBeenCalledTimes(2);
184 });
185
186 it('logs render times for both mount and update', () => {