Skip to content

Commit 3888bbc

Browse files
committed
Measure time between scheduling an async callback and flushing it
Helps detect starvation issues.
1 parent 17aa4d4 commit 3888bbc

File tree

4 files changed

+132
-32
lines changed

4 files changed

+132
-32
lines changed

packages/react-reconciler/src/ReactDebugFiberPerf.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ let hasScheduledUpdateInCurrentCommit: boolean = false;
5858
let hasScheduledUpdateInCurrentPhase: boolean = false;
5959
let commitCountInCurrentWorkLoop: number = 0;
6060
let effectCountInCurrentCommit: number = 0;
61+
let isWaitingForCallback: boolean = false;
6162
// During commits, we only show a measurement once per method name
6263
// to avoid stretch the commit phase with measurement overhead.
6364
const labelsInCurrentCommit: Set<string> = new Set();
@@ -231,6 +232,25 @@ export function recordScheduleUpdate(): void {
231232
}
232233
}
233234

235+
export function startRequestCallbackTimer(): void {
236+
if (enableUserTimingAPI && !isWaitingForCallback) {
237+
isWaitingForCallback = true;
238+
beginMark('(Waiting for async callback...)');
239+
}
240+
}
241+
242+
export function stopRequestCallbackTimer(didExpire: boolean): void {
243+
if (enableUserTimingAPI) {
244+
isWaitingForCallback = false;
245+
const warning = didExpire ? 'Async work expired' : null;
246+
endMark(
247+
'(Waiting for async callback...)',
248+
'(Waiting for async callback...)',
249+
warning,
250+
);
251+
}
252+
}
253+
234254
export function startWorkTimer(fiber: Fiber): void {
235255
if (enableUserTimingAPI) {
236256
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
@@ -344,9 +364,7 @@ export function stopWorkLoopTimer(interruptedBy: Fiber | null): void {
344364
warning = 'A top-level update interrupted the previous render';
345365
} else {
346366
const componentName = getComponentName(interruptedBy) || 'Unknown';
347-
warning = `An update to ${
348-
componentName
349-
} interrupted the previous render`;
367+
warning = `An update to ${componentName} interrupted the previous render`;
350368
}
351369
} else if (commitCountInCurrentWorkLoop > 1) {
352370
warning = 'There were cascading updates';

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import type {FiberRoot} from './ReactFiberRoot';
1313
import type {HydrationContext} from './ReactFiberHydrationContext';
1414
import type {ExpirationTime} from './ReactFiberExpirationTime';
1515

16-
import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
16+
import {
17+
getStackAddendumByWorkInProgressFiber,
18+
} from 'shared/ReactFiberComponentTreeHook';
1719
import ReactErrorUtils from 'shared/ReactErrorUtils';
1820
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
1921
import {
@@ -33,6 +35,7 @@ import {
3335
HostPortal,
3436
ClassComponent,
3537
} from 'shared/ReactTypeOfWork';
38+
import {enableUserTimingAPI} from 'shared/ReactFeatureFlags';
3639
import getComponentName from 'shared/getComponentName';
3740
import invariant from 'fbjs/lib/invariant';
3841
import warning from 'fbjs/lib/warning';
@@ -47,6 +50,8 @@ import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
4750
import {
4851
recordEffect,
4952
recordScheduleUpdate,
53+
startRequestCallbackTimer,
54+
stopRequestCallbackTimer,
5055
startWorkTimer,
5156
stopWorkTimer,
5257
stopFailedWorkTimer,
@@ -1337,6 +1342,7 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
13371342
performWork(Sync, null);
13381343
} else if (!isCallbackScheduled) {
13391344
isCallbackScheduled = true;
1345+
startRequestCallbackTimer();
13401346
scheduleDeferredCallback(performAsyncWork);
13411347
}
13421348
}
@@ -1427,6 +1433,12 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
14271433
// Keep working on roots until there's no more work, or until the we reach
14281434
// the deadlne.
14291435
findHighestPriorityRoot();
1436+
1437+
if (enableUserTimingAPI && deadline !== null) {
1438+
const didExpire = nextFlushedExpirationTime < recalculateCurrentTime();
1439+
stopRequestCallbackTimer(didExpire);
1440+
}
1441+
14301442
while (
14311443
nextFlushedRoot !== null &&
14321444
nextFlushedExpirationTime !== NoWork &&
@@ -1449,6 +1461,7 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
14491461
// If there's work left over, schedule a new callback.
14501462
if (nextFlushedRoot !== null && !isCallbackScheduled) {
14511463
isCallbackScheduled = true;
1464+
startRequestCallbackTimer();
14521465
scheduleDeferredCallback(performAsyncWork);
14531466
}
14541467

packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,15 @@ describe('ReactDebugFiberPerf', () => {
546546
ReactNoop.flush();
547547
expect(getFlameChart()).toMatchSnapshot();
548548
});
549+
550+
it('warns if async work expires (starvation)', () => {
551+
function Foo() {
552+
return <span />;
553+
}
554+
555+
ReactNoop.render(<Foo />);
556+
ReactNoop.expire(5000);
557+
ReactNoop.flush();
558+
expect(getFlameChart()).toMatchSnapshot();
559+
});
549560
});

packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
4-
"// Mount
4+
"⚛ (Waiting for async callback...)
5+
// Mount
6+
57
⚛ (React Tree Reconciliation)
68
⚛ AllLifecycles [mount]
79
⚛ AllLifecycles.componentWillMount
@@ -12,7 +14,9 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
1214
⚛ (Calling Lifecycle Methods: 1 Total)
1315
⚛ AllLifecycles.componentDidMount
1416
15-
// Update
17+
⚛ (Waiting for async callback...)
18+
// Update
19+
1620
⚛ (React Tree Reconciliation)
1721
⚛ AllLifecycles [update]
1822
⚛ AllLifecycles.componentWillReceiveProps
@@ -25,7 +29,9 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
2529
⚛ (Calling Lifecycle Methods: 2 Total)
2630
⚛ AllLifecycles.componentDidUpdate
2731
28-
// Unmount
32+
⚛ (Waiting for async callback...)
33+
// Unmount
34+
2935
⚛ (React Tree Reconciliation)
3036
3137
⚛ (Committing Changes)
@@ -36,7 +42,9 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
3642
`;
3743

3844
exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = `
39-
"// The commit phase should mention A and B just once
45+
"⚛ (Waiting for async callback...)
46+
// The commit phase should mention A and B just once
47+
4048
⚛ (React Tree Reconciliation)
4149
⚛ Parent [update]
4250
⚛ A [update]
@@ -50,8 +58,10 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc
5058
⚛ A.componentDidUpdate
5159
⚛ B.componentDidUpdate
5260
53-
// Because of deduplication, we don't know B was cascading,
54-
// but we should still see the warning for the commit phase.
61+
⚛ (Waiting for async callback...)
62+
// Because of deduplication, we don't know B was cascading,
63+
// but we should still see the warning for the commit phase.
64+
5565
⚛ (React Tree Reconciliation)
5666
⚛ Parent [update]
5767
⚛ A [update]
@@ -94,7 +104,9 @@ exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is
94104
`;
95105

96106
exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = `
97-
"// Should not print a warning
107+
"⚛ (Waiting for async callback...)
108+
// Should not print a warning
109+
98110
⚛ (React Tree Reconciliation)
99111
⚛ Parent [mount]
100112
⚛ NotCascading [mount]
@@ -104,7 +116,9 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
104116
⚛ (Committing Host Effects: 1 Total)
105117
⚛ (Calling Lifecycle Methods: 0 Total)
106118
107-
// Should not print a warning
119+
⚛ (Waiting for async callback...)
120+
// Should not print a warning
121+
108122
⚛ (React Tree Reconciliation)
109123
⚛ Parent [update]
110124
⚛ NotCascading [update]
@@ -117,7 +131,9 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
117131
`;
118132

119133
exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
120-
"// Mount
134+
"⚛ (Waiting for async callback...)
135+
// Mount
136+
121137
⚛ (React Tree Reconciliation)
122138
⚛ Parent [mount]
123139
⚛ Child [mount]
@@ -126,7 +142,9 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
126142
⚛ (Committing Host Effects: 1 Total)
127143
⚛ (Calling Lifecycle Methods: 0 Total)
128144
129-
// Update
145+
⚛ (Waiting for async callback...)
146+
// Update
147+
130148
⚛ (React Tree Reconciliation)
131149
⚛ Parent [update]
132150
⚛ Child [update]
@@ -135,7 +153,9 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
135153
⚛ (Committing Host Effects: 2 Total)
136154
⚛ (Calling Lifecycle Methods: 2 Total)
137155
138-
// Unmount
156+
⚛ (Waiting for async callback...)
157+
// Unmount
158+
139159
⚛ (React Tree Reconciliation)
140160
141161
⚛ (Committing Changes)
@@ -145,18 +165,24 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
145165
`;
146166

147167
exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
148-
"// Start mounting Parent and A
168+
"⚛ (Waiting for async callback...)
169+
// Start mounting Parent and A
170+
149171
⚛ (React Tree Reconciliation)
150172
⚛ Parent [mount]
151173
⚛ A [mount]
152174
⚛ Child [mount]
153175
154-
// Mount B just a little (but not enough to memoize)
176+
⚛ (Waiting for async callback...)
177+
// Mount B just a little (but not enough to memoize)
178+
155179
⚛ (React Tree Reconciliation)
156180
⚛ Parent [mount]
157181
⚛ B [mount]
158182
159-
// Complete B and Parent
183+
⚛ (Waiting for async callback...)
184+
// Complete B and Parent
185+
160186
⚛ (React Tree Reconciliation)
161187
⚛ Parent [mount]
162188
⚛ B [mount]
@@ -177,7 +203,9 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
177203
⚛ (Committing Host Effects: 1 Total)
178204
⚛ (Calling Lifecycle Methods: 0 Total)
179205
180-
// Flush the child
206+
⚛ (Waiting for async callback...)
207+
// Flush the child
208+
181209
⚛ (React Tree Reconciliation)
182210
⚛ Child [mount]
183211
@@ -188,7 +216,9 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
188216
`;
189217

190218
exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
191-
"// Stop on Baddie and restart from Boundary
219+
"⚛ (Waiting for async callback...)
220+
// Stop on Baddie and restart from Boundary
221+
192222
⚛ (React Tree Reconciliation)
193223
⚛ Parent [mount]
194224
⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
@@ -211,7 +241,9 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
211241
`;
212242

213243
exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
214-
"// Will fatal
244+
"⚛ (Waiting for async callback...)
245+
// Will fatal
246+
215247
⚛ (React Tree Reconciliation)
216248
⚛ Parent [mount]
217249
⚛ Baddie [mount]
@@ -220,7 +252,9 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
220252
⚛ (Committing Host Effects: 1 Total)
221253
⚛ (Calling Lifecycle Methods: 1 Total)
222254
223-
// Will reconcile from a clean state
255+
⚛ (Waiting for async callback...)
256+
// Will reconcile from a clean state
257+
224258
⚛ (React Tree Reconciliation)
225259
⚛ Parent [mount]
226260
⚛ Child [mount]
@@ -232,7 +266,9 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
232266
`;
233267

234268
exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
235-
"// Should include just A and B, no Parents
269+
"⚛ (Waiting for async callback...)
270+
// Should include just A and B, no Parents
271+
236272
⚛ (React Tree Reconciliation)
237273
⚛ A [update]
238274
⚛ B [update]
@@ -244,7 +280,9 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
244280
`;
245281

246282
exports[`ReactDebugFiberPerf supports portals 1`] = `
247-
"⚛ (React Tree Reconciliation)
283+
"⚛ (Waiting for async callback...)
284+
285+
⚛ (React Tree Reconciliation)
248286
⚛ Parent [mount]
249287
⚛ Child [mount]
250288
@@ -255,7 +293,9 @@ exports[`ReactDebugFiberPerf supports portals 1`] = `
255293
`;
256294

257295
exports[`ReactDebugFiberPerf supports returns 1`] = `
258-
"⚛ (React Tree Reconciliation)
296+
"⚛ (Waiting for async callback...)
297+
298+
⚛ (React Tree Reconciliation)
259299
⚛ App [mount]
260300
⚛ CoParent [mount]
261301
⚛ HandleReturns [mount]
@@ -272,26 +312,42 @@ exports[`ReactDebugFiberPerf supports returns 1`] = `
272312
`;
273313

274314
exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = `
275-
"⚛ (React Tree Reconciliation)
276-
⚛ Foo [mount]
315+
"⚛ (Waiting for async callback...)
277316
278-
(React Tree Reconciliation) Warning: A top-level update interrupted the previous render
317+
(React Tree Reconciliation)
279318
⚛ Foo [mount]
280319
320+
⚛ (Waiting for async callback...)
321+
⛔ (React Tree Reconciliation) Warning: A top-level update interrupted the previous render
322+
⚛ Foo [mount]
323+
⚛ (Committing Changes)
324+
⚛ (Committing Host Effects: 1 Total)
325+
⚛ (Calling Lifecycle Methods: 0 Total)
326+
327+
⚛ (React Tree Reconciliation)
328+
281329
⚛ (Committing Changes)
282330
⚛ (Committing Host Effects: 1 Total)
283-
⚛ (Calling Lifecycle Methods: 0 Total)
331+
⚛ (Calling Lifecycle Methods: 1 Total)
332+
"
333+
`;
334+
335+
exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
336+
"⛔ (Waiting for async callback...) Warning: Async work expired
284337
285338
⚛ (React Tree Reconciliation)
339+
⚛ Foo [mount]
286340
287341
⚛ (Committing Changes)
288342
⚛ (Committing Host Effects: 1 Total)
289-
⚛ (Calling Lifecycle Methods: 1 Total)
343+
⚛ (Calling Lifecycle Methods: 0 Total)
290344
"
291345
`;
292346

293347
exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
294-
"// Should print a warning
348+
"⚛ (Waiting for async callback...)
349+
// Should print a warning
350+
295351
⚛ (React Tree Reconciliation)
296352
⚛ Parent [mount]
297353
⚛ Cascading [mount]
@@ -311,7 +367,9 @@ exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
311367
`;
312368

313369
exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = `
314-
"// Rendering the first root
370+
"⚛ (Waiting for async callback...)
371+
// Rendering the first root
372+
315373
⚛ (React Tree Reconciliation)
316374
⚛ Cascading [mount]
317375

0 commit comments

Comments
 (0)