Skip to content

Commit 6764526

Browse files
author
Brian Vaughn
committed
Scheduling Profiler: De-emphasize React internal frames
This commit adds code to all React bundles to explicitly register the beginning and ending of their own code. This is done by creating Error objects (which capture a file name, line number, and column number) and registering them to a DevTools hook if 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 b81de86 commit 6764526

File tree

15 files changed

+332
-13
lines changed

15 files changed

+332
-13
lines changed

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

Lines changed: 5 additions & 3 deletions
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

Lines changed: 6 additions & 4 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 88 additions & 6 deletions
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,
@@ -20,6 +21,11 @@ import type {
2021
ViewRefs,
2122
} from '../view-base';
2223

24+
import {
25+
CHROME_WEBSTORE_EXTENSION_ID,
26+
INTERNAL_EXTENSION_ID,
27+
LOCAL_EXTENSION_ID,
28+
} from 'react-devtools-shared/src/constants';
2329
import {
2430
BackgroundColorView,
2531
Surface,
@@ -69,13 +75,65 @@ function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
6975
return hslaColorToString(color);
7076
}
7177

78+
function isInternalModule(
79+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
80+
flamechartStackFrame: FlamechartStackFrame,
81+
): boolean {
82+
const {locationColumn, locationLine, scriptUrl} = flamechartStackFrame;
83+
84+
if (scriptUrl == null || locationColumn == null || locationLine == null) {
85+
return true;
86+
}
87+
88+
// Internal modules are only registered if DevTools was running when the profile was captured,
89+
// but DevTools should also hide its own frames to avoid over-emphasizing them.
90+
if (
91+
// Handle webpack-internal:// sources
92+
scriptUrl.includes('/react-devtools') ||
93+
scriptUrl.includes('/react_devtools') ||
94+
// Filter out known extension IDs
95+
scriptUrl.includes(CHROME_WEBSTORE_EXTENSION_ID) ||
96+
scriptUrl.includes(INTERNAL_EXTENSION_ID) ||
97+
scriptUrl.includes(LOCAL_EXTENSION_ID)
98+
99+
// Unfortunately this won't get everything, like relatively loaded chunks or Web Worker files.
100+
) {
101+
return true;
102+
}
103+
104+
// Filter out React internal packages.
105+
const ranges = internalModuleSourceToRanges.get(scriptUrl);
106+
if (ranges != null) {
107+
for (let i = 0; i < ranges.length; i++) {
108+
const [startStackFrame, stopStackFrame] = ranges[i];
109+
110+
const isAfterStart =
111+
locationLine > startStackFrame.lineNumber ||
112+
(locationLine === startStackFrame.lineNumber &&
113+
locationColumn >= startStackFrame.columnNumber);
114+
const isBeforeStop =
115+
locationLine < stopStackFrame.lineNumber ||
116+
(locationLine === stopStackFrame.lineNumber &&
117+
locationColumn <= stopStackFrame.columnNumber);
118+
119+
if (isAfterStart && isBeforeStop) {
120+
return true;
121+
}
122+
}
123+
}
124+
125+
return false;
126+
}
127+
72128
class FlamechartStackLayerView extends View {
73129
/** Layer to display */
74130
_stackLayer: FlamechartStackLayer;
75131

76132
/** A set of `stackLayer`'s frames, for efficient lookup. */
77133
_stackFrameSet: Set<FlamechartStackFrame>;
78134

135+
_internalModuleSourceToRanges: InternalModuleSourceToRanges;
136+
79137
_intrinsicSize: Size;
80138

81139
_hoveredStackFrame: FlamechartStackFrame | null = null;
@@ -85,11 +143,13 @@ class FlamechartStackLayerView extends View {
85143
surface: Surface,
86144
frame: Rect,
87145
stackLayer: FlamechartStackLayer,
146+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
88147
duration: number,
89148
) {
90149
super(surface, frame);
91150
this._stackLayer = stackLayer;
92151
this._stackFrameSet = new Set(stackLayer);
152+
this._internalModuleSourceToRanges = internalModuleSourceToRanges;
93153
this._intrinsicSize = {
94154
width: duration,
95155
height: FLAMECHART_FRAME_HEIGHT,
@@ -160,9 +220,19 @@ class FlamechartStackLayerView extends View {
160220
}
161221

162222
const showHoverHighlight = _hoveredStackFrame === _stackLayer[i];
163-
context.fillStyle = showHoverHighlight
164-
? hoverColorForStackFrame(stackFrame)
165-
: defaultColorForStackFrame(stackFrame);
223+
224+
let textFillStyle;
225+
if (isInternalModule(this._internalModuleSourceToRanges, stackFrame)) {
226+
context.fillStyle = showHoverHighlight
227+
? COLORS.INTERNAL_MODULE_FRAME_HOVER
228+
: COLORS.INTERNAL_MODULE_FRAME;
229+
textFillStyle = COLORS.INTERNAL_MODULE_FRAME_TEXT;
230+
} else {
231+
context.fillStyle = showHoverHighlight
232+
? hoverColorForStackFrame(stackFrame)
233+
: defaultColorForStackFrame(stackFrame);
234+
textFillStyle = COLORS.TEXT_COLOR;
235+
}
166236

167237
const drawableRect = intersectionOfRects(nodeRect, visibleArea);
168238
context.fillRect(
@@ -172,7 +242,9 @@ class FlamechartStackLayerView extends View {
172242
drawableRect.size.height,
173243
);
174244

175-
drawText(name, context, nodeRect, drawableRect);
245+
drawText(name, context, nodeRect, drawableRect, {
246+
fillStyle: textFillStyle,
247+
});
176248
}
177249

178250
// Render bottom border.
@@ -264,13 +336,22 @@ export class FlamechartView extends View {
264336
surface: Surface,
265337
frame: Rect,
266338
flamechart: Flamechart,
339+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
267340
duration: number,
268341
) {
269342
super(surface, frame, layeredLayout);
270-
this.setDataAndUpdateSubviews(flamechart, duration);
343+
this.setDataAndUpdateSubviews(
344+
flamechart,
345+
internalModuleSourceToRanges,
346+
duration,
347+
);
271348
}
272349

273-
setDataAndUpdateSubviews(flamechart: Flamechart, duration: number) {
350+
setDataAndUpdateSubviews(
351+
flamechart: Flamechart,
352+
internalModuleSourceToRanges: InternalModuleSourceToRanges,
353+
duration: number,
354+
) {
274355
const {surface, frame, _onHover, _hoveredStackFrame} = this;
275356

276357
// Clear existing rows on data update
@@ -285,6 +366,7 @@ export class FlamechartView extends View {
285366
surface,
286367
frame,
287368
stackLayer,
369+
internalModuleSourceToRanges,
288370
duration,
289371
);
290372
this._verticalStackView.addSubview(rowView);

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

Lines changed: 12 additions & 0 deletions
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
),

packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ describe('preprocessData', () => {
282282
"componentMeasures": Array [],
283283
"duration": 0.005,
284284
"flamechart": Array [],
285+
"internalModuleSourceToRanges": Map {},
285286
"laneToLabelMap": Map {
286287
0 => "Sync",
287288
1 => "InputContinuousHydration",
@@ -449,6 +450,7 @@ describe('preprocessData', () => {
449450
"componentMeasures": Array [],
450451
"duration": 0.011,
451452
"flamechart": Array [],
453+
"internalModuleSourceToRanges": Map {},
452454
"laneToLabelMap": Map {
453455
0 => "Sync",
454456
1 => "InputContinuousHydration",
@@ -636,6 +638,7 @@ describe('preprocessData', () => {
636638
"componentMeasures": Array [],
637639
"duration": 0.013,
638640
"flamechart": Array [],
641+
"internalModuleSourceToRanges": Map {},
639642
"laneToLabelMap": Map {
640643
0 => "Sync",
641644
1 => "InputContinuousHydration",
@@ -914,6 +917,7 @@ describe('preprocessData', () => {
914917
],
915918
"duration": 0.031,
916919
"flamechart": Array [],
920+
"internalModuleSourceToRanges": Map {},
917921
"laneToLabelMap": Map {
918922
0 => "Sync",
919923
1 => "InputContinuousHydration",

packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@elg/speedscope';
1414
import type {TimelineEvent} from '@elg/speedscope';
1515
import type {
16+
ErrorStackFrame,
1617
BatchUID,
1718
Flamechart,
1819
Milliseconds,
@@ -30,6 +31,7 @@ import type {
3031
import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants';
3132
import InvalidProfileError from './InvalidProfileError';
3233
import {getBatchRange} from '../utils/getBatchRange';
34+
import ErrorStackParser from 'error-stack-parser';
3335

3436
type MeasureStackElement = {|
3537
type: ReactMeasureType,
@@ -43,6 +45,8 @@ type ProcessorState = {|
4345
asyncProcessingPromises: Promise<any>[],
4446
batchUID: BatchUID,
4547
currentReactComponentMeasure: ReactComponentMeasure | null,
48+
internalModuleCurrentStackFrame: ErrorStackFrame | null,
49+
internalModuleStackStringSet: Set<string>,
4650
measureStack: MeasureStackElement[],
4751
nativeEventStack: NativeEvent[],
4852
nextRenderShouldGenerateNewBatchID: boolean,
@@ -793,6 +797,49 @@ function processTimelineEvent(
793797
);
794798
} // eslint-disable-line brace-style
795799

800+
// Internal module ranges
801+
else if (name.startsWith('--react-internal-module-start-')) {
802+
const stackFrameStart = name.substr(30);
803+
804+
if (!state.internalModuleStackStringSet.has(stackFrameStart)) {
805+
state.internalModuleStackStringSet.add(stackFrameStart);
806+
807+
const parsedStackFrameStart = parseStackFrame(stackFrameStart);
808+
809+
state.internalModuleCurrentStackFrame = parsedStackFrameStart;
810+
}
811+
} else if (name.startsWith('--react-internal-module-stop-')) {
812+
const stackFrameStop = name.substr(19);
813+
814+
if (!state.internalModuleStackStringSet.has(stackFrameStop)) {
815+
state.internalModuleStackStringSet.add(stackFrameStop);
816+
817+
const parsedStackFrameStop = parseStackFrame(stackFrameStop);
818+
819+
if (
820+
parsedStackFrameStop !== null &&
821+
state.internalModuleCurrentStackFrame !== null
822+
) {
823+
const parsedStackFrameStart = state.internalModuleCurrentStackFrame;
824+
825+
state.internalModuleCurrentStackFrame = null;
826+
827+
const range = [parsedStackFrameStart, parsedStackFrameStop];
828+
const ranges = currentProfilerData.internalModuleSourceToRanges.get(
829+
parsedStackFrameStart.fileName,
830+
);
831+
if (ranges == null) {
832+
currentProfilerData.internalModuleSourceToRanges.set(
833+
parsedStackFrameStart.fileName,
834+
[range],
835+
);
836+
} else {
837+
ranges.push(range);
838+
}
839+
}
840+
}
841+
} // eslint-disable-line brace-style
842+
796843
// Other user timing marks/measures
797844
else if (ph === 'R' || ph === 'n') {
798845
// User Timing mark
@@ -855,6 +902,15 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
855902
return flamechart;
856903
}
857904

905+
function parseStackFrame(stackFrame: string): ErrorStackFrame | null {
906+
const error = new Error();
907+
error.stack = stackFrame;
908+
909+
const frames = ErrorStackParser.parse(error);
910+
911+
return frames.length === 1 ? frames[0] : null;
912+
}
913+
858914
export default async function preprocessData(
859915
timeline: TimelineEvent[],
860916
): Promise<ReactProfilerData> {
@@ -870,6 +926,7 @@ export default async function preprocessData(
870926
componentMeasures: [],
871927
duration: 0,
872928
flamechart,
929+
internalModuleSourceToRanges: new Map(),
873930
laneToLabelMap: new Map(),
874931
laneToReactMeasureMap,
875932
nativeEvents: [],
@@ -913,6 +970,8 @@ export default async function preprocessData(
913970
asyncProcessingPromises: [],
914971
batchUID: 0,
915972
currentReactComponentMeasure: null,
973+
internalModuleCurrentStackFrame: null,
974+
internalModuleStackStringSet: new Set(),
916975
measureStack: [],
917976
nativeEventStack: [],
918977
nextRenderShouldGenerateNewBatchID: true,

0 commit comments

Comments
 (0)