@samitouri / QOS-React / commits / 3779a5d18c

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