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