Skip to content

Commit

Permalink
Updated treeBaseTime bubbling behavior to handle update cases too
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed Nov 7, 2018
1 parent b51501b commit 2b52374
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 72 deletions.
37 changes: 26 additions & 11 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,19 @@ function updateSuspenseComponent(
}
}

// Because primaryChildFragment is a new fiber that we're inserting as the
// parent of a new tree, we need to set its treeBaseDuration.
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// treeBaseDuration is the sum of all the child tree base durations.
let treeBaseDuration = 0;
let hiddenChild = primaryChildFragment.child;
while (hiddenChild !== null) {
treeBaseDuration += hiddenChild.treeBaseDuration;
hiddenChild = hiddenChild.sibling;
}
primaryChildFragment.treeBaseDuration = treeBaseDuration;
}

// Clone the fallback child fragment, too. These we'll continue
// working on.
const fallbackChildFragment = (primaryChildFragment.sibling = createWorkInProgress(
Expand Down Expand Up @@ -1252,17 +1265,6 @@ function updateSuspenseComponent(
null,
);

if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// treeBaseDuration is the sum of all the child tree base durations.
let treeBaseDuration = 0;
let hiddenChild = currentPrimaryChild;
while (hiddenChild !== null) {
treeBaseDuration += hiddenChild.treeBaseDuration;
hiddenChild = hiddenChild.sibling;
}
primaryChildFragment.treeBaseDuration = treeBaseDuration;
}

primaryChildFragment.effectTag |= Placement;
primaryChildFragment.child = currentPrimaryChild;
currentPrimaryChild.return = primaryChildFragment;
Expand All @@ -1282,6 +1284,19 @@ function updateSuspenseComponent(
);
}

// Because primaryChildFragment is a new fiber that we're inserting as the
// parent of a new tree, we need to set its treeBaseDuration.
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// treeBaseDuration is the sum of all the child tree base durations.
let treeBaseDuration = 0;
let hiddenChild = primaryChildFragment.child;
while (hiddenChild !== null) {
treeBaseDuration += hiddenChild.treeBaseDuration;
hiddenChild = hiddenChild.sibling;
}
primaryChildFragment.treeBaseDuration = treeBaseDuration;
}

// Create a fragment from the fallback children, too.
const fallbackChildFragment = (primaryChildFragment.sibling = createFiberFromFragment(
nextFallbackChildren,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
textResourceShouldFail = false;
});

function Text(props) {
ReactTestRenderer.unstable_yield(props.text);
return props.text;
function Text({fakeRenderDuration = 0, text = 'Text'}) {
advanceTimeBy(fakeRenderDuration);
ReactTestRenderer.unstable_yield(text);
return text;
}

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

const NonSuspending = () => {
ReactTestRenderer.unstable_yield('NonSuspending');
advanceTimeBy(5);
return 'NonSuspending';
};

App = () => {
App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
ReactTestRenderer.unstable_yield('App');
return (
<Profiler id="root" onRender={onRender}>
<Suspense maxDuration={500} fallback={<Fallback />}>
<Suspending />
<NonSuspending />
{shouldSuspend && <Suspending />}
<Text fakeRenderDuration={textRenderDuration} text={text} />
</Suspense>
</Profiler>
);
};
});

it('properly accounts for base durations when a suspended times out in a sync tree', () => {
const root = ReactTestRenderer.create(<App />);
expect(root.toJSON()).toEqual(['Loading...']);
expect(onRender).toHaveBeenCalledTimes(1);
describe('when suspending during mount', () => {
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
const root = ReactTestRenderer.create(<App shouldSuspend={true} />);
expect(root.toJSON()).toEqual(['Loading...']);
expect(onRender).toHaveBeenCalledTimes(1);

// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
expect(onRender.mock.calls[0][2]).toBe(18);
expect(onRender.mock.calls[0][3]).toBe(10);
// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
expect(onRender.mock.calls[0][2]).toBe(18);
expect(onRender.mock.calls[0][3]).toBe(10);

jest.advanceTimersByTime(1000);
jest.advanceTimersByTime(1000);

expect(root.toJSON()).toEqual(['Loaded', 'NonSuspending']);
expect(onRender).toHaveBeenCalledTimes(2);
expect(root.toJSON()).toEqual(['Loaded', 'Text']);
expect(onRender).toHaveBeenCalledTimes(2);

// When the suspending data is resolved and our final UI is rendered,
// the baseDuration should only include the 1ms re-rendering AsyncText,
// but the treeBaseDuration should include the full 8ms spent in the tree.
expect(onRender.mock.calls[1][2]).toBe(1);
expect(onRender.mock.calls[1][3]).toBe(8);
// When the suspending data is resolved and our final UI is rendered,
// the baseDuration should only include the 1ms re-rendering AsyncText,
// but the treeBaseDuration should include the full 8ms spent in the tree.
expect(onRender.mock.calls[1][2]).toBe(1);
expect(onRender.mock.calls[1][3]).toBe(8);
});

it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
const root = ReactTestRenderer.create(<App shouldSuspend={true} />, {
unstable_isConcurrent: true,
});

expect(root).toFlushAndYield([
'App',
'Suspending',
'Suspend! [Loaded]',
'Text',
'Fallback',
]);
expect(root).toMatchRenderedOutput(null);

// Show the fallback UI.
jest.advanceTimersByTime(750);
expect(root).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(1);

// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
expect(onRender.mock.calls[0][2]).toBe(18);
expect(onRender.mock.calls[0][3]).toBe(10);

// Resolve the pending promise.
jest.advanceTimersByTime(250);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [Loaded]',
]);
expect(root).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
expect(root).toMatchRenderedOutput('LoadedText');
expect(onRender).toHaveBeenCalledTimes(2);

