Skip to content

Commit 103d9d2

Browse files
author
Brian Vaughn
committed
Updated treeBaseTime bubbling behavior to handle update cases too
1 parent 1f8f723 commit 103d9d2

File tree

2 files changed

+213
-72
lines changed

2 files changed

+213
-72
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,19 @@ function updateSuspenseComponent(
11991199
}
12001200
}
12011201

1202+
// Because primaryChildFragment is a new fiber that we're inserting as the
1203+
// parent of a new tree, we need to set its treeBaseDuration.
1204+
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1205+
// treeBaseDuration is the sum of all the child tree base durations.
1206+
let treeBaseDuration = 0;
1207+
let hiddenChild = primaryChildFragment.child;
1208+
while (hiddenChild !== null) {
1209+
treeBaseDuration += hiddenChild.treeBaseDuration;
1210+
hiddenChild = hiddenChild.sibling;
1211+
}
1212+
primaryChildFragment.treeBaseDuration = treeBaseDuration;
1213+
}
1214+
12021215
// Clone the fallback child fragment, too. These we'll continue
12031216
// working on.
12041217
const fallbackChildFragment = (primaryChildFragment.sibling = createWorkInProgress(
@@ -1252,17 +1265,6 @@ function updateSuspenseComponent(
12521265
null,
12531266
);
12541267

1255-
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1256-
// treeBaseDuration is the sum of all the child tree base durations.
1257-
let treeBaseDuration = 0;
1258-
let hiddenChild = currentPrimaryChild;
1259-
while (hiddenChild !== null) {
1260-
treeBaseDuration += hiddenChild.treeBaseDuration;
1261-
hiddenChild = hiddenChild.sibling;
1262-
}
1263-
primaryChildFragment.treeBaseDuration = treeBaseDuration;
1264-
}
1265-
12661268
primaryChildFragment.effectTag |= Placement;
12671269
primaryChildFragment.child = currentPrimaryChild;
12681270
currentPrimaryChild.return = primaryChildFragment;
@@ -1282,6 +1284,19 @@ function updateSuspenseComponent(
12821284
);
12831285
}
12841286

1287+
// Because primaryChildFragment is a new fiber that we're inserting as the
1288+
// parent of a new tree, we need to set its treeBaseDuration.
1289+
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1290+
// treeBaseDuration is the sum of all the child tree base durations.
1291+
let treeBaseDuration = 0;
1292+
let hiddenChild = primaryChildFragment.child;
1293+
while (hiddenChild !== null) {
1294+
treeBaseDuration += hiddenChild.treeBaseDuration;
1295+
hiddenChild = hiddenChild.sibling;
1296+
}
1297+
primaryChildFragment.treeBaseDuration = treeBaseDuration;
1298+
}
1299+
12851300
// Create a fragment from the fallback children, too.
12861301
const fallbackChildFragment = (primaryChildFragment.sibling = createFiberFromFragment(
12871302
nextFallbackChildren,

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

Lines changed: 187 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
9797
textResourceShouldFail = false;
9898
});
9999

100-
function Text(props) {
101-
ReactTestRenderer.unstable_yield(props.text);
102-
return props.text;
100+
function Text({fakeRenderDuration = 0, text = 'Text'}) {
101+
advanceTimeBy(fakeRenderDuration);
102+
ReactTestRenderer.unstable_yield(text);
103+
return text;
103104
}
104105

105106
function AsyncText({fakeRenderDuration = 0, ms, text}) {
@@ -250,82 +251,207 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
250251
return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
251252
};
252253

253-
const NonSuspending = () => {
254-
ReactTestRenderer.unstable_yield('NonSuspending');
255-
advanceTimeBy(5);
256-
return 'NonSuspending';
257-
};
258-
259-
App = () => {
254+
App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
260255
ReactTestRenderer.unstable_yield('App');
261256
return (
262257
<Profiler id="root" onRender={onRender}>
263258
<Suspense maxDuration={500} fallback={<Fallback />}>
264-
<Suspending />
265-
<NonSuspending />
259+
{shouldSuspend && <Suspending />}
260+
<Text fakeRenderDuration={textRenderDuration} text={text} />
266261
</Suspense>
267262
</Profiler>
268263
);
269264
};
270265
});
271266

