Skip to content

Commit f64f3e2

Browse files
author
Brian Vaughn
committed
Updated treeBaseTime bubbling behavior to handle update cases too
1 parent aa2ef2f commit f64f3e2

2 files changed

Lines changed: 213 additions & 72 deletions

File tree

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,19 @@ function updateSuspenseComponent(
11931193
}
11941194
}
11951195

1196+
// Because primaryChildFragment is a new fiber that we're inserting as the
1197+
// parent of a new tree, we need to set its treeBaseDuration.
1198+
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1199+
// treeBaseDuration is the sum of all the child tree base durations.
1200+
let treeBaseDuration = 0;
1201+
let hiddenChild = primaryChildFragment.child;
1202+
while (hiddenChild !== null) {
1203+
treeBaseDuration += hiddenChild.treeBaseDuration;
1204+
hiddenChild = hiddenChild.sibling;
1205+
}
1206+
primaryChildFragment.treeBaseDuration = treeBaseDuration;
1207+
}
1208+
11961209
// Clone the fallback child fragment, too. These we'll continue
11971210
// working on.
11981211
const fallbackChildFragment = (primaryChildFragment.sibling = createWorkInProgress(
@@ -1245,17 +1258,6 @@ function updateSuspenseComponent(
12451258
null,
12461259
);
12471260

1248-
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1249-
// treeBaseDuration is the sum of all the child tree base durations.
1250-
let treeBaseDuration = 0;
1251-
let hiddenChild = currentPrimaryChild;
1252-
while (hiddenChild !== null) {
1253-
treeBaseDuration += hiddenChild.treeBaseDuration;
1254-
hiddenChild = hiddenChild.sibling;
1255-
}
1256-
primaryChildFragment.treeBaseDuration = treeBaseDuration;
1257-
}
1258-
12591261
primaryChildFragment.effectTag |= Placement;
12601262
primaryChildFragment.child = currentPrimaryChild;
12611263
currentPrimaryChild.return = primaryChildFragment;
@@ -1271,6 +1273,19 @@ function updateSuspenseComponent(
12711273
primaryChildFragment.child = progressedPrimaryChild;
12721274
}
12731275

1276+
// Because primaryChildFragment is a new fiber that we're inserting as the
1277+
// parent of a new tree, we need to set its treeBaseDuration.
1278+
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1279+
// treeBaseDuration is the sum of all the child tree base durations.
1280+
let treeBaseDuration = 0;
1281+
let hiddenChild = primaryChildFragment.child;
1282+
while (hiddenChild !== null) {
1283+
treeBaseDuration += hiddenChild.treeBaseDuration;
1284+
hiddenChild = hiddenChild.sibling;
1285+
}
1286+
primaryChildFragment.treeBaseDuration = treeBaseDuration;
1287+
}
1288+
12741289
// Create a fragment from the fallback children, too.
12751290
const fallbackChildFragment = (primaryChildFragment.sibling = createFiberFromFragment(
12761291
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)