[perf] New ReactDefaultPerf

Simplified version of https://github.com/facebook/react/pull/962. More goodies coming soon since the inclusive time isn't very helpful right now.
This commit is contained in:
Pete Hunt
2014-02-12 12:30:02 -08:00
committed by Paul O’Shannessy
parent 9ac27cb551
commit 439bca78ed
2 changed files with 231 additions and 438 deletions
+62 -56
View File
@@ -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;
+169 -382
View File
@@ -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<object>}
*/
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<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<string>}
*/
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<string>}
*/
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;