From 3779a5d18c3e624bd12846592469e9c69a8c19c9 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 12 May 2016 02:05:18 +0100 Subject: [PATCH] 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 --- src/isomorphic/ReactDebugTool.js | 49 ++++++---- src/isomorphic/__tests__/ReactPerf-test.js | 15 +++ .../devtools/ReactComponentTreeDevtool.js | 5 + .../ReactNativeOperationHistoryDevtool.js | 5 + .../ReactComponentTreeDevtool-test.js | 56 +++++------ .../ReactComponentTreeDevtool-test.native.js | 48 ++++------ ...ReactNativeOperationHistoryDevtool-test.js | 96 +++++++++++++------ .../dom/server/ReactServerRendering.js | 4 + src/renderers/native/ReactNativeMount.js | 6 ++ 9 files changed, 177 insertions(+), 107 deletions(-) diff --git a/src/isomorphic/ReactDebugTool.js b/src/isomorphic/ReactDebugTool.js index 005378b76d76c..f88513b387157 100644 --- a/src/isomorphic/ReactDebugTool.js +++ b/src/isomorphic/ReactDebugTool.js @@ -48,45 +48,54 @@ var currentTimerDebugID = null; var currentTimerStartTime = null; var currentTimerType = null; +function clearHistory() { + ReactComponentTreeDevtool.purgeUnmountedComponents(); + ReactNativeOperationHistoryDevtool.clearHistory(); +} + +function getTreeSnapshot(registeredIDs) { + return registeredIDs.reduce((tree, id) => { + var ownerID = ReactComponentTreeDevtool.getOwnerID(id); + var parentID = ReactComponentTreeDevtool.getParentID(id); + tree[id] = { + displayName: ReactComponentTreeDevtool.getDisplayName(id), + text: ReactComponentTreeDevtool.getText(id), + updateCount: ReactComponentTreeDevtool.getUpdateCount(id), + childIDs: ReactComponentTreeDevtool.getChildIDs(id), + // Text nodes don't have owners but this is close enough. + ownerID: ownerID || ReactComponentTreeDevtool.getOwnerID(parentID), + parentID, + }; + return tree; + }, {}); +} + function resetMeasurements() { if (__DEV__) { + var previousStartTime = currentFlushStartTime; + var previousMeasurements = currentFlushMeasurements || []; + var previousOperations = ReactNativeOperationHistoryDevtool.getHistory(); + if (!isProfiling || currentFlushNesting === 0) { currentFlushStartTime = null; currentFlushMeasurements = null; + clearHistory(); return; } - var previousStartTime = currentFlushStartTime; - var previousMeasurements = currentFlushMeasurements || []; - var previousOperations = ReactNativeOperationHistoryDevtool.getHistory(); - if (previousMeasurements.length || previousOperations.length) { var registeredIDs = ReactComponentTreeDevtool.getRegisteredIDs(); flushHistory.push({ duration: performanceNow() - previousStartTime, measurements: previousMeasurements || [], operations: previousOperations || [], - treeSnapshot: registeredIDs.reduce((tree, id) => { - var ownerID = ReactComponentTreeDevtool.getOwnerID(id); - var parentID = ReactComponentTreeDevtool.getParentID(id); - tree[id] = { - displayName: ReactComponentTreeDevtool.getDisplayName(id), - text: ReactComponentTreeDevtool.getText(id), - updateCount: ReactComponentTreeDevtool.getUpdateCount(id), - childIDs: ReactComponentTreeDevtool.getChildIDs(id), - // Text nodes don't have owners but this is close enough. - ownerID: ownerID || ReactComponentTreeDevtool.getOwnerID(parentID), - parentID, - }; - return tree; - }, {}), + treeSnapshot: getTreeSnapshot(registeredIDs), }); } + clearHistory(); currentFlushStartTime = performanceNow(); currentFlushMeasurements = []; - ReactComponentTreeDevtool.purgeUnmountedComponents(); - ReactNativeOperationHistoryDevtool.clearHistory(); } } diff --git a/src/isomorphic/__tests__/ReactPerf-test.js b/src/isomorphic/__tests__/ReactPerf-test.js index c81a4e82a79d9..644bc618b849f 100644 --- a/src/isomorphic/__tests__/ReactPerf-test.js +++ b/src/isomorphic/__tests__/ReactPerf-test.js @@ -241,6 +241,21 @@ describe('ReactPerf', function() { }); }); + it('should include stats for components unmounted during measurement', function() { + var container = document.createElement('div'); + var measurements = measure(() => { + ReactDOM.render(
, container); + ReactDOM.render(
, container); + }); + expect(ReactPerf.getExclusive(measurements)).toEqual([{ + key: 'Div', + instanceCount: 3, + counts: { ctor: 3, render: 4 }, + durations: { ctor: 3, render: 4 }, + totalDuration: 7, + }]); + }); + it('warns once when using getMeasurementsSummaryMap', function() { var measurements = measure(() => {}); spyOn(console, 'error'); diff --git a/src/isomorphic/devtools/ReactComponentTreeDevtool.js b/src/isomorphic/devtools/ReactComponentTreeDevtool.js index 57e85b39dfc3c..85902c30d09cc 100644 --- a/src/isomorphic/devtools/ReactComponentTreeDevtool.js +++ b/src/isomorphic/devtools/ReactComponentTreeDevtool.js @@ -106,6 +106,11 @@ var ReactComponentTreeDevtool = { }, purgeUnmountedComponents() { + if (ReactComponentTreeDevtool._preventPurging) { + // Should only be used for testing. + return; + } + Object.keys(tree) .filter(id => !tree[id].isMounted) .forEach(purgeDeep); diff --git a/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js b/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js index 50478cc405d6c..46b8194465db2 100644 --- a/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js +++ b/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js @@ -23,6 +23,11 @@ var ReactNativeOperationHistoryDevtool = { }, clearHistory() { + if (ReactNativeOperationHistoryDevtool._preventClearing) { + // Should only be used for tests. + return; + } + history = []; }, diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js index 106307d0d25ac..1c5cdb7049ab0 100644 --- a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js @@ -97,13 +97,20 @@ describe('ReactComponentTreeDevtool', () => { // Ensure the tree is correct on every step. pairs.forEach(([element, expectedTree]) => { currentElement = element; + + // Mount a new tree or update the existing tree. ReactDOM.render(, node); expect(getActualTree()).toEqual(expectedTree); + + // Purging should have no effect + // on the tree we expect to see. ReactComponentTreeDevtool.purgeUnmountedComponents(); expect(getActualTree()).toEqual(expectedTree); }); + + // Unmounting the root node should purge + // the whole subtree automatically. ReactDOM.unmountComponentAtNode(node); - ReactComponentTreeDevtool.purgeUnmountedComponents(); expect(getActualTree()).toBe(undefined); expect(getRootDisplayNames()).toEqual([]); expect(getRegisteredDisplayNames()).toEqual([]); @@ -112,8 +119,23 @@ describe('ReactComponentTreeDevtool', () => { // Ensure the tree is correct on every step. pairs.forEach(([element, expectedTree]) => { currentElement = element; + + // Rendering to string should not produce any entries + // because ReactDebugTool purges it when the flush ends. + ReactDOMServer.renderToString(); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + + // To test it, we tell the devtool to ignore next purge + // so the cleanup request by ReactDebugTool is ignored. + // This lets us make assertions on the actual tree. + ReactComponentTreeDevtool._preventPurging = true; ReactDOMServer.renderToString(); + ReactComponentTreeDevtool._preventPurging = false; expect(getActualTree()).toEqual(expectedTree); + + // Purge manually since we skipped the automatic purge. ReactComponentTreeDevtool.purgeUnmountedComponents(); expect(getActualTree()).toBe(undefined); expect(getRootDisplayNames()).toEqual([]); @@ -1631,7 +1653,7 @@ describe('ReactComponentTreeDevtool', () => { assertTreeMatches([element, tree], {includeOwnerDisplayName: true}); }); - it('preserves unmounted components until purge', () => { + it('purges unmounted components automatically', () => { var node = document.createElement('div'); var renderBar = true; var fooInstance; @@ -1666,31 +1688,15 @@ describe('ReactComponentTreeDevtool', () => { renderBar = false; ReactDOM.render(, node); expect( - getTree(barInstance._debugID, { - includeParentDisplayName: true, - expectedParentID: fooInstance._debugID, - }) + getTree(barInstance._debugID, {expectedParentID: null}) ).toEqual({ - displayName: 'Bar', - parentDisplayName: 'Foo', + displayName: 'Unknown', children: [], }); ReactDOM.unmountComponentAtNode(node); expect( - getTree(barInstance._debugID, { - includeParentDisplayName: true, - expectedParentID: fooInstance._debugID, - }) - ).toEqual({ - displayName: 'Bar', - parentDisplayName: 'Foo', - children: [], - }); - - ReactComponentTreeDevtool.purgeUnmountedComponents(); - expect( - getTree(barInstance._debugID, {includeParentDisplayName: true}) + getTree(barInstance._debugID, {expectedParentID: null}) ).toEqual({ displayName: 'Unknown', children: [], @@ -1719,7 +1725,7 @@ describe('ReactComponentTreeDevtool', () => { ReactDOM.unmountComponentAtNode(node); expect(ReactComponentTreeDevtool.getUpdateCount(divID)).toEqual(0); - expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(2); + expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(0); }); it('does not report top-level wrapper as a root', () => { @@ -1733,12 +1739,6 @@ describe('ReactComponentTreeDevtool', () => { ReactDOM.unmountComponentAtNode(node); expect(getRootDisplayNames()).toEqual([]); - - ReactComponentTreeDevtool.purgeUnmountedComponents(); - expect(getRootDisplayNames()).toEqual([]); - - // This currently contains TopLevelWrapper until purge - // so we only check it at the very end. expect(getRegisteredDisplayNames()).toEqual([]); }); }); diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js index 3113d6fcad898..f3b026b459ccf 100644 --- a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js @@ -119,13 +119,20 @@ describe('ReactComponentTreeDevtool', () => { // Ensure the tree is correct on every step. pairs.forEach(([element, expectedTree]) => { currentElement = element; + + // Mount a new tree or update the existing tree. ReactNative.render(, 1); expect(getActualTree()).toEqual(expectedTree); + + // Purging should have no effect + // on the tree we expect to see. ReactComponentTreeDevtool.purgeUnmountedComponents(); expect(getActualTree()).toEqual(expectedTree); }); + + // Unmounting the root node should purge + // the whole subtree automatically. ReactNative.unmountComponentAtNode(1); - ReactComponentTreeDevtool.purgeUnmountedComponents(); expect(getActualTree()).toBe(undefined); expect(getRootDisplayNames()).toEqual([]); expect(getRegisteredDisplayNames()).toEqual([]); @@ -134,10 +141,13 @@ describe('ReactComponentTreeDevtool', () => { // Ensure the tree is correct on every step. pairs.forEach(([element, expectedTree]) => { currentElement = element; + + // Mount a new tree. ReactNative.render(, 1); - ReactNative.unmountComponentAtNode(1); expect(getActualTree()).toEqual(expectedTree); - ReactComponentTreeDevtool.purgeUnmountedComponents(); + + // Unmounting should clean it up. + ReactNative.unmountComponentAtNode(1); expect(getActualTree()).toBe(undefined); expect(getRootDisplayNames()).toEqual([]); expect(getRegisteredDisplayNames()).toEqual([]); @@ -1620,7 +1630,7 @@ describe('ReactComponentTreeDevtool', () => { assertTreeMatches([element, tree], {includeOwnerDisplayName: true}); }); - it('preserves unmounted components until purge', () => { + it('purges unmounted components automatically', () => { var renderBar = true; var fooInstance; var barInstance; @@ -1654,31 +1664,15 @@ describe('ReactComponentTreeDevtool', () => { renderBar = false; ReactNative.render(, 1); expect( - getTree(barInstance._debugID, { - includeParentDisplayName: true, - expectedParentID: fooInstance._debugID, - }) + getTree(barInstance._debugID, {expectedParentID: null}) ).toEqual({ - displayName: 'Bar', - parentDisplayName: 'Foo', + displayName: 'Unknown', children: [], }); ReactNative.unmountComponentAtNode(1); expect( - getTree(barInstance._debugID, { - includeParentDisplayName: true, - expectedParentID: fooInstance._debugID, - }) - ).toEqual({ - displayName: 'Bar', - parentDisplayName: 'Foo', - children: [], - }); - - ReactComponentTreeDevtool.purgeUnmountedComponents(); - expect( - getTree(barInstance._debugID, {includeParentDisplayName: true}) + getTree(barInstance._debugID, {expectedParentID: null}) ).toEqual({ displayName: 'Unknown', children: [], @@ -1705,7 +1699,7 @@ describe('ReactComponentTreeDevtool', () => { ReactNative.unmountComponentAtNode(1); expect(ReactComponentTreeDevtool.getUpdateCount(viewID)).toEqual(0); - expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(2); + expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(0); }); it('does not report top-level wrapper as a root', () => { @@ -1717,12 +1711,6 @@ describe('ReactComponentTreeDevtool', () => { ReactNative.unmountComponentAtNode(1); expect(getRootDisplayNames()).toEqual([]); - - ReactComponentTreeDevtool.purgeUnmountedComponents(); - expect(getRootDisplayNames()).toEqual([]); - - // This currently contains TopLevelWrapper until purge - // so we only check it at the very end. expect(getRegisteredDisplayNames()).toEqual([]); }); }); diff --git a/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js index 73ee7511d1ecb..769fe675b1edc 100644 --- a/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js +++ b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js @@ -36,9 +36,10 @@ describe('ReactNativeOperationHistoryDevtool', () => { describe('mount', () => { it('gets recorded for native roots', () => { var node = document.createElement('div'); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(

Hi.

, node); - var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); assertHistoryMatches([{ instanceID: inst._debugID, type: 'mount', @@ -53,7 +54,10 @@ describe('ReactNativeOperationHistoryDevtool', () => { return

Hi.

; } var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); assertHistoryMatches([{ instanceID: inst._debugID, @@ -70,6 +74,8 @@ describe('ReactNativeOperationHistoryDevtool', () => { return null; } var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(, node); // Empty DOM components should be invisible to devtools. @@ -82,11 +88,12 @@ describe('ReactNativeOperationHistoryDevtool', () => { return element; } + ReactNativeOperationHistoryDevtool._preventClearing = true; + var node = document.createElement('div'); element = null; ReactDOM.render(, node); - ReactNativeOperationHistoryDevtool.clearHistory(); element = ; ReactDOM.render(, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); @@ -104,12 +111,14 @@ describe('ReactNativeOperationHistoryDevtool', () => { describe('update styles', () => { it('gets recorded during mount', () => { var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); - var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); if (ReactDOMFeatureFlags.useCreateElement) { assertHistoryMatches([{ instanceID: inst._debugID, @@ -138,7 +147,7 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); ReactDOM.render(
{ ReactDOM.render(
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
{ describe('simple attribute', () => { it('gets recorded during mount', () => { var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); - var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); if (ReactDOMFeatureFlags.useCreateElement) { assertHistoryMatches([{ instanceID: inst._debugID, @@ -228,10 +239,11 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); ReactDOM.render(
, node); ReactDOM.render(
, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'update attribute', @@ -262,9 +274,10 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); ReactDOM.render(
, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'update attribute', @@ -280,9 +293,11 @@ describe('ReactNativeOperationHistoryDevtool', () => { describe('custom attribute', () => { it('gets recorded during mount', () => { var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); - var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); if (ReactDOMFeatureFlags.useCreateElement) { assertHistoryMatches([{ instanceID: inst._debugID, @@ -312,10 +327,11 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); ReactDOM.render(
, node); ReactDOM.render(
, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'update attribute', @@ -343,9 +359,11 @@ describe('ReactNativeOperationHistoryDevtool', () => { describe('attribute on a web component', () => { it('gets recorded during mount', () => { var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(, node); - var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); if (ReactDOMFeatureFlags.useCreateElement) { assertHistoryMatches([{ instanceID: inst._debugID, @@ -375,10 +393,11 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(, node); ReactDOM.render(, node); ReactDOM.render(, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'update attribute', @@ -411,8 +430,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
Hi.
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
Bye.
, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'replace text', @@ -425,8 +445,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
Bye.
, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'replace text', @@ -439,8 +460,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(

, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
Bye.
, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'remove child', @@ -460,8 +482,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { var node = document.createElement('div'); ReactDOM.render(
Hi.
, node); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
Hi.
, node); + assertHistoryMatches([]); }); }); @@ -473,8 +496,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { var inst1 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[0]); var inst2 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[3]); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
{'Bye.'}{43}
, node); + assertHistoryMatches([{ instanceID: inst1._debugID, type: 'replace text', @@ -490,8 +514,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { var node = document.createElement('div'); ReactDOM.render(
{'Hi.'}{42}
, node); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
{'Hi.'}{42}
, node); + assertHistoryMatches([]); }); }); @@ -509,9 +534,11 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); element = ; + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'replace with', @@ -528,11 +555,13 @@ describe('ReactNativeOperationHistoryDevtool', () => { var node = document.createElement('div'); element = ; ReactDOM.render(, node); - var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); element = null; + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'replace with', @@ -550,9 +579,11 @@ describe('ReactNativeOperationHistoryDevtool', () => { element =
; ReactDOM.render(, node); - ReactNativeOperationHistoryDevtool.clearHistory(); element =
; + + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(, node); + assertHistoryMatches([]); }); }); @@ -563,11 +594,12 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
Hi.
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node ); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'replace children', @@ -583,11 +615,12 @@ describe('ReactNativeOperationHistoryDevtool', () => { ); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node ); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'replace children', @@ -600,11 +633,12 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(

, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node ); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'remove child', @@ -627,11 +661,12 @@ describe('ReactNativeOperationHistoryDevtool', () => { node ); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node ); + assertHistoryMatches([]); }); }); @@ -642,8 +677,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(
, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(

, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'insert child', @@ -658,8 +694,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(

, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(

, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'move child', @@ -674,8 +711,9 @@ describe('ReactNativeOperationHistoryDevtool', () => { ReactDOM.render(

, node); var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); - ReactNativeOperationHistoryDevtool.clearHistory(); + ReactNativeOperationHistoryDevtool._preventClearing = true; ReactDOM.render(
, node); + assertHistoryMatches([{ instanceID: inst._debugID, type: 'remove child', diff --git a/src/renderers/dom/server/ReactServerRendering.js b/src/renderers/dom/server/ReactServerRendering.js index 73ce86c46045d..ee57e07438e98 100644 --- a/src/renderers/dom/server/ReactServerRendering.js +++ b/src/renderers/dom/server/ReactServerRendering.js @@ -37,6 +37,9 @@ function renderToStringImpl(element, makeStaticMarkup) { transaction = ReactServerRenderingTransaction.getPooled(makeStaticMarkup); return transaction.perform(function() { + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginFlush(); + } var componentInstance = instantiateReactComponent(element); var markup = ReactReconciler.mountComponent( componentInstance, @@ -49,6 +52,7 @@ function renderToStringImpl(element, makeStaticMarkup) { ReactInstrumentation.debugTool.onUnmountComponent( componentInstance._debugID ); + ReactInstrumentation.debugTool.onEndFlush(); } if (!makeStaticMarkup) { markup = ReactMarkupChecksum.addChecksumToMarkup(markup); diff --git a/src/renderers/native/ReactNativeMount.js b/src/renderers/native/ReactNativeMount.js index 0e5c4838bbdd8..0694ec262c2b5 100644 --- a/src/renderers/native/ReactNativeMount.js +++ b/src/renderers/native/ReactNativeMount.js @@ -216,8 +216,14 @@ var ReactNativeMount = { if (!instance) { return false; } + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginFlush(); + } ReactNativeMount.unmountComponentFromNode(instance, containerTag); delete ReactNativeMount._instancesByContainerID[containerTag]; + if (__DEV__) { + ReactInstrumentation.debugTool.onEndFlush(); + } return true; },