Skip to content

Commit b0f6089

Browse files
authored
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
1 parent ae8c6dd commit b0f6089

File tree

4 files changed

+135
-1
lines changed

4 files changed

+135
-1
lines changed

packages/react-reconciler/src/ReactFiber.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from 'shared/ReactTypeOfWork';
3636
import getComponentName from 'shared/getComponentName';
3737

38+
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
3839
import {NoWork} from './ReactFiberExpirationTime';
3940
import {NoContext, AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode';
4041
import {
@@ -345,7 +346,15 @@ export function createWorkInProgress(
345346
}
346347

347348
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+
349358
return createFiber(HostRoot, null, null, mode);
350359
}
351360

packages/react-reconciler/src/ReactFiberDevToolsHook.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ function catchErrors(fn) {
3131
};
3232
}
3333

34+
export const isDevToolsPresent =
35+
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined';
36+
3437
export function injectInternals(internals: Object): boolean {
3538
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
3639
// No DevTools

packages/react-test-renderer/src/ReactTestRenderer.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
Profiler,
3030
} from 'shared/ReactTypeOfWork';
3131
import invariant from 'shared/invariant';
32+
import ReactVersion from 'shared/ReactVersion';
3233

3334
import * as ReactTestHostConfig from './ReactTestHostConfig';
3435
import * as TestRendererScheduling from './ReactTestRendererScheduling';
@@ -510,4 +511,14 @@ const ReactTestRendererFiber = {
510511
unstable_setNowImplementation: TestRendererScheduling.setNowImplementation,
511512
};
512513

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+
513524
export default ReactTestRendererFiber;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});

0 commit comments

Comments
 (0)