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) +" +`;