Skip to content

Commit c21e8fc

Browse files
author
Brian Vaughn
authored
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.)
1 parent 392253a commit c21e8fc

File tree

11 files changed

+265
-123
lines changed

11 files changed

+265
-123
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ import {
3232
} from './utils/positioning';
3333
import {
3434
COLORS,
35-
FLAMECHART_FONT_SIZE,
35+
FONT_SIZE,
3636
FLAMECHART_FRAME_HEIGHT,
37-
FLAMECHART_TEXT_PADDING,
37+
TEXT_PADDING,
3838
COLOR_HOVER_DIM_DELTA,
3939
BORDER_SIZE,
4040
} from './constants';
@@ -157,7 +157,7 @@ class FlamechartStackLayerView extends View {
157157

158158
context.textAlign = 'left';
159159
context.textBaseline = 'middle';
160-
context.font = `${FLAMECHART_FONT_SIZE}px sans-serif`;
160+
context.font = `${FONT_SIZE}px sans-serif`;
161161

162162
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
163163

@@ -195,15 +195,15 @@ class FlamechartStackLayerView extends View {
195195
drawableRect.size.height,
196196
);
197197

198-
if (width > FLAMECHART_TEXT_PADDING * 2) {
198+
if (width > TEXT_PADDING * 2) {
199199
const trimmedName = trimFlamechartText(
200200
context,
201201
name,
202-
width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0),
202+
width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
203203
);
204204

205205
if (trimmedName !== null) {
206-
context.fillStyle = COLORS.FLAME_GRAPH_LABEL;
206+
context.fillStyle = COLORS.TEXT_COLOR;
207207

208208
// Prevent text from being drawn outside `viewableArea`
209209
const textOverflowsViewableArea = !rectEqualToRect(
@@ -225,7 +225,7 @@ class FlamechartStackLayerView extends View {
225225

226226
context.fillText(
227227
trimmedName,
228-
nodeRect.origin.x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0),
228+
nodeRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0),
229229
nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2,
230230
);
231231

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

Lines changed: 149 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import type {NativeEvent, ReactProfilerData} from '../types';
1111
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
1212

1313
import {
14+
durationToWidth,
1415
positioningScaleFactor,
15-
timestampToPosition,
1616
positionToTimestamp,
17+
timestampToPosition,
1718
} from './utils/positioning';
1819
import {
1920
View,
@@ -24,28 +25,77 @@ import {
2425
} from '../view-base';
2526
import {
2627
COLORS,
27-
EVENT_ROW_PADDING,
28-
EVENT_DIAMETER,
28+
TEXT_PADDING,
29+
NATIVE_EVENT_HEIGHT,
30+
FONT_SIZE,
2931
BORDER_SIZE,
3032
} from './constants';
3133

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

3560
export class NativeEventsView extends View {
36-
_profilerData: ReactProfilerData;
61+
_depthToNativeEvent: Map<number, NativeEvent[]>;
62+
_hoveredEvent: NativeEvent | null = null;
3763
_intrinsicSize: Size;
64+
_maxDepth: number = 0;
65+
_profilerData: ReactProfilerData;
3866

39-
_hoveredEvent: NativeEvent | null = null;
4067
onHover: ((event: NativeEvent | null) => void) | null = null;
4168

4269
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
4370
super(surface, frame);
71+
4472
this._profilerData = profilerData;
4573

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+
4696
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,
4999
};
50100
}
51101

@@ -73,7 +123,9 @@ export class NativeEventsView extends View {
73123
showHoverHighlight: boolean,
74124
) {
75125
const {frame} = this;
76-
const {duration, timestamp} = event;
126+
const {depth, duration, highlight, timestamp, type} = event;
127+
128+
baseY += depth * ROW_WITH_BORDER_HEIGHT;
77129

78130
const xStart = timestampToPosition(timestamp, scaleFactor, frame);
79131
const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
@@ -82,25 +134,60 @@ export class NativeEventsView extends View {
82134
x: xStart,
83135
y: baseY,
84136
},
85-
size: {width: xStop - xStart, height: EVENT_DIAMETER},
137+
size: {width: xStop - xStart, height: NATIVE_EVENT_HEIGHT},
86138
};
87139
if (!rectIntersectsRect(eventRect, rect)) {
88140
return; // Not in view
89141
}
90142

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+
}
94147

95148
const drawableRect = intersectionOfRects(eventRect, rect);
96149
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+
}
98159
context.fillRect(
99160
drawableRect.origin.x,
100161
drawableRect.origin.y,
101162
drawableRect.size.width,
102163
drawableRect.size.height,
103164
);
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+
}
104191
}
105192

106193
draw(context: CanvasRenderingContext2D) {
@@ -111,7 +198,7 @@ export class NativeEventsView extends View {
111198
visibleArea,
112199
} = this;
113200

114-
context.fillStyle = COLORS.BACKGROUND;
201+
context.fillStyle = COLORS.PRIORITY_BACKGROUND;
115202
context.fillRect(
116203
visibleArea.origin.x,
117204
visibleArea.origin.y,
@@ -120,57 +207,43 @@ export class NativeEventsView extends View {
120207
);
121208

122209
// Draw events
123-
const baseY = frame.origin.y + EVENT_ROW_PADDING;
124210
const scaleFactor = positioningScaleFactor(
125211
this._intrinsicSize.width,
126212
frame,
127213
);
128214

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

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+
}
174247
}
175248
}
176249

@@ -189,25 +262,26 @@ export class NativeEventsView extends View {
189262
return;
190263
}
191264

192-
const {nativeEvents} = this._profilerData;
193-
194265
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
195266
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
196267

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+
}
211285
}
212286
}
213287

0 commit comments

Comments
 (0)