Skip to content

Commit 3794162

Browse files
author
Brian Vaughn
committed
Scheduling profiler: Vertically separate overlapping events
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 3794162

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

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

+149-75
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)