diff --git a/package.json b/package.json
index c91e902a6c17..a236574febd6 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
"./scripts/jest/environment.js"
],
"setupTestFrameworkScriptFile": "./scripts/jest/test-framework-setup.js",
- "testRegex": "/__tests__/",
+ "testRegex": "/__tests__/.*(\\.js|coffee|ts)$",
"moduleFileExtensions": [
"js",
"json",
diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt
index 9c2bebbd84db..325811e74910 100644
--- a/scripts/fiber/tests-failing.txt
+++ b/scripts/fiber/tests-failing.txt
@@ -1,21 +1,6 @@
src/isomorphic/classic/__tests__/ReactContextValidator-test.js
* should pass previous context to lifecycles
-src/renderers/__tests__/ReactComponentTreeHook-test.js
-* can be retrieved by ID
-
-src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
-* gets recorded during an update
-
-src/renderers/__tests__/ReactPerf-test.js
-* should count no-op update as waste
-* should count no-op update in child as waste
-* should include stats for components unmounted during measurement
-* should include lifecycle methods in measurements
-* should include render time of functional components
-* should not count time in a portal towards lifecycle method
-* should work when measurement starts during reconciliation
-
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* gives source code refs for unknown prop warning (ssr)
* gives source code refs for unknown prop warning for exact elements (ssr)
diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt
index bd22db6cbaa9..ea25216a39b1 100644
--- a/scripts/fiber/tests-passing-except-dev.txt
+++ b/scripts/fiber/tests-passing-except-dev.txt
@@ -1,132 +1,3 @@
-src/renderers/__tests__/ReactComponentTreeHook-test.js
-* uses displayName or Unknown for classic components
-* uses displayName, name, or ReactComponent for modern components
-* uses displayName, name, or Object for factory components
-* uses displayName, name, or StatelessComponent for functional components
-* reports a host tree correctly
-* reports a simple tree with composites correctly
-* reports a tree with composites correctly
-* ignores null children
-* ignores false children
-* reports text nodes as children
-* reports a single text node as a child
-* reports a single number node as a child
-* reports a zero as a child
-* skips empty nodes for multiple children
-* reports html content as no children
-* updates text of a single text child
-* updates from no children to a single text child
-* updates from a single text child to no children
-* updates from html content to a single text child
-* updates from a single text child to html content
-* updates from no children to multiple text children
-* updates from multiple text children to no children
-* updates from html content to multiple text children
-* updates from multiple text children to html content
-* updates from html content to no children
-* updates from no children to html content
-* updates from one text child to multiple text children
-* updates from multiple text children to one text child
-* updates text nodes when reordering
-* updates host nodes when reordering with keys
-* updates host nodes when reordering without keys
-* updates a single composite child of a different type
-* updates a single composite child of the same type
-* updates from no children to a single composite child
-* updates from a single composite child to no children
-* updates mixed children
-* updates with a host child
-* updates from null to a host child
-* updates from a host child to null
-* updates from a host child to a composite child
-* updates from a composite child to a host child
-* updates from null to a composite child
-* updates from a composite child to null
-* updates with a host child
-* updates from null to a host child
-* updates from a host child to null
-* updates from a host child to a composite child
-* updates from a composite child to a host child
-* updates from null to a composite child
-* updates from a composite child to null
-* tracks owner correctly
-* purges unmounted components automatically
-* reports update counts
-* does not report top-level wrapper as a root
-* registers inlined text nodes
-* works
-
-src/renderers/__tests__/ReactComponentTreeHook-test.native.js
-* uses displayName or Unknown for classic components
-* uses displayName, name, or ReactComponent for modern components
-* uses displayName, name, or Object for factory components
-* uses displayName, name, or StatelessComponent for functional components
-* reports a host tree correctly
-* reports a simple tree with composites correctly
-* reports a tree with composites correctly
-* ignores null children
-* ignores false children
-* reports text nodes as children
-* reports a single text node as a child
-* reports a single number node as a child
-* reports a zero as a child
-* skips empty nodes for multiple children
-* updates text of a single text child
-* updates from no children to a single text child
-* updates from a single text child to no children
-* updates from no children to multiple text children
-* updates from multiple text children to no children
-* updates from one text child to multiple text children
-* updates from multiple text children to one text child
-* updates text nodes when reordering
-* updates host nodes when reordering with keys
-* updates host nodes when reordering with keys
-* updates a single composite child of a different type
-* updates a single composite child of the same type
-* updates from no children to a single composite child
-* updates from a single composite child to no children
-* updates mixed children
-* updates with a host child
-* updates from null to a host child
-* updates from a host child to null
-* updates from a host child to a composite child
-* updates from a composite child to a host child
-* updates from null to a composite child
-* updates from a composite child to null
-* updates with a host child
-* updates from null to a host child
-* updates from a host child to null
-* updates from a host child to a composite child
-* updates from a composite child to a host child
-* updates from null to a composite child
-* updates from a composite child to null
-* tracks owner correctly
-* purges unmounted components automatically
-* reports update counts
-* does not report top-level wrapper as a root
-
-src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
-* gets recorded for host roots
-* gets recorded for composite roots
-* gets recorded when a native is mounted deeply instead of null
-* gets recorded during mount
-* gets recorded during an update
-* gets ignored if the styles are shallowly equal
-* gets recorded during mount
-* gets recorded during mount
-* gets recorded during mount
-* gets recorded during an update from text content
-* gets recorded during an update from html
-* gets recorded during an update from children
-* gets recorded when composite renders to a different type
-* gets recorded when composite renders to null after a native
-* gets recorded during an update from text content
-* gets recorded during an update from html
-* gets recorded during an update from children
-* gets reported when a child is inserted
-* gets reported when a child is inserted
-* gets reported when a child is removed
-
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* should not warn when server-side rendering `onScroll`
* should warn about incorrect casing on properties (ssr)
diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt
index c2cfe1b9350b..e6ffdc28ab98 100644
--- a/scripts/fiber/tests-passing.txt
+++ b/scripts/fiber/tests-passing.txt
@@ -561,8 +561,6 @@ src/renderers/__tests__/ReactComponentLifeCycle-test.js
src/renderers/__tests__/ReactComponentTreeHook-test.js
* gets created
-* is created during mounting
-* is created when calling renderToString during render
src/renderers/__tests__/ReactCompositeComponent-test.js
* should support module pattern components
@@ -673,17 +671,6 @@ src/renderers/__tests__/ReactErrorBoundaries-test.js
* renders empty output if error boundary does not handle the error
* passes first error when two errors happen in commit
-src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
-* gets ignored for composite roots that return null
-* gets recorded during an update
-* gets recorded as a removal during an update
-* gets recorded during an update
-* gets recorded during an update
-* gets ignored if new text is equal
-* gets ignored if new text is equal
-* gets ignored if the type has not changed
-* gets ignored if new html is equal
-
src/renderers/__tests__/ReactIdentity-test.js
* should allow key property to express identity
* should use composite identity
@@ -750,33 +737,6 @@ src/renderers/__tests__/ReactMultiChildText-test.js
* should throw if rendering both HTML and children
* should render between nested components and inline children
-src/renderers/__tests__/ReactPerf-test.js
-* should not count initial render as waste
-* should not count unmount as waste
-* should not count content update as waste
-* should not count child addition as waste
-* should not count child removal as waste
-* should not count property update as waste
-* should not count style update as waste
-* should not count property removal as waste
-* should not count raw HTML update as waste
-* should not count child reordering as waste
-* should not count text update as waste
-* should not count replacing null with a host as waste
-* should not count replacing a host with null as waste
-* warns once when using getMeasurementsSummaryMap
-* warns once when using printDOM
-* returns isRunning state
-* start has no effect when already running
-* stop has no effect when already stopped
-* should print console error only once
-* should not print errant warnings if render() throws
-* should not print errant warnings if componentWillMount() throws
-* should not print errant warnings if componentDidMount() throws
-* should not print errant warnings if portal throws in render()
-* should not print errant warnings if portal throws in componentWillMount()
-* should not print errant warnings if portal throws in componentDidMount()
-
src/renderers/__tests__/ReactStatelessComponent-test.js
* should render stateless component
* should update stateless component
@@ -1565,6 +1525,21 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
* should ignore errors thrown in log method to prevent cycle
* should relay info about error boundary and retry attempts if applicable
+src/renderers/shared/fiber/__tests__/ReactIncrementalPerf-test.js
+* measures a simple reconciliation
+* skips parents during setState
+* warns on cascading renders from setState
+* warns on cascading renders from top-level render
+* does not treat setState from cWM or cWRP as cascading
+* captures all lifecycles
+* measures deprioritized work
+* measures deferred work in chunks
+* recovers from fatal errors
+* recovers from caught errors
+* deduplicates lifecycle names during commit to reduce overhead
+* supports coroutines
+* supports portals
+
src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
* handles isMounted even when the initial render is deferred
* handles isMounted when an unmount is deferred
diff --git a/src/renderers/__tests__/ReactComponentTreeHook-test.js b/src/renderers/__tests__/ReactComponentTreeHook-test.js
index a9c19c13eb96..da91ab262767 100644
--- a/src/renderers/__tests__/ReactComponentTreeHook-test.js
+++ b/src/renderers/__tests__/ReactComponentTreeHook-test.js
@@ -11,6 +11,9 @@
'use strict';
+var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
+var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe;
+
describe('ReactComponentTreeHook', () => {
var React;
var ReactDOM;
@@ -30,6 +33,148 @@ describe('ReactComponentTreeHook', () => {
ReactComponentTreeTestUtils = require('ReactComponentTreeTestUtils');
});
+ // This is the only part used both by Stack and Fiber.
+ describe('stack addenda', () => {
+ it('gets created', () => {
+ function getAddendum(element) {
+ var addendum = ReactComponentTreeHook.getCurrentStackAddendum(element);
+ return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
+ }
+
+ var Anon = React.createClass({displayName: null, render: () => null});
+ var Orange = React.createClass({render: () => null});
+
+ expectDev(getAddendum()).toBe(
+ ''
+ );
+ expectDev(getAddendum(
)).toBe(
+ '\n in div (at **)'
+ );
+ expectDev(getAddendum()).toBe(
+ '\n in Unknown (at **)'
+ );
+ expectDev(getAddendum()).toBe(
+ '\n in Orange (at **)'
+ );
+ expectDev(getAddendum(React.createElement(Orange))).toBe(
+ '\n in Orange'
+ );
+
+ var renders = 0;
+ var rOwnedByQ;
+
+ function Q() {
+ return (rOwnedByQ = React.createElement(R));
+ }
+ function R() {
+ return
;
+ }
+ class S extends React.Component {
+ componentDidMount() {
+ // Check that the parent path is still fetched when only S itself is on
+ // the stack.
+ this.forceUpdate();
+ }
+ render() {
+ expectDev(getAddendum()).toBe(
+ '\n in S (at **)' +
+ '\n in div (at **)' +
+ '\n in R (created by Q)' +
+ '\n in Q (at **)'
+ );
+ expectDev(getAddendum()).toBe(
+ '\n in span (at **)' +
+ '\n in S (at **)' +
+ '\n in div (at **)' +
+ '\n in R (created by Q)' +
+ '\n in Q (at **)'
+ );
+ expectDev(getAddendum(React.createElement('span'))).toBe(
+ '\n in span (created by S)' +
+ '\n in S (at **)' +
+ '\n in div (at **)' +
+ '\n in R (created by Q)' +
+ '\n in Q (at **)'
+ );
+ renders++;
+ return null;
+ }
+ }
+ ReactDOM.render(, document.createElement('div'));
+ expectDev(renders).toBe(2);
+
+ // Make sure owner is fetched for the top element too.
+ expectDev(getAddendum(rOwnedByQ)).toBe(
+ '\n in R (created by Q)'
+ );
+ });
+
+ // These are features and regression tests that only affect
+ // the Stack implementation of the stack addendum.
+ if (!ReactDOMFeatureFlags.useFiber) {
+ it('can be retrieved by ID', () => {
+ function getAddendum(id) {
+ var addendum = ReactComponentTreeHook.getStackAddendumByID(id);
+ return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
+ }
+
+ class Q extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ var q = ReactDOM.render(, document.createElement('div'));
+ expectDev(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe(
+ '\n in Q (at **)'
+ );
+
+ spyOn(console, 'error');
+ getAddendum(-17);
+ expectDev(console.error.calls.count()).toBe(1);
+ expectDev(console.error.calls.argsFor(0)[0]).toBe(
+ 'Warning: ReactComponentTreeHook: Missing React element for ' +
+ 'debugID -17 when building stack'
+ );
+ });
+
+ it('is created during mounting', () => {
+ // https://github.com/facebook/react/issues/7187
+ var el = document.createElement('div');
+ var portalEl = document.createElement('div');
+ class Foo extends React.Component {
+ componentWillMount() {
+ ReactDOM.render(, portalEl);
+ }
+ render() {
+ return ;
+ }
+ }
+ ReactDOM.render(, el);
+ });
+
+ it('is created when calling renderToString during render', () => {
+ // https://github.com/facebook/react/issues/7190
+ var el = document.createElement('div');
+ class Foo extends React.Component {
+ render() {
+ return (
+
+
+ {ReactDOMServer.renderToString(
)}
+
+
+ );
+ }
+ }
+ ReactDOM.render(, el);
+ });
+ }
+ });
+
+ // The rest of this file is not relevant for Fiber.
+ // TODO: remove tests below when we delete Stack.
+
function assertTreeMatches(pairs) {
if (!Array.isArray(pairs[0])) {
pairs = [pairs];
@@ -106,7 +251,7 @@ describe('ReactComponentTreeHook', () => {
});
}
- describe('mount', () => {
+ describeStack('mount', () => {
it('uses displayName or Unknown for classic components', () => {
class Foo extends React.Component {
render() {
@@ -520,7 +665,7 @@ describe('ReactComponentTreeHook', () => {
});
});
- describe('update', () => {
+ describeStack('update', () => {
describe('host component', () => {
it('updates text of a single text child', () => {
var elementBefore = Hi.
;
@@ -1601,276 +1746,144 @@ describe('ReactComponentTreeHook', () => {
});
});
- it('tracks owner correctly', () => {
- class Foo extends React.Component {
- render() {
- return Hi.
;
+ describeStack('misc', () => {
+ it('tracks owner correctly', () => {
+ class Foo extends React.Component {
+ render() {
+ return Hi.
;
+ }
+ }
+ function Bar({children}) {
+ return {children} Mom
;
}
- }
- function Bar({children}) {
- return {children} Mom
;
- }
- // Note that owner is not calculated for text nodes
- // because they are not created from real elements.
- var element = ;
- var tree = {
- displayName: 'article',
- children: [{
- displayName: 'Foo',
+ // Note that owner is not calculated for text nodes
+ // because they are not created from real elements.
+ var element = ;
+ var tree = {
+ displayName: 'article',
children: [{
- displayName: 'Bar',
- ownerDisplayName: 'Foo',
+ displayName: 'Foo',
children: [{
- displayName: 'div',
- ownerDisplayName: 'Bar',
+ displayName: 'Bar',
+ ownerDisplayName: 'Foo',
children: [{
- displayName: 'h1',
- ownerDisplayName: 'Foo',
+ displayName: 'div',
+ ownerDisplayName: 'Bar',
children: [{
+ displayName: 'h1',
+ ownerDisplayName: 'Foo',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ }, {
displayName: '#text',
- text: 'Hi.',
+ text: ' Mom',
}],
- }, {
- displayName: '#text',
- text: ' Mom',
}],
}],
}],
- }],
- };
- assertTreeMatches([element, tree]);
- });
+ };
+ assertTreeMatches([element, tree]);
+ });
- it('purges unmounted components automatically', () => {
- var node = document.createElement('div');
- var renderBar = true;
- var fooInstance;
- var barInstance;
+ it('purges unmounted components automatically', () => {
+ var node = document.createElement('div');
+ var renderBar = true;
+ var fooInstance;
+ var barInstance;
- class Foo extends React.Component {
- render() {
- fooInstance = ReactInstanceMap.get(this);
- return renderBar ? : null;
+ class Foo extends React.Component {
+ render() {
+ fooInstance = ReactInstanceMap.get(this);
+ return renderBar ? : null;
+ }
}
- }
- class Bar extends React.Component {
- render() {
- barInstance = ReactInstanceMap.get(this);
- return null;
+ class Bar extends React.Component {
+ render() {
+ barInstance = ReactInstanceMap.get(this);
+ return null;
+ }
}
- }
-
- ReactDOM.render(, node);
- ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
- displayName: 'Bar',
- parentDisplayName: 'Foo',
- parentID: fooInstance._debugID,
- children: [],
- }, 'Foo');
-
- renderBar = false;
- ReactDOM.render(, node);
- ReactDOM.render(, node);
- ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
- displayName: 'Unknown',
- children: [],
- parentID: null,
- }, 'Foo');
-
- ReactDOM.unmountComponentAtNode(node);
- ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
- displayName: 'Unknown',
- children: [],
- parentID: null,
- }, 'Foo');
- });
-
- it('reports update counts', () => {
- var node = document.createElement('div');
- ReactDOM.render(, node);
- var divID = ReactComponentTreeHook.getRootIDs()[0];
- expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
-
- ReactDOM.render(, node);
- var spanID = ReactComponentTreeHook.getRootIDs()[0];
- expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
- expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
-
- ReactDOM.render(, node);
- expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
- expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(1);
-
- ReactDOM.render(, node);
- expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
- expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(2);
-
- ReactDOM.unmountComponentAtNode(node);
- expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
- expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
- });
-
- it('does not report top-level wrapper as a root', () => {
- var node = document.createElement('div');
-
- ReactDOM.render(, node);
- expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
-
- ReactDOM.render(, node);
- expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
-
- ReactDOM.unmountComponentAtNode(node);
- expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
- expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
- });
+ ReactDOM.render(, node);
+ ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
+ displayName: 'Bar',
+ parentDisplayName: 'Foo',
+ parentID: fooInstance._debugID,
+ children: [],
+ }, 'Foo');
- it('registers inlined text nodes', () => {
- var node = document.createElement('div');
+ renderBar = false;
+ ReactDOM.render(, node);
+ ReactDOM.render(, node);
+ ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
+ displayName: 'Unknown',
+ children: [],
+ parentID: null,
+ }, 'Foo');
- ReactDOM.render(hi
, node);
- expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual(['div', '#text']);
+ ReactDOM.unmountComponentAtNode(node);
+ ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
+ displayName: 'Unknown',
+ children: [],
+ parentID: null,
+ }, 'Foo');
+ });
- ReactDOM.unmountComponentAtNode(node);
- expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
- });
+ it('reports update counts', () => {
+ var node = document.createElement('div');
- describe('stack addenda', () => {
- it('gets created', () => {
- function getAddendum(element) {
- var addendum = ReactComponentTreeHook.getCurrentStackAddendum(element);
- return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
- }
+ ReactDOM.render(, node);
+ var divID = ReactComponentTreeHook.getRootIDs()[0];
+ expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
- var Anon = React.createClass({displayName: null, render: () => null});
- var Orange = React.createClass({render: () => null});
+ ReactDOM.render(, node);
+ var spanID = ReactComponentTreeHook.getRootIDs()[0];
+ expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
+ expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
- expectDev(getAddendum()).toBe(
- ''
- );
- expectDev(getAddendum()).toBe(
- '\n in div (at **)'
- );
- expectDev(getAddendum()).toBe(
- '\n in Unknown (at **)'
- );
- expectDev(getAddendum()).toBe(
- '\n in Orange (at **)'
- );
- expectDev(getAddendum(React.createElement(Orange))).toBe(
- '\n in Orange'
- );
+ ReactDOM.render(, node);
+ expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
+ expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(1);
- var renders = 0;
- var rOwnedByQ;
+ ReactDOM.render(, node);
+ expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
+ expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(2);
- function Q() {
- return (rOwnedByQ = React.createElement(R));
- }
- function R() {
- return
;
- }
- class S extends React.Component {
- componentDidMount() {
- // Check that the parent path is still fetched when only S itself is on
- // the stack.
- this.forceUpdate();
- }
- render() {
- expectDev(getAddendum()).toBe(
- '\n in S (at **)' +
- '\n in div (at **)' +
- '\n in R (created by Q)' +
- '\n in Q (at **)'
- );
- expectDev(getAddendum()).toBe(
- '\n in span (at **)' +
- '\n in S (at **)' +
- '\n in div (at **)' +
- '\n in R (created by Q)' +
- '\n in Q (at **)'
- );
- expectDev(getAddendum(React.createElement('span'))).toBe(
- '\n in span (created by S)' +
- '\n in S (at **)' +
- '\n in div (at **)' +
- '\n in R (created by Q)' +
- '\n in Q (at **)'
- );
- renders++;
- return null;
- }
- }
- ReactDOM.render(, document.createElement('div'));
- expectDev(renders).toBe(2);
-
- // Make sure owner is fetched for the top element too.
- expectDev(getAddendum(rOwnedByQ)).toBe(
- '\n in R (created by Q)'
- );
+ ReactDOM.unmountComponentAtNode(node);
+ expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
+ expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
});
- it('can be retrieved by ID', () => {
- function getAddendum(id) {
- var addendum = ReactComponentTreeHook.getStackAddendumByID(id);
- return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
- }
+ it('does not report top-level wrapper as a root', () => {
+ var node = document.createElement('div');
- class Q extends React.Component {
- render() {
- return null;
- }
- }
+ ReactDOM.render(, node);
+ expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
- var q = ReactDOM.render(, document.createElement('div'));
- expectDev(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe(
- '\n in Q (at **)'
- );
+ ReactDOM.render(, node);
+ expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
- spyOn(console, 'error');
- getAddendum(-17);
- expectDev(console.error.calls.count()).toBe(1);
- expectDev(console.error.calls.argsFor(0)[0]).toBe(
- 'Warning: ReactComponentTreeHook: Missing React element for ' +
- 'debugID -17 when building stack'
- );
+ ReactDOM.unmountComponentAtNode(node);
+ expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
+ expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
});
- it('is created during mounting', () => {
- // https://github.com/facebook/react/issues/7187
- var el = document.createElement('div');
- var portalEl = document.createElement('div');
- class Foo extends React.Component {
- componentWillMount() {
- ReactDOM.render(, portalEl);
- }
- render() {
- return ;
- }
- }
- ReactDOM.render(, el);
- });
+ it('registers inlined text nodes', () => {
+ var node = document.createElement('div');
- it('is created when calling renderToString during render', () => {
- // https://github.com/facebook/react/issues/7190
- var el = document.createElement('div');
- class Foo extends React.Component {
- render() {
- return (
-
-
- {ReactDOMServer.renderToString(
)}
-
-
- );
- }
- }
- ReactDOM.render(, el);
+ ReactDOM.render(hi
, node);
+ expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual(['div', '#text']);
+
+ ReactDOM.unmountComponentAtNode(node);
+ expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
});
});
- describe('in environment without Map, Set and Array.from', () => {
+ describeStack('in environment without Map, Set and Array.from', () => {
var realMap;
var realSet;
var realArrayFrom;
diff --git a/src/renderers/__tests__/ReactComponentTreeHook-test.native.js b/src/renderers/__tests__/ReactComponentTreeHook-test.native.js
index 23c934c27498..17a8fa49868b 100644
--- a/src/renderers/__tests__/ReactComponentTreeHook-test.native.js
+++ b/src/renderers/__tests__/ReactComponentTreeHook-test.native.js
@@ -11,7 +11,15 @@
'use strict';
-describe('ReactComponentTreeHook', () => {
+var ReactNativeFeatureFlags = require('ReactNativeFeatureFlags');
+var describeStack = ReactNativeFeatureFlags.useFiber ? describe.skip : describe;
+
+// These tests are only relevant for the Stack version of the tree hook.
+// This file is for RN. There is a sibling file that has some tree hook
+// tests that are still relevant in Fiber.
+// TODO: remove this file when we delete Stack.
+
+describeStack('ReactComponentTreeHook', () => {
var React;
var ReactNative;
var ReactInstanceMap;
diff --git a/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js b/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
index b830342b758a..d6c12a6f4c77 100644
--- a/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
+++ b/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
@@ -11,12 +11,16 @@
'use strict';
-describe('ReactHostOperationHistoryHook', () => {
+var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
+var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe;
+
+// This is only used by ReactPerf which is currently not supported on Fiber.
+// Use browser timeline integration instead.
+describeStack('ReactHostOperationHistoryHook', () => {
var React;
var ReactPerf;
var ReactDOM;
var ReactDOMComponentTree;
- var ReactDOMFeatureFlags;
var ReactHostOperationHistoryHook;
beforeEach(() => {
@@ -26,7 +30,6 @@ describe('ReactHostOperationHistoryHook', () => {
ReactPerf = require('react-dom/lib/ReactPerf');
ReactDOM = require('react-dom');
ReactDOMComponentTree = require('ReactDOMComponentTree');
- ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactHostOperationHistoryHook = require('ReactHostOperationHistoryHook');
ReactPerf.start();
diff --git a/src/renderers/__tests__/ReactPerf-test.js b/src/renderers/__tests__/ReactPerf-test.js
index 7d5cb809c28c..2abc0d17dff7 100644
--- a/src/renderers/__tests__/ReactPerf-test.js
+++ b/src/renderers/__tests__/ReactPerf-test.js
@@ -11,7 +11,12 @@
'use strict';
-describe('ReactPerf', () => {
+var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
+var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe;
+
+// ReactPerf is currently not supported on Fiber.
+// Use browser timeline integration instead.
+describeStack('ReactPerf', () => {
var React;
var ReactDOM;
var ReactPerf;
diff --git a/src/renderers/shared/fiber/ReactDebugFiberPerf.js b/src/renderers/shared/fiber/ReactDebugFiberPerf.js
new file mode 100644
index 000000000000..0c16eca8c08b
--- /dev/null
+++ b/src/renderers/shared/fiber/ReactDebugFiberPerf.js
@@ -0,0 +1,408 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDebugFiberPerf
+ * @flow
+ */
+
+import type { Fiber } from 'ReactFiber';
+
+type MeasurementPhase =
+ 'componentWillMount' |
+ 'componentWillUnmount' |
+ 'componentWillReceiveProps' |
+ 'shouldComponentUpdate' |
+ 'componentWillUpdate' |
+ 'componentDidUpdate' |
+ 'componentDidMount' |
+ 'getChildContext';
+
+// Trust the developer to only use this with a __DEV__ check
+let ReactDebugFiberPerf = ((null: any): typeof ReactDebugFiberPerf);
+
+if (__DEV__) {
+ const {
+ HostRoot,
+ HostComponent,
+ HostText,
+ HostPortal,
+ YieldComponent,
+ Fragment,
+ } = require('ReactTypeOfWork');
+
+ const getComponentName = require('getComponentName');
+
+ // Prefix measurements so that it's possible to filter them.
+ // Longer prefixes are hard to read in DevTools.
+ const reactEmoji = '\u269B';
+ const warningEmoji = '\uD83D\uDED1';
+ const supportsUserTiming =
+ typeof performance !== 'undefined' &&
+ typeof performance.mark === 'function' &&
+ typeof performance.clearMarks === 'function' &&
+ typeof performance.measure === 'function' &&
+ typeof performance.clearMeasures === 'function';
+
+ // Keep track of current fiber so that we know the path to unwind on pause.
+ // TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them?
+ let currentFiber : Fiber | null = null;
+ // If we're in the middle of user code, which fiber and method is it?
+ // Reusing `currentFiber` would be confusing for this because user code fiber
+ // can change during commit phase too, but we don't need to unwind it (since
+ // lifecycles in the commit phase don't resemble a tree).
+ let currentPhase : MeasurementPhase | null = null;
+ let currentPhaseFiber : Fiber | null = null;
+ // Did lifecycle hook schedule an update? This is often a performance problem,
+ // so we will keep track of it, and include it in the report.
+ // Track commits caused by cascading updates.
+ let isCommitting : boolean = false;
+ let hasScheduledUpdateInCurrentCommit : boolean = false;
+ let hasScheduledUpdateInCurrentPhase : boolean = false;
+ let commitCountInCurrentWorkLoop : number = 0;
+ let effectCountInCurrentCommit : number = 0;
+ // During commits, we only show a measurement once per method name
+ // to avoid stretch the commit phase with measurement overhead.
+ const labelsInCurrentCommit : Set = new Set();
+
+ const formatMarkName = (markName : string) => {
+ return `${reactEmoji} ${markName}`;
+ };
+
+ const formatLabel = (label : string, warning : string | null) => {
+ const prefix = warning ? `${warningEmoji} ` : `${reactEmoji} `;
+ const suffix = warning ? ` Warning: ${warning}` : '';
+ return `${prefix}${label}${suffix}`;
+ };
+
+ const beginMark = (markName : string) => {
+ performance.mark(formatMarkName(markName));
+ };
+
+ const clearMark = (markName : string) => {
+ performance.clearMarks(formatMarkName(markName));
+ };
+
+ const endMark = (label : string, markName : string, warning : string | null) => {
+ const formattedMarkName = formatMarkName(markName);
+ const formattedLabel = formatLabel(label, warning);
+ try {
+ performance.measure(formattedLabel, formattedMarkName);
+ } catch (err) {
+ // If previous mark was missing for some reason, this will throw.
+ // This could only happen if React crashed in an unexpected place earlier.
+ // Don't pile on with more errors.
+ }
+ // Clear marks immediately to avoid growing buffer.
+ performance.clearMarks(formattedMarkName);
+ performance.clearMeasures(formattedLabel);
+ };
+
+ const getFiberMarkName = (label : string, debugID : number) => {
+ return `${label} (#${debugID})`;
+ };
+
+ const getFiberLabel = (
+ componentName : string,
+ isMounted : boolean,
+ phase : MeasurementPhase | null,
+ ) => {
+ if (phase === null) {
+ // These are composite component total time measurements.
+ return `${componentName} [${isMounted ? 'update' : 'mount'}]`;
+ } else {
+ // Composite component methods.
+ return `${componentName}.${phase}`;
+ }
+ };
+
+ const beginFiberMark = (
+ fiber : Fiber,
+ phase : MeasurementPhase | null,
+ ) : boolean => {
+ const componentName = getComponentName(fiber) || 'Unknown';
+ const debugID = ((fiber._debugID : any) : number);
+ const isMounted = fiber.alternate !== null;
+ const label = getFiberLabel(componentName, isMounted, phase);
+
+ if (isCommitting && labelsInCurrentCommit.has(label)) {
+ // During the commit phase, we don't show duplicate labels because
+ // there is a fixed overhead for every measurement, and we don't
+ // want to stretch the commit phase beyond necessary.
+ return false;
+ }
+ labelsInCurrentCommit.add(label);
+
+ const markName = getFiberMarkName(label, debugID);
+ beginMark(markName);
+ return true;
+ };
+
+ const clearFiberMark = (fiber : Fiber, phase : MeasurementPhase | null) => {
+ const componentName = getComponentName(fiber) || 'Unknown';
+ const debugID = ((fiber._debugID : any) : number);
+ const isMounted = fiber.alternate !== null;
+ const label = getFiberLabel(componentName, isMounted, phase);
+ const markName = getFiberMarkName(label, debugID);
+ clearMark(markName);
+ };
+
+ const endFiberMark = (
+ fiber : Fiber,
+ phase : MeasurementPhase | null,
+ warning : string | null,
+ ) => {
+ const componentName = getComponentName(fiber) || 'Unknown';
+ const debugID = ((fiber._debugID : any) : number);
+ const isMounted = fiber.alternate !== null;
+ const label = getFiberLabel(componentName, isMounted, phase);
+ const markName = getFiberMarkName(label, debugID);
+ endMark(label, markName, warning);
+ };
+
+ const shouldIgnoreFiber = (fiber : Fiber) : boolean => {
+ // Host components should be skipped in the timeline.
+ // We could check typeof fiber.type, but does this work with RN?
+ switch (fiber.tag) {
+ case HostRoot:
+ case HostComponent:
+ case HostText:
+ case HostPortal:
+ case YieldComponent:
+ case Fragment:
+ return true;
+ default:
+ return false;
+ }
+ };
+
+ const clearPendingPhaseMeasurement = () => {
+ if (currentPhase !== null && currentPhaseFiber !== null) {
+ clearFiberMark(currentPhaseFiber, currentPhase);
+ }
+ currentPhaseFiber = null;
+ currentPhase = null;
+ hasScheduledUpdateInCurrentPhase = false;
+ };
+
+ const pauseTimers = () => {
+ // Stops all currently active measurements so that they can be resumed
+ // if we continue in a later deferred loop from the same unit of work.
+ let fiber = currentFiber;
+ while (fiber) {
+ if (fiber._debugIsCurrentlyTiming) {
+ endFiberMark(fiber, null, null);
+ }
+ fiber = fiber.return;
+ }
+ };
+
+ const resumeTimersRecursively = (fiber : Fiber) => {
+ if (fiber.return !== null) {
+ resumeTimersRecursively(fiber.return);
+ }
+ if (fiber._debugIsCurrentlyTiming) {
+ beginFiberMark(fiber, null);
+ }
+ };
+
+ const resumeTimers = () => {
+ // Resumes all measurements that were active during the last deferred loop.
+ if (currentFiber !== null) {
+ resumeTimersRecursively(currentFiber);
+ }
+ };
+
+ ReactDebugFiberPerf = {
+ recordEffect() : void {
+ effectCountInCurrentCommit++;
+ },
+
+ recordScheduleUpdate() : void {
+ if (isCommitting) {
+ hasScheduledUpdateInCurrentCommit = true;
+ }
+ if (
+ currentPhase !== null &&
+ currentPhase !== 'componentWillMount' &&
+ currentPhase !== 'componentWillReceiveProps'
+ ) {
+ hasScheduledUpdateInCurrentPhase = true;
+ }
+ },
+
+ startWorkTimer(fiber : Fiber) : void {
+ if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
+ return;
+ }
+ // If we pause, this is the fiber to unwind from.
+ currentFiber = fiber;
+ if (!beginFiberMark(fiber, null)) {
+ return;
+ }
+ fiber._debugIsCurrentlyTiming = true;
+ },
+
+ cancelWorkTimer(fiber : Fiber) : void {
+ if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
+ return;
+ }
+ // Remember we shouldn't complete measurement for this fiber.
+ // Otherwise flamechart will be deep even for small updates.
+ fiber._debugIsCurrentlyTiming = false;
+ clearFiberMark(fiber, null);
+ },
+
+ stopWorkTimer(fiber : Fiber) : void {
+ if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
+ return;
+ }
+ // If we pause, its parent is the fiber to unwind from.
+ currentFiber = fiber.return;
+ if (!fiber._debugIsCurrentlyTiming) {
+ return;
+ }
+ fiber._debugIsCurrentlyTiming = false;
+ endFiberMark(fiber, null, null);
+ },
+
+ startPhaseTimer(
+ fiber : Fiber,
+ phase : MeasurementPhase,
+ ) : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ clearPendingPhaseMeasurement();
+ if (!beginFiberMark(fiber, phase)) {
+ return;
+ }
+ currentPhaseFiber = fiber;
+ currentPhase = phase;
+ },
+
+ stopPhaseTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ if (currentPhase !== null && currentPhaseFiber !== null) {
+ const warning = hasScheduledUpdateInCurrentPhase ?
+ 'Scheduled a cascading update' :
+ null;
+ endFiberMark(currentPhaseFiber, currentPhase, warning);
+ }
+ currentPhase = null;
+ currentPhaseFiber = null;
+ },
+
+ startWorkLoopTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ commitCountInCurrentWorkLoop = 0;
+ // This is top level call.
+ // Any other measurements are performed within.
+ beginMark('(React Tree Reconciliation)');
+ // Resume any measurements that were in progress during the last loop.
+ resumeTimers();
+ },
+
+ stopWorkLoopTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ const warning = commitCountInCurrentWorkLoop > 1 ?
+ 'There were cascading updates' :
+ null;
+ commitCountInCurrentWorkLoop = 0;
+ // Pause any measurements until the next loop.
+ pauseTimers();
+ endMark(
+ '(React Tree Reconciliation)',
+ '(React Tree Reconciliation)',
+ warning,
+ );
+ },
+
+ startCommitTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ isCommitting = true;
+ hasScheduledUpdateInCurrentCommit = false;
+ labelsInCurrentCommit.clear();
+ beginMark('(Committing Changes)');
+ },
+
+ stopCommitTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+
+ let warning = null;
+ if (hasScheduledUpdateInCurrentCommit) {
+ warning = 'Lifecycle hook scheduled a cascading update';
+ } else if (commitCountInCurrentWorkLoop > 0) {
+ warning = 'Caused by a cascading update in earlier commit';
+ }
+ hasScheduledUpdateInCurrentCommit = false;
+ commitCountInCurrentWorkLoop++;
+ isCommitting = false;
+ labelsInCurrentCommit.clear();
+
+ endMark(
+ '(Committing Changes)',
+ '(Committing Changes)',
+ warning,
+ );
+ },
+
+ startCommitHostEffectsTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ effectCountInCurrentCommit = 0;
+ beginMark('(Committing Host Effects)');
+ },
+
+ stopCommitHostEffectsTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ const count = effectCountInCurrentCommit;
+ effectCountInCurrentCommit = 0;
+ endMark(
+ `(Committing Host Effects: ${count} Total)`,
+ '(Committing Host Effects)',
+ null,
+ );
+ },
+
+ startCommitLifeCyclesTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ effectCountInCurrentCommit = 0;
+ beginMark('(Calling Lifecycle Methods)');
+ },
+
+ stopCommitLifeCyclesTimer() : void {
+ if (!supportsUserTiming) {
+ return;
+ }
+ const count = effectCountInCurrentCommit;
+ effectCountInCurrentCommit = 0;
+ endMark(
+ `(Calling Lifecycle Methods: ${count} Total)`,
+ '(Calling Lifecycle Methods)',
+ null,
+ );
+ },
+ };
+}
+
+module.exports = ReactDebugFiberPerf;
diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js
index bd89a93f82c3..0b34566378e7 100644
--- a/src/renderers/shared/fiber/ReactFiber.js
+++ b/src/renderers/shared/fiber/ReactFiber.js
@@ -60,6 +60,7 @@ export type Fiber = {
_debugID ?: DebugID,
_debugSource ?: Source | null,
_debugOwner ?: Fiber | ReactInstance | null, // Stack compatible
+ _debugIsCurrentlyTiming ?: boolean,
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
@@ -223,6 +224,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
fiber._debugID = debugCounter++;
fiber._debugSource = null;
fiber._debugOwner = null;
+ fiber._debugIsCurrentlyTiming = false;
if (typeof Object.preventExtensions === 'function') {
Object.preventExtensions(fiber);
}
diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js
index 1d1d6a75c15d..3fa3cfda99c4 100644
--- a/src/renderers/shared/fiber/ReactFiberBeginWork.js
+++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js
@@ -66,7 +66,9 @@ var invariant = require('fbjs/lib/invariant');
if (__DEV__) {
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
+ var {cancelWorkTimer} = require('ReactDebugFiberPerf');
var warning = require('fbjs/lib/warning');
+
var warnedAboutStatelessRefs = {};
}
@@ -652,6 +654,10 @@ module.exports = function(
*/
function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : Fiber | null {
+ if (__DEV__) {
+ cancelWorkTimer(workInProgress);
+ }
+
const priorityLevel = workInProgress.pendingWorkPriority;
// TODO: We should ideally be able to bail out early if the children have no
// more work to do. However, since we don't have a separation of this
@@ -679,6 +685,10 @@ module.exports = function(
}
function bailoutOnLowPriority(current, workInProgress) {
+ if (__DEV__) {
+ cancelWorkTimer(workInProgress);
+ }
+
// TODO: Handle HostComponent tags here as well and call pushHostContext()?
// See PR 8590 discussion for context
switch (workInProgress.tag) {
diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js
index ae35f8db6d62..47017cf29297 100644
--- a/src/renderers/shared/fiber/ReactFiberClassComponent.js
+++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js
@@ -40,6 +40,10 @@ var invariant = require('fbjs/lib/invariant');
const isArray = Array.isArray;
if (__DEV__) {
+ var {
+ startPhaseTimer,
+ stopPhaseTimer,
+ } = require('ReactDebugFiberPerf');
var warning = require('fbjs/lib/warning');
var warnOnInvalidCallback = function(callback : mixed, callerName : string) {
warning(
@@ -102,7 +106,13 @@ module.exports = function(
const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'shouldComponentUpdate');
+ }
const shouldUpdate = instance.shouldComponentUpdate(newProps, newState, newContext);
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
if (__DEV__) {
warning(
@@ -278,7 +288,13 @@ module.exports = function(
instance.context = getMaskedContext(workInProgress, unmaskedContext);
if (typeof instance.componentWillMount === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'componentWillMount');
+ }
instance.componentWillMount();
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
// If we had additional state updates during this life-cycle, let's
// process them now.
const updateQueue = workInProgress.updateQueue;
@@ -347,7 +363,13 @@ module.exports = function(
newInstance.context = newContext;
if (typeof newInstance.componentWillMount === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'componentWillMount');
+ }
newInstance.componentWillMount();
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
}
// If we had additional state updates, process them now.
// They may be from componentWillMount() or from error boundary's setState()
@@ -396,7 +418,13 @@ module.exports = function(
if (oldProps !== newProps || oldContext !== newContext) {
if (typeof instance.componentWillReceiveProps === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'componentWillReceiveProps');
+ }
instance.componentWillReceiveProps(newProps, newContext);
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
if (instance.state !== workInProgress.memoizedState) {
if (__DEV__) {
@@ -457,7 +485,13 @@ module.exports = function(
if (shouldUpdate) {
if (typeof instance.componentWillUpdate === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'componentWillUpdate');
+ }
instance.componentWillUpdate(newProps, newState, newContext);
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
}
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js
index 32cd4fb52f7f..30a2f4962cb4 100644
--- a/src/renderers/shared/fiber/ReactFiberCommitWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js
@@ -37,6 +37,13 @@ var {
var invariant = require('fbjs/lib/invariant');
+if (__DEV__) {
+ var {
+ startPhaseTimer,
+ stopPhaseTimer,
+ } = require('ReactDebugFiberPerf');
+}
+
module.exports = function(
config : HostConfig,
captureError : (failedFiber : Fiber, error: Error) => Fiber | null
@@ -53,10 +60,24 @@ module.exports = function(
getPublicInstance,
} = config;
+ if (__DEV__) {
+ var callComponentWillUnmountWithTimerInDev = function(current, instance) {
+ startPhaseTimer(current, 'componentWillUnmount');
+ instance.componentWillUnmount();
+ stopPhaseTimer();
+ };
+ }
+
// Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(current, instance) {
if (__DEV__) {
- const unmountError = invokeGuardedCallback(null, instance.componentWillUnmount, instance);
+ const unmountError = invokeGuardedCallback(
+ null,
+ callComponentWillUnmountWithTimerInDev,
+ null,
+ current,
+ instance,
+ );
if (unmountError) {
captureError(current, unmountError);
}
@@ -423,11 +444,23 @@ module.exports = function(
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
+ if (__DEV__) {
+ startPhaseTimer(finishedWork, 'componentDidMount');
+ }
instance.componentDidMount();
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
} else {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
+ if (__DEV__) {
+ startPhaseTimer(finishedWork, 'componentDidUpdate');
+ }
instance.componentDidUpdate(prevProps, prevState);
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
}
}
if ((finishedWork.effectTag & Callback) && finishedWork.updateQueue !== null) {
diff --git a/src/renderers/shared/fiber/ReactFiberContext.js b/src/renderers/shared/fiber/ReactFiberContext.js
index d921285a6b35..7e0122fd9ff0 100644
--- a/src/renderers/shared/fiber/ReactFiberContext.js
+++ b/src/renderers/shared/fiber/ReactFiberContext.js
@@ -36,6 +36,10 @@ if (__DEV__) {
var checkReactTypeSpec = require('checkReactTypeSpec');
var ReactDebugCurrentFrame = require('react/lib/ReactDebugCurrentFrame');
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
+ var {
+ startPhaseTimer,
+ stopPhaseTimer,
+ } = require('ReactDebugFiberPerf');
var warnedAboutMissingGetChildContext = {};
}
@@ -172,7 +176,9 @@ function processChildContext(fiber : Fiber, parentContext : Object, isReconcilin
let childContext;
if (__DEV__) {
ReactDebugCurrentFiber.phase = 'getChildContext';
+ startPhaseTimer(fiber, 'getChildContext');
childContext = instance.getChildContext();
+ stopPhaseTimer();
ReactDebugCurrentFiber.phase = null;
} else {
childContext = instance.getChildContext();
diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js
index ecd907835d86..3f1698d4c8a4 100644
--- a/src/renderers/shared/fiber/ReactFiberScheduler.js
+++ b/src/renderers/shared/fiber/ReactFiberScheduler.js
@@ -95,6 +95,20 @@ if (__DEV__) {
var warning = require('fbjs/lib/warning');
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
+ var {
+ recordEffect,
+ recordScheduleUpdate,
+ startWorkTimer,
+ stopWorkTimer,
+ startWorkLoopTimer,
+ stopWorkLoopTimer,
+ startCommitTimer,
+ stopCommitTimer,
+ startCommitHostEffectsTimer,
+ stopCommitHostEffectsTimer,
+ startCommitLifeCyclesTimer,
+ stopCommitLifeCyclesTimer,
+ } = require('ReactDebugFiberPerf');
var warnAboutUpdateOnUnmounted = function(instance : ReactClass) {
const ctor = instance.constructor;
@@ -293,6 +307,7 @@ module.exports = function(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig {
+ let React;
+ let ReactCoroutine;
+ let ReactFeatureFlags;
+ let ReactNoop;
+ let ReactPortal;
+
+ let root;
+ let activeMeasure;
+ let knownMarks;
+ let knownMeasures;
+
+ function resetFlamechart() {
+ root = {
+ children: [],
+ indent: -1,
+ markName: null,
+ label: null,
+ parent: null,
+ toString() {
+ return this.children.map(c => c.toString()).join('\n');
+ },
+ };
+ activeMeasure = root;
+ knownMarks = new Set();
+ knownMeasures = new Set();
+ }
+
+ function addComment(comment) {
+ activeMeasure.children.push(
+ `${' '.repeat(activeMeasure.indent + 1)}// ${comment}`
+ );
+ }
+
+ function getFlameChart() {
+ // Make sure we unwind the measurement stack every time.
+ expect(activeMeasure.indent).toBe(-1);
+ expect(activeMeasure).toBe(root);
+ // We should always clean them up because browsers
+ // buffer user timing measurements forever.
+ expect(knownMarks.size).toBe(0);
+ expect(knownMeasures.size).toBe(0);
+ return root.toString();
+ }
+
+ function createUserTimingPolyfill() {
+ // This is not a true polyfill, but it gives us enough
+ // to capture measurements in a readable tree-like output.
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API
+ return {
+ mark(markName) {
+ const measure = {
+ children: [],
+ indent: activeMeasure.indent + 1,
+ markName: markName,
+ // Will be assigned on measure() call:
+ label: null,
+ parent: activeMeasure,
+ toString() {
+ return [
+ ' '.repeat(this.indent) + this.label,
+ ...this.children.map(c => c.toString()),
+ ].join('\n') + (
+ // Extra newline after each root reconciliation
+ this.indent === 0 ? '\n' : ''
+ );
+ },
+ };
+ // Step one level deeper
+ activeMeasure.children.push(measure);
+ activeMeasure = measure;
+ knownMarks.add(markName);
+ },
+ // We don't use the overload with three arguments.
+ measure(label, markName) {
+ if (markName !== activeMeasure.markName) {
+ throw new Error('Unexpected measure() call.');
+ }
+ // Step one level up
+ activeMeasure.label = label;
+ activeMeasure = activeMeasure.parent;
+ knownMeasures.add(label);
+ },
+ clearMarks(markName) {
+ if (markName === activeMeasure.markName) {
+ // Step one level up if we're in this measure
+ activeMeasure = activeMeasure.parent;
+ activeMeasure.children.length--;
+ }
+ knownMarks.delete(markName);
+ },
+ clearMeasures(label) {
+ knownMeasures.delete(label);
+ },
+ };
+ }
+
+ beforeEach(() => {
+ jest.resetModules();
+ resetFlamechart();
+ global.performance = createUserTimingPolyfill();
+
+ // Import after the polyfill is set up:
+ React = require('React');
+ ReactCoroutine = require('ReactCoroutine');
+ ReactFeatureFlags = require('ReactFeatureFlags');
+ ReactNoop = require('ReactNoop');
+ ReactPortal = require('ReactPortal');
+ ReactFeatureFlags.disableNewFiberFeatures = false;
+ });
+
+ afterEach(() => {
+ delete global.performance;
+ });
+
+ function Parent(props) {
+ return {props.children}
;
+ }
+
+ function Child(props) {
+ return {props.children}
;
+ }
+
+ it('measures a simple reconciliation', () => {
+ ReactNoop.render();
+ addComment('Mount');
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ addComment('Update');
+ ReactNoop.flush();
+
+ ReactNoop.render(null);
+ addComment('Unmount');
+ ReactNoop.flush();
+
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('skips parents during setState', () => {
+ class A extends React.Component {
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ class B extends React.Component {
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ let a;
+ let b;
+ ReactNoop.render(
+
+
+
+ a = inst} />
+
+
+
+ b = inst} />
+
+
+ );
+ ReactNoop.flush();
+ resetFlamechart();
+
+ a.setState({});
+ b.setState({});
+ addComment('Should include just A and B, no Parents');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('warns on cascading renders from setState', () => {
+ class Cascading extends React.Component {
+ componentDidMount() {
+ this.setState({});
+ }
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ ReactNoop.render();
+ addComment('Should print a warning');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('warns on cascading renders from top-level render', () => {
+ class Cascading extends React.Component {
+ componentDidMount() {
+ ReactNoop.renderToRootWithID(, 'b');
+ addComment('Scheduling another root from componentDidMount');
+ ReactNoop.flush();
+ }
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ ReactNoop.renderToRootWithID(, 'a');
+ addComment('Rendering the first root');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('does not treat setState from cWM or cWRP as cascading', () => {
+ class NotCascading extends React.Component {
+ componentWillMount() {
+ this.setState({});
+ }
+ componentWillReceiveProps() {
+ this.setState({});
+ }
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ ReactNoop.render();
+ addComment('Should not print a warning');
+ ReactNoop.flush();
+ ReactNoop.render();
+ addComment('Should not print a warning');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('captures all lifecycles', () => {
+ class AllLifecycles extends React.Component {
+ static childContextTypes = {
+ foo: React.PropTypes.any,
+ };
+ shouldComponentUpdate() {
+ return true;
+ }
+ getChildContext() {
+ return {foo: 42};
+ }
+ componentWillMount() {}
+ componentDidMount() {}
+ componentWillReceiveProps() {}
+ componentWillUpdate() {}
+ componentDidUpdate() {}
+ componentWillUnmount() {}
+ render() {
+ return ;
+ }
+ }
+ ReactNoop.render();
+ addComment('Mount');
+ ReactNoop.flush();
+ ReactNoop.render();
+ addComment('Update');
+ ReactNoop.flush();
+ ReactNoop.render(null);
+ addComment('Unmount');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('measures deprioritized work', () => {
+ ReactNoop.performAnimationWork(() => {
+ ReactNoop.render(
+
+
+
+
+
+ );
+ });
+ addComment('Flush the parent');
+ ReactNoop.flushAnimationPri();
+ addComment('Flush the child');
+ ReactNoop.flushDeferredPri();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('measures deferred work in chunks', () => {
+ class A extends React.Component {
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ class B extends React.Component {
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ ReactNoop.render(
+
+
+
+
+
+
+
+
+ );
+ addComment('Start mounting Parent and A');
+ ReactNoop.flushDeferredPri(40);
+ addComment('Mount B just a little (but not enough to memoize)');
+ ReactNoop.flushDeferredPri(10);
+ addComment('Complete B and Parent');
+ ReactNoop.flushDeferredPri();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('recovers from fatal errors', () => {
+ function Baddie() {
+ throw new Error('Game over');
+ }
+
+ ReactNoop.render();
+ try {
+ addComment('Will fatal');
+ ReactNoop.flush();
+ } catch (err) {
+ expect(err.message).toBe('Game over');
+ }
+ ReactNoop.render();
+ addComment('Will reconcile from a clean state');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('recovers from caught errors', () => {
+ function Baddie() {
+ throw new Error('Game over');
+ }
+
+ function ErrorReport() {
+ return ;
+ }
+
+ class Boundary extends React.Component {
+ state = {error: null};
+ unstable_handleError(error) {
+ this.setState({error});
+ }
+ render() {
+ if (this.state.error) {
+ return ;
+ }
+ return this.props.children;
+ }
+ }
+
+ ReactNoop.render(
+
+
+
+
+
+
+
+ );
+ addComment('Stop on Baddie and restart from Boundary');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('deduplicates lifecycle names during commit to reduce overhead', () => {
+ class A extends React.Component {
+ componentDidUpdate() {}
+ render() {
+ return ;
+ }
+ }
+
+ class B extends React.Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.cascade && !prevProps.cascade) {
+ this.setState({});
+ }
+ }
+ render() {
+ return ;
+ }
+ }
+
+ ReactNoop.render(
+
+
+
+
+
+
+ );
+ ReactNoop.flush();
+ resetFlamechart();
+
+ ReactNoop.render(
+
+
+
+
+
+
+ );
+ addComment('The commit phase should mention A and B just once');
+ ReactNoop.flush();
+ ReactNoop.render(
+
+
+
+
+
+
+ );
+ addComment('Because of deduplication, we don\'t know B was cascading,');
+ addComment('but we should still see the warning for the commit phase.');
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('supports coroutines', () => {
+ function Continuation({ isSame }) {
+ return ;
+ }
+
+ function CoChild({ bar }) {
+ return ReactCoroutine.createYield({
+ props: {
+ bar: bar,
+ },
+ continuation: Continuation,
+ });
+ }
+
+ function Indirection() {
+ return [, ];
+ }
+
+ function HandleYields(props, yields) {
+ return yields.map(y =>
+
+ );
+ }
+
+ function CoParent(props) {
+ return ReactCoroutine.createCoroutine(
+ props.children,
+ HandleYields,
+ props
+ );
+ }
+
+ function App() {
+ return
;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+
+ it('supports portals', () => {
+ const noopContainer = {children: []};
+ ReactNoop.render(
+
+ {ReactPortal.createPortal(, noopContainer, null)}
+
+ );
+ ReactNoop.flush();
+ expect(getFlameChart()).toMatchSnapshot();
+ });
+});
diff --git a/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
new file mode 100644
index 000000000000..4d5132388456
--- /dev/null
+++ b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
@@ -0,0 +1,260 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
+"// Mount
+⚛ (React Tree Reconciliation)
+ ⚛ AllLifecycles [mount]
+ ⚛ AllLifecycles.componentWillMount
+ ⚛ AllLifecycles.getChildContext
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 1 Total)
+ ⚛ AllLifecycles.componentDidMount
+
+// Update
+⚛ (React Tree Reconciliation)
+ ⚛ AllLifecycles [update]
+ ⚛ AllLifecycles.componentWillReceiveProps
+ ⚛ AllLifecycles.shouldComponentUpdate
+ ⚛ AllLifecycles.componentWillUpdate
+ ⚛ AllLifecycles.getChildContext
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 2 Total)
+ ⚛ (Calling Lifecycle Methods: 2 Total)
+ ⚛ AllLifecycles.componentDidUpdate
+
+// Unmount
+⚛ (React Tree Reconciliation)
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ AllLifecycles.componentWillUnmount
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = `
+"// The commit phase should mention A and B just once
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [update]
+ ⚛ A [update]
+ ⚛ B [update]
+ ⚛ A [update]
+ ⚛ B [update]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 9 Total)
+ ⚛ (Calling Lifecycle Methods: 9 Total)
+ ⚛ A.componentDidUpdate
+ ⚛ B.componentDidUpdate
+
+// Because of deduplication, we don't know B was cascading,
+// but we should still see the warning for the commit phase.
+🛑 (React Tree Reconciliation) Warning: There were cascading updates
+ ⚛ Parent [update]
+ ⚛ A [update]
+ ⚛ B [update]
+ ⚛ A [update]
+ ⚛ B [update]
+ 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
+ ⚛ (Committing Host Effects: 9 Total)
+ ⚛ (Calling Lifecycle Methods: 9 Total)
+ ⚛ A.componentDidUpdate
+ ⚛ B.componentDidUpdate
+ ⚛ B [update]
+ 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
+ ⚛ (Committing Host Effects: 3 Total)
+ ⚛ (Calling Lifecycle Methods: 3 Total)
+ ⚛ B.componentDidUpdate
+"
+`;
+
+exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = `
+"// Should not print a warning
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ NotCascading [mount]
+ ⚛ NotCascading.componentWillMount
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+
+// Should not print a warning
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [update]
+ ⚛ NotCascading [update]
+ ⚛ NotCascading.componentWillReceiveProps
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 2 Total)
+ ⚛ (Calling Lifecycle Methods: 2 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
+"// Mount
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ Child [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+
+// Update
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [update]
+ ⚛ Child [update]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 2 Total)
+ ⚛ (Calling Lifecycle Methods: 2 Total)
+
+// Unmount
+⚛ (React Tree Reconciliation)
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
+"// Start mounting Parent and A
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ A [mount]
+ ⚛ Child [mount]
+
+// Mount B just a little (but not enough to memoize)
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ B [mount]
+
+// Complete B and Parent
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ B [mount]
+ ⚛ Child [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
+"// Flush the parent
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+
+// Flush the child
+⚛ (React Tree Reconciliation)
+ ⚛ Child [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 3 Total)
+ ⚛ (Calling Lifecycle Methods: 2 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
+"// Stop on Baddie and restart from Boundary
+🛑 (React Tree Reconciliation) Warning: There were cascading updates
+ ⚛ Parent [mount]
+ ⚛ Boundary [mount]
+ ⚛ Parent [mount]
+ ⚛ Baddie [mount]
+ 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
+ ⚛ (Committing Host Effects: 2 Total)
+ ⚛ (Calling Lifecycle Methods: 1 Total)
+ ⚛ Boundary [update]
+ ⚛ ErrorReport [mount]
+ 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
+ ⚛ (Committing Host Effects: 2 Total)
+ ⚛ (Calling Lifecycle Methods: 1 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
+"// Will fatal
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ Baddie [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 1 Total)
+
+// Will reconcile from a clean state
+⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ Child [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
+"// Should include just A and B, no Parents
+⚛ (React Tree Reconciliation)
+ ⚛ A [update]
+ ⚛ B [update]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 6 Total)
+ ⚛ (Calling Lifecycle Methods: 6 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf supports coroutines 1`] = `
+"⚛ (React Tree Reconciliation)
+ ⚛ App [mount]
+ ⚛ CoParent [mount]
+ ⚛ HandleYields [mount]
+ ⚛ Indirection [mount]
+ ⚛ CoChild [mount]
+ ⚛ CoChild [mount]
+ ⚛ Continuation [mount]
+ ⚛ Continuation [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 3 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf supports portals 1`] = `
+"⚛ (React Tree Reconciliation)
+ ⚛ Parent [mount]
+ ⚛ Child [mount]
+ ⚛ (Committing Changes)
+ ⚛ (Committing Host Effects: 3 Total)
+ ⚛ (Calling Lifecycle Methods: 1 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
+"// Should print a warning
+🛑 (React Tree Reconciliation) Warning: There were cascading updates
+ ⚛ Parent [mount]
+ ⚛ Cascading [mount]
+ 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
+ ⚛ (Committing Host Effects: 2 Total)
+ ⚛ (Calling Lifecycle Methods: 1 Total)
+ 🛑 Cascading.componentDidMount Warning: Scheduled a cascading update
+ ⚛ Cascading [update]
+ 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
+ ⚛ (Committing Host Effects: 2 Total)
+ ⚛ (Calling Lifecycle Methods: 2 Total)
+"
+`;
+
+exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = `
+"// Rendering the first root
+🛑 (React Tree Reconciliation) Warning: There were cascading updates
+ ⚛ Cascading [mount]
+ 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 1 Total)
+ 🛑 Cascading.componentDidMount Warning: Scheduled a cascading update
+ // Scheduling another root from componentDidMount
+ ⚛ Child [mount]
+ 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
+ ⚛ (Committing Host Effects: 1 Total)
+ ⚛ (Calling Lifecycle Methods: 0 Total)
+"
+`;