@samitouri / QOS-React-2 / commits / 22f7663f14

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 });