Scheduling profiler: Improve native events UI (#21966)
Also highlight events that have synchronous updates inside of them. (We may want to relax this highlighting later to not warn about event handlers that are still fast enough.)
Brian Vaughn committed
Jul 26, 2021 at 19:30 UTC
c21e8fccadc0e0351bc45c2df729c92807a647ef
11 files changed
+265
-123
packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js
+7
-7
@@ -32,9 +32,9 @@ import {
32
} from './utils/positioning';
33
import {
34
COLORS,
35
- FLAMECHART_FONT_SIZE,
35
+ FONT_SIZE,
36
FLAMECHART_FRAME_HEIGHT,
37
- FLAMECHART_TEXT_PADDING,
37
+ TEXT_PADDING,
38
COLOR_HOVER_DIM_DELTA,
39
BORDER_SIZE,
40
} from './constants';
@@ -157,7 +157,7 @@ class FlamechartStackLayerView extends View {
157
158
context.textAlign = 'left';
159
context.textBaseline = 'middle';
160
- context.font = `${FLAMECHART_FONT_SIZE}px sans-serif`;
160
+ context.font = `${FONT_SIZE}px sans-serif`;
161
162
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
163
@@ -195,15 +195,15 @@ class FlamechartStackLayerView extends View {
195
drawableRect.size.height,
196
);
197
198
- if (width > FLAMECHART_TEXT_PADDING * 2) {
198
+ if (width > TEXT_PADDING * 2) {
199
const trimmedName = trimFlamechartText(
200
context,
201
name,
202
- width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0),
202
+ width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
203
);
204
205
if (trimmedName !== null) {
206
- context.fillStyle = COLORS.FLAME_GRAPH_LABEL;
206
+ context.fillStyle = COLORS.TEXT_COLOR;
207
208
// Prevent text from being drawn outside `viewableArea`
209
const textOverflowsViewableArea = !rectEqualToRect(
@@ -225,7 +225,7 @@ class FlamechartStackLayerView extends View {
225
226
context.fillText(
227
trimmedName,
228
- nodeRect.origin.x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0),
228
+ nodeRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0),
229
nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2,
230
);
231
packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js
+149
-75
@@ -11,9 +11,10 @@ import type {NativeEvent, ReactProfilerData} from '../types';
11
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
12
13
import {
14
+ durationToWidth,
15
positioningScaleFactor,
15
- timestampToPosition,
16
positionToTimestamp,
17
+ timestampToPosition,
18
} from './utils/positioning';
19
import {
20
View,
@@ -24,28 +25,77 @@ import {
25
} from '../view-base';
26
import {
27
COLORS,
27
- EVENT_ROW_PADDING,
28
- EVENT_DIAMETER,
28
+ TEXT_PADDING,
29
+ NATIVE_EVENT_HEIGHT,
30
+ FONT_SIZE,
31
BORDER_SIZE,
32
} from './constants';
33
32
-const EVENT_ROW_HEIGHT_FIXED =
33
- EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING;
34
+const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE;
35
+
36
+// TODO (scheduling profiler) Make this a reusable util
37
+const cachedFlamechartTextWidths = new Map();
38
+const trimFlamechartText = (
39
+ context: CanvasRenderingContext2D,
40
+ text: string,
41
+ width: number,
42
+) => {
43
+ for (let i = text.length - 1; i >= 0; i--) {
44
+ const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
45
+
46
+ let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
47
+ if (measuredWidth == null) {
48
+ measuredWidth = context.measureText(trimmedText).width;
49
+ cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
50
+ }
51
+
52
+ if (measuredWidth <= width) {
53
+ return trimmedText;
54
+ }
55
+ }
56
+
57
+ return null;
58
+};
59
60
export class NativeEventsView extends View {
36
- _profilerData: ReactProfilerData;
61
+ _depthToNativeEvent: Map<number, NativeEvent[]>;
62
+ _hoveredEvent: NativeEvent | null = null;
63
_intrinsicSize: Size;
64
+ _maxDepth: number = 0;
65
+ _profilerData: ReactProfilerData;
66
39
- _hoveredEvent: NativeEvent | null = null;
67
onHover: ((event: NativeEvent | null) => void) | null = null;
68
69
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
70
super(surface, frame);
71
+
72
this._profilerData = profilerData;
73
74
+ this._performPreflightComputations();
75
+ console.log(this._depthToNativeEvent);
76
+ }
77
+
78
+ _performPreflightComputations() {
79
+ this._depthToNativeEvent = new Map();
80
+
81
+ const {duration, nativeEvents} = this._profilerData;
82
+
83
+ nativeEvents.forEach(event => {
84
+ const depth = event.depth;
85
+
86
+ this._maxDepth = Math.max(this._maxDepth, depth);
87
+
88
+ if (!this._depthToNativeEvent.has(depth)) {
89
+ this._depthToNativeEvent.set(depth, [event]);
90
+ } else {
91
+ // $FlowFixMe This is unnecessary.
92
+ this._depthToNativeEvent.get(depth).push(event);
93
+ }
94
+ });
95
+
96
this._intrinsicSize = {
47
- width: this._profilerData.duration,
48
- height: EVENT_ROW_HEIGHT_FIXED,
97
+ width: duration,
98
+ height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
99
};
100
}
101
@@ -73,7 +123,9 @@ export class NativeEventsView extends View {
123
showHoverHighlight: boolean,
124
) {
125
const {frame} = this;
76
- const {duration, timestamp} = event;
126
+ const {depth, duration, highlight, timestamp, type} = event;
127
+
128
+ baseY += depth * ROW_WITH_BORDER_HEIGHT;
129
130
const xStart = timestampToPosition(timestamp, scaleFactor, frame);
131
const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
@@ -82,25 +134,60 @@ export class NativeEventsView extends View {
134
x: xStart,
135
y: baseY,
136
},
85
- size: {width: xStop - xStart, height: EVENT_DIAMETER},
137
+ size: {width: xStop - xStart, height: NATIVE_EVENT_HEIGHT},
138
};
139
if (!rectIntersectsRect(eventRect, rect)) {
140
return; // Not in view
141
}
142
91
- const fillStyle = showHoverHighlight
92
- ? COLORS.NATIVE_EVENT_HOVER
93
- : COLORS.NATIVE_EVENT;
143
+ const width = durationToWidth(duration, scaleFactor);
144
+ if (width < 1) {
145
+ return; // Too small to render at this zoom level
146
+ }
147
148
const drawableRect = intersectionOfRects(eventRect, rect);
149
context.beginPath();
97
- context.fillStyle = fillStyle;
150
+ if (highlight) {
151
+ context.fillStyle = showHoverHighlight
152
+ ? COLORS.NATIVE_EVENT_WARNING_HOVER
153
+ : COLORS.NATIVE_EVENT_WARNING;
154
+ } else {
155
+ context.fillStyle = showHoverHighlight
156
+ ? COLORS.NATIVE_EVENT_HOVER
157
+ : COLORS.NATIVE_EVENT;
158
+ }
159
context.fillRect(
160
drawableRect.origin.x,
161
drawableRect.origin.y,
162
drawableRect.size.width,
163
drawableRect.size.height,
164
);
165
+
166
+ // Render event type label
167
+ context.textAlign = 'left';
168
+ context.textBaseline = 'middle';
169
+ context.font = `${FONT_SIZE}px sans-serif`;
170
+
171
+ if (width > TEXT_PADDING * 2) {
172
+ const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
173
+ const trimmedName = trimFlamechartText(
174
+ context,
175
+ type,
176
+ width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
177
+ );
178
+
179
+ if (trimmedName !== null) {
180
+ context.fillStyle = highlight
181
+ ? COLORS.NATIVE_EVENT_WARNING_TEXT
182
+ : COLORS.TEXT_COLOR;
183
+
184
+ context.fillText(
185
+ trimmedName,
186
+ eventRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0),
187
+ eventRect.origin.y + NATIVE_EVENT_HEIGHT / 2,
188
+ );
189
+ }
190
+ }
191
}
192
193
draw(context: CanvasRenderingContext2D) {
@@ -111,7 +198,7 @@ export class NativeEventsView extends View {
198
visibleArea,
199
} = this;
200
114
- context.fillStyle = COLORS.BACKGROUND;
201
+ context.fillStyle = COLORS.PRIORITY_BACKGROUND;
202
context.fillRect(
203
visibleArea.origin.x,
204
visibleArea.origin.y,
@@ -120,57 +207,43 @@ export class NativeEventsView extends View {
207
);
208
209
// Draw events
123
- const baseY = frame.origin.y + EVENT_ROW_PADDING;
210
const scaleFactor = positioningScaleFactor(
211
this._intrinsicSize.width,
212
frame,
213
);
214
215
nativeEvents.forEach(event => {
130
- if (event === _hoveredEvent) {
131
- // Draw the highlighted items on top so they stand out.
132
- // This is helpful if there are multiple (overlapping) items close to each other.
133
- this._drawSingleNativeEvent(
134
- context,
135
- visibleArea,
136
- event,
137
- baseY,
138
- scaleFactor,
139
- true,
140
- );
141
- } else {
142
- this._drawSingleNativeEvent(
143
- context,
144
- visibleArea,
145
- event,
146
- baseY,
147
- scaleFactor,
148
- false,
149
- );
150
- }
216
+ this._drawSingleNativeEvent(
217
+ context,
218
+ visibleArea,
219
+ event,
220
+ frame.origin.y,
221
+ scaleFactor,
222
+ event === _hoveredEvent,
223
+ );
224
});
225
153
- // Render bottom border.
154
- // Propose border rect, check if intersects with `rect`, draw intersection.
155
- const borderFrame: Rect = {
156
- origin: {
157
- x: frame.origin.x,
158
- y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE,
159
- },
160
- size: {
161
- width: frame.size.width,
162
- height: BORDER_SIZE,
163
- },
164
- };
165
- if (rectIntersectsRect(borderFrame, visibleArea)) {
166
- const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
167
- context.fillStyle = COLORS.PRIORITY_BORDER;
168
- context.fillRect(
169
- borderDrawableRect.origin.x,
170
- borderDrawableRect.origin.y,
171
- borderDrawableRect.size.width,
172
- borderDrawableRect.size.height,
173
- );
226
+ // Render bottom borders.
227
+ for (let i = 0; i <= this._maxDepth; i++) {
228
+ const borderFrame: Rect = {
229
+ origin: {
230
+ x: frame.origin.x,
231
+ y: frame.origin.y + NATIVE_EVENT_HEIGHT,
232
+ },
233
+ size: {
234
+ width: frame.size.width,
235
+ height: BORDER_SIZE,
236
+ },
237
+ };
238
+ if (rectIntersectsRect(borderFrame, visibleArea)) {
239
+ context.fillStyle = COLORS.PRIORITY_BORDER;
240
+ context.fillRect(
241
+ visibleArea.origin.x,
242
+ frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
243
+ visibleArea.size.width,
244
+ BORDER_SIZE,
245
+ );
246
+ }
247
}
248
}
249
@@ -189,25 +262,26 @@ export class NativeEventsView extends View {
262
return;
263
}
264
192
- const {nativeEvents} = this._profilerData;
193
-
265
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
266
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
267
197
- // Find the event being hovered over.
198
- //
199
- // Because data ranges may overlap, we want to find the last intersecting item.
200
- // This will always be the one on "top" (the one the user is hovering over).
201
- for (let index = nativeEvents.length - 1; index >= 0; index--) {
202
- const nativeEvent = nativeEvents[index];
203
- const {duration, timestamp} = nativeEvent;
204
-
205
- if (
206
- hoverTimestamp >= timestamp &&
207
- hoverTimestamp <= timestamp + duration
208
- ) {
209
- onHover(nativeEvent);
210
- return;
268
+ const adjustedCanvasMouseY = location.y - frame.origin.y;
269
+ const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT);
270
+ const nativeEventsAtDepth = this._depthToNativeEvent.get(depth);
271
+
272
+ if (nativeEventsAtDepth) {
273
+ // Find the event being hovered over.
274
+ for (let index = nativeEventsAtDepth.length - 1; index >= 0; index--) {
275
+ const nativeEvent = nativeEventsAtDepth[index];
276
+ const {duration, timestamp} = nativeEvent;
277
+
278
+ if (
279
+ hoverTimestamp >= timestamp &&
280
+ hoverTimestamp <= timestamp + duration
281
+ ) {
282
+ onHover(nativeEvent);
283
+ return;
284
+ }
285
}
286
}
287
packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js
+7
-7
@@ -25,13 +25,13 @@ import {
25
} from '../view-base';
26
import {
27
COLORS,
28
- EVENT_ROW_PADDING,
29
- EVENT_DIAMETER,
28
+ TOP_ROW_PADDING,
29
+ REACT_EVENT_DIAMETER,
30
BORDER_SIZE,
31
} from './constants';
32
33
const EVENT_ROW_HEIGHT_FIXED =
34
- EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING;
34
+ TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING;
35
36
function isSuspenseEvent(event: ReactEvent): boolean %checks {
37
return (
@@ -85,13 +85,13 @@ export class ReactEventsView extends View {
85
const {timestamp, type} = event;
86
87
const x = timestampToPosition(timestamp, scaleFactor, frame);
88
- const radius = EVENT_DIAMETER / 2;
88
+ const radius = REACT_EVENT_DIAMETER / 2;
89
const eventRect: Rect = {
90
origin: {
91
x: x - radius,
92
y: baseY,
93
},
94
- size: {width: EVENT_DIAMETER, height: EVENT_DIAMETER},
94
+ size: {width: REACT_EVENT_DIAMETER, height: REACT_EVENT_DIAMETER},
95
};
96
if (!rectIntersectsRect(eventRect, rect)) {
97
return; // Not in view
@@ -156,7 +156,7 @@ export class ReactEventsView extends View {
156
);
157
158
// Draw events
159
- const baseY = frame.origin.y + EVENT_ROW_PADDING;
159
+ const baseY = frame.origin.y + TOP_ROW_PADDING;
160
const scaleFactor = positioningScaleFactor(
161
this._intrinsicSize.width,
162
frame,
@@ -246,7 +246,7 @@ export class ReactEventsView extends View {
246
);
247
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
248
const eventTimestampAllowance = widthToDuration(
249
- EVENT_DIAMETER / 2,
249
+ REACT_EVENT_DIAMETER / 2,
250
scaleFactor,
251
);
252
packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
+1
-1
@@ -54,7 +54,7 @@ export class ReactMeasuresView extends View {
54
55
_performPreflightComputations() {
56
this._lanesToRender = [];
57
- this._laneToMeasures = new Map<ReactLane, ReactMeasure[]>();
57
+ this._laneToMeasures = new Map();
58
59
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
60
const measuresForLane = getMeasuresForLane(
packages/react-devtools-scheduling-profiler/src/content-views/TimeAxisMarkersView.js
+2
-2
@@ -25,7 +25,7 @@ import {
25
COLORS,
26
INTERVAL_TIMES,
27
LABEL_SIZE,
28
- MARKER_FONT_SIZE,
28
+ FONT_SIZE,
29
MARKER_HEIGHT,
30
MARKER_TEXT_PADDING,
31
MARKER_TICK_HEIGHT,
@@ -129,7 +129,7 @@ export class TimeAxisMarkersView extends View {
129
context.fillStyle = COLORS.TIME_MARKER_LABEL;
130
context.textAlign = 'right';
131
context.textBaseline = 'middle';
132
- context.font = `${MARKER_FONT_SIZE}px sans-serif`;
132
+ context.font = `${FONT_SIZE}px sans-serif`;
133
context.fillText(
134
`${markerLabel}ms`,
135
x - MARKER_TEXT_PADDING,
packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js
+3
-3
@@ -25,13 +25,13 @@ import {
25
} from '../view-base';
26
import {
27
COLORS,
28
- EVENT_ROW_PADDING,
28
+ TOP_ROW_PADDING,
29
USER_TIMING_MARK_SIZE,
30
BORDER_SIZE,
31
} from './constants';
32
33
const ROW_HEIGHT_FIXED =
34
- EVENT_ROW_PADDING + USER_TIMING_MARK_SIZE + EVENT_ROW_PADDING;
34
+ TOP_ROW_PADDING + USER_TIMING_MARK_SIZE + TOP_ROW_PADDING;
35
36
export class UserTimingMarksView extends View {
37
_marks: UserTimingMark[];
@@ -125,7 +125,7 @@ export class UserTimingMarksView extends View {
125
);
126
127
// Draw marks
128
- const baseY = frame.origin.y + EVENT_ROW_PADDING;
128
+ const baseY = frame.origin.y + TOP_ROW_PADDING;
129
const scaleFactor = positioningScaleFactor(
130
this._intrinsicSize.width,
131
frame,
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
+25
-16
@@ -8,12 +8,19 @@
8
*/
9
10
export const LABEL_SIZE = 80;
11
-export const LABEL_FONT_SIZE = 11;
11
export const MARKER_HEIGHT = 20;
12
export const MARKER_TICK_HEIGHT = 8;
14
-export const MARKER_FONT_SIZE = 10;
13
+export const FONT_SIZE = 10;
14
export const MARKER_TEXT_PADDING = 8;
15
export const COLOR_HOVER_DIM_DELTA = 5;
16
+export const TOP_ROW_PADDING = 4;
17
+export const NATIVE_EVENT_HEIGHT = 14;
18
+export const REACT_EVENT_DIAMETER = 6;
19
+export const USER_TIMING_MARK_SIZE = 8;
20
+export const REACT_MEASURE_HEIGHT = 9;
21
+export const BORDER_SIZE = 1;
22
+export const FLAMECHART_FRAME_HEIGHT = 14;
23
+export const TEXT_PADDING = 3;
24
25
export const INTERVAL_TIMES = [
26
1,
@@ -31,22 +38,14 @@ export const INTERVAL_TIMES = [
38
];
39
export const MIN_INTERVAL_SIZE_PX = 70;
40
34
-export const EVENT_ROW_PADDING = 4;
35
-export const EVENT_DIAMETER = 6;
36
-export const USER_TIMING_MARK_SIZE = 8;
37
-export const REACT_MEASURE_HEIGHT = 9;
38
-export const BORDER_SIZE = 1;
39
-
40
-export const FLAMECHART_FONT_SIZE = 10;
41
-export const FLAMECHART_FRAME_HEIGHT = 16;
42
-export const FLAMECHART_TEXT_PADDING = 3;
43
-
41
// TODO Replace this with "export let" vars
42
export let COLORS = {
43
BACKGROUND: '',
47
- FLAME_GRAPH_LABEL: '',
44
NATIVE_EVENT: '',
45
NATIVE_EVENT_HOVER: '',
46
+ NATIVE_EVENT_WARNING: '',
47
+ NATIVE_EVENT_WARNING_HOVER: '',
48
+ NATIVE_EVENT_WARNING_TEXT: '',
49
PRIORITY_BACKGROUND: '',
50
PRIORITY_BORDER: '',
51
PRIORITY_LABEL: '',
@@ -75,6 +74,7 @@ export let COLORS = {
74
REACT_SUSPEND: '',
75
REACT_SUSPEND_HOVER: '',
76
REACT_WORK_BORDER: '',
77
+ TEXT_COLOR: '',
78
TIME_MARKER_LABEL: '',
79
};
80
@@ -83,15 +83,21 @@ export function updateColorsToMatchTheme(): void {
83
84
COLORS = {
85
BACKGROUND: computedStyle.getPropertyValue('--color-background'),
86
- FLAME_GRAPH_LABEL: computedStyle.getPropertyValue(
87
- '--color-scheduling-profiler-flame-graph-label',
88
- ),
86
NATIVE_EVENT: computedStyle.getPropertyValue(
87
'--color-scheduling-profiler-native-event',
88
),
89
NATIVE_EVENT_HOVER: computedStyle.getPropertyValue(
90
'--color-scheduling-profiler-native-event-hover',
91
),
92
+ NATIVE_EVENT_WARNING: computedStyle.getPropertyValue(
93
+ '--color-scheduling-profiler-native-event-warning',
94
+ ),
95
+ NATIVE_EVENT_WARNING_HOVER: computedStyle.getPropertyValue(
96
+ '--color-scheduling-profiler-native-event-warning-hover',
97
+ ),
98
+ NATIVE_EVENT_WARNING_TEXT: computedStyle.getPropertyValue(
99
+ '--color-scheduling-profiler-native-event-warning-text',
100
+ ),
101
PRIORITY_BACKGROUND: computedStyle.getPropertyValue(
102
'--color-scheduling-profiler-priority-background',
103
),
@@ -172,6 +178,9 @@ export function updateColorsToMatchTheme(): void {
178
REACT_WORK_BORDER: computedStyle.getPropertyValue(
179
'--color-scheduling-profiler-react-work-border',
180
),
181
+ TEXT_COLOR: computedStyle.getPropertyValue(
182
+ '--color-scheduling-profiler-text-color',
183
+ ),
184
TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'),
185
};
186
}
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+41
-5
@@ -16,6 +16,7 @@ import type {
16
Milliseconds,
17
BatchUID,
18
Flamechart,
19
+ NativeEvent,
20
ReactLane,
21
ReactMeasureType,
22
ReactProfilerData,
@@ -39,6 +40,8 @@ type ProcessorState = {|
40
measureStack: MeasureStackElement[],
41
|};
42
43
+let nativeEventStack: Array<NativeEvent> = [];
44
+
45
// Exported for tests
46
export function getLanesFromTransportDecimalBitmask(
47
laneBitmaskString: string,
@@ -178,15 +181,37 @@ function processTimelineEvent(
181
}
182
}
183
181
- const startTime = (ts - currentProfilerData.startTime) / 1000;
184
+ const timestamp = (ts - currentProfilerData.startTime) / 1000;
185
const duration = event.dur / 1000;
186
184
- // TODO (scheduling profiler) Should we filter out certain event types?
185
- currentProfilerData.nativeEvents.push({
187
+ let depth = 0;
188
+
189
+ while (nativeEventStack.length > 0) {
190
+ const prevNativeEvent = nativeEventStack[nativeEventStack.length - 1];
191
+ const prevStopTime =
192
+ prevNativeEvent.timestamp + prevNativeEvent.duration;
193
+
194
+ if (timestamp < prevStopTime) {
195
+ depth = prevNativeEvent.depth + 1;
196
+ break;
197
+ } else {
198
+ nativeEventStack.pop();
199
+ }
200
+ }
201
+
202
+ const nativeEvent = {
203
+ depth,
204
duration,
187
- timestamp: startTime,
205
+ highlight: false,
206
+ timestamp,
207
type,
189
- });
208
+ };
209
+
210
+ currentProfilerData.nativeEvents.push(nativeEvent);
211
+
212
+ // Keep track of curent event in case future ones overlap.
213
+ // We separate them into different vertical lanes in this case.
214
+ nativeEventStack.push(nativeEvent);
215
}
216
break;
217
case 'blink.user_timing':
@@ -309,6 +334,15 @@ function processTimelineEvent(
334
currentProfilerData,
335
state,
336
);
337
+
338
+ for (let i = 0; i < nativeEventStack.length; i++) {
339
+ const nativeEvent = nativeEventStack[i];
340
+ const stopTime = nativeEvent.timestamp + nativeEvent.duration;
341
+ if (stopTime > startTime) {
342
+ // Warn about sync updates that happen an event handler.
343
+ nativeEvent.highlight = true;
344
+ }
345
+ }
346
} else if (
347
name.startsWith('--render-stop') ||
348
name.startsWith('--render-yield')
@@ -470,6 +504,8 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
504
export default function preprocessData(
505
timeline: TimelineEvent[],
506
): ReactProfilerData {
507
+ nativeEventStack = [];
508
+
509
const flamechart = preprocessFlamechart(timeline);
510
511
const profilerData: ReactProfilerData = {
packages/react-devtools-scheduling-profiler/src/types.js
+2
@@ -22,7 +22,9 @@ export type Milliseconds = number;
22
export type ReactLane = number;
23
24
export type NativeEvent = {|
25
+ +depth: number,
26
+duration: Milliseconds,
27
+ highlight: boolean,
28
+timestamp: Milliseconds,
29
+type: string,
30
|};
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+16
-1
@@ -418,7 +418,7 @@ export function updateThemeVariables(
418
updateStyleHelper(theme, 'color-search-match-current', documentElements);
419
updateStyleHelper(
420
theme,
421
- 'color-scheduling-profiler-flame-graph-label',
421
+ 'color-scheduling-profiler-text-color',
422
documentElements,
423
);
424
updateStyleHelper(
@@ -431,6 +431,21 @@ export function updateThemeVariables(
431
'color-scheduling-profiler-native-event-hover',
432
documentElements,
433
);
434
+ updateStyleHelper(
435
+ theme,
436
+ 'color-scheduling-profiler-native-event-warning',
437
+ documentElements,
438
+ );
439
+ updateStyleHelper(
440
+ theme,
441
+ 'color-scheduling-profiler-native-event-warning-hover',
442
+ documentElements,
443
+ );
444
+ updateStyleHelper(
445
+ theme,
446
+ 'color-scheduling-profiler-native-event-warning-text',
447
+ documentElements,
448
+ );
449
updateStyleHelper(
450
theme,
451
'color-selected-tree-highlight-active',
packages/react-devtools-shared/src/devtools/views/root.css
+12
-6
@@ -78,9 +78,11 @@
78
--light-color-record-hover: #3578e5;
79
--light-color-record-inactive: #0088fa;
80
--light-color-resize-bar: #cccccc;
81
- --light-color-scheduling-profiler-flame-graph-label: #000000;
82
- --light-color-scheduling-profiler-native-event: #aaaaaa;
83
- --light-color-scheduling-profiler-native-event-hover: #888888;
81
+ --light-color-scheduling-profiler-native-event: #ccc;
82
+ --light-color-scheduling-profiler-native-event-hover: #aaa;
83
+ --light-color-scheduling-profiler-native-event-warning: #ee1638;
84
+ --light-color-scheduling-profiler-native-event-warning-hover: #da1030;
85
+ --light-color-scheduling-profiler-native-event-warning-text: #fff;
86
--light-color-scheduling-profiler-priority-background: #f6f6f6;
87
--light-color-scheduling-profiler-priority-border: #eeeeee;
88
--light-color-scheduling-profiler-user-timing: #c9cacd;
@@ -106,6 +108,7 @@
108
--light-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
109
--light-color-scheduling-profiler-react-suspend: #a6e59f;
110
--light-color-scheduling-profiler-react-suspend-hover:#13bc00;
111
+ --light-color-scheduling-profiler-text-color: #000000;
112
--light-color-scheduling-profiler-react-work-border:#ffffff;
113
--light-color-scroll-thumb: #c2c2c2;
114
--light-color-scroll-track: #fafafa;
@@ -200,9 +203,11 @@
203
--dark-color-record-hover: #a2e9fc;
204
--dark-color-record-inactive: #61dafb;
205
--dark-color-resize-bar: #3d424a;
203
- --dark-color-scheduling-profiler-flame-graph-label: #000000;
204
- --dark-color-scheduling-profiler-native-event: #aaaaaa;
205
- --dark-color-scheduling-profiler-native-event-hover: #888888;
206
+ --dark-color-scheduling-profiler-native-event: #b2b2b2;
207
+ --dark-color-scheduling-profiler-native-event-hover: #949494;
208
+ --dark-color-scheduling-profiler-native-event-warning: #ee1638;
209
+ --dark-color-scheduling-profiler-native-event-warning-hover: #da1030;
210
+ --dark-color-scheduling-profiler-native-event-warning-text: #fff;
211
--dark-color-scheduling-profiler-priority-background: #1d2129;
212
--dark-color-scheduling-profiler-priority-border: #282c34;
213
--dark-color-scheduling-profiler-user-timing: #c9cacd;
@@ -228,6 +233,7 @@
233
--dark-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
234
--dark-color-scheduling-profiler-react-suspend: #a6e59f;
235
--dark-color-scheduling-profiler-react-suspend-hover:#13bc00;
236
+ --dark-color-scheduling-profiler-text-color: #000000;
237
--dark-color-scheduling-profiler-react-work-border:#ffffff;
238
--dark-color-scroll-thumb: #afb3b9;
239
--dark-color-scroll-track: #313640;