From 67b1dbf75f3960c5aabf905876eddd5b1260c541 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Apr 2016 19:56:35 +0100 Subject: [PATCH] Add new ReactPerf --- grunt/tasks/npm-react-addons.js | 2 +- src/addons/ReactWithAddons.js | 2 +- src/isomorphic/ReactDebugTool.js | 156 +++++++- src/isomorphic/ReactPerfAnalysis.js | 361 ++++++++++++++++++ .../ReactComponentTreeDevtool-test.js | 8 - ...ReactNativeOperationHistoryDevtool-test.js | 8 - src/renderers/dom/client/ReactMount.js | 5 + src/renderers/dom/shared/ReactDOMComponent.js | 8 +- .../dom/shared/ReactDOMTextComponent.js | 5 +- .../reconciler/ReactCompositeComponent.js | 202 +++++++++- .../shared/reconciler/ReactReconciler.js | 65 +++- .../shared/reconciler/ReactUpdates.js | 9 + 12 files changed, 792 insertions(+), 39 deletions(-) create mode 100644 src/isomorphic/ReactPerfAnalysis.js diff --git a/grunt/tasks/npm-react-addons.js b/grunt/tasks/npm-react-addons.js index 948acebf1c653..1cb2cf1c30436 100644 --- a/grunt/tasks/npm-react-addons.js +++ b/grunt/tasks/npm-react-addons.js @@ -16,7 +16,7 @@ var addons = { docs: 'two-way-binding-helpers', }, Perf: { - module: 'ReactDefaultPerf', + module: 'ReactPerfAnalysis', name: 'perf', docs: 'perf', }, diff --git a/src/addons/ReactWithAddons.js b/src/addons/ReactWithAddons.js index bd13ea495f990..89e4e84482434 100644 --- a/src/addons/ReactWithAddons.js +++ b/src/addons/ReactWithAddons.js @@ -34,7 +34,7 @@ React.addons = { }; if (__DEV__) { - React.addons.Perf = require('ReactDefaultPerf'); + React.addons.Perf = require('ReactPerfAnalysis'); React.addons.TestUtils = require('ReactTestUtils'); } diff --git a/src/isomorphic/ReactDebugTool.js b/src/isomorphic/ReactDebugTool.js index 9f86052ea5ca5..5d166bca7f04b 100644 --- a/src/isomorphic/ReactDebugTool.js +++ b/src/isomorphic/ReactDebugTool.js @@ -11,7 +11,9 @@ 'use strict'; -var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool'); +var ExecutionEnvironment = require('ExecutionEnvironment'); + +var performanceNow = require('performanceNow'); var warning = require('warning'); var eventHandlers = []; @@ -37,6 +39,56 @@ function emitEvent(handlerFunctionName, arg1, arg2, arg3, arg4, arg5) { } } +var isProfiling = false; +var flushHistory = []; +var currentFlushNesting = 0; +var currentFlushMeasurements = null; +var currentFlushStartTime = null; +var currentTimerDebugID = null; +var currentTimerStartTime = null; +var currentTimerType = null; + +function resetMeasurements() { + if (__DEV__) { + if (!isProfiling || currentFlushNesting === 0) { + currentFlushStartTime = null; + currentFlushMeasurements = null; + return; + } + + var previousStartTime = currentFlushStartTime; + var previousMeasurements = currentFlushMeasurements || []; + var previousOperations = ReactNativeOperationHistoryDevtool.getHistory(); + + if (previousMeasurements.length || previousOperations.length) { + var registeredIDs = ReactComponentTreeDevtool.getRegisteredIDs(); + flushHistory.push({ + duration: performanceNow() - previousStartTime, + measurements: previousMeasurements || [], + operations: previousOperations || [], + treeSnapshot: registeredIDs.reduce((tree, id) => { + var ownerID = ReactComponentTreeDevtool.getOwnerID(id); + var parentID = ReactComponentTreeDevtool.getParentID(id); + tree[id] = { + displayName: ReactComponentTreeDevtool.getDisplayName(id), + text: ReactComponentTreeDevtool.getText(id), + childIDs: ReactComponentTreeDevtool.getChildIDs(id), + // Text nodes don't have owners but this is close enough. + ownerID: ownerID || ReactComponentTreeDevtool.getOwnerID(parentID), + parentID, + }; + return tree; + }, {}), + }); + } + + currentFlushStartTime = performanceNow(); + currentFlushMeasurements = []; + ReactComponentTreeDevtool.purgeUnmountedComponents(); + ReactNativeOperationHistoryDevtool.clearHistory(); + } +} + var ReactDebugTool = { addDevtool(devtool) { eventHandlers.push(devtool); @@ -49,6 +101,95 @@ var ReactDebugTool = { } } }, + beginProfiling() { + if (__DEV__) { + if (isProfiling) { + return; + } + + isProfiling = true; + flushHistory.length = 0; + resetMeasurements(); + } + }, + endProfiling() { + if (__DEV__) { + if (!isProfiling) { + return; + } + + isProfiling = false; + resetMeasurements(); + } + }, + getFlushHistory() { + if (__DEV__) { + return flushHistory; + } + }, + onBeginFlush() { + if (__DEV__) { + currentFlushNesting++; + resetMeasurements(); + } + emitEvent('onBeginFlush'); + }, + onEndFlush() { + if (__DEV__) { + resetMeasurements(); + currentFlushNesting--; + } + emitEvent('onEndFlush'); + }, + onBeginLifeCycleTimer(debugID, timerType) { + emitEvent('onBeginLifeCycleTimer', debugID, timerType); + if (__DEV__) { + if (isProfiling) { + warning( + !currentTimerType, + 'There is an internal error in the React performance measurement code. ' + + 'Did not expect %s timer to start while %s timer is still in ' + + 'progress for %s instance.', + timerType, + currentTimerType || 'no', + (debugID === currentTimerDebugID) ? 'the same' : 'another' + ); + currentTimerStartTime = performanceNow(); + currentTimerDebugID = debugID; + currentTimerType = timerType; + } + } + }, + onEndLifeCycleTimer(debugID, timerType) { + if (__DEV__) { + if (isProfiling) { + warning( + currentTimerType === timerType, + 'There is an internal error in the React performance measurement code. ' + + 'We did not expect %s timer to stop while %s timer is still in ' + + 'progress for %s instance. Please report this as a bug in React.', + timerType, + currentTimerType || 'no', + (debugID === currentTimerDebugID) ? 'the same' : 'another' + ); + currentFlushMeasurements.push({ + timerType, + instanceID: debugID, + duration: performance.now() - currentTimerStartTime, + }); + currentTimerStartTime = null; + currentTimerDebugID = null; + currentTimerType = null; + } + } + emitEvent('onEndLifeCycleTimer', debugID, timerType); + }, + onBeginReconcilerTimer(debugID, timerType) { + emitEvent('onBeginReconcilerTimer', debugID, timerType); + }, + onEndReconcilerTimer(debugID, timerType) { + emitEvent('onEndReconcilerTimer', debugID, timerType); + }, onBeginProcessingChildContext() { emitEvent('onBeginProcessingChildContext'); }, @@ -90,6 +231,17 @@ var ReactDebugTool = { }, }; -ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool); +if (__DEV__) { + var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool'); + var ReactNativeOperationHistoryDevtool = require('ReactNativeOperationHistoryDevtool'); + var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); + ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool); + ReactDebugTool.addDevtool(ReactComponentTreeDevtool); + ReactDebugTool.addDevtool(ReactNativeOperationHistoryDevtool); + var url = (ExecutionEnvironment.canUseDOM && window.location.href) || ''; + if ((/[?&]react_perf\b/).test(url)) { + ReactDebugTool.beginProfiling(); + } +} module.exports = ReactDebugTool; diff --git a/src/isomorphic/ReactPerfAnalysis.js b/src/isomorphic/ReactPerfAnalysis.js new file mode 100644 index 0000000000000..f32d6a9b4d40c --- /dev/null +++ b/src/isomorphic/ReactPerfAnalysis.js @@ -0,0 +1,361 @@ +/** + * Copyright 2016-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 ReactPerfAnalysis + */ + +'use strict'; + +var ReactDebugTool = require('ReactDebugTool'); +var warning = require('warning'); + +function roundFloat(val, base = 2) { + var n = Math.pow(10, base); + return Math.floor(val * n) / n; +} + +function getFlushHistory() { + return ReactDebugTool.getFlushHistory(); +} + +function getExclusive(flushHistory = getFlushHistory()) { + var aggregatedStats = {}; + var affectedIDs = {}; + + function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) { + var {displayName} = treeSnapshot[instanceID]; + + var key = displayName; + var stats = aggregatedStats[key]; + + if (!stats) { + affectedIDs[key] = {}; + stats = aggregatedStats[key] = { + key, + instanceCount: 0, + counts: {}, + durations: {}, + totalDuration: 0, + }; + } + + affectedIDs[key][instanceID] = true; + applyUpdate(stats); + } + + flushHistory.forEach(flush => { + var {measurements, treeSnapshot} = flush; + measurements.forEach(measurement => { + var {duration, instanceID, timerType} = measurement; + updateAggregatedStats(treeSnapshot, instanceID, stats => { + stats.totalDuration += duration; + + if (!stats.durations[timerType]) { + stats.durations[timerType] = 0; + } + stats.durations[timerType] += duration; + + if (!stats.counts[timerType]) { + stats.counts[timerType] = 0; + } + stats.counts[timerType]++; + }); + }); + }); + + return Object.keys(aggregatedStats) + .map(key => ({ + ...aggregatedStats[key], + instanceCount: Object.keys(affectedIDs[key]).length, + })) + .sort((a, b) => b.totalDuration - a.totalDuration); +} + +function getInclusive(flushHistory = getFlushHistory(), wastedOnly) { + var aggregatedStats = {}; + var affectedIDs = {}; + + function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) { + var {displayName, ownerID} = treeSnapshot[instanceID]; + + var owner = treeSnapshot[ownerID]; + var key = `${owner ? owner.displayName : '(no owner)'} > ${displayName}`; + var stats = aggregatedStats[key]; + + if (!stats) { + affectedIDs[key] = {}; + stats = aggregatedStats[key] = { + key, + instanceCount: 0, + inclusiveRenderDuration: 0, + renderCount: 0, + }; + } + + affectedIDs[key][instanceID] = true; + applyUpdate(stats); + } + + var hasRenderedByID = {}; + flushHistory.forEach(flush => { + var {measurements} = flush; + measurements.forEach(measurement => { + var {instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + hasRenderedByID[instanceID] = true; + }); + }); + + flushHistory.forEach(flush => { + var {measurements, treeSnapshot} = flush; + measurements.forEach(measurement => { + var {duration, instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + updateAggregatedStats(treeSnapshot, instanceID, stats => { + stats.renderCount++; + }); + var nextParentID = instanceID; + while (nextParentID) { + if (hasRenderedByID[nextParentID]) { + updateAggregatedStats(treeSnapshot, nextParentID, stats => { + stats.inclusiveRenderDuration += duration; + }); + } + nextParentID = treeSnapshot[nextParentID].parentID; + } + }); + }); + + return Object.keys(aggregatedStats) + .map(key => ({ + ...aggregatedStats[key], + instanceCount: Object.keys(affectedIDs[key]).length, + })) + .sort((a, b) => b.inclusiveRenderDuration - a.inclusiveRenderDuration); +} + +function getWasted(flushHistory = getFlushHistory()) { + var aggregatedStats = {}; + var affectedIDs = {}; + + function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) { + var {displayName, ownerID} = treeSnapshot[instanceID]; + + var owner = treeSnapshot[ownerID]; + var key = `${owner ? owner.displayName : '(no owner)'} > ${displayName}`; + var stats = aggregatedStats[key]; + + if (!stats) { + affectedIDs[key] = {}; + stats = aggregatedStats[key] = { + key, + instanceCount: 0, + inclusiveRenderDuration: 0, + renderCount: 0, + }; + } + + affectedIDs[key][instanceID] = true; + applyUpdate(stats); + } + + flushHistory.forEach(flush => { + var {measurements, treeSnapshot, operations} = flush; + var dirtyInstanceIDs = {}; + + operations.forEach(operation => { + var {instanceID} = operation; + + var nextParentID = instanceID; + while (nextParentID) { + dirtyInstanceIDs[nextParentID] = true; + nextParentID = treeSnapshot[nextParentID].parentID; + } + }); + + var renderedCompositeIDs = {}; + measurements.forEach(measurement => { + var {instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + renderedCompositeIDs[instanceID] = true; + }); + + measurements.forEach(measurement => { + var {duration, instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + var { updateCount } = treeSnapshot[instanceID]; + if (dirtyInstanceIDs[instanceID] || updateCount === 0) { + return; + } + updateAggregatedStats(treeSnapshot, instanceID, stats => { + stats.renderCount++; + }); + var nextParentID = instanceID; + while (nextParentID) { + if (!renderedCompositeIDs[nextParentID]) { + break; + } + updateAggregatedStats(treeSnapshot, nextParentID, stats => { + stats.inclusiveRenderDuration += duration; + }); + nextParentID = treeSnapshot[nextParentID].parentID; + } + }); + }); + + return Object.keys(aggregatedStats) + .map(key => ({ + ...aggregatedStats[key], + instanceCount: Object.keys(affectedIDs[key]).length, + })) + .sort((a, b) => b.inclusiveRenderDuration - a.inclusiveRenderDuration); +} + +function getOperations(flushHistory = getFlushHistory()) { + var stats = []; + flushHistory.forEach((flush, flushIndex) => { + var {operations, treeSnapshot} = flush; + operations.forEach(operation => { + var {instanceID, type, payload} = operation; + var {displayName, ownerID} = treeSnapshot[instanceID]; + var owner = treeSnapshot[ownerID]; + var key = `${(owner ? owner.displayName : '(no owner)')} > ${displayName}`; + + stats.push({ + flushIndex, + instanceID, + key, + type, + ownerID, + payload, + }); + }); + }); + return stats; +} + +function printExclusive(flushHistory) { + var stats = getExclusive(flushHistory); + var table = stats.map(item => { + var {key, instanceCount, totalDuration} = item; + var renderCount = item.counts.render || 0; + var renderDuration = item.durations.render || 0; + return { + 'Component': key, + 'Total time (ms)': roundFloat(totalDuration), + 'Instance count': instanceCount, + 'Total render time (ms)': roundFloat(renderDuration), + 'Average render time (ms)': renderCount ? + roundFloat(renderDuration / renderCount) : + undefined, + 'Render count': renderCount, + 'Total lifecycle time (ms)': roundFloat(totalDuration - renderDuration), + }; + }); + console.table(table); +} + +function printInclusive(flushHistory) { + var stats = getInclusive(flushHistory); + var table = stats.map(item => { + var {key, instanceCount, inclusiveRenderDuration, renderCount} = item; + return { + 'Owner > Component': key, + 'Inclusive render time (ms)': roundFloat(inclusiveRenderDuration), + 'Instance count': instanceCount, + 'Render count': renderCount, + }; + }); + console.table(table); +} + +function printWasted(flushHistory) { + var stats = getWasted(flushHistory); + var table = stats.map(item => { + var {key, instanceCount, inclusiveRenderDuration, renderCount} = item; + return { + 'Owner > Component': key, + 'Inclusive wasted time (ms)': roundFloat(inclusiveRenderDuration), + 'Instance count': instanceCount, + 'Render count': renderCount, + }; + }); + console.table(table); +} + +function printOperations(flushHistory) { + var stats = getOperations(flushHistory); + var table = stats.map(stat => ({ + 'Owner > Node': stat.key, + 'Operation': stat.type, + 'Payload': typeof stat.payload === 'object' ? + JSON.stringify(stat.payload) : + stat.payload, + 'Flush index': stat.flushIndex, + 'Owner Component ID': stat.ownerID, + 'DOM Component ID': stat.instanceID, + })); + console.table(table); +} + +var warnedAboutPrintDOM = false; +function printDOM(measurements) { + warning( + warnedAboutPrintDOM, + '`ReactPerf.printDOM(...)` is deprecated. Use ' + + '`ReactPerf.printOperations(...)` instead.' + ); + warnedAboutPrintDOM = true; + return printOperations(measurements); +} + +var warnedAboutGetMeasurementsSummaryMap = false; +function getMeasurementsSummaryMap(measurements) { + warning( + warnedAboutGetMeasurementsSummaryMap, + '`ReactPerf.getMeasurementsSummaryMap(...)` is deprecated. Use ' + + '`ReactPerf.getWasted(...)` instead.' + ); + warnedAboutGetMeasurementsSummaryMap = true; + return getWasted(measurements); +} + +function start() { + ReactDebugTool.beginProfiling(); +} + +function stop() { + ReactDebugTool.endProfiling(); +} + +var ReactPerfAnalysis = { + getFlushHistory, + getExclusive, + getInclusive, + getWasted, + getOperations, + printExclusive, + printInclusive, + printWasted, + printOperations, + start, + stop, + // Deprecated: + printDOM, + getMeasurementsSummaryMap, +}; + +module.exports = ReactPerfAnalysis; diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js index 48f3b9d3d9f3e..7ac5be4b9803c 100644 --- a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js @@ -13,7 +13,6 @@ describe('ReactComponentTreeDevtool', () => { var React; - var ReactDebugTool; var ReactDOM; var ReactDOMServer; var ReactInstanceMap; @@ -23,17 +22,10 @@ describe('ReactComponentTreeDevtool', () => { jest.resetModuleRegistry(); React = require('React'); - ReactDebugTool = require('ReactDebugTool'); ReactDOM = require('ReactDOM'); ReactDOMServer = require('ReactDOMServer'); ReactInstanceMap = require('ReactInstanceMap'); ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); - - ReactDebugTool.addDevtool(ReactComponentTreeDevtool); - }); - - afterEach(() => { - ReactDebugTool.removeDevtool(ReactComponentTreeDevtool); }); function getRootDisplayNames() { diff --git a/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js index 3b5924aae5f72..667e0f3981849 100644 --- a/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js +++ b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js @@ -13,7 +13,6 @@ describe('ReactNativeOperationHistoryDevtool', () => { var React; - var ReactDebugTool; var ReactDOM; var ReactDOMComponentTree; var ReactDOMFeatureFlags; @@ -23,17 +22,10 @@ describe('ReactNativeOperationHistoryDevtool', () => { jest.resetModuleRegistry(); React = require('React'); - ReactDebugTool = require('ReactDebugTool'); ReactDOM = require('ReactDOM'); ReactDOMComponentTree = require('ReactDOMComponentTree'); ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); ReactNativeOperationHistoryDevtool = require('ReactNativeOperationHistoryDevtool'); - - ReactDebugTool.addDevtool(ReactNativeOperationHistoryDevtool); - }); - - afterEach(() => { - ReactDebugTool.removeDevtool(ReactNativeOperationHistoryDevtool); }); function assertHistoryMatches(expectedHistory) { diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index 2981814ead287..d7c8b3aed533d 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -308,6 +308,10 @@ var ReactMount = { shouldReuseMarkup, context ) { + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginFlush(); + } + // Various parts of our code (such as ReactCompositeComponent's // _renderValidatedComponent) assume that calls to render aren't nested; // verify that that's the case. @@ -359,6 +363,7 @@ var ReactMount = { ReactInstrumentation.debugTool.onMountRootComponent( componentInstance._renderedComponent._debugID ); + ReactInstrumentation.debugTool.onEndFlush(); } return componentInstance; diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index d2c77fe1cd7b7..a9b30e1ca87d2 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -1115,9 +1115,11 @@ ReactDOMComponent.Mixin = { this._domID = null; this._wrapperState = null; - if (this._contentDebugID) { - ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID); - this._contentDebugID = null; + if (__DEV__) { + if (this._contentDebugID) { + ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID); + this._contentDebugID = null; + } } }, diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js index 5041706287355..97ed919d78e66 100644 --- a/src/renderers/dom/shared/ReactDOMTextComponent.js +++ b/src/renderers/dom/shared/ReactDOMTextComponent.js @@ -145,7 +145,10 @@ Object.assign(ReactDOMTextComponent.prototype, { ); if (__DEV__) { - ReactInstrumentation.debugTool.onSetText(this._debugID, nextStringText); + ReactInstrumentation.debugTool.onSetText( + this._debugID, + nextStringText + ); } } } diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js index f27cbdbe8018b..0b5982e3496ab 100644 --- a/src/renderers/shared/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js @@ -60,6 +60,40 @@ function warnIfInvalidElement(Component, element) { } } +function invokeComponentDidMountWithTimer() { + var publicInstance = this._instance; + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'componentDidMount' + ); + } + publicInstance.componentDidMount(); + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'componentDidMount' + ); + } +} + +function invokeComponentDidUpdateWithTimer(prevProps, prevState, prevContext) { + var publicInstance = this._instance; + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'componentDidUpdate' + ); + } + publicInstance.componentDidUpdate(prevProps, prevState, prevContext); + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'componentDidUpdate' + ); + } +} + function shouldConstruct(Component) { return Component.prototype && Component.prototype.isReactComponent; } @@ -302,7 +336,11 @@ var ReactCompositeComponentMixin = { } if (inst.componentDidMount) { - transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); + if (__DEV__) { + transaction.getReactMountReady().enqueue(invokeComponentDidMountWithTimer, this); + } else { + transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); + } } return markup; @@ -323,11 +361,47 @@ var ReactCompositeComponentMixin = { _constructComponentWithoutOwner: function(publicProps, publicContext) { var Component = this._currentElement.type; + var instanceOrElement; if (shouldConstruct(Component)) { - return new Component(publicProps, publicContext, ReactUpdateQueue); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'ctor' + ); + } + } + instanceOrElement = new Component(publicProps, publicContext, ReactUpdateQueue); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'ctor' + ); + } + } } else { - return Component(publicProps, publicContext, ReactUpdateQueue); + // This can still be an instance in case of factory components + // but we'll count this as time spent rendering as the more common case. + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'render' + ); + } + } + instanceOrElement = Component(publicProps, publicContext, ReactUpdateQueue); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'render' + ); + } + } } + return instanceOrElement; }, performInitialMountWithErrorHandling: function( @@ -363,7 +437,23 @@ var ReactCompositeComponentMixin = { performInitialMount: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) { var inst = this._instance; if (inst.componentWillMount) { + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'componentWillMount' + ); + } + } inst.componentWillMount(); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'componentWillMount' + ); + } + } // When mounting, calls to `setState` by `componentWillMount` will set // `this._pendingStateQueue` without triggering a re-render. if (this._pendingStateQueue) { @@ -421,12 +511,28 @@ var ReactCompositeComponentMixin = { if (inst.componentWillUnmount && !inst._calledComponentWillUnmount) { inst._calledComponentWillUnmount = true; + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'componentWillUnmount' + ); + } + } if (safely) { var name = this.getName() + '.componentWillUnmount()'; ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst)); } else { inst.componentWillUnmount(); } + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'componentWillUnmount' + ); + } + } } if (this._renderedComponent) { @@ -721,15 +827,47 @@ var ReactCompositeComponentMixin = { // _pendingStateQueue which will ensure that any state updates gets // immediately reconciled instead of waiting for the next batch. if (willReceive && inst.componentWillReceiveProps) { + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'componentWillReceiveProps' + ); + } + } inst.componentWillReceiveProps(nextProps, nextContext); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'componentWillReceiveProps' + ); + } + } } var nextState = this._processPendingState(nextProps, nextContext); + var shouldUpdate = true; - var shouldUpdate = - this._pendingForceUpdate || - !inst.shouldComponentUpdate || - inst.shouldComponentUpdate(nextProps, nextState, nextContext); + if (!this._pendingForceUpdate && inst.shouldComponentUpdate) { + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'shouldComponentUpdate' + ); + } + } + shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'shouldComponentUpdate' + ); + } + } + } if (__DEV__) { warning( @@ -824,7 +962,23 @@ var ReactCompositeComponentMixin = { } if (inst.componentWillUpdate) { + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'componentWillUpdate' + ); + } + } inst.componentWillUpdate(nextProps, nextState, nextContext); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'componentWillUpdate' + ); + } + } } this._currentElement = nextElement; @@ -836,10 +990,17 @@ var ReactCompositeComponentMixin = { this._updateRenderedComponent(transaction, unmaskedContext); if (hasComponentDidUpdate) { - transaction.getReactMountReady().enqueue( - inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), - inst - ); + if (__DEV__) { + transaction.getReactMountReady().enqueue( + invokeComponentDidUpdateWithTimer.bind(this, prevProps, prevState, prevContext), + this + ); + } else { + transaction.getReactMountReady().enqueue( + inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), + inst + ); + } } }, @@ -914,7 +1075,25 @@ var ReactCompositeComponentMixin = { */ _renderValidatedComponentWithoutOwnerOrContext: function() { var inst = this._instance; + + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginLifeCycleTimer( + this._debugID, + 'render' + ); + } + } var renderedComponent = inst.render(); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onEndLifeCycleTimer( + this._debugID, + 'render' + ); + } + } + if (__DEV__) { // We allow auto-mocks to proceed as if they're returning null. if (renderedComponent === undefined && @@ -948,6 +1127,7 @@ var ReactCompositeComponentMixin = { 'returned undefined, an array or some other invalid object.', this.getName() || 'ReactCompositeComponent' ); + return renderedComponent; }, diff --git a/src/renderers/shared/reconciler/ReactReconciler.js b/src/renderers/shared/reconciler/ReactReconciler.js index d2a2dbcac851e..cef23ca8506fa 100644 --- a/src/renderers/shared/reconciler/ReactReconciler.js +++ b/src/renderers/shared/reconciler/ReactReconciler.js @@ -42,6 +42,14 @@ var ReactReconciler = { nativeContainerInfo, context ) { + if (__DEV__) { + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginReconcilerTimer( + internalInstance._debugID, + 'mountComponent' + ); + } + } var markup = internalInstance.mountComponent( transaction, nativeParent, @@ -54,7 +62,13 @@ var ReactReconciler = { } if (__DEV__) { if (internalInstance._debugID !== 0) { - ReactInstrumentation.debugTool.onMountComponent(internalInstance._debugID); + ReactInstrumentation.debugTool.onEndReconcilerTimer( + internalInstance._debugID, + 'mountComponent' + ); + ReactInstrumentation.debugTool.onMountComponent( + internalInstance._debugID + ); } } return markup; @@ -75,11 +89,25 @@ var ReactReconciler = { * @internal */ unmountComponent: function(internalInstance, safely) { + if (__DEV__) { + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginReconcilerTimer( + internalInstance._debugID, + 'unmountComponent' + ); + } + } ReactRef.detachRefs(internalInstance, internalInstance._currentElement); internalInstance.unmountComponent(safely); if (__DEV__) { if (internalInstance._debugID !== 0) { - ReactInstrumentation.debugTool.onUnmountComponent(internalInstance._debugID); + ReactInstrumentation.debugTool.onEndReconcilerTimer( + internalInstance._debugID, + 'unmountComponent' + ); + ReactInstrumentation.debugTool.onUnmountComponent( + internalInstance._debugID + ); } } }, @@ -114,6 +142,15 @@ var ReactReconciler = { return; } + if (__DEV__) { + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginReconcilerTimer( + internalInstance._debugID, + 'receiveComponent' + ); + } + } + var refsChanged = ReactRef.shouldUpdateRefs( prevElement, nextElement @@ -133,7 +170,13 @@ var ReactReconciler = { if (__DEV__) { if (internalInstance._debugID !== 0) { - ReactInstrumentation.debugTool.onUpdateComponent(internalInstance._debugID); + ReactInstrumentation.debugTool.onEndReconcilerTimer( + internalInstance._debugID, + 'receiveComponent' + ); + ReactInstrumentation.debugTool.onUpdateComponent( + internalInstance._debugID + ); } } }, @@ -149,10 +192,24 @@ var ReactReconciler = { internalInstance, transaction ) { + if (__DEV__) { + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onBeginReconcilerTimer( + internalInstance._debugID, + 'performUpdateIfNecessary' + ); + } + } internalInstance.performUpdateIfNecessary(transaction); if (__DEV__) { if (internalInstance._debugID !== 0) { - ReactInstrumentation.debugTool.onUpdateComponent(internalInstance._debugID); + ReactInstrumentation.debugTool.onEndReconcilerTimer( + internalInstance._debugID, + 'performUpdateIfNecessary' + ); + ReactInstrumentation.debugTool.onUpdateComponent( + internalInstance._debugID + ); } } }, diff --git a/src/renderers/shared/reconciler/ReactUpdates.js b/src/renderers/shared/reconciler/ReactUpdates.js index 6fd1d2f3b5284..ff593be663984 100644 --- a/src/renderers/shared/reconciler/ReactUpdates.js +++ b/src/renderers/shared/reconciler/ReactUpdates.js @@ -14,6 +14,7 @@ var CallbackQueue = require('CallbackQueue'); var PooledClass = require('PooledClass'); var ReactFeatureFlags = require('ReactFeatureFlags'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactPerf = require('ReactPerf'); var ReactReconciler = require('ReactReconciler'); var Transaction = require('Transaction'); @@ -184,6 +185,10 @@ function runBatchedUpdates(transaction) { } var flushBatchedUpdates = function() { + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginFlush(); + } + // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents // array and perform any updates enqueued by mount-ready handlers (i.e., // componentDidUpdate) but we need to check here too in order to catch @@ -203,6 +208,10 @@ var flushBatchedUpdates = function() { CallbackQueue.release(queue); } } + + if (__DEV__) { + ReactInstrumentation.debugTool.onEndFlush(); + } }; flushBatchedUpdates = ReactPerf.measure( 'ReactUpdates',