272-
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
273-
const root = ReactTestRenderer.create(<App />);
274-
expect(root.toJSON()).toEqual(['Loading...']);
275-
expect(onRender).toHaveBeenCalledTimes(1);
267+
describe('when suspending during mount', () => {
268+
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
269+
const root = ReactTestRenderer.create(<App shouldSuspend={true} />);
270+
expect(root.toJSON()).toEqual(['Loading...']);
271+
expect(onRender).toHaveBeenCalledTimes(1);
276272

277-
// Initial mount only shows the "Loading..." Fallback.
278-
// The treeBaseDuration then should be 10ms spent rendering Fallback,
279-
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
280-
expect(onRender.mock.calls[0][2]).toBe(18);
281-
expect(onRender.mock.calls[0][3]).toBe(10);
273+
// Initial mount only shows the "Loading..." Fallback.
274+
// The treeBaseDuration then should be 10ms spent rendering Fallback,
275+
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
276+
expect(onRender.mock.calls[0][2]).toBe(18);
277+
expect(onRender.mock.calls[0][3]).toBe(10);
282278

283-
jest.advanceTimersByTime(1000);
279+
jest.advanceTimersByTime(1000);
284280

285-
expect(root.toJSON()).toEqual(['Loaded', 'NonSuspending']);
286-
expect(onRender).toHaveBeenCalledTimes(2);
281+
expect(root.toJSON()).toEqual(['Loaded', 'Text']);
282+
expect(onRender).toHaveBeenCalledTimes(2);
287283

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 the full 8ms spent in the tree.
291-
expect(onRender.mock.calls[1][2]).toBe(1);
292-
expect(onRender.mock.calls[1][3]).toBe(8);
284+
// When the suspending data is resolved and our final UI is rendered,
285+
// the baseDuration should only include the 1ms re-rendering AsyncText,
286+
// but the treeBaseDuration should include the full 8ms spent in the tree.
287+
expect(onRender.mock.calls[1][2]).toBe(1);
288+
expect(onRender.mock.calls[1][3]).toBe(8);
289+
});
290+
291+
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
292+
const root = ReactTestRenderer.create(<App shouldSuspend={true} />, {
293+
unstable_isConcurrent: true,
294+
});
295+
296+
expect(root).toFlushAndYield([
297+
'App',
298+
'Suspending',
299+
'Suspend! [Loaded]',
300+
'Text',
301+
'Fallback',
302+
]);
303+
expect(root).toMatchRenderedOutput(null);
304+
305+
// Show the fallback UI.
306+
jest.advanceTimersByTime(750);
307+
expect(root).toMatchRenderedOutput('Loading...');
308+
expect(onRender).toHaveBeenCalledTimes(1);
309+
310+
// Initial mount only shows the "Loading..." Fallback.
311+
// The treeBaseDuration then should be 10ms spent rendering Fallback,
312+
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
313+
expect(onRender.mock.calls[0][2]).toBe(18);
314+
expect(onRender.mock.calls[0][3]).toBe(10);
315+
316+
// Resolve the pending promise.
317+
jest.advanceTimersByTime(250);
318+
expect(ReactTestRenderer).toHaveYielded([
319+
'Promise resolved [Loaded]',
320+
]);
321+
expect(root).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
322+
expect(root).toMatchRenderedOutput('LoadedText');
323+
expect(onRender).toHaveBeenCalledTimes(2);
324+
325+
// When the suspending data is resolved and our final UI is rendered,
326+
// both times should include the 8ms re-rendering Suspending and AsyncText.
327+
expect(onRender.mock.calls[1][2]).toBe(8);
328+
expect(onRender.mock.calls[1][3]).toBe(8);
329+
});
293330
});
294331

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,
332+
describe('when suspending during update', () => {
333+
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
334+
const root = ReactTestRenderer.create(
335+
<App shouldSuspend={false} textRenderDuration={5} />,
336+
);
337+
expect(root.toJSON()).toEqual('Text');
338+
expect(onRender).toHaveBeenCalledTimes(1);
339+
340+
// Initial mount only shows the "Text" text.
341+
// It should take 5ms to render.
342+
expect(onRender.mock.calls[0][2]).toBe(5);
343+
expect(onRender.mock.calls[0][3]).toBe(5);
344+
345+
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
346+
expect(root.toJSON()).toEqual(['Loading...']);
347+
expect(onRender).toHaveBeenCalledTimes(2);
348+
349+
// The suspense update should only show the "Loading..." Fallback.
350+
// Both durations should include 10ms spent rendering Fallback
351+
// plus the 8ms rendering the (hidden) components.
352+
expect(onRender.mock.calls[1][2]).toBe(18);
353+
expect(onRender.mock.calls[1][3]).toBe(18);
354+
355+
root.update(
356+
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
357+
);
358+
expect(root.toJSON()).toEqual(['Loading...']);
359+
expect(onRender).toHaveBeenCalledTimes(3);
360+
361+
// If we force another update while still timed out,
362+
// but this time the Text component took 1ms longer to render.
363+
// This should impact both actualDuration and treeBaseDuration.
364+
expect(onRender.mock.calls[2][2]).toBe(19);
365+
expect(onRender.mock.calls[2][3]).toBe(19);
366+
367+
jest.advanceTimersByTime(1000);
368+
369+
// TODO Change expected onRender count to 4.
370+
// At the moment, every time we suspended while rendering will cause a commit.
371+
// This will probably change in the future, but that's why there are two new ones.
372+
expect(root.toJSON()).toEqual(['Loaded', 'New']);
373+
expect(onRender).toHaveBeenCalledTimes(5);
374+
375+
// When the suspending data is resolved and our final UI is rendered,
376+
// the baseDuration should only include the 1ms re-rendering AsyncText,
377+
// but the treeBaseDuration should include the full 9ms spent in the tree.
378+
expect(onRender.mock.calls[3][2]).toBe(1);
379+
expect(onRender.mock.calls[3][3]).toBe(9);
380+
381+
// TODO Remove these assertions once this commit is gone.
382+
// For now, there was no actual work done during this commit; see above comment.
383+
expect(onRender.mock.calls[4][2]).toBe(0);
384+
expect(onRender.mock.calls[4][3]).toBe(9);
298385
});
299386

