@samitouri / QOS-React / commits / b0f60895f7

Automatically Profile roots when DevTools is present (#13058)

* react-test-renderer injects itself into DevTools if present * Fibers are always opted into ProfileMode if DevTools is present * Added simple test for DevTools + always profiling behavior

Brian Vaughn committed Jun 20, 2018 at 09:24 UTC b0f60895f759253d4180dfa7e1901fe36d4abd3a
4 files changed +135 -1
packages/react-reconciler/src/ReactFiber.js
+10 -1
@@ -35,6 +35,7 @@ import {
35 } from 'shared/ReactTypeOfWork';
36 import getComponentName from 'shared/getComponentName';
37
38 +import {isDevToolsPresent} from './ReactFiberDevToolsHook';
39 import {NoWork} from './ReactFiberExpirationTime';
40 import {NoContext, AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode';
41 import {
@@ -345,7 +346,15 @@ export function createWorkInProgress(
346 }
347
348 export function createHostRootFiber(isAsync: boolean): Fiber {
348 - const mode = isAsync ? AsyncMode | StrictMode : NoContext;
349 + let mode = isAsync ? AsyncMode | StrictMode : NoContext;
350 +
351 + if (enableProfilerTimer && isDevToolsPresent) {
352 + // Always collect profile timings when DevTools are present.
353 + // This enables DevTools to start capturing timing at any point–
354 + // Without some nodes in the tree having empty base times.
355 + mode |= ProfileMode;
356 + }
357 +
358 return createFiber(HostRoot, null, null, mode);
359 }
360
packages/react-reconciler/src/ReactFiberDevToolsHook.js
+3
@@ -31,6 +31,9 @@ function catchErrors(fn) {
31 };
32 }
33
34 +export const isDevToolsPresent =
35 + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined';
36 +
37 export function injectInternals(internals: Object): boolean {
38 if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
39 // No DevTools
packages/react-test-renderer/src/ReactTestRenderer.js
+11
@@ -29,6 +29,7 @@ import {
29 Profiler,
30 } from 'shared/ReactTypeOfWork';
31 import invariant from 'shared/invariant';
32 +import ReactVersion from 'shared/ReactVersion';
33
34 import * as ReactTestHostConfig from './ReactTestHostConfig';
35 import * as TestRendererScheduling from './ReactTestRendererScheduling';
@@ -510,4 +511,14 @@ const ReactTestRendererFiber = {
511 unstable_setNowImplementation: TestRendererScheduling.setNowImplementation,
512 };
513
514 +// Enable ReactTestRenderer to be used to test DevTools integration.
515 +TestRenderer.injectIntoDevTools({
516 + findFiberByHostInstance: (() => {
517 + throw new Error('TestRenderer does not support findFiberByHostInstance()');
518 + }: any),
519 + bundleType: __DEV__ ? 1 : 0,
520 + version: ReactVersion,
521 + rendererPackageName: 'react-test-renderer',
522 +});
523 +
524 export default ReactTestRendererFiber;
packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js new
+111
@@ -0,0 +1,111 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +describe('ReactProfiler DevTools integration', () => {
13 + let React;
14 + let ReactFeatureFlags;
15 + let ReactTestRenderer;
16 + let AdvanceTime;
17 + let advanceTimeBy;
18 + let hook;
19 + let mockNow;
20 +
21 + const mockNowForTests = () => {
22 + let currentTime = 0;
23 +
24 + mockNow = jest.fn().mockImplementation(() => currentTime);
25 +
26 + ReactTestRenderer.unstable_setNowImplementation(mockNow);
27 + advanceTimeBy = amount => {
28 + currentTime += amount;
29 + };
30 + };
31 +
32 + beforeEach(() => {
33 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
34 + inject: () => {},
35 + onCommitFiberRoot: jest.fn((rendererId, root) => {}),
36 + onCommitFiberUnmount: () => {},
37 + supportsFiber: true,
38 + };
39 +
40 + jest.resetModules();
41 +
42 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
43 + ReactFeatureFlags.enableProfilerTimer = true;
44 + React = require('react');
45 + ReactTestRenderer = require('react-test-renderer');
46 +
47 + mockNowForTests();
48 +
49 + AdvanceTime = class extends React.Component {
50 + static defaultProps = {
51 + byAmount: 10,
52 + shouldComponentUpdate: true,
53 + };
54 + shouldComponentUpdate(nextProps) {
55 + return nextProps.shouldComponentUpdate;
56 + }
57 + render() {
58 + // Simulate time passing when this component is rendered
59 + advanceTimeBy(this.props.byAmount);
60 + return this.props.children || null;
61 + }
62 + };
63 + });
64 +
65 + it('should auto-Profile all fibers if the DevTools hook is detected', () => {
66 + const App = ({multiplier}) => {
67 + advanceTimeBy(2);
68 + return (
69 + <React.unstable_Profiler id="Profiler" onRender={onRender}>
70 + <AdvanceTime byAmount={3 * multiplier} shouldComponentUpdate={true} />
71 + <AdvanceTime
72 + byAmount={7 * multiplier}
73 + shouldComponentUpdate={false}
74 + />
75 + </React.unstable_Profiler>
76 + );
77 + };
78 +
79 + const onRender = jest.fn(() => {});
80 + const rendered = ReactTestRenderer.create(<App multiplier={1} />);
81 +
82 + expect(hook.onCommitFiberRoot).toHaveBeenCalledTimes(1);
83 +
84 + // Measure observable timing using the Profiler component.
85 + // The time spent in App (above the Profiler) won't be included in the durations,
86 + // But needs to be accounted for in the offset times.
87 + expect(onRender).toHaveBeenCalledTimes(1);
88 + expect(onRender).toHaveBeenCalledWith('Profiler', 'mount', 10, 10, 2, 12);
89 + onRender.mockClear();
90 +
91 + // Measure unobservable timing required by the DevTools profiler.
92 + // At this point, the base time should include both:
93 + // The time 2ms in the App component itself, and
94 + // The 10ms spend in the Profiler sub-tree beneath.
95 + expect(rendered.root.findByType(App)._currentFiber().treeBaseTime).toBe(12);
96 +
97 + rendered.update(<App multiplier={2} />);
98 +
99 + // Measure observable timing using the Profiler component.
100 + // The time spent in App (above the Profiler) won't be included in the durations,
101 + // But needs to be accounted for in the offset times.
102 + expect(onRender).toHaveBeenCalledTimes(1);
103 + expect(onRender).toHaveBeenCalledWith('Profiler', 'update', 6, 13, 14, 20);
104 +
105 + // Measure unobservable timing required by the DevTools profiler.
106 + // At this point, the base time should include both:
107 + // The initial 9ms for the components that do not re-render, and
108 + // The updated 6ms for the component that does.
109 + expect(rendered.root.findByType(App)._currentFiber().treeBaseTime).toBe(15);
110 + });
111 +});