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); + }); +});