Skip to content

Commit 9af69ea

Browse files
author
Brian Vaughn
committed
Hacky fix for actualDuration in a suspended concurrent tree
1 parent 273d5c0 commit 9af69ea

File tree

2 files changed

+97
-41
lines changed

2 files changed

+97
-41
lines changed

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,16 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
12861286
// Record the time spent rendering before an error was thrown.
12871287
// This avoids inaccurate Profiler durations in the case of a suspended render.
12881288
stopProfilerTimerIfRunningAndRecordDelta(nextUnitOfWork, true);
1289+
1290+
// HACK Also propagate actualDuration for the time spent in the fiber that errored.
1291+
// This avoids inaccurate Profiler durations in the case of a suspended render.
1292+
// This happens automatically for sync renders, because of resetChildExpirationTime.
1293+
if (nextUnitOfWork.mode & ConcurrentMode) {
1294+
const returnFiber = nextUnitOfWork.return;
1295+
if (returnFiber !== null) {
1296+
returnFiber.actualDuration += nextUnitOfWork.actualDuration;
1297+
}
1298+
}
12891299
}
12901300

12911301
if (__DEV__) {

packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -230,56 +230,102 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
230230
expect(root).toMatchRenderedOutput('AB2C');
231231
});
232232

233-
it('properly accounts for base durations when a suspended times out', () => {
234-
// Order of parameters: id, phase, actualDuration, treeBaseDuration
235-
const onRender = jest.fn();
233+
describe('profiler durations', () => {
234+
let App;
235+
let Fallback;
236+
let Suspending;
237+
let onRender;
238+
239+
beforeEach(() => {
240+
// Order of parameters: id, phase, actualDuration, treeBaseDuration
241+
onRender = jest.fn();
242+
243+
Fallback = () => {
244+
ReactTestRenderer.unstable_yield('Fallback');
245+
advanceTimeBy(5);
246+
return 'Loading...';
247+
};
236248

237-
const Fallback = () => {
238-
advanceTimeBy(5);
239-
return 'Loading...';
240-
};
249+
Suspending = () => {
250+
ReactTestRenderer.unstable_yield('Suspending');
251+
advanceTimeBy(2);
252+
return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
253+
};
241254

242-
const Suspending = () => {
243-
advanceTimeBy(2);
244-
return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
245-
};
255+
App = () => {
256+
ReactTestRenderer.unstable_yield('App');
257+
return (
258+
<Profiler id="root" onRender={onRender}>
259+
<Suspense maxDuration={500} fallback={<Fallback />}>
260+
<Suspending />
261+
</Suspense>
262+
</Profiler>
263+
);
264+
};
265+
});
246266

247-
function App() {
248-
return (
249-
<Profiler id="root" onRender={onRender}>
250-
<Suspense maxDuration={500} fallback={<Fallback />}>
251-
<Suspending />
252-
</Suspense>
253-
</Profiler>
254-
);
255-
}
267+
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
268+
const root = ReactTestRenderer.create(<App />);
269+
expect(root.toJSON()).toEqual('Loading...');
270+
expect(onRender).toHaveBeenCalledTimes(2);
256271

257-
// Initial mount
258-
const root = ReactTestRenderer.create(<App />);
259-
expect(root.toJSON()).toEqual('Loading...');
260-
expect(onRender).toHaveBeenCalledTimes(2);
272+
// Initial mount should be 3ms–
273+
// 2ms from Suspending, and 1ms from the AsyncText it renders.
274+
expect(onRender.mock.calls[0][2]).toBe(3);
275+
expect(onRender.mock.calls[0][3]).toBe(3);
261276

262-
// Initial mount should be 3ms–
263-
// 2ms from Suspending, and 1ms from the AsyncText it renders.
264-
expect(onRender.mock.calls[0][2]).toBe(3);
265-
expect(onRender.mock.calls[0][3]).toBe(3);
277+
// When the fallback UI is displayed, and the origina UI hidden,
278+
// the baseDuration should only include the 5ms spent rendering Fallback,
279+
// but the treeBaseDuration should include that and the 3ms spent in Suspending.
280+
expect(onRender.mock.calls[1][2]).toBe(5);
281+
expect(onRender.mock.calls[1][3]).toBe(8);
266282

267-
// When the fallback UI is displayed, and the origina UI hidden,
268-
// the baseDuration should only include the 5ms spent rendering Fallback,
269-
// but the treeBaseDuration should include that and the 3ms spent in Suspending.
270-
expect(onRender.mock.calls[1][2]).toBe(5);
271-
expect(onRender.mock.calls[1][3]).toBe(8);
283+
jest.advanceTimersByTime(1000);
272284

273-
jest.advanceTimersByTime(1000);
285+
expect(root.toJSON()).toEqual('Loaded');
286+
expect(onRender).toHaveBeenCalledTimes(3);
274287

275-
expect(root.toJSON()).toEqual('Loaded');
276-
expect(onRender).toHaveBeenCalledTimes(3);
288+
// When the suspending data is resolved and our final UI is rendered,
289+
// the baseDuration should only include the 1ms re-rendering AsyncText,
290+
// but the treeBaseDuration should include that and the 2ms spent rendering Suspending.
291+
expect(onRender.mock.calls[2][2]).toBe(1);
292+
expect(onRender.mock.calls[2][3]).toBe(3);
293+
});
277294

278-
// When the suspending data is resolved and our final UI is rendered,
279-
// the baseDuration should only include the 1ms re-rendering AsyncText,
280-
// but the treeBaseDuration should include that and the 2ms spent rendering Suspending.
281-
expect(onRender.mock.calls[2][2]).toBe(1);
282-
expect(onRender.mock.calls[2][3]).toBe(3);
295+
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
296+
const root = ReactTestRenderer.create(<App />, {
297+
unstable_isConcurrent: true,
298+
});
299+
300+
expect(root).toFlushAndYield([
301+
'App',
302+
'Suspending',
303+
'Suspend! [Loaded]',
304+
'Fallback',
305+
]);
306+
expect(root).toMatchRenderedOutput(null);
307+
308+
jest.advanceTimersByTime(1000);
309+
310+
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Loaded]']);
311+
expect(root).toMatchRenderedOutput('Loading...');
312+
expect(onRender).toHaveBeenCalledTimes(1);
313+
314+
// Initial mount only shows the "Loading..." Fallback.
315+
// The treeBaseDuration then should be 5ms spent rendering Fallback,
316+
// but the actualDuration should include that and the 3ms spent in Suspending.
317+
expect(onRender.mock.calls[0][2]).toBe(8);
318+
expect(onRender.mock.calls[0][3]).toBe(5);
319+
320+
expect(root).toFlushAndYield(['Suspending', 'Loaded']);
321+
expect(root).toMatchRenderedOutput('Loaded');
322+
expect(onRender).toHaveBeenCalledTimes(2);
323+
324+
// When the suspending data is resolved and our final UI is rendered,
325+
// both times should include the 3ms re-rendering Suspending and AsyncText.
326+
expect(onRender.mock.calls[1][2]).toBe(3);
327+
expect(onRender.mock.calls[1][3]).toBe(3);
328+
});
283329
});
284330
});
285331
}

0 commit comments

Comments
 (0)