@samitouri / QOS-React-2 / commits / 7b241d1d7f

Add tests and fix wasted render calculation

Dan Abramov committed Apr 29, 2016 at 23:09 UTC 7b241d1d7fa285218fcdb3522b24de1bce9c8b25
2 files changed +283 -35
src/isomorphic/ReactPerf.js
+45 -35
@@ -27,12 +27,10 @@ function getExclusive(flushHistory = getFlushHistory()) {
27 var aggregatedStats = {};
28 var affectedIDs = {};
29
30 - function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) {
30 + function updateAggregatedStats(treeSnapshot, instanceID, timerType, applyUpdate) {
31 var {displayName} = treeSnapshot[instanceID];
32 -
32 var key = displayName;
33 var stats = aggregatedStats[key];
35 -
34 if (!stats) {
35 affectedIDs[key] = {};
36 stats = aggregatedStats[key] = {
@@ -43,7 +41,12 @@ function getExclusive(flushHistory = getFlushHistory()) {
41 totalDuration: 0,
42 };
43 }
46 -
44 + if (!stats.durations[timerType]) {
45 + stats.durations[timerType] = 0;
46 + }
47 + if (!stats.counts[timerType]) {
48 + stats.counts[timerType] = 0;
49 + }
50 affectedIDs[key][instanceID] = true;
51 applyUpdate(stats);
52 }
@@ -52,17 +55,9 @@ function getExclusive(flushHistory = getFlushHistory()) {
55 var {measurements, treeSnapshot} = flush;
56 measurements.forEach(measurement => {
57 var {duration, instanceID, timerType} = measurement;
55 - updateAggregatedStats(treeSnapshot, instanceID, stats => {
58 + updateAggregatedStats(treeSnapshot, instanceID, timerType, stats => {
59 stats.totalDuration += duration;
57 -
58 - if (!stats.durations[timerType]) {
59 - stats.durations[timerType] = 0;
60 - }
60 stats.durations[timerType] += duration;
62 -
63 - if (!stats.counts[timerType]) {
64 - stats.counts[timerType] = 0;
65 - }
61 stats.counts[timerType]++;
62 });
63 });
@@ -73,20 +68,20 @@ function getExclusive(flushHistory = getFlushHistory()) {
68 ...aggregatedStats[key],
69 instanceCount: Object.keys(affectedIDs[key]).length,
70 }))
76 - .sort((a, b) => b.totalDuration - a.totalDuration);
71 + .sort((a, b) =>
72 + b.totalDuration - a.totalDuration
73 + );
74 }
75
79 -function getInclusive(flushHistory = getFlushHistory(), wastedOnly) {
76 +function getInclusive(flushHistory = getFlushHistory()) {
77 var aggregatedStats = {};
78 var affectedIDs = {};
79
80 function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) {
81 var {displayName, ownerID} = treeSnapshot[instanceID];
85 -
82 var owner = treeSnapshot[ownerID];
83 var key = `${owner ? owner.displayName + ' >' : ''} ${displayName}`;
84 var stats = aggregatedStats[key];
89 -
85 if (!stats) {
86 affectedIDs[key] = {};
87 stats = aggregatedStats[key] = {
@@ -96,12 +91,11 @@ function getInclusive(flushHistory = getFlushHistory(), wastedOnly) {
91 renderCount: 0,
92 };
93 }
99 -
94 affectedIDs[key][instanceID] = true;
95 applyUpdate(stats);
96 }
97
104 - var hasRenderedByID = {};
98 + var isCompositeByID = {};
99 flushHistory.forEach(flush => {
100 var {measurements} = flush;
101 measurements.forEach(measurement => {
@@ -109,7 +103,7 @@ function getInclusive(flushHistory = getFlushHistory(), wastedOnly) {
103 if (timerType !== 'render') {
104 return;
105 }
112 - hasRenderedByID[instanceID] = true;
106 + isCompositeByID[instanceID] = true;
107 });
108 });
109
@@ -125,7 +119,9 @@ function getInclusive(flushHistory = getFlushHistory(), wastedOnly) {
119 });
120 var nextParentID = instanceID;
121 while (nextParentID) {
128 - if (hasRenderedByID[nextParentID]) {
122 + // As we traverse parents, only count inclusive time towards composites.
123 + // We know something is a composite if its render() was called.
124 + if (isCompositeByID[nextParentID]) {
125 updateAggregatedStats(treeSnapshot, nextParentID, stats => {
126 stats.inclusiveRenderDuration += duration;
127 });
@@ -140,7 +136,9 @@ function getInclusive(flushHistory = getFlushHistory(), wastedOnly) {
136 ...aggregatedStats[key],
137 instanceCount: Object.keys(affectedIDs[key]).length,
138 }))
143 - .sort((a, b) => b.inclusiveRenderDuration - a.inclusiveRenderDuration);
139 + .sort((a, b) =>
140 + b.inclusiveRenderDuration - a.inclusiveRenderDuration
141 + );
142 }
143
144 function getWasted(flushHistory = getFlushHistory()) {
@@ -149,11 +147,9 @@ function getWasted(flushHistory = getFlushHistory()) {
147
148 function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) {
149 var {displayName, ownerID} = treeSnapshot[instanceID];
152 -
150 var owner = treeSnapshot[ownerID];
151 var key = (owner ? owner.displayName + ' > ' : '') + displayName;
152 var stats = aggregatedStats[key];
156 -
153 if (!stats) {
154 affectedIDs[key] = {};
155 stats = aggregatedStats[key] = {
@@ -163,25 +159,27 @@ function getWasted(flushHistory = getFlushHistory()) {
159 renderCount: 0,
160 };
161 }
166 -
162 affectedIDs[key][instanceID] = true;
163 applyUpdate(stats);
164 }
165
166 flushHistory.forEach(flush => {
167 var {measurements, treeSnapshot, operations} = flush;
173 - var dirtyInstanceIDs = {};
168 + var isDefinitelyNotWastedByID = {};
169
170 + // Find native components associated with an operation in this batch.
171 + // Mark all components in their parent tree as definitely not wasted.
172 operations.forEach(operation => {
173 var {instanceID} = operation;
177 -
174 var nextParentID = instanceID;
175 while (nextParentID) {
180 - dirtyInstanceIDs[nextParentID] = true;
176 + isDefinitelyNotWastedByID[nextParentID] = true;
177 nextParentID = treeSnapshot[nextParentID].parentID;
178 }
179 });
180
181 + // Find composite components that rendered in this batch.
182 + // These are potential candidates for being wasted renders.
183 var renderedCompositeIDs = {};
184 measurements.forEach(measurement => {
185 var {instanceID, timerType} = measurement;
@@ -196,21 +194,31 @@ function getWasted(flushHistory = getFlushHistory()) {
194 if (timerType !== 'render') {
195 return;
196 }
197 +
198 + // If there was a DOM update below this component, or it has just been
199 + // mounted, its render() is not considered wasted.
200 var { updateCount } = treeSnapshot[instanceID];
200 - if (dirtyInstanceIDs[instanceID] || updateCount === 0) {
201 + if (isDefinitelyNotWastedByID[instanceID] || updateCount === 0) {
202 return;
203 }
204 +
205 + // We consider this render() wasted.
206 updateAggregatedStats(treeSnapshot, instanceID, stats => {
207 stats.renderCount++;
208 });
209 +
210 var nextParentID = instanceID;
211 while (nextParentID) {
208 - if (!renderedCompositeIDs[nextParentID]) {
209 - break;
212 + // Any parents rendered during this batch are considered wasted
213 + // unless we previously marked them as dirty.
214 + var isWasted =
215 + renderedCompositeIDs[nextParentID] &&
216 + !isDefinitelyNotWastedByID[nextParentID];
217 + if (isWasted) {
218 + updateAggregatedStats(treeSnapshot, nextParentID, stats => {
219 + stats.inclusiveRenderDuration += duration;
220 + });
221 }
211 - updateAggregatedStats(treeSnapshot, nextParentID, stats => {
212 - stats.inclusiveRenderDuration += duration;
213 - });
222 nextParentID = treeSnapshot[nextParentID].parentID;
223 }
224 });
@@ -221,7 +229,9 @@ function getWasted(flushHistory = getFlushHistory()) {
229 ...aggregatedStats[key],
230 instanceCount: Object.keys(affectedIDs[key]).length,
231 }))
224 - .sort((a, b) => b.inclusiveRenderDuration - a.inclusiveRenderDuration);
232 + .sort((a, b) =>
233 + b.inclusiveRenderDuration - a.inclusiveRenderDuration
234 + );
235 }
236
237 function getOperations(flushHistory = getFlushHistory()) {
src/isomorphic/__tests__/ReactPerf-test.js new
+238
@@ -0,0 +1,238 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +describe('ReactPerf', function() {
15 + var React;
16 + var ReactDOM;
17 + var ReactPerf;
18 + var ReactTestUtils;
19 +
20 + var App;
21 + var Box;
22 + var Div;
23 +
24 + beforeEach(function() {
25 + var now = 0;
26 + jest.setMock('fbjs/lib/performanceNow', function() {
27 + return now++;
28 + });
29 +
30 + if (typeof console.table !== 'function') {
31 + console.table = () => {};
32 + console.table.isFake = true;
33 + }
34 +
35 + React = require('React');
36 + ReactDOM = require('ReactDOM');
37 + ReactPerf = require('ReactPerf');
38 + ReactTestUtils = require('ReactTestUtils');
39 +
40 + App = React.createClass({
41 + render: function() {
42 + return <div><Box /><Box flip={this.props.flipSecond} /></div>;
43 + },
44 + });
45 +
46 + Box = React.createClass({
47 + render: function() {
48 + return <div key={!!this.props.flip}><input /></div>;
49 + },
50 + });
51 +
52 + // ReactPerf only measures composites, so we put everything in one.
53 + Div = React.createClass({
54 + render: function() {
55 + return <div {...this.props} />;
56 + },
57 + });
58 + });
59 +
60 + afterEach(function() {
61 + if (console.table.isFake) {
62 + delete console.table;
63 + }
64 + });
65 +
66 + function measure(fn) {
67 + ReactPerf.start();
68 + fn();
69 + ReactPerf.stop();
70 + return ReactPerf.getLastMeasurements();
71 + }
72 +
73 + it('should count no-op update as waste', function() {
74 + var container = document.createElement('div');
75 + ReactDOM.render(<App />, container);
76 + var measurements = measure(() => {
77 + ReactDOM.render(<App />, container);
78 + });
79 +
80 + var summary = ReactPerf.getWasted(measurements);
81 + expect(summary).toEqual([{
82 + key: 'App',
83 + instanceCount: 1,
84 + inclusiveRenderDuration: 3,
85 + renderCount: 1,
86 + }, {
87 + key: 'App > Box',
88 + instanceCount: 2,
89 + inclusiveRenderDuration: 2,
90 + renderCount: 2,
91 + }]);
92 + });
93 +
94 + it('should count no-op update in child as waste', function() {
95 + var container = document.createElement('div');
96 + ReactDOM.render(<App />, container);
97 +
98 + // Here, we add a Box -- two of the <Box /> updates are wasted time (but the
99 + // addition of the third is not)
100 + var measurements = measure(() => {
101 + ReactDOM.render(<App flipSecond={true} />, container);
102 + });
103 +
104 + var summary = ReactPerf.getWasted(measurements);
105 + expect(summary).toEqual([{
106 + key: 'App > Box',
107 + instanceCount: 1,
108 + inclusiveRenderDuration: 1,
109 + renderCount: 1,
110 + }]);
111 + });
112 +
113 + function expectNoWaste(fn) {
114 + var measurements = measure(fn);
115 + var summary = ReactPerf.getWasted(measurements);
116 + expect(summary).toEqual([]);
117 + }
118 +
119 + it('should not count initial render as waste', function() {
120 + expectNoWaste(() => {
121 + ReactTestUtils.renderIntoDocument(<App />);
122 + });
123 + });
124 +
125 + it('should not count unmount as waste', function() {
126 + var container = document.createElement('div');
127 + ReactDOM.render(<Div>hello</Div>, container);
128 + expectNoWaste(() => {
129 + ReactDOM.unmountComponentAtNode(container);
130 + });
131 + });
132 +
133 + it('should not count content update as waste', function() {
134 + var container = document.createElement('div');
135 + ReactDOM.render(<Div>hello</Div>, container);
136 + expectNoWaste(() => {
137 + ReactDOM.render(<Div>hello world</Div>, container);
138 + });
139 + });
140 +
141 + it('should not count child addition as waste', function() {
142 + var container = document.createElement('div');
143 + ReactDOM.render(<Div><span /></Div>, container);
144 + expectNoWaste(() => {
145 + ReactDOM.render(<Div><span /><span /></Div>, container);
146 + });
147 + });
148 +
149 + it('should not count child removal as waste', function() {
150 + var container = document.createElement('div');
151 + ReactDOM.render(<Div><span /><span /></Div>, container);
152 + expectNoWaste(() => {
153 + ReactDOM.render(<Div><span /></Div>, container);
154 + });
155 + });
156 +
157 + it('should not count property update as waste', function() {
158 + var container = document.createElement('div');
159 + ReactDOM.render(<Div className="yellow">hey</Div>, container);
160 + expectNoWaste(() => {
161 + ReactDOM.render(<Div className="blue">hey</Div>, container);
162 + });
163 + });
164 +
165 + it('should not count style update as waste', function() {
166 + var container = document.createElement('div');
167 + ReactDOM.render(<Div style={{color: 'yellow'}}>hey</Div>, container);
168 + expectNoWaste(() => {
169 + ReactDOM.render(<Div style={{color: 'blue'}}>hey</Div>, container);
170 + });
171 + });
172 +
173 + it('should not count property removal as waste', function() {
174 + var container = document.createElement('div');
175 + ReactDOM.render(<Div className="yellow">hey</Div>, container);
176 + expectNoWaste(() => {
177 + ReactDOM.render(<Div>hey</Div>, container);
178 + });
179 + });
180 +
181 + it('should not count raw HTML update as waste', function() {
182 + var container = document.createElement('div');
183 + ReactDOM.render(
184 + <Div dangerouslySetInnerHTML={{__html: 'me'}} />,
185 + container
186 + );
187 + expectNoWaste(() => {
188 + ReactDOM.render(
189 + <Div dangerouslySetInnerHTML={{__html: 'you'}} />,
190 + container
191 + );
192 + });
193 + });
194 +
195 + it('should not count child reordering as waste', function() {
196 + var container = document.createElement('div');
197 + ReactDOM.render(<Div><div key="A" /><div key="B" /></Div>, container);
198 + expectNoWaste(() => {
199 + ReactDOM.render(<Div><div key="B" /><div key="A" /></Div>, container);
200 + });
201 + });
202 +
203 + it('should not count text update as waste', function() {
204 + var container = document.createElement('div');
205 + ReactDOM.render(<Div>{'hello'}{'world'}</Div>, container);
206 + expectNoWaste(() => {
207 + ReactDOM.render(<Div>{'hello'}{'friend'}</Div>, container);
208 + });
209 + });
210 +
211 + it('warns once when using getMeasurementsSummaryMap', function() {
212 + var measurements = measure(() => {});
213 + spyOn(console, 'error');
214 + ReactPerf.getMeasurementsSummaryMap(measurements);
215 + expect(console.error.calls.length).toBe(1);
216 + expect(console.error.argsForCall[0][0]).toContain(
217 + '`ReactPerf.getMeasurementsSummaryMap(...)` is deprecated. Use ' +
218 + '`ReactPerf.getWasted(...)` instead.'
219 + );
220 +
221 + ReactPerf.getMeasurementsSummaryMap(measurements);
222 + expect(console.error.calls.length).toBe(1);
223 + });
224 +
225 + it('warns once when using printDOM', function() {
226 + var measurements = measure(() => {});
227 + spyOn(console, 'error');
228 + ReactPerf.printDOM(measurements);
229 + expect(console.error.calls.length).toBe(1);
230 + expect(console.error.argsForCall[0][0]).toContain(
231 + '`ReactPerf.printDOM(...)` is deprecated. Use ' +
232 + '`ReactPerf.printOperations(...)` instead.'
233 + );
234 +
235 + ReactPerf.printDOM(measurements);
236 + expect(console.error.calls.length).toBe(1);
237 + });
238 +});