@samitouri / QOS-React / commits / 73378c9293

[Fiber] Performance measurements (#9071)

* wip * better * better * track commits * better * wip * Fix * Add some lifecycles * wip * Naming * Moar emojis * Remove stacks in favor of a flag * Fix Flow * Gate behind __DEV__ * Revert flag for testing * Measure all lifecycles * Push it to the limits * Polish * Indent * Refactor and track cascading updates * More prominent warnings * Make mark names themselves readable This is useful for RN Systrace which doesn't let us assign labels after the fact. * Keep track of how many effects we call * Fix typo * Do less work to reduce the overhead * Fix lint * Remove closure * Remove unintentional formatting changes * Add tests * Fix test regex and record tests * Disable irrelevant tests needed for ReactPerf * Fix typo * Fix lint and flow * Don't treat cWM or cWRP as cascading * Whitespace * Update tests * Gate callComponentWillUnmountWithTimerInDev definition by DEV

Dan Abramov committed Mar 8, 2017 at 18:28 UTC 73378c92939d17706d1063d67d28f75459b47129
17 files changed +1593 -427
package.json
+1 -1
@@ -107,7 +107,7 @@
107 "./scripts/jest/environment.js"
108 ],
109 "setupTestFrameworkScriptFile": "./scripts/jest/test-framework-setup.js",
110 - "testRegex": "/__tests__/",
110 + "testRegex": "/__tests__/.*(\\.js|coffee|ts)$",
111 "moduleFileExtensions": [
112 "js",
113 "json",
scripts/fiber/tests-failing.txt
-15
@@ -1,21 +1,6 @@
1 src/isomorphic/classic/__tests__/ReactContextValidator-test.js
2 * should pass previous context to lifecycles
3
4 -src/renderers/__tests__/ReactComponentTreeHook-test.js
5 -* can be retrieved by ID
6 -
7 -src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
8 -* gets recorded during an update
9 -
10 -src/renderers/__tests__/ReactPerf-test.js
11 -* should count no-op update as waste
12 -* should count no-op update in child as waste
13 -* should include stats for components unmounted during measurement
14 -* should include lifecycle methods in measurements
15 -* should include render time of functional components
16 -* should not count time in a portal towards lifecycle method
17 -* should work when measurement starts during reconciliation
18 -
4 src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
5 * gives source code refs for unknown prop warning (ssr)
6 * gives source code refs for unknown prop warning for exact elements (ssr)
scripts/fiber/tests-passing-except-dev.txt
-129
@@ -1,132 +1,3 @@
1 -src/renderers/__tests__/ReactComponentTreeHook-test.js
2 -* uses displayName or Unknown for classic components
3 -* uses displayName, name, or ReactComponent for modern components
4 -* uses displayName, name, or Object for factory components
5 -* uses displayName, name, or StatelessComponent for functional components
6 -* reports a host tree correctly
7 -* reports a simple tree with composites correctly
8 -* reports a tree with composites correctly
9 -* ignores null children
10 -* ignores false children
11 -* reports text nodes as children
12 -* reports a single text node as a child
13 -* reports a single number node as a child
14 -* reports a zero as a child
15 -* skips empty nodes for multiple children
16 -* reports html content as no children
17 -* updates text of a single text child
18 -* updates from no children to a single text child
19 -* updates from a single text child to no children
20 -* updates from html content to a single text child
21 -* updates from a single text child to html content
22 -* updates from no children to multiple text children
23 -* updates from multiple text children to no children
24 -* updates from html content to multiple text children
25 -* updates from multiple text children to html content
26 -* updates from html content to no children
27 -* updates from no children to html content
28 -* updates from one text child to multiple text children
29 -* updates from multiple text children to one text child
30 -* updates text nodes when reordering
31 -* updates host nodes when reordering with keys
32 -* updates host nodes when reordering without keys
33 -* updates a single composite child of a different type
34 -* updates a single composite child of the same type
35 -* updates from no children to a single composite child
36 -* updates from a single composite child to no children
37 -* updates mixed children
38 -* updates with a host child
39 -* updates from null to a host child
40 -* updates from a host child to null
41 -* updates from a host child to a composite child
42 -* updates from a composite child to a host child
43 -* updates from null to a composite child
44 -* updates from a composite child to null
45 -* updates with a host child
46 -* updates from null to a host child
47 -* updates from a host child to null
48 -* updates from a host child to a composite child
49 -* updates from a composite child to a host child
50 -* updates from null to a composite child
51 -* updates from a composite child to null
52 -* tracks owner correctly
53 -* purges unmounted components automatically
54 -* reports update counts
55 -* does not report top-level wrapper as a root
56 -* registers inlined text nodes
57 -* works
58 -
59 -src/renderers/__tests__/ReactComponentTreeHook-test.native.js
60 -* uses displayName or Unknown for classic components
61 -* uses displayName, name, or ReactComponent for modern components
62 -* uses displayName, name, or Object for factory components
63 -* uses displayName, name, or StatelessComponent for functional components
64 -* reports a host tree correctly
65 -* reports a simple tree with composites correctly
66 -* reports a tree with composites correctly
67 -* ignores null children
68 -* ignores false children
69 -* reports text nodes as children
70 -* reports a single text node as a child
71 -* reports a single number node as a child
72 -* reports a zero as a child
73 -* skips empty nodes for multiple children
74 -* updates text of a single text child
75 -* updates from no children to a single text child
76 -* updates from a single text child to no children
77 -* updates from no children to multiple text children
78 -* updates from multiple text children to no children
79 -* updates from one text child to multiple text children
80 -* updates from multiple text children to one text child
81 -* updates text nodes when reordering
82 -* updates host nodes when reordering with keys
83 -* updates host nodes when reordering with keys
84 -* updates a single composite child of a different type
85 -* updates a single composite child of the same type
86 -* updates from no children to a single composite child
87 -* updates from a single composite child to no children
88 -* updates mixed children
89 -* updates with a host child
90 -* updates from null to a host child
91 -* updates from a host child to null
92 -* updates from a host child to a composite child
93 -* updates from a composite child to a host child
94 -* updates from null to a composite child
95 -* updates from a composite child to null
96 -* updates with a host child
97 -* updates from null to a host child
98 -* updates from a host child to null
99 -* updates from a host child to a composite child
100 -* updates from a composite child to a host child
101 -* updates from null to a composite child
102 -* updates from a composite child to null
103 -* tracks owner correctly
104 -* purges unmounted components automatically
105 -* reports update counts
106 -* does not report top-level wrapper as a root
107 -
108 -src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
109 -* gets recorded for host roots
110 -* gets recorded for composite roots
111 -* gets recorded when a native is mounted deeply instead of null
112 -* gets recorded during mount
113 -* gets recorded during an update
114 -* gets ignored if the styles are shallowly equal
115 -* gets recorded during mount
116 -* gets recorded during mount
117 -* gets recorded during mount
118 -* gets recorded during an update from text content
119 -* gets recorded during an update from html
120 -* gets recorded during an update from children
121 -* gets recorded when composite renders to a different type
122 -* gets recorded when composite renders to null after a native
123 -* gets recorded during an update from text content
124 -* gets recorded during an update from html
125 -* gets recorded during an update from children
126 -* gets reported when a child is inserted
127 -* gets reported when a child is inserted
128 -* gets reported when a child is removed
129 -
1 src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
2 * should not warn when server-side rendering `onScroll`
3 * should warn about incorrect casing on properties (ssr)
scripts/fiber/tests-passing.txt
+15 -40
@@ -561,8 +561,6 @@ src/renderers/__tests__/ReactComponentLifeCycle-test.js
561
562 src/renderers/__tests__/ReactComponentTreeHook-test.js
563 * gets created
564 -* is created during mounting
565 -* is created when calling renderToString during render
564
565 src/renderers/__tests__/ReactCompositeComponent-test.js
566 * should support module pattern components
@@ -673,17 +671,6 @@ src/renderers/__tests__/ReactErrorBoundaries-test.js
671 * renders empty output if error boundary does not handle the error
672 * passes first error when two errors happen in commit
673
676 -src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
677 -* gets ignored for composite roots that return null
678 -* gets recorded during an update
679 -* gets recorded as a removal during an update
680 -* gets recorded during an update
681 -* gets recorded during an update
682 -* gets ignored if new text is equal
683 -* gets ignored if new text is equal
684 -* gets ignored if the type has not changed
685 -* gets ignored if new html is equal
686 -
674 src/renderers/__tests__/ReactIdentity-test.js
675 * should allow key property to express identity
676 * should use composite identity
@@ -750,33 +737,6 @@ src/renderers/__tests__/ReactMultiChildText-test.js
737 * should throw if rendering both HTML and children
738 * should render between nested components and inline children
739
753 -src/renderers/__tests__/ReactPerf-test.js
754 -* should not count initial render as waste
755 -* should not count unmount as waste
756 -* should not count content update as waste
757 -* should not count child addition as waste
758 -* should not count child removal as waste
759 -* should not count property update as waste
760 -* should not count style update as waste
761 -* should not count property removal as waste
762 -* should not count raw HTML update as waste
763 -* should not count child reordering as waste
764 -* should not count text update as waste
765 -* should not count replacing null with a host as waste
766 -* should not count replacing a host with null as waste
767 -* warns once when using getMeasurementsSummaryMap
768 -* warns once when using printDOM
769 -* returns isRunning state
770 -* start has no effect when already running
771 -* stop has no effect when already stopped
772 -* should print console error only once
773 -* should not print errant warnings if render() throws
774 -* should not print errant warnings if componentWillMount() throws
775 -* should not print errant warnings if componentDidMount() throws
776 -* should not print errant warnings if portal throws in render()
777 -* should not print errant warnings if portal throws in componentWillMount()
778 -* should not print errant warnings if portal throws in componentDidMount()
779 -
740 src/renderers/__tests__/ReactStatelessComponent-test.js
741 * should render stateless component
742 * should update stateless component
@@ -1565,6 +1525,21 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
1525 * should ignore errors thrown in log method to prevent cycle
1526 * should relay info about error boundary and retry attempts if applicable
1527
1528 +src/renderers/shared/fiber/__tests__/ReactIncrementalPerf-test.js
1529 +* measures a simple reconciliation
1530 +* skips parents during setState
1531 +* warns on cascading renders from setState
1532 +* warns on cascading renders from top-level render
1533 +* does not treat setState from cWM or cWRP as cascading
1534 +* captures all lifecycles
1535 +* measures deprioritized work
1536 +* measures deferred work in chunks
1537 +* recovers from fatal errors
1538 +* recovers from caught errors
1539 +* deduplicates lifecycle names during commit to reduce overhead
1540 +* supports coroutines
1541 +* supports portals
1542 +
1543 src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
1544 * handles isMounted even when the initial render is deferred
1545 * handles isMounted when an unmount is deferred
src/renderers/__tests__/ReactComponentTreeHook-test.js
+249 -236
@@ -11,6 +11,9 @@
11
12 'use strict';
13
14 +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
15 +var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe;
16 +
17 describe('ReactComponentTreeHook', () => {
18 var React;
19 var ReactDOM;
@@ -30,6 +33,148 @@ describe('ReactComponentTreeHook', () => {
33 ReactComponentTreeTestUtils = require('ReactComponentTreeTestUtils');
34 });
35
36 + // This is the only part used both by Stack and Fiber.
37 + describe('stack addenda', () => {
38 + it('gets created', () => {
39 + function getAddendum(element) {
40 + var addendum = ReactComponentTreeHook.getCurrentStackAddendum(element);
41 + return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
42 + }
43 +
44 + var Anon = React.createClass({displayName: null, render: () => null});
45 + var Orange = React.createClass({render: () => null});
46 +
47 + expectDev(getAddendum()).toBe(
48 + ''
49 + );
50 + expectDev(getAddendum(<div />)).toBe(
51 + '\n in div (at **)'
52 + );
53 + expectDev(getAddendum(<Anon />)).toBe(
54 + '\n in Unknown (at **)'
55 + );
56 + expectDev(getAddendum(<Orange />)).toBe(
57 + '\n in Orange (at **)'
58 + );
59 + expectDev(getAddendum(React.createElement(Orange))).toBe(
60 + '\n in Orange'
61 + );
62 +
63 + var renders = 0;
64 + var rOwnedByQ;
65 +
66 + function Q() {
67 + return (rOwnedByQ = React.createElement(R));
68 + }
69 + function R() {
70 + return <div><S /></div>;
71 + }
72 + class S extends React.Component {
73 + componentDidMount() {
74 + // Check that the parent path is still fetched when only S itself is on
75 + // the stack.
76 + this.forceUpdate();
77 + }
78 + render() {
79 + expectDev(getAddendum()).toBe(
80 + '\n in S (at **)' +
81 + '\n in div (at **)' +
82 + '\n in R (created by Q)' +
83 + '\n in Q (at **)'
84 + );
85 + expectDev(getAddendum(<span />)).toBe(
86 + '\n in span (at **)' +
87 + '\n in S (at **)' +
88 + '\n in div (at **)' +
89 + '\n in R (created by Q)' +
90 + '\n in Q (at **)'
91 + );
92 + expectDev(getAddendum(React.createElement('span'))).toBe(
93 + '\n in span (created by S)' +
94 + '\n in S (at **)' +
95 + '\n in div (at **)' +
96 + '\n in R (created by Q)' +
97 + '\n in Q (at **)'
98 + );
99 + renders++;
100 + return null;
101 + }
102 + }
103 + ReactDOM.render(<Q />, document.createElement('div'));
104 + expectDev(renders).toBe(2);
105 +
106 + // Make sure owner is fetched for the top element too.
107 + expectDev(getAddendum(rOwnedByQ)).toBe(
108 + '\n in R (created by Q)'
109 + );
110 + });
111 +
112 + // These are features and regression tests that only affect
113 + // the Stack implementation of the stack addendum.
114 + if (!ReactDOMFeatureFlags.useFiber) {
115 + it('can be retrieved by ID', () => {
116 + function getAddendum(id) {
117 + var addendum = ReactComponentTreeHook.getStackAddendumByID(id);
118 + return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
119 + }
120 +
121 + class Q extends React.Component {
122 + render() {
123 + return null;
124 + }
125 + }
126 +
127 + var q = ReactDOM.render(<Q />, document.createElement('div'));
128 + expectDev(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe(
129 + '\n in Q (at **)'
130 + );
131 +
132 + spyOn(console, 'error');
133 + getAddendum(-17);
134 + expectDev(console.error.calls.count()).toBe(1);
135 + expectDev(console.error.calls.argsFor(0)[0]).toBe(
136 + 'Warning: ReactComponentTreeHook: Missing React element for ' +
137 + 'debugID -17 when building stack'
138 + );
139 + });
140 +
141 + it('is created during mounting', () => {
142 + // https://github.com/facebook/react/issues/7187
143 + var el = document.createElement('div');
144 + var portalEl = document.createElement('div');
145 + class Foo extends React.Component {
146 + componentWillMount() {
147 + ReactDOM.render(<div />, portalEl);
148 + }
149 + render() {
150 + return <div><div /></div>;
151 + }
152 + }
153 + ReactDOM.render(<Foo />, el);
154 + });
155 +
156 + it('is created when calling renderToString during render', () => {
157 + // https://github.com/facebook/react/issues/7190
158 + var el = document.createElement('div');
159 + class Foo extends React.Component {
160 + render() {
161 + return (
162 + <div>
163 + <div>
164 + {ReactDOMServer.renderToString(<div />)}
165 + </div>
166 + </div>
167 + );
168 + }
169 + }
170 + ReactDOM.render(<Foo />, el);
171 + });
172 + }
173 + });
174 +
175 + // The rest of this file is not relevant for Fiber.
176 + // TODO: remove tests below when we delete Stack.
177 +
178 function assertTreeMatches(pairs) {
179 if (!Array.isArray(pairs[0])) {
180 pairs = [pairs];
@@ -106,7 +251,7 @@ describe('ReactComponentTreeHook', () => {
251 });
252 }
253
109 - describe('mount', () => {
254 + describeStack('mount', () => {
255 it('uses displayName or Unknown for classic components', () => {
256 class Foo extends React.Component {
257 render() {
@@ -520,7 +665,7 @@ describe('ReactComponentTreeHook', () => {
665 });
666 });
667
523 - describe('update', () => {
668 + describeStack('update', () => {
669 describe('host component', () => {
670 it('updates text of a single text child', () => {
671 var elementBefore = <div>Hi.</div>;
@@ -1601,276 +1746,144 @@ describe('ReactComponentTreeHook', () => {
1746 });
1747 });
1748
1604 - it('tracks owner correctly', () => {
1605 - class Foo extends React.Component {
1606 - render() {
1607 - return <Bar><h1>Hi.</h1></Bar>;
1749 + describeStack('misc', () => {
1750 + it('tracks owner correctly', () => {
1751 + class Foo extends React.Component {
1752 + render() {
1753 + return <Bar><h1>Hi.</h1></Bar>;
1754 + }
1755 + }
1756 + function Bar({children}) {
1757 + return <div>{children} Mom</div>;
1758 }
1609 - }
1610 - function Bar({children}) {
1611 - return <div>{children} Mom</div>;
1612 - }
1759
1614 - // Note that owner is not calculated for text nodes
1615 - // because they are not created from real elements.
1616 - var element = <article><Foo /></article>;
1617 - var tree = {
1618 - displayName: 'article',
1619 - children: [{
1620 - displayName: 'Foo',
1760 + // Note that owner is not calculated for text nodes
1761 + // because they are not created from real elements.
1762 + var element = <article><Foo /></article>;
1763 + var tree = {
1764 + displayName: 'article',
1765 children: [{
1622 - displayName: 'Bar',
1623 - ownerDisplayName: 'Foo',
1766 + displayName: 'Foo',
1767 children: [{
1625 - displayName: 'div',
1626 - ownerDisplayName: 'Bar',
1768 + displayName: 'Bar',
1769 + ownerDisplayName: 'Foo',
1770 children: [{
1628 - displayName: 'h1',
1629 - ownerDisplayName: 'Foo',
1771 + displayName: 'div',
1772 + ownerDisplayName: 'Bar',
1773 children: [{
1774 + displayName: 'h1',
1775 + ownerDisplayName: 'Foo',
1776 + children: [{
1777 + displayName: '#text',
1778 + text: 'Hi.',
1779 + }],
1780 + }, {
1781 displayName: '#text',
1632 - text: 'Hi.',
1782 + text: ' Mom',
1783 }],
1634 - }, {
1635 - displayName: '#text',
1636 - text: ' Mom',
1784 }],
1785 }],
1786 }],
1640 - }],
1641 - };
1642 - assertTreeMatches([element, tree]);
1643 - });
1787 + };
1788 + assertTreeMatches([element, tree]);
1789 + });
1790
1645 - it('purges unmounted components automatically', () => {
1646 - var node = document.createElement('div');
1647 - var renderBar = true;
1648 - var fooInstance;
1649 - var barInstance;
1791 + it('purges unmounted components automatically', () => {
1792 + var node = document.createElement('div');
1793 + var renderBar = true;
1794 + var fooInstance;
1795 + var barInstance;
1796
1651 - class Foo extends React.Component {
1652 - render() {
1653 - fooInstance = ReactInstanceMap.get(this);
1654 - return renderBar ? <Bar /> : null;
1797 + class Foo extends React.Component {
1798 + render() {
1799 + fooInstance = ReactInstanceMap.get(this);
1800 + return renderBar ? <Bar /> : null;
1801 + }
1802 }
1656 - }
1803
1658 - class Bar extends React.Component {
1659 - render() {
1660 - barInstance = ReactInstanceMap.get(this);
1661 - return null;
1804 + class Bar extends React.Component {
1805 + render() {
1806 + barInstance = ReactInstanceMap.get(this);
1807 + return null;
1808 + }
1809 }
1663 - }
1664 -
1665 - ReactDOM.render(<Foo />, node);
1666 - ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
1667 - displayName: 'Bar',
1668 - parentDisplayName: 'Foo',
1669 - parentID: fooInstance._debugID,
1670 - children: [],
1671 - }, 'Foo');
1672 -
1673 - renderBar = false;
1674 - ReactDOM.render(<Foo />, node);
1675 - ReactDOM.render(<Foo />, node);
1676 - ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
1677 - displayName: 'Unknown',
1678 - children: [],
1679 - parentID: null,
1680 - }, 'Foo');
1681 -
1682 - ReactDOM.unmountComponentAtNode(node);
1683 - ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
1684 - displayName: 'Unknown',
1685 - children: [],
1686 - parentID: null,
1687 - }, 'Foo');
1688 - });
1689 -
1690 - it('reports update counts', () => {
1691 - var node = document.createElement('div');
1810
1693 - ReactDOM.render(<div className="a" />, node);
1694 - var divID = ReactComponentTreeHook.getRootIDs()[0];
1695 - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1696 -
1697 - ReactDOM.render(<span className="a" />, node);
1698 - var spanID = ReactComponentTreeHook.getRootIDs()[0];
1699 - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1700 - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
1701 -
1702 - ReactDOM.render(<span className="b" />, node);
1703 - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1704 - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(1);
1705 -
1706 - ReactDOM.render(<span className="c" />, node);
1707 - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1708 - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(2);
1709 -
1710 - ReactDOM.unmountComponentAtNode(node);
1711 - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1712 - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
1713 - });
1714 -
1715 - it('does not report top-level wrapper as a root', () => {
1716 - var node = document.createElement('div');
1717 -
1718 - ReactDOM.render(<div className="a" />, node);
1719 - expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
1720 -
1721 - ReactDOM.render(<div className="b" />, node);
1722 - expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
1723 -
1724 - ReactDOM.unmountComponentAtNode(node);
1725 - expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
1726 - expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
1727 - });
1811 + ReactDOM.render(<Foo />, node);
1812 + ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
1813 + displayName: 'Bar',
1814 + parentDisplayName: 'Foo',
1815 + parentID: fooInstance._debugID,
1816 + children: [],
1817 + }, 'Foo');
1818
1729 - it('registers inlined text nodes', () => {
1730 - var node = document.createElement('div');
1819 + renderBar = false;
1820 + ReactDOM.render(<Foo />, node);
1821 + ReactDOM.render(<Foo />, node);
1822 + ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
1823 + displayName: 'Unknown',
1824 + children: [],
1825 + parentID: null,
1826 + }, 'Foo');
1827
1732 - ReactDOM.render(<div>hi</div>, node);
1733 - expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual(['div', '#text']);
1828 + ReactDOM.unmountComponentAtNode(node);
1829 + ReactComponentTreeTestUtils.expectTree(barInstance._debugID, {
1830 + displayName: 'Unknown',
1831 + children: [],
1832 + parentID: null,
1833 + }, 'Foo');
1834 + });
1835
1735 - ReactDOM.unmountComponentAtNode(node);
1736 - expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
1737 - });
1836 + it('reports update counts', () => {
1837 + var node = document.createElement('div');
1838
1739 - describe('stack addenda', () => {
1740 - it('gets created', () => {
1741 - function getAddendum(element) {
1742 - var addendum = ReactComponentTreeHook.getCurrentStackAddendum(element);
1743 - return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
1744 - }
1839 + ReactDOM.render(<div className="a" />, node);
1840 + var divID = ReactComponentTreeHook.getRootIDs()[0];
1841 + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1842
1746 - var Anon = React.createClass({displayName: null, render: () => null});
1747 - var Orange = React.createClass({render: () => null});
1843 + ReactDOM.render(<span className="a" />, node);
1844 + var spanID = ReactComponentTreeHook.getRootIDs()[0];
1845 + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1846 + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
1847
1749 - expectDev(getAddendum()).toBe(
1750 - ''
1751 - );
1752 - expectDev(getAddendum(<div />)).toBe(
1753 - '\n in div (at **)'
1754 - );
1755 - expectDev(getAddendum(<Anon />)).toBe(
1756 - '\n in Unknown (at **)'
1757 - );
1758 - expectDev(getAddendum(<Orange />)).toBe(
1759 - '\n in Orange (at **)'
1760 - );
1761 - expectDev(getAddendum(React.createElement(Orange))).toBe(
1762 - '\n in Orange'
1763 - );
1848 + ReactDOM.render(<span className="b" />, node);
1849 + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1850 + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(1);
1851
1765 - var renders = 0;
1766 - var rOwnedByQ;
1852 + ReactDOM.render(<span className="c" />, node);
1853 + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1854 + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(2);
1855
1768 - function Q() {
1769 - return (rOwnedByQ = React.createElement(R));
1770 - }
1771 - function R() {
1772 - return <div><S /></div>;
1773 - }
1774 - class S extends React.Component {
1775 - componentDidMount() {
1776 - // Check that the parent path is still fetched when only S itself is on
1777 - // the stack.
1778 - this.forceUpdate();
1779 - }
1780 - render() {
1781 - expectDev(getAddendum()).toBe(
1782 - '\n in S (at **)' +
1783 - '\n in div (at **)' +
1784 - '\n in R (created by Q)' +
1785 - '\n in Q (at **)'
1786 - );
1787 - expectDev(getAddendum(<span />)).toBe(
1788 - '\n in span (at **)' +
1789 - '\n in S (at **)' +
1790 - '\n in div (at **)' +
1791 - '\n in R (created by Q)' +
1792 - '\n in Q (at **)'
1793 - );
1794 - expectDev(getAddendum(React.createElement('span'))).toBe(
1795 - '\n in span (created by S)' +
1796 - '\n in S (at **)' +
1797 - '\n in div (at **)' +
1798 - '\n in R (created by Q)' +
1799 - '\n in Q (at **)'
1800 - );
1801 - renders++;
1802 - return null;
1803 - }
1804 - }
1805 - ReactDOM.render(<Q />, document.createElement('div'));
1806 - expectDev(renders).toBe(2);
1807 -
1808 - // Make sure owner is fetched for the top element too.
1809 - expectDev(getAddendum(rOwnedByQ)).toBe(
1810 - '\n in R (created by Q)'
1811 - );
1856 + ReactDOM.unmountComponentAtNode(node);
1857 + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0);
1858 + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0);
1859 });
1860
1814 - it('can be retrieved by ID', () => {
1815 - function getAddendum(id) {
1816 - var addendum = ReactComponentTreeHook.getStackAddendumByID(id);
1817 - return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
1818 - }
1861 + it('does not report top-level wrapper as a root', () => {
1862 + var node = document.createElement('div');
1863
1820 - class Q extends React.Component {
1821 - render() {
1822 - return null;
1823 - }
1824 - }
1864 + ReactDOM.render(<div className="a" />, node);
1865 + expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
1866
1826 - var q = ReactDOM.render(<Q />, document.createElement('div'));
1827 - expectDev(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe(
1828 - '\n in Q (at **)'
1829 - );
1867 + ReactDOM.render(<div className="b" />, node);
1868 + expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']);
1869
1831 - spyOn(console, 'error');
1832 - getAddendum(-17);
1833 - expectDev(console.error.calls.count()).toBe(1);
1834 - expectDev(console.error.calls.argsFor(0)[0]).toBe(
1835 - 'Warning: ReactComponentTreeHook: Missing React element for ' +
1836 - 'debugID -17 when building stack'
1837 - );
1870 + ReactDOM.unmountComponentAtNode(node);
1871 + expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
1872 + expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
1873 });
1874
1840 - it('is created during mounting', () => {
1841 - // https://github.com/facebook/react/issues/7187
1842 - var el = document.createElement('div');
1843 - var portalEl = document.createElement('div');
1844 - class Foo extends React.Component {
1845 - componentWillMount() {
1846 - ReactDOM.render(<div />, portalEl);
1847 - }
1848 - render() {
1849 - return <div><div /></div>;
1850 - }
1851 - }
1852 - ReactDOM.render(<Foo />, el);
1853 - });
1875 + it('registers inlined text nodes', () => {
1876 + var node = document.createElement('div');
1877
1855 - it('is created when calling renderToString during render', () => {
1856 - // https://github.com/facebook/react/issues/7190
1857 - var el = document.createElement('div');
1858 - class Foo extends React.Component {
1859 - render() {
1860 - return (
1861 - <div>
1862 - <div>
1863 - {ReactDOMServer.renderToString(<div />)}
1864 - </div>
1865 - </div>
1866 - );
1867 - }
1868 - }
1869 - ReactDOM.render(<Foo />, el);
1878 + ReactDOM.render(<div>hi</div>, node);
1879 + expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual(['div', '#text']);
1880 +
1881 + ReactDOM.unmountComponentAtNode(node);
1882 + expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
1883 });
1884 });
1885
1873 - describe('in environment without Map, Set and Array.from', () => {
1886 + describeStack('in environment without Map, Set and Array.from', () => {
1887 var realMap;
1888 var realSet;
1889 var realArrayFrom;
src/renderers/__tests__/ReactComponentTreeHook-test.native.js
+9 -1
@@ -11,7 +11,15 @@
11
12 'use strict';
13
14 -describe('ReactComponentTreeHook', () => {
14 +var ReactNativeFeatureFlags = require('ReactNativeFeatureFlags');
15 +var describeStack = ReactNativeFeatureFlags.useFiber ? describe.skip : describe;
16 +
17 +// These tests are only relevant for the Stack version of the tree hook.
18 +// This file is for RN. There is a sibling file that has some tree hook
19 +// tests that are still relevant in Fiber.
20 +// TODO: remove this file when we delete Stack.
21 +
22 +describeStack('ReactComponentTreeHook', () => {
23 var React;
24 var ReactNative;
25 var ReactInstanceMap;
src/renderers/__tests__/ReactHostOperationHistoryHook-test.js
+6 -3
@@ -11,12 +11,16 @@
11
12 'use strict';
13
14 -describe('ReactHostOperationHistoryHook', () => {
14 +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
15 +var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe;
16 +
17 +// This is only used by ReactPerf which is currently not supported on Fiber.
18 +// Use browser timeline integration instead.
19 +describeStack('ReactHostOperationHistoryHook', () => {
20 var React;
21 var ReactPerf;
22 var ReactDOM;
23 var ReactDOMComponentTree;
19 - var ReactDOMFeatureFlags;
24 var ReactHostOperationHistoryHook;
25
26 beforeEach(() => {
@@ -26,7 +30,6 @@ describe('ReactHostOperationHistoryHook', () => {
30 ReactPerf = require('react-dom/lib/ReactPerf');
31 ReactDOM = require('react-dom');
32 ReactDOMComponentTree = require('ReactDOMComponentTree');
29 - ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
33 ReactHostOperationHistoryHook = require('ReactHostOperationHistoryHook');
34
35 ReactPerf.start();
src/renderers/__tests__/ReactPerf-test.js
+6 -1
@@ -11,7 +11,12 @@
11
12 'use strict';
13
14 -describe('ReactPerf', () => {
14 +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
15 +var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe;
16 +
17 +// ReactPerf is currently not supported on Fiber.
18 +// Use browser timeline integration instead.
19 +describeStack('ReactPerf', () => {
20 var React;
21 var ReactDOM;
22 var ReactPerf;
src/renderers/shared/fiber/ReactDebugFiberPerf.js new
+408
@@ -0,0 +1,408 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactDebugFiberPerf
10 + * @flow
11 + */
12 +
13 +import type { Fiber } from 'ReactFiber';
14 +
15 +type MeasurementPhase =
16 + 'componentWillMount' |
17 + 'componentWillUnmount' |
18 + 'componentWillReceiveProps' |
19 + 'shouldComponentUpdate' |
20 + 'componentWillUpdate' |
21 + 'componentDidUpdate' |
22 + 'componentDidMount' |
23 + 'getChildContext';
24 +
25 +// Trust the developer to only use this with a __DEV__ check
26 +let ReactDebugFiberPerf = ((null: any): typeof ReactDebugFiberPerf);
27 +
28 +if (__DEV__) {
29 + const {
30 + HostRoot,
31 + HostComponent,
32 + HostText,
33 + HostPortal,
34 + YieldComponent,
35 + Fragment,
36 + } = require('ReactTypeOfWork');
37 +
38 + const getComponentName = require('getComponentName');
39 +
40 + // Prefix measurements so that it's possible to filter them.
41 + // Longer prefixes are hard to read in DevTools.
42 + const reactEmoji = '\u269B';
43 + const warningEmoji = '\uD83D\uDED1';
44 + const supportsUserTiming =
45 + typeof performance !== 'undefined' &&
46 + typeof performance.mark === 'function' &&
47 + typeof performance.clearMarks === 'function' &&
48 + typeof performance.measure === 'function' &&
49 + typeof performance.clearMeasures === 'function';
50 +
51 + // Keep track of current fiber so that we know the path to unwind on pause.
52 + // TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them?
53 + let currentFiber : Fiber | null = null;
54 + // If we're in the middle of user code, which fiber and method is it?
55 + // Reusing `currentFiber` would be confusing for this because user code fiber
56 + // can change during commit phase too, but we don't need to unwind it (since
57 + // lifecycles in the commit phase don't resemble a tree).
58 + let currentPhase : MeasurementPhase | null = null;
59 + let currentPhaseFiber : Fiber | null = null;
60 + // Did lifecycle hook schedule an update? This is often a performance problem,
61 + // so we will keep track of it, and include it in the report.
62 + // Track commits caused by cascading updates.
63 + let isCommitting : boolean = false;
64 + let hasScheduledUpdateInCurrentCommit : boolean = false;
65 + let hasScheduledUpdateInCurrentPhase : boolean = false;
66 + let commitCountInCurrentWorkLoop : number = 0;
67 + let effectCountInCurrentCommit : number = 0;
68 + // During commits, we only show a measurement once per method name
69 + // to avoid stretch the commit phase with measurement overhead.
70 + const labelsInCurrentCommit : Set<string> = new Set();
71 +
72 + const formatMarkName = (markName : string) => {
73 + return `${reactEmoji} ${markName}`;
74 + };
75 +
76 + const formatLabel = (label : string, warning : string | null) => {
77 + const prefix = warning ? `${warningEmoji} ` : `${reactEmoji} `;
78 + const suffix = warning ? ` Warning: ${warning}` : '';
79 + return `${prefix}${label}${suffix}`;
80 + };
81 +
82 + const beginMark = (markName : string) => {
83 + performance.mark(formatMarkName(markName));
84 + };
85 +
86 + const clearMark = (markName : string) => {
87 + performance.clearMarks(formatMarkName(markName));
88 + };
89 +
90 + const endMark = (label : string, markName : string, warning : string | null) => {
91 + const formattedMarkName = formatMarkName(markName);
92 + const formattedLabel = formatLabel(label, warning);
93 + try {
94 + performance.measure(formattedLabel, formattedMarkName);
95 + } catch (err) {
96 + // If previous mark was missing for some reason, this will throw.
97 + // This could only happen if React crashed in an unexpected place earlier.
98 + // Don't pile on with more errors.
99 + }
100 + // Clear marks immediately to avoid growing buffer.
101 + performance.clearMarks(formattedMarkName);
102 + performance.clearMeasures(formattedLabel);
103 + };
104 +
105 + const getFiberMarkName = (label : string, debugID : number) => {
106 + return `${label} (#${debugID})`;
107 + };
108 +
109 + const getFiberLabel = (
110 + componentName : string,
111 + isMounted : boolean,
112 + phase : MeasurementPhase | null,
113 + ) => {
114 + if (phase === null) {
115 + // These are composite component total time measurements.
116 + return `${componentName} [${isMounted ? 'update' : 'mount'}]`;
117 + } else {
118 + // Composite component methods.
119 + return `${componentName}.${phase}`;
120 + }
121 + };
122 +
123 + const beginFiberMark = (
124 + fiber : Fiber,
125 + phase : MeasurementPhase | null,
126 + ) : boolean => {
127 + const componentName = getComponentName(fiber) || 'Unknown';
128 + const debugID = ((fiber._debugID : any) : number);
129 + const isMounted = fiber.alternate !== null;
130 + const label = getFiberLabel(componentName, isMounted, phase);
131 +
132 + if (isCommitting && labelsInCurrentCommit.has(label)) {
133 + // During the commit phase, we don't show duplicate labels because
134 + // there is a fixed overhead for every measurement, and we don't
135 + // want to stretch the commit phase beyond necessary.
136 + return false;
137 + }
138 + labelsInCurrentCommit.add(label);
139 +
140 + const markName = getFiberMarkName(label, debugID);
141 + beginMark(markName);
142 + return true;
143 + };
144 +
145 + const clearFiberMark = (fiber : Fiber, phase : MeasurementPhase | null) => {
146 + const componentName = getComponentName(fiber) || 'Unknown';
147 + const debugID = ((fiber._debugID : any) : number);
148 + const isMounted = fiber.alternate !== null;
149 + const label = getFiberLabel(componentName, isMounted, phase);
150 + const markName = getFiberMarkName(label, debugID);
151 + clearMark(markName);
152 + };
153 +
154 + const endFiberMark = (
155 + fiber : Fiber,
156 + phase : MeasurementPhase | null,
157 + warning : string | null,
158 + ) => {
159 + const componentName = getComponentName(fiber) || 'Unknown';
160 + const debugID = ((fiber._debugID : any) : number);
161 + const isMounted = fiber.alternate !== null;
162 + const label = getFiberLabel(componentName, isMounted, phase);
163 + const markName = getFiberMarkName(label, debugID);
164 + endMark(label, markName, warning);
165 + };
166 +
167 + const shouldIgnoreFiber = (fiber : Fiber) : boolean => {
168 + // Host components should be skipped in the timeline.
169 + // We could check typeof fiber.type, but does this work with RN?
170 + switch (fiber.tag) {
171 + case HostRoot:
172 + case HostComponent:
173 + case HostText:
174 + case HostPortal:
175 + case YieldComponent:
176 + case Fragment:
177 + return true;
178 + default:
179 + return false;
180 + }
181 + };
182 +
183 + const clearPendingPhaseMeasurement = () => {
184 + if (currentPhase !== null && currentPhaseFiber !== null) {
185 + clearFiberMark(currentPhaseFiber, currentPhase);
186 + }
187 + currentPhaseFiber = null;
188 + currentPhase = null;
189 + hasScheduledUpdateInCurrentPhase = false;
190 + };
191 +
192 + const pauseTimers = () => {
193 + // Stops all currently active measurements so that they can be resumed
194 + // if we continue in a later deferred loop from the same unit of work.
195 + let fiber = currentFiber;
196 + while (fiber) {
197 + if (fiber._debugIsCurrentlyTiming) {
198 + endFiberMark(fiber, null, null);
199 + }
200 + fiber = fiber.return;
201 + }
202 + };
203 +
204 + const resumeTimersRecursively = (fiber : Fiber) => {
205 + if (fiber.return !== null) {
206 + resumeTimersRecursively(fiber.return);
207 + }
208 + if (fiber._debugIsCurrentlyTiming) {
209 + beginFiberMark(fiber, null);
210 + }
211 + };
212 +
213 + const resumeTimers = () => {
214 + // Resumes all measurements that were active during the last deferred loop.
215 + if (currentFiber !== null) {
216 + resumeTimersRecursively(currentFiber);
217 + }
218 + };
219 +
220 + ReactDebugFiberPerf = {
221 + recordEffect() : void {
222 + effectCountInCurrentCommit++;
223 + },
224 +
225 + recordScheduleUpdate() : void {
226 + if (isCommitting) {
227 + hasScheduledUpdateInCurrentCommit = true;
228 + }
229 + if (
230 + currentPhase !== null &&
231 + currentPhase !== 'componentWillMount' &&
232 + currentPhase !== 'componentWillReceiveProps'
233 + ) {
234 + hasScheduledUpdateInCurrentPhase = true;
235 + }
236 + },
237 +
238 + startWorkTimer(fiber : Fiber) : void {
239 + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
240 + return;
241 + }
242 + // If we pause, this is the fiber to unwind from.
243 + currentFiber = fiber;
244 + if (!beginFiberMark(fiber, null)) {
245 + return;
246 + }
247 + fiber._debugIsCurrentlyTiming = true;
248 + },
249 +
250 + cancelWorkTimer(fiber : Fiber) : void {
251 + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
252 + return;
253 + }
254 + // Remember we shouldn't complete measurement for this fiber.
255 + // Otherwise flamechart will be deep even for small updates.
256 + fiber._debugIsCurrentlyTiming = false;
257 + clearFiberMark(fiber, null);
258 + },
259 +
260 + stopWorkTimer(fiber : Fiber) : void {
261 + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
262 + return;
263 + }
264 + // If we pause, its parent is the fiber to unwind from.
265 + currentFiber = fiber.return;
266 + if (!fiber._debugIsCurrentlyTiming) {
267 + return;
268 + }
269 + fiber._debugIsCurrentlyTiming = false;
270 + endFiberMark(fiber, null, null);
271 + },
272 +
273 + startPhaseTimer(
274 + fiber : Fiber,
275 + phase : MeasurementPhase,
276 + ) : void {
277 + if (!supportsUserTiming) {
278 + return;
279 + }
280 + clearPendingPhaseMeasurement();
281 + if (!beginFiberMark(fiber, phase)) {
282 + return;
283 + }
284 + currentPhaseFiber = fiber;
285 + currentPhase = phase;
286 + },
287 +
288 + stopPhaseTimer() : void {
289 + if (!supportsUserTiming) {
290 + return;
291 + }
292 + if (currentPhase !== null && currentPhaseFiber !== null) {
293 + const warning = hasScheduledUpdateInCurrentPhase ?
294 + 'Scheduled a cascading update' :
295 + null;
296 + endFiberMark(currentPhaseFiber, currentPhase, warning);
297 + }
298 + currentPhase = null;
299 + currentPhaseFiber = null;
300 + },
301 +
302 + startWorkLoopTimer() : void {
303 + if (!supportsUserTiming) {
304 + return;
305 + }
306 + commitCountInCurrentWorkLoop = 0;
307 + // This is top level call.
308 + // Any other measurements are performed within.
309 + beginMark('(React Tree Reconciliation)');
310 + // Resume any measurements that were in progress during the last loop.
311 + resumeTimers();
312 + },
313 +
314 + stopWorkLoopTimer() : void {
315 + if (!supportsUserTiming) {
316 + return;
317 + }
318 + const warning = commitCountInCurrentWorkLoop > 1 ?
319 + 'There were cascading updates' :
320 + null;
321 + commitCountInCurrentWorkLoop = 0;
322 + // Pause any measurements until the next loop.
323 + pauseTimers();
324 + endMark(
325 + '(React Tree Reconciliation)',
326 + '(React Tree Reconciliation)',
327 + warning,
328 + );
329 + },
330 +
331 + startCommitTimer() : void {
332 + if (!supportsUserTiming) {
333 + return;
334 + }
335 + isCommitting = true;
336 + hasScheduledUpdateInCurrentCommit = false;
337 + labelsInCurrentCommit.clear();
338 + beginMark('(Committing Changes)');
339 + },
340 +
341 + stopCommitTimer() : void {
342 + if (!supportsUserTiming) {
343 + return;
344 + }
345 +
346 + let warning = null;
347 + if (hasScheduledUpdateInCurrentCommit) {
348 + warning = 'Lifecycle hook scheduled a cascading update';
349 + } else if (commitCountInCurrentWorkLoop > 0) {
350 + warning = 'Caused by a cascading update in earlier commit';
351 + }
352 + hasScheduledUpdateInCurrentCommit = false;
353 + commitCountInCurrentWorkLoop++;
354 + isCommitting = false;
355 + labelsInCurrentCommit.clear();
356 +
357 + endMark(
358 + '(Committing Changes)',
359 + '(Committing Changes)',
360 + warning,
361 + );
362 + },
363 +
364 + startCommitHostEffectsTimer() : void {
365 + if (!supportsUserTiming) {
366 + return;
367 + }
368 + effectCountInCurrentCommit = 0;
369 + beginMark('(Committing Host Effects)');
370 + },
371 +
372 + stopCommitHostEffectsTimer() : void {
373 + if (!supportsUserTiming) {
374 + return;
375 + }
376 + const count = effectCountInCurrentCommit;
377 + effectCountInCurrentCommit = 0;
378 + endMark(
379 + `(Committing Host Effects: ${count} Total)`,
380 + '(Committing Host Effects)',
381 + null,
382 + );
383 + },
384 +
385 + startCommitLifeCyclesTimer() : void {
386 + if (!supportsUserTiming) {
387 + return;
388 + }
389 + effectCountInCurrentCommit = 0;
390 + beginMark('(Calling Lifecycle Methods)');
391 + },
392 +
393 + stopCommitLifeCyclesTimer() : void {
394 + if (!supportsUserTiming) {
395 + return;
396 + }
397 + const count = effectCountInCurrentCommit;
398 + effectCountInCurrentCommit = 0;
399 + endMark(
400 + `(Calling Lifecycle Methods: ${count} Total)`,
401 + '(Calling Lifecycle Methods)',
402 + null,
403 + );
404 + },
405 + };
406 +}
407 +
408 +module.exports = ReactDebugFiberPerf;
src/renderers/shared/fiber/ReactFiber.js
+2
@@ -60,6 +60,7 @@ export type Fiber = {
60 _debugID ?: DebugID,
61 _debugSource ?: Source | null,
62 _debugOwner ?: Fiber | ReactInstance | null, // Stack compatible
63 + _debugIsCurrentlyTiming ?: boolean,
64
65 // These first fields are conceptually members of an Instance. This used to
66 // 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 {
224 fiber._debugID = debugCounter++;
225 fiber._debugSource = null;
226 fiber._debugOwner = null;
227 + fiber._debugIsCurrentlyTiming = false;
228 if (typeof Object.preventExtensions === 'function') {
229 Object.preventExtensions(fiber);
230 }
src/renderers/shared/fiber/ReactFiberBeginWork.js
+10
@@ -66,7 +66,9 @@ var invariant = require('fbjs/lib/invariant');
66
67 if (__DEV__) {
68 var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
69 + var {cancelWorkTimer} = require('ReactDebugFiberPerf');
70 var warning = require('fbjs/lib/warning');
71 +
72 var warnedAboutStatelessRefs = {};
73 }
74
@@ -652,6 +654,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
654 */
655
656 function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : Fiber | null {
657 + if (__DEV__) {
658 + cancelWorkTimer(workInProgress);
659 + }
660 +
661 const priorityLevel = workInProgress.pendingWorkPriority;
662 // TODO: We should ideally be able to bail out early if the children have no
663 // more work to do. However, since we don't have a separation of this
@@ -679,6 +685,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
685 }
686
687 function bailoutOnLowPriority(current, workInProgress) {
688 + if (__DEV__) {
689 + cancelWorkTimer(workInProgress);
690 + }
691 +
692 // TODO: Handle HostComponent tags here as well and call pushHostContext()?
693 // See PR 8590 discussion for context
694 switch (workInProgress.tag) {
src/renderers/shared/fiber/ReactFiberClassComponent.js
+34
@@ -40,6 +40,10 @@ var invariant = require('fbjs/lib/invariant');
40 const isArray = Array.isArray;
41
42 if (__DEV__) {
43 + var {
44 + startPhaseTimer,
45 + stopPhaseTimer,
46 + } = require('ReactDebugFiberPerf');
47 var warning = require('fbjs/lib/warning');
48 var warnOnInvalidCallback = function(callback : mixed, callerName : string) {
49 warning(
@@ -102,7 +106,13 @@ module.exports = function(
106
107 const instance = workInProgress.stateNode;
108 if (typeof instance.shouldComponentUpdate === 'function') {
109 + if (__DEV__) {
110 + startPhaseTimer(workInProgress, 'shouldComponentUpdate');
111 + }
112 const shouldUpdate = instance.shouldComponentUpdate(newProps, newState, newContext);
113 + if (__DEV__) {
114 + stopPhaseTimer();
115 + }
116
117 if (__DEV__) {
118 warning(
@@ -278,7 +288,13 @@ module.exports = function(
288 instance.context = getMaskedContext(workInProgress, unmaskedContext);
289
290 if (typeof instance.componentWillMount === 'function') {
291 + if (__DEV__) {
292 + startPhaseTimer(workInProgress, 'componentWillMount');
293 + }
294 instance.componentWillMount();
295 + if (__DEV__) {
296 + stopPhaseTimer();
297 + }
298 // If we had additional state updates during this life-cycle, let's
299 // process them now.
300 const updateQueue = workInProgress.updateQueue;
@@ -347,7 +363,13 @@ module.exports = function(
363 newInstance.context = newContext;
364
365 if (typeof newInstance.componentWillMount === 'function') {
366 + if (__DEV__) {
367 + startPhaseTimer(workInProgress, 'componentWillMount');
368 + }
369 newInstance.componentWillMount();
370 + if (__DEV__) {
371 + stopPhaseTimer();
372 + }
373 }
374 // If we had additional state updates, process them now.
375 // They may be from componentWillMount() or from error boundary's setState()
@@ -396,7 +418,13 @@ module.exports = function(
418
419 if (oldProps !== newProps || oldContext !== newContext) {
420 if (typeof instance.componentWillReceiveProps === 'function') {
421 + if (__DEV__) {
422 + startPhaseTimer(workInProgress, 'componentWillReceiveProps');
423 + }
424 instance.componentWillReceiveProps(newProps, newContext);
425 + if (__DEV__) {
426 + stopPhaseTimer();
427 + }
428
429 if (instance.state !== workInProgress.memoizedState) {
430 if (__DEV__) {
@@ -457,7 +485,13 @@ module.exports = function(
485
486 if (shouldUpdate) {
487 if (typeof instance.componentWillUpdate === 'function') {
488 + if (__DEV__) {
489 + startPhaseTimer(workInProgress, 'componentWillUpdate');
490 + }
491 instance.componentWillUpdate(newProps, newState, newContext);
492 + if (__DEV__) {
493 + stopPhaseTimer();
494 + }
495 }
496 if (typeof instance.componentDidUpdate === 'function') {
497 workInProgress.effectTag |= Update;
src/renderers/shared/fiber/ReactFiberCommitWork.js
+34 -1
@@ -37,6 +37,13 @@ var {
37
38 var invariant = require('fbjs/lib/invariant');
39
40 +if (__DEV__) {
41 + var {
42 + startPhaseTimer,
43 + stopPhaseTimer,
44 + } = require('ReactDebugFiberPerf');
45 +}
46 +
47 module.exports = function<T, P, I, TI, PI, C, CX, PL>(
48 config : HostConfig<T, P, I, TI, PI, C, CX, PL>,
49 captureError : (failedFiber : Fiber, error: Error) => Fiber | null
@@ -53,10 +60,24 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
60 getPublicInstance,
61 } = config;
62
63 + if (__DEV__) {
64 + var callComponentWillUnmountWithTimerInDev = function(current, instance) {
65 + startPhaseTimer(current, 'componentWillUnmount');
66 + instance.componentWillUnmount();
67 + stopPhaseTimer();
68 + };
69 + }
70 +
71 // Capture errors so they don't interrupt unmounting.
72 function safelyCallComponentWillUnmount(current, instance) {
73 if (__DEV__) {
59 - const unmountError = invokeGuardedCallback(null, instance.componentWillUnmount, instance);
74 + const unmountError = invokeGuardedCallback(
75 + null,
76 + callComponentWillUnmountWithTimerInDev,
77 + null,
78 + current,
79 + instance,
80 + );
81 if (unmountError) {
82 captureError(current, unmountError);
83 }
@@ -423,11 +444,23 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
444 const instance = finishedWork.stateNode;
445 if (finishedWork.effectTag & Update) {
446 if (current === null) {
447 + if (__DEV__) {
448 + startPhaseTimer(finishedWork, 'componentDidMount');
449 + }
450 instance.componentDidMount();
451 + if (__DEV__) {
452 + stopPhaseTimer();
453 + }
454 } else {
455 const prevProps = current.memoizedProps;
456 const prevState = current.memoizedState;
457 + if (__DEV__) {
458 + startPhaseTimer(finishedWork, 'componentDidUpdate');
459 + }
460 instance.componentDidUpdate(prevProps, prevState);
461 + if (__DEV__) {
462 + stopPhaseTimer();
463 + }
464 }
465 }
466 if ((finishedWork.effectTag & Callback) && finishedWork.updateQueue !== null) {
src/renderers/shared/fiber/ReactFiberContext.js
+6
@@ -36,6 +36,10 @@ if (__DEV__) {
36 var checkReactTypeSpec = require('checkReactTypeSpec');
37 var ReactDebugCurrentFrame = require('react/lib/ReactDebugCurrentFrame');
38 var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
39 + var {
40 + startPhaseTimer,
41 + stopPhaseTimer,
42 + } = require('ReactDebugFiberPerf');
43 var warnedAboutMissingGetChildContext = {};
44 }
45
@@ -172,7 +176,9 @@ function processChildContext(fiber : Fiber, parentContext : Object, isReconcilin
176 let childContext;
177 if (__DEV__) {
178 ReactDebugCurrentFiber.phase = 'getChildContext';
179 + startPhaseTimer(fiber, 'getChildContext');
180 childContext = instance.getChildContext();
181 + stopPhaseTimer();
182 ReactDebugCurrentFiber.phase = null;
183 } else {
184 childContext = instance.getChildContext();
src/renderers/shared/fiber/ReactFiberScheduler.js
+66
@@ -95,6 +95,20 @@ if (__DEV__) {
95 var warning = require('fbjs/lib/warning');
96 var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
97 var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
98 + var {
99 + recordEffect,
100 + recordScheduleUpdate,
101 + startWorkTimer,
102 + stopWorkTimer,
103 + startWorkLoopTimer,
104 + stopWorkLoopTimer,
105 + startCommitTimer,
106 + stopCommitTimer,
107 + startCommitHostEffectsTimer,
108 + stopCommitHostEffectsTimer,
109 + startCommitLifeCyclesTimer,
110 + stopCommitLifeCyclesTimer,
111 + } = require('ReactDebugFiberPerf');
112
113 var warnAboutUpdateOnUnmounted = function(instance : ReactClass<any>) {
114 const ctor = instance.constructor;
@@ -293,6 +307,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
307 while (nextEffect !== null) {
308 if (__DEV__) {
309 ReactDebugCurrentFiber.current = nextEffect;
310 + recordEffect();
311 }
312
313 const effectTag = nextEffect.effectTag;
@@ -361,15 +376,24 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
376
377 // Use Task priority for lifecycle updates
378 if (effectTag & (Update | Callback)) {
379 + if (__DEV__) {
380 + recordEffect();
381 + }
382 const current = nextEffect.alternate;
383 commitLifeCycles(current, nextEffect);
384 }
385
386 if (effectTag & Ref) {
387 + if (__DEV__) {
388 + recordEffect();
389 + }
390 commitAttachRef(nextEffect);
391 }
392
393 if (effectTag & Err) {
394 + if (__DEV__) {
395 + recordEffect();
396 + }
397 commitErrorHandling(nextEffect);
398 }
399
@@ -391,6 +415,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
415 // local to this function is because errors that occur during cWU are
416 // captured elsewhere, to prevent the unmount from being interrupted.
417 isCommitting = true;
418 + if (__DEV__) {
419 + startCommitTimer();
420 + }
421
422 pendingCommit = null;
423 const root : FiberRoot = (finishedWork.stateNode : any);
@@ -431,6 +458,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
458 // The first pass performs all the host insertions, updates, deletions and
459 // ref unmounts.
460 nextEffect = firstEffect;
461 + if (__DEV__) {
462 + startCommitHostEffectsTimer();
463 + }
464 while (nextEffect !== null) {
465 let error = null;
466 if (__DEV__) {
@@ -455,6 +485,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
485 }
486 }
487 }
488 + if (__DEV__) {
489 + stopCommitHostEffectsTimer();
490 + }
491
492 resetAfterCommit(commitInfo);
493
@@ -469,6 +502,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
502 // and deletions in the entire tree have already been invoked.
503 // This pass also triggers any renderer-specific initial effects.
504 nextEffect = firstEffect;
505 + if (__DEV__) {
506 + startCommitLifeCyclesTimer();
507 + }
508 while (nextEffect !== null) {
509 let error = null;
510 if (__DEV__) {
@@ -494,6 +530,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
530 }
531
532 isCommitting = false;
533 + if (__DEV__) {
534 + stopCommitLifeCyclesTimer();
535 + stopCommitTimer();
536 + }
537 if (typeof onCommitRoot === 'function') {
538 onCommitRoot(finishedWork.stateNode);
539 }
@@ -554,6 +594,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
594 resetWorkPriority(workInProgress);
595
596 if (next !== null) {
597 + if (__DEV__) {
598 + stopWorkTimer(workInProgress);
599 + }
600 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
601 ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
602 }
@@ -592,6 +635,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
635 }
636 }
637
638 + if (__DEV__) {
639 + stopWorkTimer(workInProgress);
640 + }
641 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
642 ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
643 }
@@ -627,6 +673,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
673 const current = workInProgress.alternate;
674
675 // See if beginning this work spawns more work.
676 + if (__DEV__) {
677 + startWorkTimer(workInProgress);
678 + }
679 let next = beginWork(current, workInProgress, nextPriorityLevel);
680 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
681 ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress);
@@ -654,6 +703,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
703 const current = workInProgress.alternate;
704
705 // See if beginning this work spawns more work.
706 + if (__DEV__) {
707 + startWorkTimer(workInProgress);
708 + }
709 let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
710 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
711 ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress);
@@ -780,6 +832,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
832 }
833
834 function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
835 + if (__DEV__) {
836 + startWorkLoopTimer();
837 + }
838 +
839 invariant(
840 !isPerformingWork,
841 'performWork was called recursively. This error is likely caused ' +
@@ -903,6 +959,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
959 firstUncaughtError = null;
960 capturedErrors = null;
961 failedBoundaries = null;
962 + if (__DEV__) {
963 + stopWorkLoopTimer();
964 + }
965
966 // It's safe to throw any unhandled errors.
967 if (errorToThrow !== null) {
@@ -1134,6 +1193,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
1193 popHostContainer(node);
1194 break;
1195 }
1196 + if (__DEV__) {
1197 + stopWorkTimer(node);
1198 + }
1199 node = node.return;
1200 }
1201 }
@@ -1158,6 +1220,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
1220 }
1221
1222 function scheduleUpdate(fiber : Fiber, priorityLevel : PriorityLevel) {
1223 + if (__DEV__) {
1224 + recordScheduleUpdate();
1225 + }
1226 +
1227 if (priorityLevel <= nextPriorityLevel) {
1228 // We must reset the current unit of work pointer so that we restart the
1229 // search from the root during the next tick, in case there is now higher
src/renderers/shared/fiber/__tests__/ReactIncrementalPerf-test.js new
+487
@@ -0,0 +1,487 @@
1 +/**
2 + * Copyright 2016-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +describe('ReactDebugFiberPerf', () => {
15 + let React;
16 + let ReactCoroutine;
17 + let ReactFeatureFlags;
18 + let ReactNoop;
19 + let ReactPortal;
20 +
21 + let root;
22 + let activeMeasure;
23 + let knownMarks;
24 + let knownMeasures;
25 +
26 + function resetFlamechart() {
27 + root = {
28 + children: [],
29 + indent: -1,
30 + markName: null,
31 + label: null,
32 + parent: null,
33 + toString() {
34 + return this.children.map(c => c.toString()).join('\n');
35 + },
36 + };
37 + activeMeasure = root;
38 + knownMarks = new Set();
39 + knownMeasures = new Set();
40 + }
41 +
42 + function addComment(comment) {
43 + activeMeasure.children.push(
44 + `${' '.repeat(activeMeasure.indent + 1)}// ${comment}`
45 + );
46 + }
47 +
48 + function getFlameChart() {
49 + // Make sure we unwind the measurement stack every time.
50 + expect(activeMeasure.indent).toBe(-1);
51 + expect(activeMeasure).toBe(root);
52 + // We should always clean them up because browsers
53 + // buffer user timing measurements forever.
54 + expect(knownMarks.size).toBe(0);
55 + expect(knownMeasures.size).toBe(0);
56 + return root.toString();
57 + }
58 +
59 + function createUserTimingPolyfill() {
60 + // This is not a true polyfill, but it gives us enough
61 + // to capture measurements in a readable tree-like output.
62 + // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API
63 + return {
64 + mark(markName) {
65 + const measure = {
66 + children: [],
67 + indent: activeMeasure.indent + 1,
68 + markName: markName,
69 + // Will be assigned on measure() call:
70 + label: null,
71 + parent: activeMeasure,
72 + toString() {
73 + return [
74 + ' '.repeat(this.indent) + this.label,
75 + ...this.children.map(c => c.toString()),
76 + ].join('\n') + (
77 + // Extra newline after each root reconciliation
78 + this.indent === 0 ? '\n' : ''
79 + );
80 + },
81 + };
82 + // Step one level deeper
83 + activeMeasure.children.push(measure);
84 + activeMeasure = measure;
85 + knownMarks.add(markName);
86 + },
87 + // We don't use the overload with three arguments.
88 + measure(label, markName) {
89 + if (markName !== activeMeasure.markName) {
90 + throw new Error('Unexpected measure() call.');
91 + }
92 + // Step one level up
93 + activeMeasure.label = label;
94 + activeMeasure = activeMeasure.parent;
95 + knownMeasures.add(label);
96 + },
97 + clearMarks(markName) {
98 + if (markName === activeMeasure.markName) {
99 + // Step one level up if we're in this measure
100 + activeMeasure = activeMeasure.parent;
101 + activeMeasure.children.length--;
102 + }
103 + knownMarks.delete(markName);
104 + },
105 + clearMeasures(label) {
106 + knownMeasures.delete(label);
107 + },
108 + };
109 + }
110 +
111 + beforeEach(() => {
112 + jest.resetModules();
113 + resetFlamechart();
114 + global.performance = createUserTimingPolyfill();
115 +
116 + // Import after the polyfill is set up:
117 + React = require('React');
118 + ReactCoroutine = require('ReactCoroutine');
119 + ReactFeatureFlags = require('ReactFeatureFlags');
120 + ReactNoop = require('ReactNoop');
121 + ReactPortal = require('ReactPortal');
122 + ReactFeatureFlags.disableNewFiberFeatures = false;
123 + });
124 +
125 + afterEach(() => {
126 + delete global.performance;
127 + });
128 +
129 + function Parent(props) {
130 + return <div>{props.children}</div>;
131 + }
132 +
133 + function Child(props) {
134 + return <div>{props.children}</div>;
135 + }
136 +
137 + it('measures a simple reconciliation', () => {
138 + ReactNoop.render(<Parent><Child /></Parent>);
139 + addComment('Mount');
140 + ReactNoop.flush();
141 +
142 + ReactNoop.render(<Parent><Child /></Parent>);
143 + addComment('Update');
144 + ReactNoop.flush();
145 +
146 + ReactNoop.render(null);
147 + addComment('Unmount');
148 + ReactNoop.flush();
149 +
150 + expect(getFlameChart()).toMatchSnapshot();
151 + });
152 +
153 + it('skips parents during setState', () => {
154 + class A extends React.Component {
155 + render() {
156 + return <div>{this.props.children}</div>;
157 + }
158 + }
159 +
160 + class B extends React.Component {
161 + render() {
162 + return <div>{this.props.children}</div>;
163 + }
164 + }
165 +
166 + let a;
167 + let b;
168 + ReactNoop.render(
169 + <Parent>
170 + <Parent>
171 + <Parent>
172 + <A ref={inst => a = inst} />
173 + </Parent>
174 + </Parent>
175 + <Parent>
176 + <B ref={inst => b = inst} />
177 + </Parent>
178 + </Parent>
179 + );
180 + ReactNoop.flush();
181 + resetFlamechart();
182 +
183 + a.setState({});
184 + b.setState({});
185 + addComment('Should include just A and B, no Parents');
186 + ReactNoop.flush();
187 + expect(getFlameChart()).toMatchSnapshot();
188 + });
189 +
190 + it('warns on cascading renders from setState', () => {
191 + class Cascading extends React.Component {
192 + componentDidMount() {
193 + this.setState({});
194 + }
195 + render() {
196 + return <div>{this.props.children}</div>;
197 + }
198 + }
199 +
200 + ReactNoop.render(<Parent><Cascading /></Parent>);
201 + addComment('Should print a warning');
202 + ReactNoop.flush();
203 + expect(getFlameChart()).toMatchSnapshot();
204 + });
205 +
206 + it('warns on cascading renders from top-level render', () => {
207 + class Cascading extends React.Component {
208 + componentDidMount() {
209 + ReactNoop.renderToRootWithID(<Child />, 'b');
210 + addComment('Scheduling another root from componentDidMount');
211 + ReactNoop.flush();
212 + }
213 + render() {
214 + return <div>{this.props.children}</div>;
215 + }
216 + }
217 +
218 + ReactNoop.renderToRootWithID(<Cascading />, 'a');
219 + addComment('Rendering the first root');
220 + ReactNoop.flush();
221 + expect(getFlameChart()).toMatchSnapshot();
222 + });
223 +
224 + it('does not treat setState from cWM or cWRP as cascading', () => {
225 + class NotCascading extends React.Component {
226 + componentWillMount() {
227 + this.setState({});
228 + }
229 + componentWillReceiveProps() {
230 + this.setState({});
231 + }
232 + render() {
233 + return <div>{this.props.children}</div>;
234 + }
235 + }
236 +
237 + ReactNoop.render(<Parent><NotCascading /></Parent>);
238 + addComment('Should not print a warning');
239 + ReactNoop.flush();
240 + ReactNoop.render(<Parent><NotCascading /></Parent>);
241 + addComment('Should not print a warning');
242 + ReactNoop.flush();
243 + expect(getFlameChart()).toMatchSnapshot();
244 + });
245 +
246 + it('captures all lifecycles', () => {
247 + class AllLifecycles extends React.Component {
248 + static childContextTypes = {
249 + foo: React.PropTypes.any,
250 + };
251 + shouldComponentUpdate() {
252 + return true;
253 + }
254 + getChildContext() {
255 + return {foo: 42};
256 + }
257 + componentWillMount() {}
258 + componentDidMount() {}
259 + componentWillReceiveProps() {}
260 + componentWillUpdate() {}
261 + componentDidUpdate() {}
262 + componentWillUnmount() {}
263 + render() {
264 + return <div />;
265 + }
266 + }
267 + ReactNoop.render(<AllLifecycles />);
268 + addComment('Mount');
269 + ReactNoop.flush();
270 + ReactNoop.render(<AllLifecycles />);
271 + addComment('Update');
272 + ReactNoop.flush();
273 + ReactNoop.render(null);
274 + addComment('Unmount');
275 + ReactNoop.flush();
276 + expect(getFlameChart()).toMatchSnapshot();
277 + });
278 +
279 + it('measures deprioritized work', () => {
280 + ReactNoop.performAnimationWork(() => {
281 + ReactNoop.render(
282 + <Parent>
283 + <div hidden={true}>
284 + <Child />
285 + </div>
286 + </Parent>
287 + );
288 + });
289 + addComment('Flush the parent');
290 + ReactNoop.flushAnimationPri();
291 + addComment('Flush the child');
292 + ReactNoop.flushDeferredPri();
293 + expect(getFlameChart()).toMatchSnapshot();
294 + });
295 +
296 + it('measures deferred work in chunks', () => {
297 + class A extends React.Component {
298 + render() {
299 + return <div>{this.props.children}</div>;
300 + }
301 + }
302 +
303 + class B extends React.Component {
304 + render() {
305 + return <div>{this.props.children}</div>;
306 + }
307 + }
308 +
309 + ReactNoop.render(
310 + <Parent>
311 + <A>
312 + <Child />
313 + </A>
314 + <B>
315 + <Child />
316 + </B>
317 + </Parent>
318 + );
319 + addComment('Start mounting Parent and A');
320 + ReactNoop.flushDeferredPri(40);
321 + addComment('Mount B just a little (but not enough to memoize)');
322 + ReactNoop.flushDeferredPri(10);
323 + addComment('Complete B and Parent');
324 + ReactNoop.flushDeferredPri();
325 + expect(getFlameChart()).toMatchSnapshot();
326 + });
327 +
328 + it('recovers from fatal errors', () => {
329 + function Baddie() {
330 + throw new Error('Game over');
331 + }
332 +
333 + ReactNoop.render(<Parent><Baddie /></Parent>);
334 + try {
335 + addComment('Will fatal');
336 + ReactNoop.flush();
337 + } catch (err) {
338 + expect(err.message).toBe('Game over');
339 + }
340 + ReactNoop.render(<Parent><Child /></Parent>);
341 + addComment('Will reconcile from a clean state');
342 + ReactNoop.flush();
343 + expect(getFlameChart()).toMatchSnapshot();
344 + });
345 +
346 + it('recovers from caught errors', () => {
347 + function Baddie() {
348 + throw new Error('Game over');
349 + }
350 +
351 + function ErrorReport() {
352 + return <div />;
353 + }
354 +
355 + class Boundary extends React.Component {
356 + state = {error: null};
357 + unstable_handleError(error) {
358 + this.setState({error});
359 + }
360 + render() {
361 + if (this.state.error) {
362 + return <ErrorReport />;
363 + }
364 + return this.props.children;
365 + }
366 + }
367 +
368 + ReactNoop.render(
369 + <Parent>
370 + <Boundary>
371 + <Parent>
372 + <Baddie />
373 + </Parent>
374 + </Boundary>
375 + </Parent>
376 + );
377 + addComment('Stop on Baddie and restart from Boundary');
378 + ReactNoop.flush();
379 + expect(getFlameChart()).toMatchSnapshot();
380 + });
381 +
382 + it('deduplicates lifecycle names during commit to reduce overhead', () => {
383 + class A extends React.Component {
384 + componentDidUpdate() {}
385 + render() {
386 + return <div />;
387 + }
388 + }
389 +
390 + class B extends React.Component {
391 + componentDidUpdate(prevProps) {
392 + if (this.props.cascade && !prevProps.cascade) {
393 + this.setState({});
394 + }
395 + }
396 + render() {
397 + return <div />;
398 + }
399 + }
400 +
401 + ReactNoop.render(
402 + <Parent>
403 + <A />
404 + <B />
405 + <A />
406 + <B />
407 + </Parent>
408 + );
409 + ReactNoop.flush();
410 + resetFlamechart();
411 +
412 + ReactNoop.render(
413 + <Parent>
414 + <A />
415 + <B />
416 + <A />
417 + <B />
418 + </Parent>
419 + );
420 + addComment('The commit phase should mention A and B just once');
421 + ReactNoop.flush();
422 + ReactNoop.render(
423 + <Parent>
424 + <A />
425 + <B />
426 + <A />
427 + <B cascade={true} />
428 + </Parent>
429 + );
430 + addComment('Because of deduplication, we don\'t know B was cascading,');
431 + addComment('but we should still see the warning for the commit phase.');
432 + ReactNoop.flush();
433 + expect(getFlameChart()).toMatchSnapshot();
434 + });
435 +
436 + it('supports coroutines', () => {
437 + function Continuation({ isSame }) {
438 + return <span prop={isSame ? 'foo==bar' : 'foo!=bar'} />;
439 + }
440 +
441 + function CoChild({ bar }) {
442 + return ReactCoroutine.createYield({
443 + props: {
444 + bar: bar,
445 + },
446 + continuation: Continuation,
447 + });
448 + }
449 +
450 + function Indirection() {
451 + return [<CoChild bar={true} />, <CoChild bar={false} />];
452 + }
453 +
454 + function HandleYields(props, yields) {
455 + return yields.map(y =>
456 + <y.continuation isSame={props.foo === y.props.bar} />
457 + );
458 + }
459 +
460 + function CoParent(props) {
461 + return ReactCoroutine.createCoroutine(
462 + props.children,
463 + HandleYields,
464 + props
465 + );
466 + }
467 +
468 + function App() {
469 + return <div><CoParent foo={true}><Indirection /></CoParent></div>;
470 + }
471 +
472 + ReactNoop.render(<App />);
473 + ReactNoop.flush();
474 + expect(getFlameChart()).toMatchSnapshot();
475 + });
476 +
477 + it('supports portals', () => {
478 + const noopContainer = {children: []};
479 + ReactNoop.render(
480 + <Parent>
481 + {ReactPortal.createPortal(<Child />, noopContainer, null)}
482 + </Parent>
483 + );
484 + ReactNoop.flush();
485 + expect(getFlameChart()).toMatchSnapshot();
486 + });
487 +});
src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap new
+260
@@ -0,0 +1,260 @@
1 +// Jest Snapshot v1, https://goo.gl/fbAQLP
2 +
3 +exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
4 +"// Mount
5 +⚛ (React Tree Reconciliation)
6 + ⚛ AllLifecycles [mount]
7 + ⚛ AllLifecycles.componentWillMount
8 + ⚛ AllLifecycles.getChildContext
9 + ⚛ (Committing Changes)
10 + ⚛ (Committing Host Effects: 1 Total)
11 + ⚛ (Calling Lifecycle Methods: 1 Total)
12 + ⚛ AllLifecycles.componentDidMount
13 +
14 +// Update
15 +⚛ (React Tree Reconciliation)
16 + ⚛ AllLifecycles [update]
17 + ⚛ AllLifecycles.componentWillReceiveProps
18 + ⚛ AllLifecycles.shouldComponentUpdate
19 + ⚛ AllLifecycles.componentWillUpdate
20 + ⚛ AllLifecycles.getChildContext
21 + ⚛ (Committing Changes)
22 + ⚛ (Committing Host Effects: 2 Total)
23 + ⚛ (Calling Lifecycle Methods: 2 Total)
24 + ⚛ AllLifecycles.componentDidUpdate
25 +
26 +// Unmount
27 +⚛ (React Tree Reconciliation)
28 + ⚛ (Committing Changes)
29 + ⚛ (Committing Host Effects: 1 Total)
30 + ⚛ AllLifecycles.componentWillUnmount
31 + ⚛ (Calling Lifecycle Methods: 0 Total)
32 +"
33 +`;
34 +
35 +exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = `
36 +"// The commit phase should mention A and B just once
37 +⚛ (React Tree Reconciliation)
38 + ⚛ Parent [update]
39 + ⚛ A [update]
40 + ⚛ B [update]
41 + ⚛ A [update]
42 + ⚛ B [update]
43 + ⚛ (Committing Changes)
44 + ⚛ (Committing Host Effects: 9 Total)
45 + ⚛ (Calling Lifecycle Methods: 9 Total)
46 + ⚛ A.componentDidUpdate
47 + ⚛ B.componentDidUpdate
48 +
49 +// Because of deduplication, we don't know B was cascading,
50 +// but we should still see the warning for the commit phase.
51 +🛑 (React Tree Reconciliation) Warning: There were cascading updates
52 + ⚛ Parent [update]
53 + ⚛ A [update]
54 + ⚛ B [update]
55 + ⚛ A [update]
56 + ⚛ B [update]
57 + 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
58 + ⚛ (Committing Host Effects: 9 Total)
59 + ⚛ (Calling Lifecycle Methods: 9 Total)
60 + ⚛ A.componentDidUpdate
61 + ⚛ B.componentDidUpdate
62 + ⚛ B [update]
63 + 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
64 + ⚛ (Committing Host Effects: 3 Total)
65 + ⚛ (Calling Lifecycle Methods: 3 Total)
66 + ⚛ B.componentDidUpdate
67 +"
68 +`;
69 +
70 +exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = `
71 +"// Should not print a warning
72 +⚛ (React Tree Reconciliation)
73 + ⚛ Parent [mount]
74 + ⚛ NotCascading [mount]
75 + ⚛ NotCascading.componentWillMount
76 + ⚛ (Committing Changes)
77 + ⚛ (Committing Host Effects: 1 Total)
78 + ⚛ (Calling Lifecycle Methods: 0 Total)
79 +
80 +// Should not print a warning
81 +⚛ (React Tree Reconciliation)
82 + ⚛ Parent [update]
83 + ⚛ NotCascading [update]
84 + ⚛ NotCascading.componentWillReceiveProps
85 + ⚛ (Committing Changes)
86 + ⚛ (Committing Host Effects: 2 Total)
87 + ⚛ (Calling Lifecycle Methods: 2 Total)
88 +"
89 +`;
90 +
91 +exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
92 +"// Mount
93 +⚛ (React Tree Reconciliation)
94 + ⚛ Parent [mount]
95 + ⚛ Child [mount]
96 + ⚛ (Committing Changes)
97 + ⚛ (Committing Host Effects: 1 Total)
98 + ⚛ (Calling Lifecycle Methods: 0 Total)
99 +
100 +// Update
101 +⚛ (React Tree Reconciliation)
102 + ⚛ Parent [update]
103 + ⚛ Child [update]
104 + ⚛ (Committing Changes)
105 + ⚛ (Committing Host Effects: 2 Total)
106 + ⚛ (Calling Lifecycle Methods: 2 Total)
107 +
108 +// Unmount
109 +⚛ (React Tree Reconciliation)
110 + ⚛ (Committing Changes)
111 + ⚛ (Committing Host Effects: 1 Total)
112 + ⚛ (Calling Lifecycle Methods: 0 Total)
113 +"
114 +`;
115 +
116 +exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
117 +"// Start mounting Parent and A
118 +⚛ (React Tree Reconciliation)
119 + ⚛ Parent [mount]
120 + ⚛ A [mount]
121 + ⚛ Child [mount]
122 +
123 +// Mount B just a little (but not enough to memoize)
124 +⚛ (React Tree Reconciliation)
125 + ⚛ Parent [mount]
126 + ⚛ B [mount]
127 +
128 +// Complete B and Parent
129 +⚛ (React Tree Reconciliation)
130 + ⚛ Parent [mount]
131 + ⚛ B [mount]
132 + ⚛ Child [mount]
133 + ⚛ (Committing Changes)
134 + ⚛ (Committing Host Effects: 1 Total)
135 + ⚛ (Calling Lifecycle Methods: 0 Total)
136 +"
137 +`;
138 +
139 +exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
140 +"// Flush the parent
141 +⚛ (React Tree Reconciliation)
142 + ⚛ Parent [mount]
143 + ⚛ (Committing Changes)
144 + ⚛ (Committing Host Effects: 1 Total)
145 + ⚛ (Calling Lifecycle Methods: 0 Total)
146 +
147 +// Flush the child
148 +⚛ (React Tree Reconciliation)
149 + ⚛ Child [mount]
150 + ⚛ (Committing Changes)
151 + ⚛ (Committing Host Effects: 3 Total)
152 + ⚛ (Calling Lifecycle Methods: 2 Total)
153 +"
154 +`;
155 +
156 +exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
157 +"// Stop on Baddie and restart from Boundary
158 +🛑 (React Tree Reconciliation) Warning: There were cascading updates
159 + ⚛ Parent [mount]
160 + ⚛ Boundary [mount]
161 + ⚛ Parent [mount]
162 + ⚛ Baddie [mount]
163 + 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
164 + ⚛ (Committing Host Effects: 2 Total)
165 + ⚛ (Calling Lifecycle Methods: 1 Total)
166 + ⚛ Boundary [update]
167 + ⚛ ErrorReport [mount]
168 + 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
169 + ⚛ (Committing Host Effects: 2 Total)
170 + ⚛ (Calling Lifecycle Methods: 1 Total)
171 +"
172 +`;
173 +
174 +exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
175 +"// Will fatal
176 +⚛ (React Tree Reconciliation)
177 + ⚛ Parent [mount]
178 + ⚛ Baddie [mount]
179 + ⚛ (Committing Changes)
180 + ⚛ (Committing Host Effects: 1 Total)
181 + ⚛ (Calling Lifecycle Methods: 1 Total)
182 +
183 +// Will reconcile from a clean state
184 +⚛ (React Tree Reconciliation)
185 + ⚛ Parent [mount]
186 + ⚛ Child [mount]
187 + ⚛ (Committing Changes)
188 + ⚛ (Committing Host Effects: 1 Total)
189 + ⚛ (Calling Lifecycle Methods: 0 Total)
190 +"
191 +`;
192 +
193 +exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
194 +"// Should include just A and B, no Parents
195 +⚛ (React Tree Reconciliation)
196 + ⚛ A [update]
197 + ⚛ B [update]
198 + ⚛ (Committing Changes)
199 + ⚛ (Committing Host Effects: 6 Total)
200 + ⚛ (Calling Lifecycle Methods: 6 Total)
201 +"
202 +`;
203 +
204 +exports[`ReactDebugFiberPerf supports coroutines 1`] = `
205 +"⚛ (React Tree Reconciliation)
206 + ⚛ App [mount]
207 + ⚛ CoParent [mount]
208 + ⚛ HandleYields [mount]
209 + ⚛ Indirection [mount]
210 + ⚛ CoChild [mount]
211 + ⚛ CoChild [mount]
212 + ⚛ Continuation [mount]
213 + ⚛ Continuation [mount]
214 + ⚛ (Committing Changes)
215 + ⚛ (Committing Host Effects: 3 Total)
216 + ⚛ (Calling Lifecycle Methods: 0 Total)
217 +"
218 +`;
219 +
220 +exports[`ReactDebugFiberPerf supports portals 1`] = `
221 +"⚛ (React Tree Reconciliation)
222 + ⚛ Parent [mount]
223 + ⚛ Child [mount]
224 + ⚛ (Committing Changes)
225 + ⚛ (Committing Host Effects: 3 Total)
226 + ⚛ (Calling Lifecycle Methods: 1 Total)
227 +"
228 +`;
229 +
230 +exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
231 +"// Should print a warning
232 +🛑 (React Tree Reconciliation) Warning: There were cascading updates
233 + ⚛ Parent [mount]
234 + ⚛ Cascading [mount]
235 + 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
236 + ⚛ (Committing Host Effects: 2 Total)
237 + ⚛ (Calling Lifecycle Methods: 1 Total)
238 + 🛑 Cascading.componentDidMount Warning: Scheduled a cascading update
239 + ⚛ Cascading [update]
240 + 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
241 + ⚛ (Committing Host Effects: 2 Total)
242 + ⚛ (Calling Lifecycle Methods: 2 Total)
243 +"
244 +`;
245 +
246 +exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = `
247 +"// Rendering the first root
248 +🛑 (React Tree Reconciliation) Warning: There were cascading updates
249 + ⚛ Cascading [mount]
250 + 🛑 (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
251 + ⚛ (Committing Host Effects: 1 Total)
252 + ⚛ (Calling Lifecycle Methods: 1 Total)
253 + 🛑 Cascading.componentDidMount Warning: Scheduled a cascading update
254 + // Scheduling another root from componentDidMount
255 + ⚛ Child [mount]
256 + 🛑 (Committing Changes) Warning: Caused by a cascading update in earlier commit
257 + ⚛ (Committing Host Effects: 1 Total)
258 + ⚛ (Calling Lifecycle Methods: 0 Total)
259 +"
260 +`;