diff --git a/src/browser/ReactComponentBrowserEnvironment.js b/src/browser/ReactComponentBrowserEnvironment.js index c733e6ec47..af8e97999d 100644 --- a/src/browser/ReactComponentBrowserEnvironment.js +++ b/src/browser/ReactComponentBrowserEnvironment.js @@ -23,6 +23,7 @@ var ReactDOMIDOperations = require('ReactDOMIDOperations'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); var ReactMount = require('ReactMount'); +var ReactPerf = require('ReactPerf'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); var getReactRootElementInContainer = require('getReactRootElementInContainer'); @@ -79,71 +80,76 @@ var ReactComponentBrowserEnvironment = { * @param {boolean} shouldReuseMarkup Should reuse the existing markup in the * container if possible. */ - mountImageIntoNode: function(markup, container, shouldReuseMarkup) { - invariant( - container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE - ), - 'mountComponentIntoNode(...): Target container is not valid.' - ); + mountImageIntoNode: ReactPerf.measure( + 'ReactComponentBrowserEnvironment', + 'mountImageIntoNode', + function(markup, container, shouldReuseMarkup) { + invariant( + container && ( + container.nodeType === ELEMENT_NODE_TYPE || + container.nodeType === DOC_NODE_TYPE + ), + 'mountComponentIntoNode(...): Target container is not valid.' + ); - if (shouldReuseMarkup) { - if (ReactMarkupChecksum.canReuseMarkup( - markup, - getReactRootElementInContainer(container))) { - return; - } else { - invariant( - container.nodeType !== DOC_NODE_TYPE, - 'You\'re trying to render a component to the document using ' + - 'server rendering but the checksum was invalid. This usually ' + - 'means you rendered a different component type or props on ' + - 'the client from the one on the server, or your render() methods ' + - 'are impure. React cannot handle this case due to cross-browser ' + - 'quirks by rendering at the document root. You should look for ' + - 'environment dependent code in your components and ensure ' + - 'the props are the same client and server side.' - ); - - if (__DEV__) { - console.warn( - 'React attempted to use reuse markup in a container but the ' + - 'checksum was invalid. This generally means that you are using ' + - 'server rendering and the markup generated on the server was ' + - 'not what the client was expecting. React injected new markup ' + - 'to compensate which works but you have lost many of the ' + - 'benefits of server rendering. Instead, figure out why the ' + - 'markup being generated is different on the client or server.' + if (shouldReuseMarkup) { + if (ReactMarkupChecksum.canReuseMarkup( + markup, + getReactRootElementInContainer(container))) { + return; + } else { + invariant( + container.nodeType !== DOC_NODE_TYPE, + 'You\'re trying to render a component to the document using ' + + 'server rendering but the checksum was invalid. This usually ' + + 'means you rendered a different component type or props on ' + + 'the client from the one on the server, or your render() ' + + 'methods are impure. React cannot handle this case due to ' + + 'cross-browser quirks by rendering at the document root. You ' + + 'should look for environment dependent code in your components ' + + 'and ensure the props are the same client and server side.' ); + + if (__DEV__) { + console.warn( + 'React attempted to use reuse markup in a container but the ' + + 'checksum was invalid. This generally means that you are ' + + 'using server rendering and the markup generated on the ' + + 'server was not what the client was expecting. React injected' + + 'new markup to compensate which works but you have lost many ' + + 'of the benefits of server rendering. Instead, figure out ' + + 'why the markup being generated is different on the client ' + + 'or server.' + ); + } } } - } - invariant( - container.nodeType !== DOC_NODE_TYPE, - 'You\'re trying to render a component to the document but ' + - 'you didn\'t use server rendering. We can\'t do this ' + - 'without using server rendering due to cross-browser quirks. ' + - 'See renderComponentToString() for server rendering.' - ); + invariant( + container.nodeType !== DOC_NODE_TYPE, + 'You\'re trying to render a component to the document but ' + + 'you didn\'t use server rendering. We can\'t do this ' + + 'without using server rendering due to cross-browser quirks. ' + + 'See renderComponentToString() for server rendering.' + ); - // Asynchronously inject markup by ensuring that the container is not in - // the document when settings its `innerHTML`. - var parent = container.parentNode; - if (parent) { - var next = container.nextSibling; - parent.removeChild(container); - container.innerHTML = markup; - if (next) { - parent.insertBefore(container, next); + // Asynchronously inject markup by ensuring that the container is not in + // the document when settings its `innerHTML`. + var parent = container.parentNode; + if (parent) { + var next = container.nextSibling; + parent.removeChild(container); + container.innerHTML = markup; + if (next) { + parent.insertBefore(container, next); + } else { + parent.appendChild(container); + } } else { - parent.appendChild(container); + container.innerHTML = markup; } - } else { - container.innerHTML = markup; } - } + ) }; module.exports = ReactComponentBrowserEnvironment; diff --git a/src/test/ReactDefaultPerf.js b/src/test/ReactDefaultPerf.js index c09528836c..e32c6526b2 100644 --- a/src/test/ReactDefaultPerf.js +++ b/src/test/ReactDefaultPerf.js @@ -17,406 +17,193 @@ * @typechecks static-only */ -"use strict"; - +var ReactMount = require('ReactMount'); var ReactPerf = require('ReactPerf'); +var merge = require('merge'); var performanceNow = require('performanceNow'); -var ReactDefaultPerf = {}; +// Don't try to save users less than 1.2ms (a number I made up) +var DONT_CARE_THRESHOLD = 1.2; -if (__DEV__) { - ReactDefaultPerf = { - _injected: false, +function getSummary(measurements, sortInclusive) { + var candidates = {}; + var totalDOMTime = 0; + var displayName; - start: function() { - if (!ReactDefaultPerf._injected) { - ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure); - } - ReactPerf.enableMeasure = true; - }, + for (var i = 0; i < measurements.length; i++) { + var measurement = measurements[i]; + var id; - stop: function() { - ReactPerf.enableMeasure = false; - }, - - /** - * Gets the stored information for a given object's function. - * - * @param {string} objName - * @param {string} fnName - * @return {?object} - */ - getInfo: function(objName, fnName) { - if (!this.info[objName] || !this.info[objName][fnName]) { - return null; - } - return this.info[objName][fnName]; - }, - - /** - * Gets the logs pertaining to a given object's function. - * - * @param {string} objName - * @param {string} fnName - * @return {?array} - */ - getLogs: function(objName, fnName) { - if (!this.getInfo(objName, fnName)) { - return null; - } - return this.logs.filter(function(log) { - return log.objName === objName && log.fnName === fnName; + for (id in measurement.writes) { + measurement.writes[id].forEach(function(write) { + totalDOMTime += write.time; }); - }, + } - /** - * Runs through the logs and builds an array of arrays, where each array - * walks through the mounting/updating of each component underneath. - * - * @param {string} rootID The reactID of the root node, e.g. '.0' - * @return {array} - */ - getRawRenderHistory: function(rootID) { - var history = []; - /** - * Since logs are added after the method returns, the logs are in a sense - * upside-down: the inner-most elements from mounting/updating are logged - * first, and the last addition to the log is the top renderComponent. - * Therefore, we flip the logs upside down for ease of processing, and - * reverse the history array at the end so the earliest event has index 0. - */ - var logs = this.logs.filter(function(log) { - return log.reactID.indexOf(rootID) === 0; - }).reverse(); + var allIDs = merge(measurement.exclusive, measurement.inclusive); - var subHistory = []; - logs.forEach(function(log, i) { - if (i && log.reactID === rootID && logs[i - 1].reactID !== rootID) { - subHistory.length && history.push(subHistory); - subHistory = []; - } - subHistory.push(log); - }); - if (subHistory.length) { - history.push(subHistory); - } - return history.reverse(); - }, - - /** - * Runs through the logs and builds an array of strings, where each string - * is a multiline formatted way of walking through the mounting/updating - * underneath. - * - * @param {string} rootID The reactID of the root node, e.g. '.0' - * @return {array} - */ - getRenderHistory: function(rootID) { - var history = this.getRawRenderHistory(rootID); - - return history.map(function(subHistory) { - var headerString = ( - 'log# Component (execution time) [bloat from logging]\n' + - '================================================================\n' - ); - return headerString + subHistory.map(function(log) { - // Add two spaces for every layer in the reactID. - var indents = '\t' + Array(log.reactID.split('.').length).join(' '); - var delta = _microTime(log.timing.delta); - var bloat = _microTime(log.timing.timeToLog); - - return log.index + indents + log.name + ' (' + delta + 'ms)' + - ' [' + bloat + 'ms]'; - }).join('\n'); - }); - }, - - /** - * Print the render history from `getRenderHistory` using console.log. - * This is currently the best way to display perf data from - * any React component; working on that. - * - * @param {string} rootID The reactID of the root node, e.g. '.0' - * @param {number} index - */ - printRenderHistory: function(rootID, index) { - var history = this.getRenderHistory(rootID); - if (!history[index]) { - console.warn( - 'Index', index, 'isn\'t available! ' + - 'The render history is', history.length, 'long.' - ); - return; - } - console.log( - 'Loading render history #' + (index + 1) + - ' of ' + history.length + ':\n' + history[index] - ); - }, - - /** - * Prints the heatmap legend to console, showing how the colors correspond - * with render times. This relies on console.log styles. - */ - printHeatmapLegend: function() { - if (!this.options.heatmap.enabled) { - return; - } - var max = this.info.React - && this.info.React.renderComponent - && this.info.React.renderComponent.max; - if (max) { - var logStr = 'Heatmap: '; - for (var ii = 0; ii <= 10 * max; ii += max) { - logStr += '%c ' + (Math.round(ii) / 10) + 'ms '; - } - console.log( - logStr, - 'background-color: hsla(100, 100%, 50%, 0.6);', - 'background-color: hsla( 90, 100%, 50%, 0.6);', - 'background-color: hsla( 80, 100%, 50%, 0.6);', - 'background-color: hsla( 70, 100%, 50%, 0.6);', - 'background-color: hsla( 60, 100%, 50%, 0.6);', - 'background-color: hsla( 50, 100%, 50%, 0.6);', - 'background-color: hsla( 40, 100%, 50%, 0.6);', - 'background-color: hsla( 30, 100%, 50%, 0.6);', - 'background-color: hsla( 20, 100%, 50%, 0.6);', - 'background-color: hsla( 10, 100%, 50%, 0.6);', - 'background-color: hsla( 0, 100%, 50%, 0.6);' - ); - } - }, - - /** - * Measure a given function with logging information, and calls a callback - * if there is one. - * - * @param {string} objName - * @param {string} fnName - * @param {function} func - * @return {function} - */ - measure: function(objName, fnName, func) { - var info = _getNewInfo(objName, fnName); - - var fnArgs = _getFnArguments(func); - - return function(...args) { - var timeBeforeFn = performanceNow(); - var fnReturn = func.apply(this, args); - var timeAfterFn = performanceNow(); - - /** - * Hold onto arguments in a readable way: args[1] -> args.component. - * args is also passed to the callback, so if you want to save an - * argument in the log, do so in the callback. - */ - var argsObject = {}; - for (var i = 0; i < args.length; i++) { - argsObject[fnArgs[i]] = args[i]; - } - - var log = { - index: ReactDefaultPerf.logs.length, - fnName: fnName, - objName: objName, - timing: { - before: timeBeforeFn, - after: timeAfterFn, - delta: timeAfterFn - timeBeforeFn - } - }; - - ReactDefaultPerf.logs.push(log); - - /** - * The callback gets: - * - this (the component) - * - the original method's arguments - * - what the method returned - * - the log object, and - * - the wrapped method's info object. - */ - var callback = _getCallback(objName, fnName); - callback && callback(this, argsObject, fnReturn, log, info); - - log.timing.timeToLog = performanceNow() - timeAfterFn; - - return fnReturn; + for (id in allIDs) { + displayName = measurement.displayNames[id]; + candidates[displayName] = candidates[displayName] || { + inclusive: 0, + exclusive: 0 }; - }, - - /** - * Holds information on wrapped objects/methods. - * For instance, ReactDefaultPerf.info.React.renderComponent - */ - info: {}, - - /** - * Holds all of the logs. Filter this to pull desired information. - */ - logs: [], - - /** - * Toggle settings for ReactDefaultPerf - */ - options: { - /** - * The heatmap sets the background color of the React containers - * according to how much total time has been spent rendering them. - * The most temporally expensive component is set as pure red, - * and the others are colored from green to red as a fraction - * of that max component time. - */ - heatmap: { - enabled: true + if (measurement.exclusive[id]) { + candidates[displayName].exclusive += measurement.exclusive[id]; + } + if (measurement.inclusive[id]) { + candidates[displayName].inclusive += measurement.inclusive[id]; } } - }; + } - /** - * Gets a info area for a given object's function, adding a new one if - * necessary. - * - * @param {string} objName - * @param {string} fnName - * @return {object} - */ - var _getNewInfo = function(objName, fnName) { - var info = ReactDefaultPerf.getInfo(objName, fnName); - if (info) { - return info; + // Now make a sorted array with the results. + var arr = []; + for (displayName in candidates) { + if (candidates[displayName] < DONT_CARE_THRESHOLD) { + continue; } - ReactDefaultPerf.info[objName] = ReactDefaultPerf.info[objName] || {}; + arr.push({ + componentName: displayName, + exclusiveTime: candidates[displayName].exclusive, + inclusiveTime: candidates[displayName].inclusive + }); + } - return ReactDefaultPerf.info[objName][fnName] = { - getLogs: function() { - return ReactDefaultPerf.getLogs(objName, fnName); + if (sortInclusive) { + arr.sort(function(a, b) { + return b.inclusiveTime - a.inclusiveTime; + }); + } else { + arr.sort(function(a, b) { + return b.exclusiveTime - a.exclusiveTime; + }); + } + + return {componentClasses: arr, totalDOMTime: totalDOMTime}; +} + +var ReactDefaultPerf = { + _allMeasurements: null, // last item in the list is the current one + _injected: false, + + start: function() { + if (!ReactDefaultPerf._injected) { + ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure); + } + + ReactDefaultPerf._allMeasurements = []; + ReactPerf.enableMeasure = true; + }, + + stop: function() { + ReactPerf.enableMeasure = false; + }, + + getLastMeasurements: function() { + return ReactDefaultPerf._allMeasurements; + }, + + printByExclusive: function(measurements) { + ReactDefaultPerf.print(measurements, false); + }, + + printByInclusive: function(measurements) { + ReactDefaultPerf.print(measurements, true); + }, + + print: function(measurements, sortInclusive) { + measurements = measurements || ReactDefaultPerf._allMeasurements; + var summary = getSummary(measurements, sortInclusive); + console.table(summary.componentClasses.map(function(item) { + return { + 'Component class name': item.componentName, + 'Inclusive time': item.inclusiveTime.toFixed(2) + ' ms', + 'Exclusive time': item.exclusiveTime.toFixed(2) + ' ms' + }; + })); + console.log('Total DOM time:', summary.totalDOMTime.toFixed(2) + ' ms'); + }, + + _recordWrite: function(id, fnName, totalTime) { + var writes = + ReactDefaultPerf + ._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1] + .writes; + writes[id] = writes[id] || []; + writes[id].push({ + type: fnName, + time: totalTime + }); + }, + + measure: function(moduleName, fnName, func) { + return function(...args) { + var totalTime; + var rv; + var start; + + if (fnName === 'flushBatchedUpdates') { + // A "measurement" is a set of metrics recorded for each flush. We want + // to group the metrics for a given flush together so we can look at the + // components that rendered and the DOM operations that actually + // happened to determine the amount of "wasted work" performed. + ReactDefaultPerf._allMeasurements.push({ + exclusive: {}, + inclusive: {}, + counts: {}, + writes: {}, + displayNames: {} + }); + return func.apply(this, args); + } else if (moduleName === 'ReactDOMIDOperations' || + moduleName === 'ReactComponentBrowserEnvironment') { + start = performanceNow(); + rv = func.apply(this, args); + totalTime = performanceNow() - start; + + if (fnName === 'mountImageIntoNode') { + var mountID = ReactMount.getID(args[1]); + ReactDefaultPerf._recordWrite(mountID, fnName, totalTime); + } else if (fnName === 'dangerouslyProcessChildrenUpdates') { + // special format + args[0].forEach(function(update) { + ReactDefaultPerf._recordWrite(update.parentID, fnName, totalTime); + }); + } else { + // basic format + ReactDefaultPerf._recordWrite(args[0], fnName, totalTime); + } + return rv; + } else if (fnName === 'updateComponent' || + fnName === '_renderValidatedComponent') { + var isInclusive = fnName === 'updateComponent'; + var entry = ReactDefaultPerf._allMeasurements[ + ReactDefaultPerf._allMeasurements.length - 1 + ]; + if (isInclusive) { + // Since both updateComponent() and _renderValidatedComponent() are + // called for each render, only record the count for one of them. + entry.counts[this._rootNodeID] = entry.counts[this._rootNodeID] || 0; + entry.counts[this._rootNodeID] += 1; + } + start = performanceNow(); + rv = func.apply(this, args); + totalTime = performanceNow() - start; + + var typeOfLog = isInclusive ? entry.inclusive : entry.exclusive; + typeOfLog[this._rootNodeID] = typeOfLog[this._rootNodeID] || 0; + typeOfLog[this._rootNodeID] += totalTime; + + entry.displayNames[this._rootNodeID] = this.constructor.displayName; + + return rv; + } else { + return func.apply(this, args); } }; - }; - - /** - * Gets a list of the argument names from a function's definition. - * This is useful for storing arguments by their names within wrapFn(). - * - * @param {function} fn - * @return {array} - */ - var _getFnArguments = function(fn) { - var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; - var fnStr = fn.toString().replace(STRIP_COMMENTS, ''); - fnStr = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')); - return fnStr.match(/([^\s,]+)/g) || []; - }; - - /** - * Store common callbacks within ReactDefaultPerf. - * - * @param {string} objName - * @param {string} fnName - * @return {?function} - */ - var _getCallback = function(objName, fnName) { - switch (objName + '.' + fnName) { - case 'React.renderComponent': - return _renderComponentCallback; - case 'ReactDOMComponent.mountComponent': - case 'ReactDOMComponent.updateComponent': - return _nativeComponentCallback; - case 'ReactCompositeComponent.mountComponent': - case 'ReactCompositeComponent.updateComponent': - return _compositeComponentCallback; - default: - return null; - } - }; - - /** - * Callback function for React.renderComponent - * - * @param {object} component - * @param {object} args - * @param {?object} fnReturn - * @param {object} log - * @param {object} info - */ - var _renderComponentCallback = - function(component, args, fnReturn, log, info) { - log.name = args.nextComponent.constructor.displayName || '[unknown]'; - log.reactID = fnReturn._rootNodeID || null; - - if (ReactDefaultPerf.options.heatmap.enabled) { - var container = args.container; - if (!container.loggedByReactDefaultPerf) { - container.loggedByReactDefaultPerf = true; - info.components = info.components || []; - info.components.push(container); - } - - container.count = container.count || 0; - container.count += log.timing.delta; - info.max = info.max || 0; - if (container.count > info.max) { - info.max = container.count; - info.components.forEach(function(component) { - _setHue(component, 100 - 100 * component.count / info.max); - }); - } else { - _setHue(container, 100 - 100 * container.count / info.max); - } - } - }; - - /** - * Callback function for ReactDOMComponent - * - * @param {object} component - * @param {object} args - * @param {?object} fnReturn - * @param {object} log - * @param {object} info - */ - var _nativeComponentCallback = - function(component, args, fnReturn, log, info) { - log.name = component.tagName || '[unknown]'; - log.reactID = component._rootNodeID; - }; - - /** - * Callback function for ReactCompositeComponent - * - * @param {object} component - * @param {object} args - * @param {?object} fnReturn - * @param {object} log - * @param {object} info - */ - var _compositeComponentCallback = - function(component, args, fnReturn, log, info) { - log.name = component.constructor.displayName || '[unknown]'; - log.reactID = component._rootNodeID; - }; - - /** - * Using the hsl() background-color attribute, colors an element. - * - * @param {DOMElement} el - * @param {number} hue [0 for red, 120 for green, 240 for blue] - */ - var _setHue = function(el, hue) { - el.style.backgroundColor = 'hsla(' + hue + ', 100%, 50%, 0.6)'; - }; - - /** - * Round to the thousandth place. - * @param {number} time - * @return {number} - */ - var _microTime = function(time) { - return Math.round(time * 1000) / 1000; - }; + } } module.exports = ReactDefaultPerf;