// When the suspending data is resolved and our final UI is rendered,
// both times should include the 8ms re-rendering Suspending and AsyncText.
expect(onRender.mock.calls[1][2]).toBe(8);
expect(onRender.mock.calls[1][3]).toBe(8);
});
});

it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
const root = ReactTestRenderer.create(<App />, {
unstable_isConcurrent: true,
describe('when suspending during update', () => {
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
const root = ReactTestRenderer.create(
<App shouldSuspend={false} textRenderDuration={5} />,
);
expect(root.toJSON()).toEqual('Text');
expect(onRender).toHaveBeenCalledTimes(1);

// Initial mount only shows the "Text" text.
// It should take 5ms to render.
expect(onRender.mock.calls[0][2]).toBe(5);
expect(onRender.mock.calls[0][3]).toBe(5);

root.update(<App shouldSuspend={true} textRenderDuration={5} />);
expect(root.toJSON()).toEqual(['Loading...']);
expect(onRender).toHaveBeenCalledTimes(2);

// The suspense update should only show the "Loading..." Fallback.
// Both durations should include 10ms spent rendering Fallback
// plus the 8ms rendering the (hidden) components.
expect(onRender.mock.calls[1][2]).toBe(18);
expect(onRender.mock.calls[1][3]).toBe(18);

root.update(
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
);
expect(root.toJSON()).toEqual(['Loading...']);
expect(onRender).toHaveBeenCalledTimes(3);

// If we force another update while still timed out,
// but this time the Text component took 1ms longer to render.
// This should impact both actualDuration and treeBaseDuration.
expect(onRender.mock.calls[2][2]).toBe(19);
expect(onRender.mock.calls[2][3]).toBe(19);

jest.advanceTimersByTime(1000);

// TODO Change expected onRender count to 4.
// At the moment, every time we suspended while rendering will cause a commit.
// This will probably change in the future, but that's why there are two new ones.
expect(root.toJSON()).toEqual(['Loaded', 'New']);
expect(onRender).toHaveBeenCalledTimes(5);

// When the suspending data is resolved and our final UI is rendered,
// the baseDuration should only include the 1ms re-rendering AsyncText,
// but the treeBaseDuration should include the full 9ms spent in the tree.
expect(onRender.mock.calls[3][2]).toBe(1);
expect(onRender.mock.calls[3][3]).toBe(9);

// TODO Remove these assertions once this commit is gone.
// For now, there was no actual work done during this commit; see above comment.
expect(onRender.mock.calls[4][2]).toBe(0);
expect(onRender.mock.calls[4][3]).toBe(9);
});

expect(root).toFlushAndYield([
'App',
'Suspending',
'Suspend! [Loaded]',
'NonSuspending',
'Fallback',
]);
expect(root).toMatchRenderedOutput(null);

jest.advanceTimersByTime(1000);

expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Loaded]']);
expect(root).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(1);

// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
expect(onRender.mock.calls[0][2]).toBe(18);
expect(onRender.mock.calls[0][3]).toBe(10);

expect(root).toFlushAndYield(['Suspending', 'Loaded', 'NonSuspending']);
expect(root).toMatchRenderedOutput('LoadedNonSuspending');
expect(onRender).toHaveBeenCalledTimes(2);

// When the suspending data is resolved and our final UI is rendered,
// both times should include the 8ms re-rendering Suspending and AsyncText.
expect(onRender.mock.calls[1][2]).toBe(8);
expect(onRender.mock.calls[1][3]).toBe(8);
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
const root = ReactTestRenderer.create(
<App shouldSuspend={false} textRenderDuration={5} />,
{
unstable_isConcurrent: true,
},
);

expect(root).toFlushAndYield(['App', 'Text']);
expect(root).toMatchRenderedOutput('Text');
expect(onRender).toHaveBeenCalledTimes(1);

// Initial mount only shows the "Text" text.
// It should take 5ms to render.
expect(onRender.mock.calls[0][2]).toBe(5);
expect(onRender.mock.calls[0][3]).toBe(5);

root.update(<App shouldSuspend={true} textRenderDuration={5} />);
expect(root).toFlushAndYield([
'App',
'Suspending',
'Suspend! [Loaded]',
'Text',
'Fallback',
]);
expect(root).toMatchRenderedOutput('Text');

// Show the fallback UI.
jest.advanceTimersByTime(750);
expect(root).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);

// The suspense update should only show the "Loading..." Fallback.
// The actual duration should include 10ms spent rendering Fallback,
// plus the 8ms render all of the hidden, suspended subtree.
// But the tree base duration should only include 10ms spent rendering Fallback,
// plus the 5ms rendering the previously committed version of the hidden tree.
expect(onRender.mock.calls[1][2]).toBe(18);
expect(onRender.mock.calls[1][3]).toBe(15);

// Update again while timed out.
root.update(
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
);
expect(root).toFlushAndYield([
'App',
'Suspending',
'Suspend! [Loaded]',
'New',
'Fallback',
]);
expect(root).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);

// Resolve the pending promise.
jest.advanceTimersByTime(250);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [Loaded]',
]);
expect(root).toFlushAndYield(['App', 'Suspending', 'Loaded', 'New']);
expect(onRender).toHaveBeenCalledTimes(3);

// When the suspending data is resolved and our final UI is rendered,
// both times should include the 6ms rendering Text,
// the 2ms rendering Suspending, and the 1ms rendering AsyncText.
expect(onRender.mock.calls[2][2]).toBe(9);
expect(onRender.mock.calls[2][3]).toBe(9);
});
});
});
});
Expand Down

0 comments on commit 2b52374

Please sign in to comment.