diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 2b29ea79aae5b..7e5a9dd3d6194 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -35,6 +35,7 @@ import {
} from 'shared/ReactTypeOfWork';
import getComponentName from 'shared/getComponentName';
+import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {NoWork} from './ReactFiberExpirationTime';
import {NoContext, AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode';
import {
@@ -345,7 +346,15 @@ export function createWorkInProgress(
}
export function createHostRootFiber(isAsync: boolean): Fiber {
- const mode = isAsync ? AsyncMode | StrictMode : NoContext;
+ let mode = isAsync ? AsyncMode | StrictMode : NoContext;
+
+ if (enableProfilerTimer && isDevToolsPresent) {
+ // Always collect profile timings when DevTools are present.
+ // This enables DevTools to start capturing timing at any point–
+ // Without some nodes in the tree having empty base times.
+ mode |= ProfileMode;
+ }
+
return createFiber(HostRoot, null, null, mode);
}
diff --git a/packages/react-reconciler/src/ReactFiberDevToolsHook.js b/packages/react-reconciler/src/ReactFiberDevToolsHook.js
index dd58854070103..4995978b11d60 100644
--- a/packages/react-reconciler/src/ReactFiberDevToolsHook.js
+++ b/packages/react-reconciler/src/ReactFiberDevToolsHook.js
@@ -31,6 +31,9 @@ function catchErrors(fn) {
};
}
+export const isDevToolsPresent =
+ typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined';
+
export function injectInternals(internals: Object): boolean {
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
// No DevTools
diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js
index 669e1e7abd5f8..ed4c59fb38534 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -29,6 +29,7 @@ import {
Profiler,
} from 'shared/ReactTypeOfWork';
import invariant from 'shared/invariant';
+import ReactVersion from 'shared/ReactVersion';
import * as ReactTestHostConfig from './ReactTestHostConfig';
import * as TestRendererScheduling from './ReactTestRendererScheduling';
@@ -510,4 +511,14 @@ const ReactTestRendererFiber = {
unstable_setNowImplementation: TestRendererScheduling.setNowImplementation,
};
+// Enable ReactTestRenderer to be used to test DevTools integration.
+TestRenderer.injectIntoDevTools({
+ findFiberByHostInstance: (() => {
+ throw new Error('TestRenderer does not support findFiberByHostInstance()');
+ }: any),
+ bundleType: __DEV__ ? 1 : 0,
+ version: ReactVersion,
+ rendererPackageName: 'react-test-renderer',
+});
+
export default ReactTestRendererFiber;
diff --git a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
new file mode 100644
index 0000000000000..6fc396405e78f
--- /dev/null
+++ b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+describe('ReactProfiler DevTools integration', () => {
+ let React;
+ let ReactFeatureFlags;
+ let ReactTestRenderer;
+ let AdvanceTime;
+ let advanceTimeBy;
+ let hook;
+ let mockNow;
+
+ const mockNowForTests = () => {
+ let currentTime = 0;
+
+ mockNow = jest.fn().mockImplementation(() => currentTime);
+
+ ReactTestRenderer.unstable_setNowImplementation(mockNow);
+ advanceTimeBy = amount => {
+ currentTime += amount;
+ };
+ };
+
+ beforeEach(() => {
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
+ inject: () => {},
+ onCommitFiberRoot: jest.fn((rendererId, root) => {}),
+ onCommitFiberUnmount: () => {},
+ supportsFiber: true,
+ };
+
+ jest.resetModules();
+
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.enableProfilerTimer = true;
+ React = require('react');
+ ReactTestRenderer = require('react-test-renderer');
+
+ mockNowForTests();
+
+ AdvanceTime = class extends React.Component {
+ static defaultProps = {
+ byAmount: 10,
+ shouldComponentUpdate: true,
+ };
+ shouldComponentUpdate(nextProps) {
+ return nextProps.shouldComponentUpdate;
+ }
+ render() {
+ // Simulate time passing when this component is rendered
+ advanceTimeBy(this.props.byAmount);
+ return this.props.children || null;
+ }
+ };
+ });
+
+ it('should auto-Profile all fibers if the DevTools hook is detected', () => {
+ const App = ({multiplier}) => {
+ advanceTimeBy(2);
+ return (
+
+
+
+
+ );
+ };
+
+ const onRender = jest.fn(() => {});
+ const rendered = ReactTestRenderer.create();
+
+ expect(hook.onCommitFiberRoot).toHaveBeenCalledTimes(1);
+
+ // Measure observable timing using the Profiler component.
+ // The time spent in App (above the Profiler) won't be included in the durations,
+ // But needs to be accounted for in the offset times.
+ expect(onRender).toHaveBeenCalledTimes(1);
+ expect(onRender).toHaveBeenCalledWith('Profiler', 'mount', 10, 10, 2, 12);
+ onRender.mockClear();
+
+ // Measure unobservable timing required by the DevTools profiler.
+ // At this point, the base time should include both:
+ // The time 2ms in the App component itself, and
+ // The 10ms spend in the Profiler sub-tree beneath.
+ expect(rendered.root.findByType(App)._currentFiber().treeBaseTime).toBe(12);
+
+ rendered.update();
+
+ // Measure observable timing using the Profiler component.
+ // The time spent in App (above the Profiler) won't be included in the durations,
+ // But needs to be accounted for in the offset times.
+ expect(onRender).toHaveBeenCalledTimes(1);
+ expect(onRender).toHaveBeenCalledWith('Profiler', 'update', 6, 13, 14, 20);
+
+ // Measure unobservable timing required by the DevTools profiler.
+ // At this point, the base time should include both:
+ // The initial 9ms for the components that do not re-render, and
+ // The updated 6ms for the component that does.
+ expect(rendered.root.findByType(App)._currentFiber().treeBaseTime).toBe(15);
+ });
+});