@samitouri / QOS-React-2 / commits / 3895353326

[perf] Refactor ReactDefaultPerf and add DOM operation tracing

Split the summary functions into a separate module, and add the ability to view a lot of all DOM operations. Removed DOM timing since it was incorrect. Will potentially replace with a dropped frame counter in the future.

Pete Hunt committed Feb 12, 2014 at 15:52 UTC 3895353326c44b05450008561d09503ce7604e92
2 files changed +268 -160
src/test/ReactDefaultPerf.js
+72 -160
@@ -17,151 +17,13 @@
17 * @typechecks static-only
18 */
19
20 +var DOMProperty = require('DOMProperty');
21 +var ReactDefaultPerfAnalysis = require('ReactDefaultPerfAnalysis');
22 var ReactMount = require('ReactMount');
23 var ReactPerf = require('ReactPerf');
24
23 -var merge = require('merge');
25 var performanceNow = require('performanceNow');
26
26 -// Don't try to save users less than 1.2ms (a number I made up)
27 -var DONT_CARE_THRESHOLD = 1.2;
28 -
29 -function getTotalDOMTime(measurements) {
30 - var totalDOMTime = 0;
31 - for (var i = 0; i < measurements.length; i++) {
32 - var measurement = measurements[i];
33 - var id;
34 -
35 - for (id in measurement.writes) {
36 - measurement.writes[id].forEach(function(write) {
37 - totalDOMTime += write.time;
38 - });
39 - }
40 - }
41 - return totalDOMTime;
42 -}
43 -
44 -function getExclusiveSummary(measurements) {
45 - var candidates = {};
46 - var displayName;
47 -
48 - for (var i = 0; i < measurements.length; i++) {
49 - var measurement = measurements[i];
50 - var allIDs = merge(measurement.exclusive, measurement.inclusive);
51 -
52 - for (var id in allIDs) {
53 - displayName = measurement.displayNames[id].current;
54 -
55 - candidates[displayName] = candidates[displayName] || {
56 - inclusive: 0,
57 - exclusive: 0
58 - };
59 - if (measurement.exclusive[id]) {
60 - candidates[displayName].exclusive += measurement.exclusive[id];
61 - }
62 - if (measurement.inclusive[id]) {
63 - candidates[displayName].inclusive += measurement.inclusive[id];
64 - }
65 - }
66 - }
67 -
68 - // Now make a sorted array with the results.
69 - var arr = [];
70 - for (displayName in candidates) {
71 - if (candidates[displayName].exclusiveTime < DONT_CARE_THRESHOLD) {
72 - continue;
73 - }
74 - arr.push({
75 - componentName: displayName,
76 - exclusiveTime: candidates[displayName].exclusive,
77 - inclusiveTime: candidates[displayName].inclusive
78 - });
79 - }
80 -
81 - arr.sort(function(a, b) {
82 - return b.exclusiveTime - a.exclusiveTime;
83 - });
84 -
85 - return arr;
86 -}
87 -
88 -function getInclusiveSummary(measurements, onlyClean) {
89 - var inclusiveTimes = {};
90 - var displayName;
91 -
92 - for (var i = 0; i < measurements.length; i++) {
93 - var measurement = measurements[i];
94 - var allIDs = merge(measurement.exclusive, measurement.inclusive);
95 - var cleanComponents;
96 -
97 - if (onlyClean) {
98 - cleanComponents = getCleanComponents(measurement);
99 - }
100 -
101 - for (var id in allIDs) {
102 - if (onlyClean && !cleanComponents[id]) {
103 - continue;
104 - }
105 -
106 - displayName = measurement.displayNames[id];
107 -
108 - // Inclusive time is not useful for many components without knowing where
109 - // they are instantiated. So we aggregate inclusive time with both the
110 - // owner and current displayName as the key.
111 - var inclusiveKey = displayName.owner + ' > ' + displayName.current;
112 -
113 - inclusiveTimes[inclusiveKey] = inclusiveTimes[inclusiveKey] || 0;
114 -
115 - if (measurement.inclusive[id]) {
116 - inclusiveTimes[inclusiveKey] += measurement.inclusive[id];
117 - }
118 - }
119 - }
120 -
121 - // Now make a sorted array with the results.
122 - var arr = [];
123 - for (displayName in inclusiveTimes) {
124 - if (inclusiveTimes[displayName] < DONT_CARE_THRESHOLD) {
125 - continue;
126 - }
127 - arr.push({
128 - componentName: displayName,
129 - inclusiveTime: inclusiveTimes[displayName]
130 - });
131 - }
132 -
133 - arr.sort(function(a, b) {
134 - return b.inclusiveTime - a.inclusiveTime;
135 - });
136 -
137 - return arr;
138 -}
139 -
140 -function getCleanComponents(measurement) {
141 - // For a given reconcile, look at which components did not actually
142 - // render anything to the DOM and return a mapping of their ID to
143 - // the amount of time it took to render the entire subtree.
144 - var cleanComponents = {};
145 - var dirtyLeafIDs = Object.keys(measurement.writes);
146 - var allIDs = merge(measurement.exclusive, measurement.inclusive);
147 -
148 - for (var id in allIDs) {
149 - var isDirty = false;
150 - // For each component that rendered, see if a component that triggerd
151 - // a DOM op is in its subtree.
152 - for (var i = 0; i < dirtyLeafIDs.length; i++) {
153 - if (dirtyLeafIDs[i].indexOf(id) === 0) {
154 - isDirty = true;
155 - break;
156 - }
157 - }
158 - if (!isDirty && measurement.counts[id] > 0) {
159 - cleanComponents[id] = true;
160 - }
161 - }
162 - return cleanComponents;
163 -}
164 -
27 var ReactDefaultPerf = {
28 _allMeasurements: null, // last item in the list is the current one
29 _injected: false,
@@ -183,9 +45,9 @@ var ReactDefaultPerf = {
45 return ReactDefaultPerf._allMeasurements;
46 },
47
186 - printByExclusive: function(measurements) {
48 + printExclusive: function(measurements) {
49 measurements = measurements || ReactDefaultPerf._allMeasurements;
188 - var summary = getExclusiveSummary(measurements);
50 + var summary = ReactDefaultPerfAnalysis.getExclusiveSummary(measurements);
51 console.table(summary.map(function(item) {
52 return {
53 'Component class name': item.componentName,
@@ -194,14 +56,14 @@ var ReactDefaultPerf = {
56 };
57 }));
58 console.log(
197 - 'Total DOM time:',
198 - getTotalDOMTime(measurements).toFixed(2) + ' ms'
59 + 'Total time:',
60 + ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms'
61 );
62 },
63
202 - printByInclusive: function(measurements) {
64 + printInclusive: function(measurements) {
65 measurements = measurements || ReactDefaultPerf._allMeasurements;
204 - var summary = getInclusiveSummary(measurements);
66 + var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements);
67 console.table(summary.map(function(item) {
68 return {
69 'Owner > component': item.componentName,
@@ -209,14 +71,17 @@ var ReactDefaultPerf = {
71 };
72 }));
73 console.log(
212 - 'Total DOM time:',
213 - getTotalDOMTime(measurements).toFixed(2) + ' ms'
74 + 'Total time:',
75 + ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms'
76 );
77 },
78
217 - printByWasted: function(measurements) {
79 + printWasted: function(measurements) {
80 measurements = measurements || ReactDefaultPerf._allMeasurements;
219 - var summary = getInclusiveSummary(measurements, true);
81 + var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(
82 + measurements,
83 + true
84 + );
85 console.table(summary.map(function(item) {
86 return {
87 'Owner > component': item.componentName,
@@ -224,12 +89,29 @@ var ReactDefaultPerf = {
89 };
90 }));
91 console.log(
227 - 'Total DOM time:',
228 - getTotalDOMTime(measurements).toFixed(2) + ' ms'
92 + 'Total time:',
93 + ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms'
94 );
95 },
96
232 - _recordWrite: function(id, fnName, totalTime) {
97 + printDOM: function(measurements) {
98 + measurements = measurements || ReactDefaultPerf._allMeasurements;
99 + var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements);
100 + console.table(summary.map(function(item) {
101 + var result = {};
102 + result[DOMProperty.ID_ATTRIBUTE_NAME] = item.id;
103 + result['type'] = item.type;
104 + result['args'] = JSON.stringify(item.args);
105 + return result;
106 + }));
107 + console.log(
108 + 'Total time:',
109 + ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms'
110 + );
111 + },
112 +
113 + _recordWrite: function(id, fnName, totalTime, args) {
114 + // TODO: totalTime isn't that useful since it doesn't count paints/reflows
115 var writes =
116 ReactDefaultPerf
117 ._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1]
@@ -237,7 +119,8 @@ var ReactDefaultPerf = {
119 writes[id] = writes[id] || [];
120 writes[id].push({
121 type: fnName,
240 - time: totalTime
122 + time: totalTime,
123 + args: args
124 });
125 },
126
@@ -257,9 +140,15 @@ var ReactDefaultPerf = {
140 inclusive: {},
141 counts: {},
142 writes: {},
260 - displayNames: {}
143 + displayNames: {},
144 + totalTime: 0
145 });
262 - return func.apply(this, args);
146 + start = performanceNow();
147 + rv = func.apply(this, args);
148 + ReactDefaultPerf._allMeasurements[
149 + ReactDefaultPerf._allMeasurements.length - 1
150 + ].totalTime = performanceNow() - start;
151 + return rv;
152 } else if (moduleName === 'ReactDOMIDOperations' ||
153 moduleName === 'ReactComponentBrowserEnvironment') {
154 start = performanceNow();
@@ -268,19 +157,42 @@ var ReactDefaultPerf = {
157
158 if (fnName === 'mountImageIntoNode') {
159 var mountID = ReactMount.getID(args[1]);
271 - ReactDefaultPerf._recordWrite(mountID, fnName, totalTime);
160 + ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]);
161 } else if (fnName === 'dangerouslyProcessChildrenUpdates') {
162 // special format
163 args[0].forEach(function(update) {
275 - ReactDefaultPerf._recordWrite(update.parentID, fnName, totalTime);
164 + var writeArgs = {};
165 + if (update.fromIndex !== null) {
166 + writeArgs.fromIndex = update.fromIndex;
167 + }
168 + if (update.toIndex !== null) {
169 + writeArgs.toIndex = update.toIndex;
170 + }
171 + if (update.textContent !== null) {
172 + writeArgs.textContent = update.textContent;
173 + }
174 + if (update.markupIndex !== null) {
175 + writeArgs.markup = args[1][update.markupIndex];
176 + }
177 + ReactDefaultPerf._recordWrite(
178 + update.parentID,
179 + update.type,
180 + totalTime,
181 + writeArgs
182 + );
183 });
184 } else {
185 // basic format
279 - ReactDefaultPerf._recordWrite(args[0], fnName, totalTime);
186 + ReactDefaultPerf._recordWrite(
187 + args[0],
188 + fnName,
189 + totalTime,
190 + Array.prototype.slice.call(args, 1)
191 + );
192 }
193 return rv;
194 } else if (moduleName === 'ReactCompositeComponent' && (
283 - fnName === 'updateComponent' ||
195 + fnName === 'updateComponent' || // TODO: receiveComponent()?
196 fnName === '_renderValidatedComponent')) {
197 var isInclusive = fnName === 'updateComponent';
198 var entry = ReactDefaultPerf._allMeasurements[
src/test/ReactDefaultPerfAnalysis.js new
+196
@@ -0,0 +1,196 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactDefaultPerfAnalysis
17 + */
18 +
19 +var merge = require('merge');
20 +
21 +// Don't try to save users less than 1.2ms (a number I made up)
22 +var DONT_CARE_THRESHOLD = 1.2;
23 +var DOM_OPERATION_TYPES = {
24 + 'mountImageIntoNode': 'set innerHTML',
25 + INSERT_MARKUP: 'set innerHTML',
26 + MOVE_EXISTING: 'move',
27 + REMOVE_NODE: 'remove',
28 + TEXT_CONTENT: 'set textContent',
29 + 'updatePropertyByID': 'update attribute',
30 + 'deletePropertyByID': 'delete attribute',
31 + 'updateStylesByID': 'update styles',
32 + 'updateInnerHTMLByID': 'set innerHTML',
33 + 'dangerouslyReplaceNodeWithMarkupByID': 'replace'
34 +};
35 +
36 +function getTotalTime(measurements) {
37 + // TODO: return number of DOM ops? could be misleading.
38 + // TODO: measure dropped frames after reconcile?
39 + // TODO: log total time of each reconcile and the top-level component
40 + // class that triggered it.
41 + var totalTime = 0;
42 + for (var i = 0; i < measurements.length; i++) {
43 + var measurement = measurements[i];
44 + totalTime += measurement.totalTime;
45 + }
46 + return totalTime;
47 +}
48 +
49 +function getDOMSummary(measurements) {
50 + var items = [];
51 + for (var i = 0; i < measurements.length; i++) {
52 + var measurement = measurements[i];
53 + var id;
54 +
55 + for (id in measurement.writes) {
56 + measurement.writes[id].forEach(function(write) {
57 + items.push({
58 + id: id,
59 + type: DOM_OPERATION_TYPES[write.type] || write.type,
60 + args: write.args
61 + });
62 + });
63 + }
64 + }
65 + return items;
66 +}
67 +
68 +function getExclusiveSummary(measurements) {
69 + var candidates = {};
70 + var displayName;
71 +
72 + for (var i = 0; i < measurements.length; i++) {
73 + var measurement = measurements[i];
74 + var allIDs = merge(measurement.exclusive, measurement.inclusive);
75 +
76 + for (var id in allIDs) {
77 + displayName = measurement.displayNames[id].current;
78 +
79 + candidates[displayName] = candidates[displayName] || {
80 + inclusive: 0,
81 + exclusive: 0
82 + };
83 + if (measurement.exclusive[id]) {
84 + candidates[displayName].exclusive += measurement.exclusive[id];
85 + }
86 + if (measurement.inclusive[id]) {
87 + candidates[displayName].inclusive += measurement.inclusive[id];
88 + }
89 + }
90 + }
91 +
92 + // Now make a sorted array with the results.
93 + var arr = [];
94 + for (displayName in candidates) {
95 + if (candidates[displayName].exclusiveTime < DONT_CARE_THRESHOLD) {
96 + continue;
97 + }
98 + arr.push({
99 + componentName: displayName,
100 + exclusiveTime: candidates[displayName].exclusive,
101 + inclusiveTime: candidates[displayName].inclusive
102 + });
103 + }
104 +
105 + arr.sort(function(a, b) {
106 + return b.exclusiveTime - a.exclusiveTime;
107 + });
108 +
109 + return arr;
110 +}
111 +
112 +function getInclusiveSummary(measurements, onlyClean) {
113 + var inclusiveTimes = {};
114 + var displayName;
115 +
116 + for (var i = 0; i < measurements.length; i++) {
117 + var measurement = measurements[i];
118 + var allIDs = merge(measurement.exclusive, measurement.inclusive);
119 + var cleanComponents;
120 +
121 + if (onlyClean) {
122 + cleanComponents = getUnchangedComponents(measurement);
123 + }
124 +
125 + for (var id in allIDs) {
126 + if (onlyClean && !cleanComponents[id]) {
127 + continue;
128 + }
129 +
130 + displayName = measurement.displayNames[id];
131 +
132 + // Inclusive time is not useful for many components without knowing where
133 + // they are instantiated. So we aggregate inclusive time with both the
134 + // owner and current displayName as the key.
135 + var inclusiveKey = displayName.owner + ' > ' + displayName.current;
136 +
137 + inclusiveTimes[inclusiveKey] = inclusiveTimes[inclusiveKey] || 0;
138 +
139 + if (measurement.inclusive[id]) {
140 + inclusiveTimes[inclusiveKey] += measurement.inclusive[id];
141 + }
142 + }
143 + }
144 +
145 + // Now make a sorted array with the results.
146 + var arr = [];
147 + for (displayName in inclusiveTimes) {
148 + if (inclusiveTimes[displayName] < DONT_CARE_THRESHOLD) {
149 + continue;
150 + }
151 + arr.push({
152 + componentName: displayName,
153 + inclusiveTime: inclusiveTimes[displayName]
154 + });
155 + }
156 +
157 + arr.sort(function(a, b) {
158 + return b.inclusiveTime - a.inclusiveTime;
159 + });
160 +
161 + return arr;
162 +}
163 +
164 +function getUnchangedComponents(measurement) {
165 + // For a given reconcile, look at which components did not actually
166 + // render anything to the DOM and return a mapping of their ID to
167 + // the amount of time it took to render the entire subtree.
168 + var cleanComponents = {};
169 + var dirtyLeafIDs = Object.keys(measurement.writes);
170 + var allIDs = merge(measurement.exclusive, measurement.inclusive);
171 +
172 + for (var id in allIDs) {
173 + var isDirty = false;
174 + // For each component that rendered, see if a component that triggerd
175 + // a DOM op is in its subtree.
176 + for (var i = 0; i < dirtyLeafIDs.length; i++) {
177 + if (dirtyLeafIDs[i].indexOf(id) === 0) {
178 + isDirty = true;
179 + break;
180 + }
181 + }
182 + if (!isDirty && measurement.counts[id] > 0) {
183 + cleanComponents[id] = true;
184 + }
185 + }
186 + return cleanComponents;
187 +}
188 +
189 +var ReactDefaultPerfAnalysis = {
190 + getExclusiveSummary: getExclusiveSummary,
191 + getInclusiveSummary: getInclusiveSummary,
192 + getDOMSummary: getDOMSummary,
193 + getTotalTime: getTotalTime
194 +};
195 +
196 +module.exports = ReactDefaultPerfAnalysis;