[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;