Skip to content

Commit 4ba2057

Browse files
author
Brian Vaughn
authored
Scheduling Profiler: De-emphasize React internal frames (#22588)
This commit adds code to all React bundles to explicitly register the beginning and ending of the module. This is done by creating Error objects (which capture the file name, line number, and column number) and passing them explicitly to a DevTools hook (when present). Next, as the Scheduling Profiler logs metadata to the User Timing API, it prints these module ranges along with other metadata (like Lane values and profiler version number). Lastly, the Scheduling Profiler UI compares stack frames to these ranges when drawing the flame graph and dims or de-emphasizes frames that fall within an internal module. The net effect of this is that user code (and 3rd party code) stands out clearly in the flame graph while React internal modules are dimmed. Internal module ranges are completely optional. Older profiling samples, or ones recorded without the React DevTools extension installed, will simply not dim the internal frames.
1 parent f6abf4b commit 4ba2057

File tree

21 files changed

+543
-15
lines changed

21 files changed

+543
-15
lines changed

packages/react-devtools-extensions/src/checkForDuplicateInstallations.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99

1010
declare var chrome: any;
1111

12-
import {__DEBUG__} from 'react-devtools-shared/src/constants';
12+
import {
13+
INTERNAL_EXTENSION_ID,
14+
LOCAL_EXTENSION_ID,
15+
__DEBUG__,
16+
} from 'react-devtools-shared/src/constants';
1317
import {getBrowserName} from './utils';
1418
import {
1519
EXTENSION_INSTALL_CHECK,
1620
EXTENSION_INSTALLATION_TYPE,
17-
INTERNAL_EXTENSION_ID,
18-
LOCAL_EXTENSION_ID,
1921
} from './constants';
2022

2123
const IS_CHROME = getBrowserName() === 'Chrome';

packages/react-devtools-extensions/src/constants.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
* @flow strict-local
88
*/
99

10+
import {
11+
CHROME_WEBSTORE_EXTENSION_ID,
12+
INTERNAL_EXTENSION_ID,
13+
LOCAL_EXTENSION_ID,
14+
} from 'react-devtools-shared/src/constants';
15+
1016
declare var chrome: any;
1117

1218
export const CURRENT_EXTENSION_ID = chrome.runtime.id;
@@ -15,10 +21,6 @@ export const EXTENSION_INSTALL_CHECK = 'extension-install-check';
1521
export const SHOW_DUPLICATE_EXTENSION_WARNING =
1622
'show-duplicate-extension-warning';
1723

18-
export const CHROME_WEBSTORE_EXTENSION_ID = 'fmkadmapgofadopljbjfkapdkoienihi';
19-
export const INTERNAL_EXTENSION_ID = 'dnjnjgbfilfphmojnmhliehogmojhclc';
20-
export const LOCAL_EXTENSION_ID = 'ikiahnapldjmdmpkmfhjdjilojjhgcbf';
21-
2224
export const EXTENSION_INSTALLATION_TYPE:
2325
| 'public'
2426
| 'internal'

packages/react-devtools-scheduling-profiler/src/CanvasPage.js

+1
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ function AutoSizedCanvas({
374374
surface,
375375
defaultFrame,
376376
data.flamechart,
377+
data.internalModuleSourceToRanges,
377378
data.duration,
378379
);
379380
flamechartViewRef.current = flamechartView;

packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js

+34-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
Flamechart,
1212
FlamechartStackFrame,
1313
FlamechartStackLayer,
14+
InternalModuleSourceToRanges,
1415
} from '../types';
1516
import type {
1617
Interaction,
@@ -30,6 +31,7 @@ import {
3031
rectIntersectsRect,
3132
verticallyStackedLayout,
3233
} from '../view-base';
34+
import {isInternalModule} from './utils/moduleFilters';
3335
import {
3436
durationToWidth,
3537
positioningScaleFactor,
@@ -76,6 +78,8 @@ class FlamechartStackLayerView extends View {
7678
/** A set of `stackLayer`'s frames, for efficient lookup. */
7779
_stackFrameSet: Set<FlamechartStackFrame>;
7880

81+
_internalModuleSourceToRanges: InternalModuleSourceToRanges;
82+
7983
_intrinsicSize: Size;
8084

8185
_hoveredStackFrame: FlamechartStackFrame | null = null;
@@ -85,11 +89,13 @@ class FlamechartStackLayerView extends View {
8589
surface: Surface,
8690
frame: Rect,
8791
stackLayer: FlamechartStackLayer,
92+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
8893
duration: number,
8994
) {
9095
super(surface, frame);
9196
this._stackLayer = stackLayer;
9297
this._stackFrameSet = new Set(stackLayer);
98+
this._internalModuleSourceToRanges = internalModuleSourceToRanges;
9399
this._intrinsicSize = {
94100
width: duration,
95101
height: FLAMECHART_FRAME_HEIGHT,
@@ -160,9 +166,19 @@ class FlamechartStackLayerView extends View {
160166
}
161167

162168
const showHoverHighlight = _hoveredStackFrame === _stackLayer[i];
163-
context.fillStyle = showHoverHighlight
164-
? hoverColorForStackFrame(stackFrame)
165-
: defaultColorForStackFrame(stackFrame);
169+
170+
let textFillStyle;
171+
if (isInternalModule(this._internalModuleSourceToRanges, stackFrame)) {
172+
context.fillStyle = showHoverHighlight
173+
? COLORS.INTERNAL_MODULE_FRAME_HOVER
174+
: COLORS.INTERNAL_MODULE_FRAME;
175+
textFillStyle = COLORS.INTERNAL_MODULE_FRAME_TEXT;
176+
} else {
177+
context.fillStyle = showHoverHighlight
178+
? hoverColorForStackFrame(stackFrame)
179+
: defaultColorForStackFrame(stackFrame);
180+
textFillStyle = COLORS.TEXT_COLOR;
181+
}
166182

167183
const drawableRect = intersectionOfRects(nodeRect, visibleArea);
168184
context.fillRect(
@@ -172,7 +188,9 @@ class FlamechartStackLayerView extends View {
172188
drawableRect.size.height,
173189
);
174190

175-
drawText(name, context, nodeRect, drawableRect);
191+
drawText(name, context, nodeRect, drawableRect, {
192+
fillStyle: textFillStyle,
193+
});
176194
}
177195

178196
// Render bottom border.
@@ -264,13 +282,22 @@ export class FlamechartView extends View {
264282
surface: Surface,
265283
frame: Rect,
266284
flamechart: Flamechart,
285+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
267286
duration: number,
268287
) {
269288
super(surface, frame, layeredLayout);
270-
this.setDataAndUpdateSubviews(flamechart, duration);
289+
this.setDataAndUpdateSubviews(
290+
flamechart,
291+
internalModuleSourceToRanges,
292+
duration,
293+
);
271294
}
272295

273-
setDataAndUpdateSubviews(flamechart: Flamechart, duration: number) {
296+
setDataAndUpdateSubviews(
297+
flamechart: Flamechart,
298+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
299+
duration: number,
300+
) {
274301
const {surface, frame, _onHover, _hoveredStackFrame} = this;
275302

276303
// Clear existing rows on data update
@@ -285,6 +312,7 @@ export class FlamechartView extends View {
285312
surface,
286313
frame,
287314
stackLayer,
315+
internalModuleSourceToRanges,
288316
duration,
289317
);
290318
this._verticalStackView.addSubview(rowView);

packages/react-devtools-scheduling-profiler/src/content-views/constants.js

+12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export const MIN_INTERVAL_SIZE_PX = 70;
4545
// TODO Replace this with "export let" vars
4646
export let COLORS = {
4747
BACKGROUND: '',
48+
INTERNAL_MODULE_FRAME: '',
49+
INTERNAL_MODULE_FRAME_HOVER: '',
50+
INTERNAL_MODULE_FRAME_TEXT: '',
4851
NATIVE_EVENT: '',
4952
NATIVE_EVENT_HOVER: '',
5053
NETWORK_PRIMARY: '',
@@ -107,6 +110,15 @@ export function updateColorsToMatchTheme(element: Element): boolean {
107110

108111
COLORS = {
109112
BACKGROUND: computedStyle.getPropertyValue('--color-background'),
113+
INTERNAL_MODULE_FRAME: computedStyle.getPropertyValue(
114+
'--color-scheduling-profiler-internal-module',
115+
),
116+
INTERNAL_MODULE_FRAME_HOVER: computedStyle.getPropertyValue(
117+
'--color-scheduling-profiler-internal-module-hover',
118+
),
119+
INTERNAL_MODULE_FRAME_TEXT: computedStyle.getPropertyValue(
120+
'--color-scheduling-profiler-internal-module-text',
121+
),
110122
NATIVE_EVENT: computedStyle.getPropertyValue(
111123
'--color-scheduling-profiler-native-event',
112124
),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
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+
* @flow
8+
*/
9+
10+
export const outerErrorA = new Error();
11+
12+
export const moduleStartError = new Error();
13+
export const innerError = new Error();
14+
export const moduleStopError = new Error();
15+
16+
export const outerErrorB = new Error();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
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+
* @flow
8+
*/
9+
10+
export const moduleAStartError = new Error();
11+
export const innerErrorA = new Error();
12+
export const moduleAStopError = new Error();
13+
14+
export const outerError = new Error();
15+
16+
export const moduleBStartError = new Error();
17+
export const innerErrorB = new Error();
18+
export const moduleBStopError = new Error();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
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+
* @flow
8+
*/
9+
10+
import {isInternalModule} from '../moduleFilters';
11+
12+
describe('isInternalModule', () => {
13+
let map;
14+
15+
function createFlamechartStackFrame(scriptUrl, locationLine, locationColumn) {
16+
return {
17+
name: 'test',
18+
timestamp: 0,
19+
duration: 1,
20+
scriptUrl,
21+
locationLine,
22+
locationColumn,
23+
};
24+
}
25+
26+
function createStackFrame(fileName, lineNumber, columnNumber) {
27+
return {
28+
columnNumber: columnNumber,
29+
lineNumber: lineNumber,
30+
fileName: fileName,
31+
functionName: 'test',
32+
source: ` at test (${fileName}:${lineNumber}:${columnNumber})`,
33+
};
34+
}
35+
36+
beforeEach(() => {
37+
map = new Map();
38+
map.set('foo', [
39+
[createStackFrame('foo', 10, 0), createStackFrame('foo', 15, 100)],
40+
]);
41+
map.set('bar', [
42+
[createStackFrame('bar', 10, 0), createStackFrame('bar', 15, 100)],
43+
[createStackFrame('bar', 20, 0), createStackFrame('bar', 25, 100)],
44+
]);
45+
});
46+
47+
it('should properly identify stack frames within the provided module ranges', () => {
48+
expect(
49+
isInternalModule(map, createFlamechartStackFrame('foo', 10, 0)),
50+
).toBe(true);
51+
expect(
52+
isInternalModule(map, createFlamechartStackFrame('foo', 12, 35)),
53+
).toBe(true);
54+
expect(
55+
isInternalModule(map, createFlamechartStackFrame('foo', 15, 100)),
56+
).toBe(true);
57+
expect(
58+
isInternalModule(map, createFlamechartStackFrame('bar', 12, 0)),
59+
).toBe(true);
60+
expect(
61+
isInternalModule(map, createFlamechartStackFrame('bar', 22, 125)),
62+
).toBe(true);
63+
});
64+
65+
it('should properly identify stack frames outside of the provided module ranges', () => {
66+
expect(isInternalModule(map, createFlamechartStackFrame('foo', 9, 0))).toBe(
67+
false,
68+
);
69+
expect(
70+
isInternalModule(map, createFlamechartStackFrame('foo', 15, 101)),
71+
).toBe(false);
72+
expect(
73+
isInternalModule(map, createFlamechartStackFrame('bar', 17, 0)),
74+
).toBe(false);
75+
expect(
76+
isInternalModule(map, createFlamechartStackFrame('baz', 12, 0)),
77+
).toBe(false);
78+
});
79+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
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+
* @flow
8+
*/
9+
10+
import type {
11+
FlamechartStackFrame,
12+
InternalModuleSourceToRanges,
13+
} from '../../types';
14+
15+
import {
16+
CHROME_WEBSTORE_EXTENSION_ID,
17+
INTERNAL_EXTENSION_ID,
18+
LOCAL_EXTENSION_ID,
19+
} from 'react-devtools-shared/src/constants';
20+
21+
export function isInternalModule(
22+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
23+
flamechartStackFrame: FlamechartStackFrame,
24+
): boolean {
25+
const {locationColumn, locationLine, scriptUrl} = flamechartStackFrame;
26+
27+
if (scriptUrl == null || locationColumn == null || locationLine == null) {
28+
// This could indicate a browser-internal API like performance.mark().
29+
return false;
30+
}
31+
32+
// Internal modules are only registered if DevTools was running when the profile was captured,
33+
// but DevTools should also hide its own frames to avoid over-emphasizing them.
34+
if (
35+
// Handle webpack-internal:// sources
36+
scriptUrl.includes('/react-devtools') ||
37+
scriptUrl.includes('/react_devtools') ||
38+
// Filter out known extension IDs
39+
scriptUrl.includes(CHROME_WEBSTORE_EXTENSION_ID) ||
40+
scriptUrl.includes(INTERNAL_EXTENSION_ID) ||
41+
scriptUrl.includes(LOCAL_EXTENSION_ID)
42+
// Unfortunately this won't get everything, like relatively loaded chunks or Web Worker files.
43+
) {
44+
return true;
45+
}
46+
47+
// Filter out React internal packages.
48+
const ranges = internalModuleSourceToRanges.get(scriptUrl);
49+
if (ranges != null) {
50+
for (let i = 0; i < ranges.length; i++) {
51+
const [startStackFrame, stopStackFrame] = ranges[i];
52+
53+
const isAfterStart =
54+
locationLine > startStackFrame.lineNumber ||
55+
(locationLine === startStackFrame.lineNumber &&
56+
locationColumn >= startStackFrame.columnNumber);
57+
const isBeforeStop =
58+
locationLine < stopStackFrame.lineNumber ||
59+
(locationLine === stopStackFrame.lineNumber &&
60+
locationColumn <= stopStackFrame.columnNumber);
61+
62+
if (isAfterStart && isBeforeStop) {
63+
return true;
64+
}
65+
}
66+
}
67+
68+
return false;
69+
}

0 commit comments

Comments
 (0)