Profiler: Don't count timed out (hidden) subtrees in base duration (#18966)
Brian Vaughn committed
May 20, 2020 at 18:36 UTC
22f7663f14f12ebd6174292931e94d2b352cf666
5 files changed
+120
-30
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+13
-1
@@ -1700,7 +1700,7 @@ function resetChildLanes(completedWork: Fiber) {
1700
// In profiling mode, resetChildExpirationTime is also used to reset
1701
// profiler durations.
1702
let actualDuration = completedWork.actualDuration;
1703
- let treeBaseDuration = completedWork.selfBaseDuration;
1703
+ let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
1704
1705
// When a fiber is cloned, its actualDuration is reset to 0. This value will
1706
// only be updated if work is done on the fiber (i.e. it doesn't bailout).
@@ -1725,6 +1725,18 @@ function resetChildLanes(completedWork: Fiber) {
1725
treeBaseDuration += child.treeBaseDuration;
1726
child = child.sibling;
1727
}
1728
+
1729
+ const isTimedOutSuspense =
1730
+ completedWork.tag === SuspenseComponent &&
1731
+ completedWork.memoizedState !== null;
1732
+ if (isTimedOutSuspense) {
1733
+ // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1734
+ const primaryChildFragment = completedWork.child;
1735
+ if (primaryChildFragment !== null) {
1736
+ treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1737
+ }
1738
+ }
1739
+
1740
completedWork.actualDuration = actualDuration;
1741
completedWork.treeBaseDuration = treeBaseDuration;
1742
} else {
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+13
-1
@@ -1775,7 +1775,7 @@ function resetChildExpirationTime(completedWork: Fiber) {
1775
// In profiling mode, resetChildExpirationTime is also used to reset
1776
// profiler durations.
1777
let actualDuration = completedWork.actualDuration;
1778
- let treeBaseDuration = completedWork.selfBaseDuration;
1778
+ let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
1779
1780
// When a fiber is cloned, its actualDuration is reset to 0. This value will
1781
// only be updated if work is done on the fiber (i.e. it doesn't bailout).
@@ -1804,6 +1804,18 @@ function resetChildExpirationTime(completedWork: Fiber) {
1804
treeBaseDuration += child.treeBaseDuration;
1805
child = child.sibling;
1806
}
1807
+
1808
+ const isTimedOutSuspense =
1809
+ completedWork.tag === SuspenseComponent &&
1810
+ completedWork.memoizedState !== null;
1811
+ if (isTimedOutSuspense) {
1812
+ // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1813
+ const primaryChildFragment = completedWork.child;
1814
+ if (primaryChildFragment !== null) {
1815
+ treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1816
+ }
1817
+ }
1818
+
1819
completedWork.actualDuration = actualDuration;
1820
completedWork.treeBaseDuration = treeBaseDuration;
1821
} else {
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+1
-7
@@ -2709,13 +2709,7 @@ describe('ReactSuspenseList', () => {
2709
// actualDuration
2710
expect(onRender.mock.calls[2][2]).toBe((1 + 4 + 5 + 3) * 2 + 3);
2711
// treeBaseDuration
2712
- expect(onRender.mock.calls[2][3]).toBe(
2713
- 1 +
2714
- 4 +
2715
- 3 +
2716
- 3 +
2717
- /* Resuspending a boundary also includes the content in base duration but it shouldn't */ 5,
2718
- );
2712
+ expect(onRender.mock.calls[2][3]).toBe(1 + 4 + 3 + 3);
2713
2714
await C.resolve();
2715
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+7
-21
@@ -405,19 +405,9 @@ describe('ReactSuspensePlaceholder', () => {
405
// The suspense update should only show the "Loading..." Fallback.
406
// The actual duration should include 10ms spent rendering Fallback,
407
// plus the 8ms render all of the hidden, suspended subtree.
408
- // Note from Andrew to Brian: I don't fully understand why this one
409
- // diverges, but I checked and it matches the times we get when
410
- // we run this same test in Concurrent Mode.
411
- if (gate(flags => flags.new)) {
412
- // But the tree base duration should only include 10ms spent rendering Fallback,
413
- // plus the 5ms rendering the previously committed version of the hidden tree.
414
- expect(onRender.mock.calls[1][2]).toBe(18);
415
- expect(onRender.mock.calls[1][3]).toBe(15);
416
- } else {
417
- // Old behavior includes the time spent on the primary tree.
418
- expect(onRender.mock.calls[1][2]).toBe(18);
419
- expect(onRender.mock.calls[1][3]).toBe(18);
420
- }
408
+ // But the tree base duration should only include 10ms spent rendering Fallback,
409
+ expect(onRender.mock.calls[1][2]).toBe(18);
410
+ expect(onRender.mock.calls[1][3]).toBe(10);
411
412
ReactNoop.renderLegacySyncRoot(
413
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
@@ -432,18 +422,15 @@ describe('ReactSuspensePlaceholder', () => {
422
expect(ReactNoop).toMatchRenderedOutput('Loading...');
423
expect(onRender).toHaveBeenCalledTimes(3);
424
435
- // Note from Andrew to Brian: I don't fully understand why this one
436
- // diverges, but I checked and it matches the times we get when
437
- // we run this same test in Concurrent Mode.
425
if (gate(flags => flags.new)) {
426
expect(onRender.mock.calls[1][2]).toBe(18);
440
- expect(onRender.mock.calls[1][3]).toBe(15);
427
+ expect(onRender.mock.calls[1][3]).toBe(10);
428
} else {
429
// If we force another update while still timed out,
430
// but this time the Text component took 1ms longer to render.
431
// This should impact both actualDuration and treeBaseDuration.
432
expect(onRender.mock.calls[2][2]).toBe(19);
446
- expect(onRender.mock.calls[2][3]).toBe(19);
433
+ expect(onRender.mock.calls[2][3]).toBe(10);
434
}
435
436
jest.advanceTimersByTime(1000);
@@ -500,10 +487,9 @@ describe('ReactSuspensePlaceholder', () => {
487
// The suspense update should only show the "Loading..." Fallback.
488
// The actual duration should include 10ms spent rendering Fallback,
489
// plus the 8ms render all of the hidden, suspended subtree.
503
- // But the tree base duration should only include 10ms spent rendering Fallback,
504
- // plus the 5ms rendering the previously committed version of the hidden tree.
490
+ // But the tree base duration should only include 10ms spent rendering Fallback.
491
expect(onRender.mock.calls[1][2]).toBe(18);
506
- expect(onRender.mock.calls[1][3]).toBe(15);
492
+ expect(onRender.mock.calls[1][3]).toBe(10);
493
494
// Update again while timed out.
495
// Since this test was originally written we added an optimization to avoid
packages/react/src/__tests__/ReactProfiler-test.internal.js
+86
@@ -4145,6 +4145,92 @@ describe('Profiler', () => {
4145
expect(call[0]).toEqual('test-profiler');
4146
expect(call[4]).toMatchInteractions([]);
4147
});
4148
+
4149
+ it('should properly report base duration wrt suspended subtrees', async () => {
4150
+ loadModulesForTracing({useNoopRenderer: true});
4151
+
4152
+ const onRender = jest.fn();
4153
+
4154
+ let resolve = null;
4155
+ const promise = new Promise(_resolve => {
4156
+ resolve = _resolve;
4157
+ });
4158
+
4159
+ function Other() {
4160
+ Scheduler.unstable_advanceTime(1);
4161
+ Scheduler.unstable_yieldValue('Other');
4162
+ return <div>Other</div>;
4163
+ }
4164
+
4165
+ function Fallback() {
4166
+ Scheduler.unstable_advanceTime(8);
4167
+ Scheduler.unstable_yieldValue('Fallback');
4168
+ return <div>Fallback</div>;
4169
+ }
4170
+
4171
+ let shouldSuspend = false;
4172
+ function Suspender() {
4173
+ Scheduler.unstable_advanceTime(15);
4174
+ if (shouldSuspend) {
4175
+ Scheduler.unstable_yieldValue('Suspender!');
4176
+ throw promise;
4177
+ }
4178
+ Scheduler.unstable_yieldValue('Suspender');
4179
+ return <div>Suspender</div>;
4180
+ }
4181
+
4182
+ function App() {
4183
+ return (
4184
+ <React.Profiler id="root" onRender={onRender}>
4185
+ <Other />
4186
+ <React.Suspense fallback={<Fallback />}>
4187
+ <Suspender />
4188
+ </React.Suspense>
4189
+ </React.Profiler>
4190
+ );
4191
+ }
4192
+
4193
+ ReactNoop.render(<App />);
4194
+ expect(Scheduler).toFlushAndYield(['Other', 'Suspender']);
4195
+ expect(ReactNoop).toMatchRenderedOutput(
4196
+ <>
4197
+ <div>Other</div>
4198
+ <div>Suspender</div>
4199
+ </>,
4200
+ );
4201
+ expect(onRender).toHaveBeenCalledTimes(1);
4202
+ expect(onRender.mock.calls[0][2]).toBe(1 + 15); // actual
4203
+ expect(onRender.mock.calls[0][3]).toBe(1 + 15); // base
4204
+
4205
+ shouldSuspend = true;
4206
+ ReactNoop.render(<App />);
4207
+ expect(Scheduler).toFlushAndYield(['Other', 'Suspender!', 'Fallback']);
4208
+ await awaitableAdvanceTimers(20000);
4209
+ expect(ReactNoop).toMatchRenderedOutput(
4210
+ <>
4211
+ <div>Other</div>
4212
+ <div hidden={true}>Suspender</div>
4213
+ <div>Fallback</div>
4214
+ </>,
4215
+ );
4216
+ expect(onRender).toHaveBeenCalledTimes(2);
4217
+ expect(onRender.mock.calls[1][2]).toBe(1 + 15 + 8); // actual
4218
+ expect(onRender.mock.calls[1][3]).toBe(1 + 8); // base
4219
+
4220
+ shouldSuspend = false;
4221
+ resolve();
4222
+ await promise;
4223
+ expect(Scheduler).toFlushAndYield(['Suspender']);
4224
+ expect(ReactNoop).toMatchRenderedOutput(
4225
+ <>
4226
+ <div>Other</div>
4227
+ <div>Suspender</div>
4228
+ </>,
4229
+ );
4230
+ expect(onRender).toHaveBeenCalledTimes(3);
4231
+ expect(onRender.mock.calls[2][2]).toBe(15); // actual
4232
+ expect(onRender.mock.calls[2][3]).toBe(1 + 15); // base
4233
+ });
4234
});
4235
});
4236
});