300-
expect(root).toFlushAndYield([
301-
'App',
302-
'Suspending',
303-
'Suspend! [Loaded]',
304-
'NonSuspending',
305-
'Fallback',
306-
]);
307-
expect(root).toMatchRenderedOutput(null);
308-
309-
jest.advanceTimersByTime(1000);
310-
311-
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Loaded]']);
312-
expect(root).toMatchRenderedOutput('Loading...');
313-
expect(onRender).toHaveBeenCalledTimes(1);
314-
315-
// Initial mount only shows the "Loading..." Fallback.
316-
// The treeBaseDuration then should be 10ms spent rendering Fallback,
317-
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
318-
expect(onRender.mock.calls[0][2]).toBe(18);
319-
expect(onRender.mock.calls[0][3]).toBe(10);
320-
321-
expect(root).toFlushAndYield(['Suspending', 'Loaded', 'NonSuspending']);
322-
expect(root).toMatchRenderedOutput('LoadedNonSuspending');
323-
expect(onRender).toHaveBeenCalledTimes(2);
324-
325-
// When the suspending data is resolved and our final UI is rendered,
326-
// both times should include the 8ms re-rendering Suspending and AsyncText.
327-
expect(onRender.mock.calls[1][2]).toBe(8);
328-
expect(onRender.mock.calls[1][3]).toBe(8);
387+
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
388+
const root = ReactTestRenderer.create(
389+
<App shouldSuspend={false} textRenderDuration={5} />,
390+
{
391+
unstable_isConcurrent: true,
392+
},
393+
);
394+
395+
expect(root).toFlushAndYield(['App', 'Text']);
396+
expect(root).toMatchRenderedOutput('Text');
397+
expect(onRender).toHaveBeenCalledTimes(1);
398+
399+
// Initial mount only shows the "Text" text.
400+
// It should take 5ms to render.
401+
expect(onRender.mock.calls[0][2]).toBe(5);
402+
expect(onRender.mock.calls[0][3]).toBe(5);
403+
404+
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
405+
expect(root).toFlushAndYield([
406+
'App',
407+
'Suspending',
408+
'Suspend! [Loaded]',
409+
'Text',
410+
'Fallback',
411+
]);
412+
expect(root).toMatchRenderedOutput('Text');
413+
414+
// Show the fallback UI.
415+
jest.advanceTimersByTime(750);
416+
expect(root).toMatchRenderedOutput('Loading...');
417+
expect(onRender).toHaveBeenCalledTimes(2);
418+
419+
// The suspense update should only show the "Loading..." Fallback.
420+
// The actual duration should include 10ms spent rendering Fallback,
421+
// plus the 8ms render all of the hidden, suspended subtree.
422+
// But the tree base duration should only include 10ms spent rendering Fallback,
423+
// plus the 5ms rendering the previously committed version of the hidden tree.
424+
expect(onRender.mock.calls[1][2]).toBe(18);
425+
expect(onRender.mock.calls[1][3]).toBe(15);
426+
427+
// Update again while timed out.
428+
root.update(
429+
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
430+
);
431+
expect(root).toFlushAndYield([
432+
'App',
433+
'Suspending',
434+
'Suspend! [Loaded]',
435+
'New',
436+
'Fallback',
437+
]);
438+
expect(root).toMatchRenderedOutput('Loading...');
439+
expect(onRender).toHaveBeenCalledTimes(2);
440+
441+
// Resolve the pending promise.
442+
jest.advanceTimersByTime(250);
443+
expect(ReactTestRenderer).toHaveYielded([
444+
'Promise resolved [Loaded]',
445+
]);
446+
expect(root).toFlushAndYield(['App', 'Suspending', 'Loaded', 'New']);
447+
expect(onRender).toHaveBeenCalledTimes(3);
448+
449+
// When the suspending data is resolved and our final UI is rendered,
450+
// both times should include the 6ms rendering Text,
451+
// the 2ms rendering Suspending, and the 1ms rendering AsyncText.
452+
expect(onRender.mock.calls[2][2]).toBe(9);
453+
expect(onRender.mock.calls[2][3]).toBe(9);
454+
});
329455
});
330456
});
331457
});

0 commit comments

Comments
 (0)