Fix a memory leak in ReactComponentTreeDevtool
`ReactDebugTool` used to only call `purgeUnmountedComponents()` while profiling, so information about unmounted instances kept accumulating when not profiling. Additionally, unmounting in React Native and rendering to string did not correctly clean up the devtool. Finally, the tests tested the wrong behavior and relied on explicit `purgeUnmountedComponent()` calls. To fix this, we: * Test specifically that unmounting is enough to clean up the tree devtool. * Add missing `onBeginFlush` and `onEndFlush` calls to server and native rendering so `ReactDebugTool` knows when to copy the tree. Fixes #6750
Dan Abramov committed
May 12, 2016 at 02:05 UTC
3779a5d18c3e624bd12846592469e9c69a8c19c9
9 files changed
+177
-107
src/isomorphic/ReactDebugTool.js
+29
-20
@@ -48,45 +48,54 @@ var currentTimerDebugID = null;
48
var currentTimerStartTime = null;
49
var currentTimerType = null;
50
51
+function clearHistory() {
52
+ ReactComponentTreeDevtool.purgeUnmountedComponents();
53
+ ReactNativeOperationHistoryDevtool.clearHistory();
54
+}
55
+
56
+function getTreeSnapshot(registeredIDs) {
57
+ return registeredIDs.reduce((tree, id) => {
58
+ var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
59
+ var parentID = ReactComponentTreeDevtool.getParentID(id);
60
+ tree[id] = {
61
+ displayName: ReactComponentTreeDevtool.getDisplayName(id),
62
+ text: ReactComponentTreeDevtool.getText(id),
63
+ updateCount: ReactComponentTreeDevtool.getUpdateCount(id),
64
+ childIDs: ReactComponentTreeDevtool.getChildIDs(id),
65
+ // Text nodes don't have owners but this is close enough.
66
+ ownerID: ownerID || ReactComponentTreeDevtool.getOwnerID(parentID),
67
+ parentID,
68
+ };
69
+ return tree;
70
+ }, {});
71
+}
72
+
73
function resetMeasurements() {
74
if (__DEV__) {
75
+ var previousStartTime = currentFlushStartTime;
76
+ var previousMeasurements = currentFlushMeasurements || [];
77
+ var previousOperations = ReactNativeOperationHistoryDevtool.getHistory();
78
+
79
if (!isProfiling || currentFlushNesting === 0) {
80
currentFlushStartTime = null;
81
currentFlushMeasurements = null;
82
+ clearHistory();
83
return;
84
}
85
59
- var previousStartTime = currentFlushStartTime;
60
- var previousMeasurements = currentFlushMeasurements || [];
61
- var previousOperations = ReactNativeOperationHistoryDevtool.getHistory();
62
-
86
if (previousMeasurements.length || previousOperations.length) {
87
var registeredIDs = ReactComponentTreeDevtool.getRegisteredIDs();
88
flushHistory.push({
89
duration: performanceNow() - previousStartTime,
90
measurements: previousMeasurements || [],
91
operations: previousOperations || [],
69
- treeSnapshot: registeredIDs.reduce((tree, id) => {
70
- var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
71
- var parentID = ReactComponentTreeDevtool.getParentID(id);
72
- tree[id] = {
73
- displayName: ReactComponentTreeDevtool.getDisplayName(id),
74
- text: ReactComponentTreeDevtool.getText(id),
75
- updateCount: ReactComponentTreeDevtool.getUpdateCount(id),
76
- childIDs: ReactComponentTreeDevtool.getChildIDs(id),
77
- // Text nodes don't have owners but this is close enough.
78
- ownerID: ownerID || ReactComponentTreeDevtool.getOwnerID(parentID),
79
- parentID,
80
- };
81
- return tree;
82
- }, {}),
92
+ treeSnapshot: getTreeSnapshot(registeredIDs),
93
});
94
}
95
96
+ clearHistory();
97
currentFlushStartTime = performanceNow();
98
currentFlushMeasurements = [];
88
- ReactComponentTreeDevtool.purgeUnmountedComponents();
89
- ReactNativeOperationHistoryDevtool.clearHistory();
99
}
100
}
101
src/isomorphic/__tests__/ReactPerf-test.js
+15
@@ -241,6 +241,21 @@ describe('ReactPerf', function() {
241
});
242
});
243
244
+ it('should include stats for components unmounted during measurement', function() {
245
+ var container = document.createElement('div');
246
+ var measurements = measure(() => {
247
+ ReactDOM.render(<Div><Div key="a" /></Div>, container);
248
+ ReactDOM.render(<Div><Div key="b" /></Div>, container);
249
+ });
250
+ expect(ReactPerf.getExclusive(measurements)).toEqual([{
251
+ key: 'Div',
252
+ instanceCount: 3,
253
+ counts: { ctor: 3, render: 4 },
254
+ durations: { ctor: 3, render: 4 },
255
+ totalDuration: 7,
256
+ }]);
257
+ });
258
+
259
it('warns once when using getMeasurementsSummaryMap', function() {
260
var measurements = measure(() => {});
261
spyOn(console, 'error');
src/isomorphic/devtools/ReactComponentTreeDevtool.js
+5
@@ -106,6 +106,11 @@ var ReactComponentTreeDevtool = {
106
},
107
108
purgeUnmountedComponents() {
109
+ if (ReactComponentTreeDevtool._preventPurging) {
110
+ // Should only be used for testing.
111
+ return;
112
+ }
113
+
114
Object.keys(tree)
115
.filter(id => !tree[id].isMounted)
116
.forEach(purgeDeep);
src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js
+5
@@ -23,6 +23,11 @@ var ReactNativeOperationHistoryDevtool = {
23
},
24
25
clearHistory() {
26
+ if (ReactNativeOperationHistoryDevtool._preventClearing) {
27
+ // Should only be used for tests.
28
+ return;
29
+ }
30
+
31
history = [];
32
},
33
src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js
+28
-28
@@ -97,13 +97,20 @@ describe('ReactComponentTreeDevtool', () => {
97
// Ensure the tree is correct on every step.
98
pairs.forEach(([element, expectedTree]) => {
99
currentElement = element;
100
+
101
+ // Mount a new tree or update the existing tree.
102
ReactDOM.render(<Wrapper />, node);
103
expect(getActualTree()).toEqual(expectedTree);
104
+
105
+ // Purging should have no effect
106
+ // on the tree we expect to see.
107
ReactComponentTreeDevtool.purgeUnmountedComponents();
108
expect(getActualTree()).toEqual(expectedTree);
109
});
110
+
111
+ // Unmounting the root node should purge
112
+ // the whole subtree automatically.
113
ReactDOM.unmountComponentAtNode(node);
106
- ReactComponentTreeDevtool.purgeUnmountedComponents();
114
expect(getActualTree()).toBe(undefined);
115
expect(getRootDisplayNames()).toEqual([]);
116
expect(getRegisteredDisplayNames()).toEqual([]);
@@ -112,8 +119,23 @@ describe('ReactComponentTreeDevtool', () => {
119
// Ensure the tree is correct on every step.
120
pairs.forEach(([element, expectedTree]) => {
121
currentElement = element;
122
+
123
+ // Rendering to string should not produce any entries
124
+ // because ReactDebugTool purges it when the flush ends.
125
+ ReactDOMServer.renderToString(<Wrapper />);
126
+ expect(getActualTree()).toBe(undefined);
127
+ expect(getRootDisplayNames()).toEqual([]);
128
+ expect(getRegisteredDisplayNames()).toEqual([]);
129
+
130
+ // To test it, we tell the devtool to ignore next purge
131
+ // so the cleanup request by ReactDebugTool is ignored.
132
+ // This lets us make assertions on the actual tree.
133
+ ReactComponentTreeDevtool._preventPurging = true;
134
ReactDOMServer.renderToString(<Wrapper />);
135
+ ReactComponentTreeDevtool._preventPurging = false;
136
expect(getActualTree()).toEqual(expectedTree);
137
+
138
+ // Purge manually since we skipped the automatic purge.
139
ReactComponentTreeDevtool.purgeUnmountedComponents();
140
expect(getActualTree()).toBe(undefined);
141
expect(getRootDisplayNames()).toEqual([]);
@@ -1631,7 +1653,7 @@ describe('ReactComponentTreeDevtool', () => {
1653
assertTreeMatches([element, tree], {includeOwnerDisplayName: true});
1654
});
1655
1634
- it('preserves unmounted components until purge', () => {
1656
+ it('purges unmounted components automatically', () => {
1657
var node = document.createElement('div');
1658
var renderBar = true;
1659
var fooInstance;
@@ -1666,31 +1688,15 @@ describe('ReactComponentTreeDevtool', () => {
1688
renderBar = false;
1689
ReactDOM.render(<Foo />, node);
1690
expect(
1669
- getTree(barInstance._debugID, {
1670
- includeParentDisplayName: true,
1671
- expectedParentID: fooInstance._debugID,
1672
- })
1691
+ getTree(barInstance._debugID, {expectedParentID: null})
1692
).toEqual({
1674
- displayName: 'Bar',
1675
- parentDisplayName: 'Foo',
1693
+ displayName: 'Unknown',
1694
children: [],
1695
});
1696
1697
ReactDOM.unmountComponentAtNode(node);
1698
expect(
1681
- getTree(barInstance._debugID, {
1682
- includeParentDisplayName: true,
1683
- expectedParentID: fooInstance._debugID,
1684
- })
1685
- ).toEqual({
1686
- displayName: 'Bar',
1687
- parentDisplayName: 'Foo',
1688
- children: [],
1689
- });
1690
-
1691
- ReactComponentTreeDevtool.purgeUnmountedComponents();
1692
- expect(
1693
- getTree(barInstance._debugID, {includeParentDisplayName: true})
1699
+ getTree(barInstance._debugID, {expectedParentID: null})
1700
).toEqual({
1701
displayName: 'Unknown',
1702
children: [],
@@ -1719,7 +1725,7 @@ describe('ReactComponentTreeDevtool', () => {
1725
1726
ReactDOM.unmountComponentAtNode(node);
1727
expect(ReactComponentTreeDevtool.getUpdateCount(divID)).toEqual(0);
1722
- expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(2);
1728
+ expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(0);
1729
});
1730
1731
it('does not report top-level wrapper as a root', () => {
@@ -1733,12 +1739,6 @@ describe('ReactComponentTreeDevtool', () => {
1739
1740
ReactDOM.unmountComponentAtNode(node);
1741
expect(getRootDisplayNames()).toEqual([]);
1736
-
1737
- ReactComponentTreeDevtool.purgeUnmountedComponents();
1738
- expect(getRootDisplayNames()).toEqual([]);
1739
-
1740
- // This currently contains TopLevelWrapper until purge
1741
- // so we only check it at the very end.
1742
expect(getRegisteredDisplayNames()).toEqual([]);
1743
});
1744
});
src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js
+18
-30
@@ -119,13 +119,20 @@ describe('ReactComponentTreeDevtool', () => {
119
// Ensure the tree is correct on every step.
120
pairs.forEach(([element, expectedTree]) => {
121
currentElement = element;
122
+
123
+ // Mount a new tree or update the existing tree.
124
ReactNative.render(<Wrapper />, 1);
125
expect(getActualTree()).toEqual(expectedTree);
126
+
127
+ // Purging should have no effect
128
+ // on the tree we expect to see.
129
ReactComponentTreeDevtool.purgeUnmountedComponents();
130
expect(getActualTree()).toEqual(expectedTree);
131
});
132
+
133
+ // Unmounting the root node should purge
134
+ // the whole subtree automatically.
135
ReactNative.unmountComponentAtNode(1);
128
- ReactComponentTreeDevtool.purgeUnmountedComponents();
136
expect(getActualTree()).toBe(undefined);
137
expect(getRootDisplayNames()).toEqual([]);
138
expect(getRegisteredDisplayNames()).toEqual([]);
@@ -134,10 +141,13 @@ describe('ReactComponentTreeDevtool', () => {
141
// Ensure the tree is correct on every step.
142
pairs.forEach(([element, expectedTree]) => {
143
currentElement = element;
144
+
145
+ // Mount a new tree.
146
ReactNative.render(<Wrapper />, 1);
138
- ReactNative.unmountComponentAtNode(1);
147
expect(getActualTree()).toEqual(expectedTree);
140
- ReactComponentTreeDevtool.purgeUnmountedComponents();
148
+
149
+ // Unmounting should clean it up.
150
+ ReactNative.unmountComponentAtNode(1);
151
expect(getActualTree()).toBe(undefined);
152
expect(getRootDisplayNames()).toEqual([]);
153
expect(getRegisteredDisplayNames()).toEqual([]);
@@ -1620,7 +1630,7 @@ describe('ReactComponentTreeDevtool', () => {
1630
assertTreeMatches([element, tree], {includeOwnerDisplayName: true});
1631
});
1632
1623
- it('preserves unmounted components until purge', () => {
1633
+ it('purges unmounted components automatically', () => {
1634
var renderBar = true;
1635
var fooInstance;
1636
var barInstance;
@@ -1654,31 +1664,15 @@ describe('ReactComponentTreeDevtool', () => {
1664
renderBar = false;
1665
ReactNative.render(<Foo />, 1);
1666
expect(
1657
- getTree(barInstance._debugID, {
1658
- includeParentDisplayName: true,
1659
- expectedParentID: fooInstance._debugID,
1660
- })
1667
+ getTree(barInstance._debugID, {expectedParentID: null})
1668
).toEqual({
1662
- displayName: 'Bar',
1663
- parentDisplayName: 'Foo',
1669
+ displayName: 'Unknown',
1670
children: [],
1671
});
1672
1673
ReactNative.unmountComponentAtNode(1);
1674
expect(
1669
- getTree(barInstance._debugID, {
1670
- includeParentDisplayName: true,
1671
- expectedParentID: fooInstance._debugID,
1672
- })
1673
- ).toEqual({
1674
- displayName: 'Bar',
1675
- parentDisplayName: 'Foo',
1676
- children: [],
1677
- });
1678
-
1679
- ReactComponentTreeDevtool.purgeUnmountedComponents();
1680
- expect(
1681
- getTree(barInstance._debugID, {includeParentDisplayName: true})
1675
+ getTree(barInstance._debugID, {expectedParentID: null})
1676
).toEqual({
1677
displayName: 'Unknown',
1678
children: [],
@@ -1705,7 +1699,7 @@ describe('ReactComponentTreeDevtool', () => {
1699
1700
ReactNative.unmountComponentAtNode(1);
1701
expect(ReactComponentTreeDevtool.getUpdateCount(viewID)).toEqual(0);
1708
- expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(2);
1702
+ expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(0);
1703
});
1704
1705
it('does not report top-level wrapper as a root', () => {
@@ -1717,12 +1711,6 @@ describe('ReactComponentTreeDevtool', () => {
1711
1712
ReactNative.unmountComponentAtNode(1);
1713
expect(getRootDisplayNames()).toEqual([]);
1720
-
1721
- ReactComponentTreeDevtool.purgeUnmountedComponents();
1722
- expect(getRootDisplayNames()).toEqual([]);
1723
-
1724
- // This currently contains TopLevelWrapper until purge
1725
- // so we only check it at the very end.
1714
expect(getRegisteredDisplayNames()).toEqual([]);
1715
});
1716
});
src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js
+67
-29
@@ -36,9 +36,10 @@ describe('ReactNativeOperationHistoryDevtool', () => {
36
describe('mount', () => {
37
it('gets recorded for native roots', () => {
38
var node = document.createElement('div');
39
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
40
ReactDOM.render(<div><p>Hi.</p></div>, node);
40
- var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
41
42
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
43
assertHistoryMatches([{
44
instanceID: inst._debugID,
45
type: 'mount',
@@ -53,7 +54,10 @@ describe('ReactNativeOperationHistoryDevtool', () => {
54
return <div><p>Hi.</p></div>;
55
}
56
var node = document.createElement('div');
57
+
58
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
59
ReactDOM.render(<Foo />, node);
60
+
61
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
62
assertHistoryMatches([{
63
instanceID: inst._debugID,
@@ -70,6 +74,8 @@ describe('ReactNativeOperationHistoryDevtool', () => {
74
return null;
75
}
76
var node = document.createElement('div');
77
+
78
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
79
ReactDOM.render(<Foo />, node);
80
81
// Empty DOM components should be invisible to devtools.
@@ -82,11 +88,12 @@ describe('ReactNativeOperationHistoryDevtool', () => {
88
return element;
89
}
90
91
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
92
+
93
var node = document.createElement('div');
94
element = null;
95
ReactDOM.render(<Foo />, node);
96
89
- ReactNativeOperationHistoryDevtool.clearHistory();
97
element = <span />;
98
ReactDOM.render(<Foo />, node);
99
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
@@ -104,12 +111,14 @@ describe('ReactNativeOperationHistoryDevtool', () => {
111
describe('update styles', () => {
112
it('gets recorded during mount', () => {
113
var node = document.createElement('div');
114
+
115
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
116
ReactDOM.render(<div style={{
117
color: 'red',
118
backgroundColor: 'yellow',
119
}} />, node);
111
- var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
120
121
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
122
if (ReactDOMFeatureFlags.useCreateElement) {
123
assertHistoryMatches([{
124
instanceID: inst._debugID,
@@ -138,7 +147,7 @@ describe('ReactNativeOperationHistoryDevtool', () => {
147
ReactDOM.render(<div />, node);
148
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
149
141
- ReactNativeOperationHistoryDevtool.clearHistory();
150
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
151
ReactDOM.render(<div style={{ color: 'red' }} />, node);
152
ReactDOM.render(<div style={{
153
color: 'blue',
@@ -171,7 +180,7 @@ describe('ReactNativeOperationHistoryDevtool', () => {
180
ReactDOM.render(<div />, node);
181
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
182
174
- ReactNativeOperationHistoryDevtool.clearHistory();
183
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
184
ReactDOM.render(<div style={{
185
color: 'red',
186
backgroundColor: 'yellow',
@@ -196,9 +205,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
205
describe('simple attribute', () => {
206
it('gets recorded during mount', () => {
207
var node = document.createElement('div');
208
+
209
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
210
ReactDOM.render(<div className="rad" tabIndex={42} />, node);
200
- var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
211
212
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
213
if (ReactDOMFeatureFlags.useCreateElement) {
214
assertHistoryMatches([{
215
instanceID: inst._debugID,
@@ -228,10 +239,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
239
ReactDOM.render(<div />, node);
240
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
241
231
- ReactNativeOperationHistoryDevtool.clearHistory();
242
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
243
ReactDOM.render(<div className="rad" />, node);
244
ReactDOM.render(<div className="mad" tabIndex={42} />, node);
245
ReactDOM.render(<div tabIndex={43} />, node);
246
+
247
assertHistoryMatches([{
248
instanceID: inst._debugID,
249
type: 'update attribute',
@@ -262,9 +274,10 @@ describe('ReactNativeOperationHistoryDevtool', () => {
274
ReactDOM.render(<div />, node);
275
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
276
265
- ReactNativeOperationHistoryDevtool.clearHistory();
277
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
278
ReactDOM.render(<div disabled={true} />, node);
279
ReactDOM.render(<div disabled={false} />, node);
280
+
281
assertHistoryMatches([{
282
instanceID: inst._debugID,
283
type: 'update attribute',
@@ -280,9 +293,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
293
describe('custom attribute', () => {
294
it('gets recorded during mount', () => {
295
var node = document.createElement('div');
296
+
297
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
298
ReactDOM.render(<div data-x="rad" data-y={42} />, node);
284
- var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
299
300
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
301
if (ReactDOMFeatureFlags.useCreateElement) {
302
assertHistoryMatches([{
303
instanceID: inst._debugID,
@@ -312,10 +327,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
327
ReactDOM.render(<div />, node);
328
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
329
315
- ReactNativeOperationHistoryDevtool.clearHistory();
330
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
331
ReactDOM.render(<div data-x="rad" />, node);
332
ReactDOM.render(<div data-x="mad" data-y={42} />, node);
333
ReactDOM.render(<div data-y={43} />, node);
334
+
335
assertHistoryMatches([{
336
instanceID: inst._debugID,
337
type: 'update attribute',
@@ -343,9 +359,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
359
describe('attribute on a web component', () => {
360
it('gets recorded during mount', () => {
361
var node = document.createElement('div');
362
+
363
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
364
ReactDOM.render(<my-component className="rad" tabIndex={42} />, node);
347
- var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
365
366
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
367
if (ReactDOMFeatureFlags.useCreateElement) {
368
assertHistoryMatches([{
369
instanceID: inst._debugID,
@@ -375,10 +393,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
393
ReactDOM.render(<my-component />, node);
394
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
395
378
- ReactNativeOperationHistoryDevtool.clearHistory();
396
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
397
ReactDOM.render(<my-component className="rad" />, node);
398
ReactDOM.render(<my-component className="mad" tabIndex={42} />, node);
399
ReactDOM.render(<my-component tabIndex={43} />, node);
400
+
401
assertHistoryMatches([{
402
instanceID: inst._debugID,
403
type: 'update attribute',
@@ -411,8 +430,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
430
ReactDOM.render(<div>Hi.</div>, node);
431
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
432
414
- ReactNativeOperationHistoryDevtool.clearHistory();
433
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
434
ReactDOM.render(<div>Bye.</div>, node);
435
+
436
assertHistoryMatches([{
437
instanceID: inst._debugID,
438
type: 'replace text',
@@ -425,8 +445,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
445
ReactDOM.render(<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />, node);
446
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
447
428
- ReactNativeOperationHistoryDevtool.clearHistory();
448
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
449
ReactDOM.render(<div>Bye.</div>, node);
450
+
451
assertHistoryMatches([{
452
instanceID: inst._debugID,
453
type: 'replace text',
@@ -439,8 +460,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
460
ReactDOM.render(<div><span /><p /></div>, node);
461
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
462
442
- ReactNativeOperationHistoryDevtool.clearHistory();
463
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
464
ReactDOM.render(<div>Bye.</div>, node);
465
+
466
assertHistoryMatches([{
467
instanceID: inst._debugID,
468
type: 'remove child',
@@ -460,8 +482,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
482
var node = document.createElement('div');
483
ReactDOM.render(<div>Hi.</div>, node);
484
463
- ReactNativeOperationHistoryDevtool.clearHistory();
485
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
486
ReactDOM.render(<div>Hi.</div>, node);
487
+
488
assertHistoryMatches([]);
489
});
490
});
@@ -473,8 +496,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
496
var inst1 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[0]);
497
var inst2 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[3]);
498
476
- ReactNativeOperationHistoryDevtool.clearHistory();
499
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
500
ReactDOM.render(<div>{'Bye.'}{43}</div>, node);
501
+
502
assertHistoryMatches([{
503
instanceID: inst1._debugID,
504
type: 'replace text',
@@ -490,8 +514,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
514
var node = document.createElement('div');
515
ReactDOM.render(<div>{'Hi.'}{42}</div>, node);
516
493
- ReactNativeOperationHistoryDevtool.clearHistory();
517
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
518
ReactDOM.render(<div>{'Hi.'}{42}</div>, node);
519
+
520
assertHistoryMatches([]);
521
});
522
});
@@ -509,9 +534,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
534
ReactDOM.render(<Foo />, node);
535
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
536
512
- ReactNativeOperationHistoryDevtool.clearHistory();
537
element = <span />;
538
+
539
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
540
ReactDOM.render(<Foo />, node);
541
+
542
assertHistoryMatches([{
543
instanceID: inst._debugID,
544
type: 'replace with',
@@ -528,11 +555,13 @@ describe('ReactNativeOperationHistoryDevtool', () => {
555
var node = document.createElement('div');
556
element = <span />;
557
ReactDOM.render(<Foo />, node);
531
- var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
558
533
- ReactNativeOperationHistoryDevtool.clearHistory();
559
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
560
element = null;
561
+
562
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
563
ReactDOM.render(<Foo />, node);
564
+
565
assertHistoryMatches([{
566
instanceID: inst._debugID,
567
type: 'replace with',
@@ -550,9 +579,11 @@ describe('ReactNativeOperationHistoryDevtool', () => {
579
element = <div />;
580
ReactDOM.render(<Foo />, node);
581
553
- ReactNativeOperationHistoryDevtool.clearHistory();
582
element = <div />;
583
+
584
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
585
ReactDOM.render(<Foo />, node);
586
+
587
assertHistoryMatches([]);
588
});
589
});
@@ -563,11 +594,12 @@ describe('ReactNativeOperationHistoryDevtool', () => {
594
ReactDOM.render(<div>Hi.</div>, node);
595
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
596
566
- ReactNativeOperationHistoryDevtool.clearHistory();
597
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
598
ReactDOM.render(
599
<div dangerouslySetInnerHTML={{__html: 'Bye.'}} />,
600
node
601
);
602
+
603
assertHistoryMatches([{
604
instanceID: inst._debugID,
605
type: 'replace children',
@@ -583,11 +615,12 @@ describe('ReactNativeOperationHistoryDevtool', () => {
615
);
616
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
617
586
- ReactNativeOperationHistoryDevtool.clearHistory();
618
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
619
ReactDOM.render(
620
<div dangerouslySetInnerHTML={{__html: 'Bye.'}} />,
621
node
622
);
623
+
624
assertHistoryMatches([{
625
instanceID: inst._debugID,
626
type: 'replace children',
@@ -600,11 +633,12 @@ describe('ReactNativeOperationHistoryDevtool', () => {
633
ReactDOM.render(<div><span /><p /></div>, node);
634
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
635
603
- ReactNativeOperationHistoryDevtool.clearHistory();
636
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
637
ReactDOM.render(
638
<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
639
node
640
);
641
+
642
assertHistoryMatches([{
643
instanceID: inst._debugID,
644
type: 'remove child',
@@ -627,11 +661,12 @@ describe('ReactNativeOperationHistoryDevtool', () => {
661
node
662
);
663
630
- ReactNativeOperationHistoryDevtool.clearHistory();
664
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
665
ReactDOM.render(
666
<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
667
node
668
);
669
+
670
assertHistoryMatches([]);
671
});
672
});
@@ -642,8 +677,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
677
ReactDOM.render(<div><span /></div>, node);
678
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
679
645
- ReactNativeOperationHistoryDevtool.clearHistory();
680
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
681
ReactDOM.render(<div><span /><p /></div>, node);
682
+
683
assertHistoryMatches([{
684
instanceID: inst._debugID,
685
type: 'insert child',
@@ -658,8 +694,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
694
ReactDOM.render(<div><span key="a" /><p key="b" /></div>, node);
695
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
696
661
- ReactNativeOperationHistoryDevtool.clearHistory();
697
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
698
ReactDOM.render(<div><p key="b" /><span key="a" /></div>, node);
699
+
700
assertHistoryMatches([{
701
instanceID: inst._debugID,
702
type: 'move child',
@@ -674,8 +711,9 @@ describe('ReactNativeOperationHistoryDevtool', () => {
711
ReactDOM.render(<div><span key="a" /><p key="b" /></div>, node);
712
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
713
677
- ReactNativeOperationHistoryDevtool.clearHistory();
714
+ ReactNativeOperationHistoryDevtool._preventClearing = true;
715
ReactDOM.render(<div><span key="a" /></div>, node);
716
+
717
assertHistoryMatches([{
718
instanceID: inst._debugID,
719
type: 'remove child',
src/renderers/dom/server/ReactServerRendering.js
+4
@@ -37,6 +37,9 @@ function renderToStringImpl(element, makeStaticMarkup) {
37
transaction = ReactServerRenderingTransaction.getPooled(makeStaticMarkup);
38
39
return transaction.perform(function() {
40
+ if (__DEV__) {
41
+ ReactInstrumentation.debugTool.onBeginFlush();
42
+ }
43
var componentInstance = instantiateReactComponent(element);
44
var markup = ReactReconciler.mountComponent(
45
componentInstance,
@@ -49,6 +52,7 @@ function renderToStringImpl(element, makeStaticMarkup) {
52
ReactInstrumentation.debugTool.onUnmountComponent(
53
componentInstance._debugID
54
);
55
+ ReactInstrumentation.debugTool.onEndFlush();
56
}
57
if (!makeStaticMarkup) {
58
markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
src/renderers/native/ReactNativeMount.js
+6
@@ -216,8 +216,14 @@ var ReactNativeMount = {
216
if (!instance) {
217
return false;
218
}
219
+ if (__DEV__) {
220
+ ReactInstrumentation.debugTool.onBeginFlush();
221
+ }
222
ReactNativeMount.unmountComponentFromNode(instance, containerTag);
223
delete ReactNativeMount._instancesByContainerID[containerTag];
224
+ if (__DEV__) {
225
+ ReactInstrumentation.debugTool.onEndFlush();
226
+ }
227
return true;
228
},
229