@samitouri / QOS-React-2 / commits / 54e170c794

[DevTools] Add screenshots to Scheduling Profiler (#22088)

Brian Vaughn committed Aug 13, 2021 at 17:14 UTC 54e170c79493dea734b95b0fddbf8f0bfc99bff7
9 files changed +375 -41
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+44
@@ -46,6 +46,7 @@ import {
46 NativeEventsView,
47 ReactMeasuresView,
48 SchedulingEventsView,
49 + SnapshotsView,
50 SuspenseEventsView,
51 TimeAxisMarkersView,
52 UserTimingMarksView,
@@ -157,6 +158,7 @@ function AutoSizedCanvas({
158 const componentMeasuresViewRef = useRef(null);
159 const reactMeasuresViewRef = useRef(null);
160 const flamechartViewRef = useRef(null);
161 + const snapshotsViewRef = useRef(null);
162
163 const {hideMenu: hideContextMenu} = useContext(RegistryContext);
164
@@ -304,6 +306,18 @@ function AutoSizedCanvas({
306 );
307 }
308
309 + let snapshotsViewWrapper = null;
310 + if (data.snapshots.length > 0) {
311 + const snapshotsView = new SnapshotsView(surface, defaultFrame, data);
312 + snapshotsViewRef.current = snapshotsView;
313 + snapshotsViewWrapper = createViewHelper(
314 + snapshotsView,
315 + 'snapshots',
316 + true,
317 + true,
318 + );
319 + }
320 +
321 const flamechartView = new FlamechartView(
322 surface,
323 defaultFrame,
@@ -340,6 +354,9 @@ function AutoSizedCanvas({
354 if (componentMeasuresViewWrapper !== null) {
355 rootView.addSubview(componentMeasuresViewWrapper);
356 }
357 + if (snapshotsViewWrapper !== null) {
358 + rootView.addSubview(snapshotsViewWrapper);
359 + }
360 rootView.addSubview(flamechartViewWrapper);
361
362 const verticalScrollOverflowView = new VerticalScrollOverflowView(
@@ -389,6 +406,7 @@ function AutoSizedCanvas({
406 measure: null,
407 nativeEvent: null,
408 schedulingEvent: null,
409 + snapshot: null,
410 suspenseEvent: null,
411 userTimingMark: null,
412 };
@@ -447,6 +465,7 @@ function AutoSizedCanvas({
465 measure: null,
466 nativeEvent: null,
467 schedulingEvent: null,
468 + snapshot: null,
469 suspenseEvent: null,
470 userTimingMark,
471 });
@@ -465,6 +484,7 @@ function AutoSizedCanvas({
484 measure: null,
485 nativeEvent,
486 schedulingEvent: null,
487 + snapshot: null,
488 suspenseEvent: null,
489 userTimingMark: null,
490 });
@@ -483,6 +503,7 @@ function AutoSizedCanvas({
503 measure: null,
504 nativeEvent: null,
505 schedulingEvent,
506 + snapshot: null,
507 suspenseEvent: null,
508 userTimingMark: null,
509 });
@@ -501,6 +522,7 @@ function AutoSizedCanvas({
522 measure: null,
523 nativeEvent: null,
524 schedulingEvent: null,
525 + snapshot: null,
526 suspenseEvent,
527 userTimingMark: null,
528 });
@@ -519,6 +541,7 @@ function AutoSizedCanvas({
541 measure,
542 nativeEvent: null,
543 schedulingEvent: null,
544 + snapshot: null,
545 suspenseEvent: null,
546 userTimingMark: null,
547 });
@@ -540,6 +563,26 @@ function AutoSizedCanvas({
563 measure: null,
564 nativeEvent: null,
565 schedulingEvent: null,
566 + snapshot: null,
567 + suspenseEvent: null,
568 + userTimingMark: null,
569 + });
570 + }
571 + };
572 + }
573 +
574 + const {current: snapshotsView} = snapshotsViewRef;
575 + if (snapshotsView) {
576 + snapshotsView.onHover = snapshot => {
577 + if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
578 + setHoveredEvent({
579 + componentMeasure: null,
580 + data,
581 + flamechartStackFrame: null,
582 + measure: null,
583 + nativeEvent: null,
584 + schedulingEvent: null,
585 + snapshot,
586 suspenseEvent: null,
587 userTimingMark: null,
588 });
@@ -561,6 +604,7 @@ function AutoSizedCanvas({
604 measure: null,
605 nativeEvent: null,
606 schedulingEvent: null,
607 + snapshot: null,
608 suspenseEvent: null,
609 userTimingMark: null,
610 });
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+21
@@ -17,6 +17,7 @@ import type {
17 ReactProfilerData,
18 Return,
19 SchedulingEvent,
20 + Snapshot,
21 SuspenseEvent,
22 UserTimingMark,
23 } from './types';
@@ -87,6 +88,7 @@ export default function EventTooltip({
88 measure,
89 nativeEvent,
90 schedulingEvent,
91 + snapshot,
92 suspenseEvent,
93 userTimingMark,
94 } = hoveredEvent;
@@ -110,6 +112,8 @@ export default function EventTooltip({
112 tooltipRef={tooltipRef}
113 />
114 );
115 + } else if (snapshot !== null) {
116 + return <TooltipSnapshot snapshot={snapshot} tooltipRef={tooltipRef} />;
117 } else if (suspenseEvent !== null) {
118 return (
119 <TooltipSuspenseEvent
@@ -301,6 +305,23 @@ const TooltipSchedulingEvent = ({
305 );
306 };
307
308 +const TooltipSnapshot = ({
309 + snapshot,
310 + tooltipRef,
311 +}: {
312 + snapshot: Snapshot,
313 + tooltipRef: Return<typeof useRef>,
314 +}) => {
315 + return (
316 + <div className={styles.Tooltip} ref={tooltipRef}>
317 + <img
318 + src={snapshot.imageSource}
319 + style={{width: snapshot.width / 2, height: snapshot.height / 2}}
320 + />
321 + </div>
322 + );
323 +};
324 +
325 const TooltipSuspenseEvent = ({
326 suspenseEvent,
327 tooltipRef,
packages/react-devtools-scheduling-profiler/src/content-views/SnapshotsView.js new
+208
@@ -0,0 +1,208 @@
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 {Snapshot, ReactProfilerData} from '../types';
11 +import type {
12 + Interaction,
13 + MouseMoveInteraction,
14 + Rect,
15 + Size,
16 + Surface,
17 + ViewRefs,
18 +} from '../view-base';
19 +
20 +import {positioningScaleFactor, timestampToPosition} from './utils/positioning';
21 +import {
22 + intersectionOfRects,
23 + rectContainsPoint,
24 + rectEqualToRect,
25 + View,
26 +} from '../view-base';
27 +import {BORDER_SIZE, COLORS, SNAPSHOT_HEIGHT} from './constants';
28 +
29 +type OnHover = (node: Snapshot | null) => void;
30 +
31 +export class SnapshotsView extends View {
32 + _intrinsicSize: Size;
33 + _profilerData: ReactProfilerData;
34 +
35 + onHover: OnHover | null = null;
36 +
37 + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
38 + super(surface, frame);
39 +
40 + this._intrinsicSize = {
41 + width: profilerData.duration,
42 + height: SNAPSHOT_HEIGHT,
43 + };
44 + this._profilerData = profilerData;
45 + }
46 +
47 + desiredSize() {
48 + return this._intrinsicSize;
49 + }
50 +
51 + draw(context: CanvasRenderingContext2D) {
52 + const {visibleArea} = this;
53 +
54 + context.fillStyle = COLORS.BACKGROUND;
55 + context.fillRect(
56 + visibleArea.origin.x,
57 + visibleArea.origin.y,
58 + visibleArea.size.width,
59 + visibleArea.size.height,
60 + );
61 +
62 + const y = visibleArea.origin.y;
63 +
64 + let x = visibleArea.origin.x;
65 +
66 + // Rather than drawing each snapshot where it occured,
67 + // draw them at fixed intervals and just show the nearest one.
68 + while (x < visibleArea.origin.x + visibleArea.size.width) {
69 + const snapshot = this._findClosestSnapshot(x);
70 +
71 + const scaledHeight = SNAPSHOT_HEIGHT;
72 + const scaledWidth = (snapshot.width * SNAPSHOT_HEIGHT) / snapshot.height;
73 +
74 + const imageRect: Rect = {
75 + origin: {
76 + x,
77 + y,
78 + },
79 + size: {width: scaledWidth, height: scaledHeight},
80 + };
81 +
82 + // Lazily create and cache Image objects as we render a snapsho for the first time.
83 + if (snapshot.image === null) {
84 + const img = (snapshot.image = new Image());
85 + img.onload = () => {
86 + this._drawSnapshotImage(context, snapshot, imageRect);
87 + };
88 + img.src = snapshot.imageSource;
89 + } else {
90 + this._drawSnapshotImage(context, snapshot, imageRect);
91 + }
92 +
93 + x += scaledWidth + BORDER_SIZE;
94 + }
95 + }
96 +
97 + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
98 + switch (interaction.type) {
99 + case 'mousemove':
100 + this._handleMouseMove(interaction, viewRefs);
101 + break;
102 + }
103 + }
104 +
105 + _drawSnapshotImage(
106 + context: CanvasRenderingContext2D,
107 + snapshot: Snapshot,
108 + imageRect: Rect,
109 + ) {
110 + const visibleArea = this.visibleArea;
111 +
112 + // Prevent snapshot from visibly overflowing its container when clipped.
113 + const shouldClip = !rectEqualToRect(imageRect, visibleArea);
114 + if (shouldClip) {
115 + const clippedRect = intersectionOfRects(imageRect, visibleArea);
116 + context.save();
117 + context.beginPath();
118 + context.rect(
119 + clippedRect.origin.x,
120 + clippedRect.origin.y,
121 + clippedRect.size.width,
122 + clippedRect.size.height,
123 + );
124 + context.closePath();
125 + context.clip();
126 + }
127 +
128 + // $FlowFixMe Flow doesn't know about the 9 argument variant of drawImage()
129 + context.drawImage(
130 + snapshot.image,
131 +
132 + // Image coordinates
133 + 0,
134 + 0,
135 +
136 + // Native image size
137 + snapshot.width,
138 + snapshot.height,
139 +
140 + // Canvas coordinates
141 + imageRect.origin.x,
142 + imageRect.origin.y,
143 +
144 + // Scaled image size
145 + imageRect.size.width,
146 + imageRect.size.height,
147 + );
148 +
149 + if (shouldClip) {
150 + context.restore();
151 + }
152 + }
153 +
154 + _findClosestSnapshot(x: number): Snapshot {
155 + const frame = this.frame;
156 + const scaleFactor = positioningScaleFactor(
157 + this._intrinsicSize.width,
158 + frame,
159 + );
160 +
161 + const snapshots = this._profilerData.snapshots;
162 +
163 + let startIndex = 0;
164 + let stopIndex = snapshots.length - 1;
165 + while (startIndex <= stopIndex) {
166 + const currentIndex = Math.floor((startIndex + stopIndex) / 2);
167 + const snapshot = snapshots[currentIndex];
168 + const {timestamp} = snapshot;
169 +
170 + const snapshotX = Math.floor(
171 + timestampToPosition(timestamp, scaleFactor, frame),
172 + );
173 +
174 + if (x < snapshotX) {
175 + stopIndex = currentIndex - 1;
176 + } else {
177 + startIndex = currentIndex + 1;
178 + }
179 + }
180 +
181 + return snapshots[stopIndex];
182 + }
183 +
184 + /**
185 + * @private
186 + */
187 + _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
188 + const {onHover, visibleArea} = this;
189 + if (!onHover) {
190 + return;
191 + }
192 +
193 + const {location} = interaction.payload;
194 + if (!rectContainsPoint(location, visibleArea)) {
195 + onHover(null);
196 + return;
197 + }
198 +
199 + const snapshot = this._findClosestSnapshot(location.x);
200 + if (snapshot) {
201 + this.currentCursor = 'context-menu';
202 + viewRefs.hoveredView = this;
203 + onHover(snapshot);
204 + } else {
205 + onHover(null);
206 + }
207 + }
208 +}
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
+1
@@ -23,6 +23,7 @@ export const REACT_MEASURE_HEIGHT = 14;
23 export const BORDER_SIZE = 1;
24 export const FLAMECHART_FRAME_HEIGHT = 14;
25 export const TEXT_PADDING = 3;
26 +export const SNAPSHOT_HEIGHT = 50;
27
28 export const INTERVAL_TIMES = [
29 1,
packages/react-devtools-scheduling-profiler/src/content-views/index.js
+1
@@ -12,6 +12,7 @@ export * from './FlamechartView';
12 export * from './NativeEventsView';
13 export * from './ReactMeasuresView';
14 export * from './SchedulingEventsView';
15 +export * from './SnapshotsView';
16 export * from './SuspenseEventsView';
17 export * from './TimeAxisMarkersView';
18 export * from './UserTimingMarksView';
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+43 -37
@@ -17,7 +17,7 @@ import {
17 } from '../../constants';
18 import REACT_VERSION from 'shared/ReactVersion';
19
20 -describe(getLanesFromTransportDecimalBitmask, () => {
20 +describe('getLanesFromTransportDecimalBitmask', () => {
21 it('should return array of lane numbers from bitmask string', () => {
22 expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]);
23 expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]);
@@ -57,7 +57,7 @@ describe(getLanesFromTransportDecimalBitmask, () => {
57 });
58 });
59
60 -describe(preprocessData, () => {
60 +describe('preprocessData', () => {
61 let React;
62 let ReactDOM;
63 let Scheduler;
@@ -217,11 +217,11 @@ describe(preprocessData, () => {
217 delete global.performance;
218 });
219
220 - it('should throw given an empty timeline', () => {
221 - expect(() => preprocessData([])).toThrow();
220 + it('should throw given an empty timeline', async () => {
221 + await expect(async () => preprocessData([])).rejects.toThrow();
222 });
223
224 - it('should throw given a timeline with no Profile event', () => {
224 + it('should throw given a timeline with no Profile event', async () => {
225 const randomSample = createUserTimingEntry({
226 dur: 100,
227 tdur: 200,
@@ -231,10 +231,10 @@ describe(preprocessData, () => {
231 args: {},
232 });
233
234 - expect(() => preprocessData([randomSample])).toThrow();
234 + await expect(async () => preprocessData([randomSample])).rejects.toThrow();
235 });
236
237 - it('should return empty data given a timeline with no React scheduling profiling marks', () => {
237 + it('should return empty data given a timeline with no React scheduling profiling marks', async () => {
238 const cpuProfilerSample = creactCpuProfilerSample();
239 const randomSample = createUserTimingEntry({
240 dur: 100,
@@ -246,7 +246,7 @@ describe(preprocessData, () => {
246 });
247
248 if (gate(flags => flags.enableSchedulingProfiler)) {
249 - const data = preprocessData([
249 + const data = await preprocessData([
250 ...createBoilerplateEntries(),
251 cpuProfilerSample,
252 randomSample,
@@ -327,6 +327,7 @@ describe(preprocessData, () => {
327 "otherUserTimingMarks": Array [],
328 "reactVersion": "17.0.3",
329 "schedulingEvents": Array [],
330 + "snapshots": Array [],
331 "startTime": 1,
332 "suspenseEvents": Array [],
333 }
@@ -334,13 +335,13 @@ describe(preprocessData, () => {
335 }
336 });
337
337 - it('should process legacy data format (before lane labels were added)', () => {
338 + it('should process legacy data format (before lane labels were added)', async () => {
339 const cpuProfilerSample = creactCpuProfilerSample();
340
341 if (gate(flags => flags.enableSchedulingProfiler)) {
342 // Data below is hard-coded based on an older profile sample.
343 // Should be fine since this is explicitly a legacy-format test.
343 - const data = preprocessData([
344 + const data = await preprocessData([
345 ...createBoilerplateEntries(),
346 cpuProfilerSample,
347 createUserTimingEntry({
@@ -541,6 +542,7 @@ describe(preprocessData, () => {
542 "warning": null,
543 },
544 ],
545 + "snapshots": Array [],
546 "startTime": 1,
547 "suspenseEvents": Array [],
548 }
@@ -548,11 +550,11 @@ describe(preprocessData, () => {
550 }
551 });
552
551 - it('should process a sample legacy render sequence', () => {
553 + it('should process a sample legacy render sequence', async () => {
554 ReactDOM.render(<div />, document.createElement('div'));
555
556 if (gate(flags => flags.enableSchedulingProfiler)) {
555 - const data = preprocessData([
557 + const data = await preprocessData([
558 ...createBoilerplateEntries(),
559 ...createUserTimingData(clearedMarks),
560 ]);
@@ -730,6 +732,7 @@ describe(preprocessData, () => {
732 "warning": null,
733 },
734 ],
735 + "snapshots": Array [],
736 "startTime": 4,
737 "suspenseEvents": Array [],
738 }
@@ -737,7 +740,7 @@ describe(preprocessData, () => {
740 }
741 });
742
740 - it('should process a sample createRoot render sequence', () => {
743 + it('should process a sample createRoot render sequence', async () => {
744 function App() {
745 const [didMount, setDidMount] = React.useState(false);
746 React.useEffect(() => {
@@ -752,7 +755,7 @@ describe(preprocessData, () => {
755 const root = ReactDOM.createRoot(document.createElement('div'));
756 act(() => root.render(<App />));
757
755 - const data = preprocessData([
758 + const data = await preprocessData([
759 ...createBoilerplateEntries(),
760 ...createUserTimingData(clearedMarks),
761 ]);
@@ -1074,6 +1077,7 @@ describe(preprocessData, () => {
1077 "warning": null,
1078 },
1079 ],
1080 + "snapshots": Array [],
1081 "startTime": 4,
1082 "suspenseEvents": Array [],
1083 }
@@ -1082,7 +1086,7 @@ describe(preprocessData, () => {
1086 });
1087
1088 // @gate enableSchedulingProfiler
1085 - it('should error if events and measures are incomplete', () => {
1089 + it('should error if events and measures are incomplete', async () => {
1090 const container = document.createElement('div');
1091 ReactDOM.render(<div />, container);
1092
@@ -1097,7 +1101,7 @@ describe(preprocessData, () => {
1101 });
1102
1103 // @gate enableSchedulingProfiler
1100 - it('should error if work is completed without being started', () => {
1104 + it('should error if work is completed without being started', async () => {
1105 const container = document.createElement('div');
1106 ReactDOM.render(<div />, container);
1107
@@ -1111,7 +1115,7 @@ describe(preprocessData, () => {
1115 expect(error).toHaveBeenCalled();
1116 });
1117
1114 - it('should populate other user timing marks', () => {
1118 + it('should populate other user timing marks', async () => {
1119 const userTimingData = createUserTimingData([]);
1120 userTimingData.push(
1121 createUserTimingEntry({
@@ -1138,7 +1142,7 @@ describe(preprocessData, () => {
1142 }),
1143 );
1144
1141 - const data = preprocessData([
1145 + const data = await preprocessData([
1146 ...createBoilerplateEntries(),
1147 ...userTimingData,
1148 ]);
@@ -1162,7 +1166,7 @@ describe(preprocessData, () => {
1166
1167 describe('warnings', () => {
1168 describe('long event handlers', () => {
1165 - it('should not warn when React scedules a (sync) update inside of a short event handler', () => {
1169 + it('should not warn when React scedules a (sync) update inside of a short event handler', async () => {
1170 function App() {
1171 return null;
1172 }
@@ -1180,13 +1184,13 @@ describe(preprocessData, () => {
1184
1185 testMarks.push(...createUserTimingData(clearedMarks));
1186
1183 - const data = preprocessData(testMarks);
1187 + const data = await preprocessData(testMarks);
1188 const event = data.nativeEvents.find(({type}) => type === 'click');
1189 expect(event.warning).toBe(null);
1190 }
1191 });
1192
1189 - it('should not warn about long events if the cause was non-React JavaScript', () => {
1193 + it('should not warn about long events if the cause was non-React JavaScript', async () => {
1194 function App() {
1195 return null;
1196 }
@@ -1206,13 +1210,13 @@ describe(preprocessData, () => {
1210
1211 testMarks.push(...createUserTimingData(clearedMarks));
1212
1209 - const data = preprocessData(testMarks);
1213 + const data = await preprocessData(testMarks);
1214 const event = data.nativeEvents.find(({type}) => type === 'click');
1215 expect(event.warning).toBe(null);
1216 }
1217 });
1218
1215 - it('should warn when React scedules a long (sync) update inside of an event', () => {
1219 + it('should warn when React scedules a long (sync) update inside of an event', async () => {
1220 function App() {
1221 return null;
1222 }
@@ -1245,7 +1249,7 @@ describe(preprocessData, () => {
1249 });
1250 });
1251
1248 - const data = preprocessData(testMarks);
1252 + const data = await preprocessData(testMarks);
1253 const event = data.nativeEvents.find(({type}) => type === 'click');
1254 expect(event.warning).toMatchInlineSnapshot(
1255 `"An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work."`,
@@ -1253,7 +1257,7 @@ describe(preprocessData, () => {
1257 }
1258 });
1259
1256 - it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', () => {
1260 + it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', async () => {
1261 function Yield({id, value}) {
1262 Scheduler.unstable_yieldValue(`${id}:${value}`);
1263 return null;
@@ -1303,7 +1307,7 @@ describe(preprocessData, () => {
1307
1308 testMarks.push(...createUserTimingData(clearedMarks));
1309
1306 - const data = preprocessData(testMarks);
1310 + const data = await preprocessData(testMarks);
1311 const event = data.nativeEvents.find(({type}) => type === 'click');
1312 expect(event.warning).toBe(null);
1313 }
@@ -1311,7 +1315,7 @@ describe(preprocessData, () => {
1315 });
1316
1317 describe('nested updates', () => {
1314 - it('should not warn about short nested (state) updates during layout effects', () => {
1318 + it('should not warn about short nested (state) updates during layout effects', async () => {
1319 function Component() {
1320 const [didMount, setDidMount] = React.useState(false);
1321 Scheduler.unstable_yieldValue(
@@ -1334,7 +1338,7 @@ describe(preprocessData, () => {
1338 'Component update',
1339 ]);
1340
1337 - const data = preprocessData([
1341 + const data = await preprocessData([
1342 ...createBoilerplateEntries(),
1343 ...createUserTimingData(clearedMarks),
1344 ]);
@@ -1346,7 +1350,7 @@ describe(preprocessData, () => {
1350 }
1351 });
1352
1349 - it('should not warn about short (forced) updates during layout effects', () => {
1353 + it('should not warn about short (forced) updates during layout effects', async () => {
1354 class Component extends React.Component {
1355 _didMount: boolean = false;
1356 componentDidMount() {
@@ -1372,7 +1376,7 @@ describe(preprocessData, () => {
1376 'Component update',
1377 ]);
1378
1375 - const data = preprocessData([
1379 + const data = await preprocessData([
1380 ...createBoilerplateEntries(),
1381 ...createUserTimingData(clearedMarks),
1382 ]);
@@ -1384,7 +1388,7 @@ describe(preprocessData, () => {
1388 }
1389 });
1390
1387 - it('should warn about long nested (state) updates during layout effects', () => {
1391 + it('should warn about long nested (state) updates during layout effects', async () => {
1392 function Component() {
1393 const [didMount, setDidMount] = React.useState(false);
1394 Scheduler.unstable_yieldValue(
@@ -1429,7 +1433,7 @@ describe(preprocessData, () => {
1433 });
1434 });
1435
1432 - const data = preprocessData([
1436 + const data = await preprocessData([
1437 cpuProfilerSample,
1438 ...createBoilerplateEntries(),
1439 ...testMarks,
@@ -1444,7 +1448,7 @@ describe(preprocessData, () => {
1448 }
1449 });
1450
1447 - it('should warn about long nested (forced) updates during layout effects', () => {
1451 + it('should warn about long nested (forced) updates during layout effects', async () => {
1452 class Component extends React.Component {
1453 _didMount: boolean = false;
1454 componentDidMount() {
@@ -1490,7 +1494,7 @@ describe(preprocessData, () => {
1494 });
1495 });
1496
1493 - const data = preprocessData([
1497 + const data = await preprocessData([
1498 cpuProfilerSample,
1499 ...createBoilerplateEntries(),
1500 ...testMarks,
@@ -1509,7 +1513,7 @@ describe(preprocessData, () => {
1513 describe('suspend during an update', () => {
1514 // This also tests an edge case where the a component suspends while profiling
1515 // before the first commit is logged (so the lane-to-labels map will not yet exist).
1512 - it('should warn about suspending during an udpate', () => {
1516 + it('should warn about suspending during an udpate', async () => {
1517 let promise = null;
1518 let resolvedValue = null;
1519 function readValue(value) {
@@ -1557,7 +1561,7 @@ describe(preprocessData, () => {
1561
1562 testMarks.push(...createUserTimingData(clearedMarks));
1563
1560 - const data = preprocessData(testMarks);
1564 + const data = await preprocessData(testMarks);
1565 expect(data.suspenseEvents).toHaveLength(1);
1566 expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot(
1567 `"A component suspended during an update which caused a fallback to be shown. Consider using the Transition API to avoid hiding components after they've been mounted."`,
@@ -1615,7 +1619,7 @@ describe(preprocessData, () => {
1619
1620 testMarks.push(...createUserTimingData(clearedMarks));
1621
1618 - const data = preprocessData(testMarks);
1622 + const data = await preprocessData(testMarks);
1623 expect(data.suspenseEvents).toHaveLength(1);
1624 expect(data.suspenseEvents[0].warning).toBe(null);
1625 }
@@ -1623,5 +1627,7 @@ describe(preprocessData, () => {
1627 });
1628 });
1629
1630 + // TODO: Add test for snapshot base64 parsing
1631 +
1632 // TODO: Add test for flamechart parsing
1633 });
packages/react-devtools-scheduling-profiler/src/import-worker/importFile.js
+3 -1
@@ -26,9 +26,11 @@ export async function importFile(file: File): Promise<ImportWorkerOutputData> {
26 throw new InvalidProfileError('No profiling data found in file.');
27 }
28
29 + const processedData = await preprocessData(events);
30 +
31 return {
32 status: 'SUCCESS',
31 - processedData: preprocessData(events),
33 + processedData,
34 };
35 } catch (error) {
36 if (error instanceof InvalidProfileError) {
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+44 -3
@@ -26,7 +26,6 @@ import type {
26 SchedulingEvent,
27 SuspenseEvent,
28 } from '../types';
29 -
29 import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants';
30 import InvalidProfileError from './InvalidProfileError';
31 import {getBatchRange} from '../utils/getBatchRange';
@@ -40,6 +39,7 @@ type MeasureStackElement = {|
39 |};
40
41 type ProcessorState = {|
42 + asyncProcessingPromises: Promise<any>[],
43 batchUID: BatchUID,
44 currentReactComponentMeasure: ReactComponentMeasure | null,
45 measureStack: MeasureStackElement[],
@@ -224,6 +224,41 @@ function processTimelineEvent(
224 ) {
225 const {args, cat, name, ts, ph} = event;
226 switch (cat) {
227 + case 'disabled-by-default-devtools.screenshot':
228 + const encodedSnapshot = args.snapshot; // Base 64 encoded
229 +
230 + const snapshot = {
231 + height: 0,
232 + image: null,
233 + imageSource: `data:image/png;base64,${encodedSnapshot}`,
234 + timestamp: (ts - currentProfilerData.startTime) / 1000,
235 + width: 0,
236 + };
237 +
238 + // Delay processing until we've extracted snapshot dimensions.
239 + let resolveFn = ((null: any): Function);
240 + state.asyncProcessingPromises.push(
241 + new Promise(resolve => {
242 + resolveFn = resolve;
243 + }),
244 + );
245 +
246 + // Parse the Base64 image data to determine native size.
247 + // This will be used later to scale for display within the thumbnail strip.
248 + fetch(snapshot.imageSource)
249 + .then(response => response.blob())
250 + .then(blob => {
251 + // $FlowFixMe createImageBitmap
252 + createImageBitmap(blob).then(bitmap => {
253 + snapshot.height = bitmap.height;
254 + snapshot.width = bitmap.width;
255 +
256 + resolveFn();
257 + });
258 + });
259 +
260 + currentProfilerData.snapshots.push(snapshot);
261 + break;
262 case 'devtools.timeline':
263 if (name === 'EventDispatch') {
264 const type = args.data.type;
@@ -661,9 +696,9 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
696 return flamechart;
697 }
698
664 -export default function preprocessData(
699 +export default async function preprocessData(
700 timeline: TimelineEvent[],
666 -): ReactProfilerData {
701 +): Promise<ReactProfilerData> {
702 const flamechart = preprocessFlamechart(timeline);
703
704 const laneToReactMeasureMap = new Map();
@@ -682,6 +717,7 @@ export default function preprocessData(
717 otherUserTimingMarks: [],
718 reactVersion: null,
719 schedulingEvents: [],
720 + snapshots: [],
721 startTime: 0,
722 suspenseEvents: [],
723 };
@@ -713,6 +749,7 @@ export default function preprocessData(
749 (timeline[timeline.length - 1].ts - profilerData.startTime) / 1000;
750
751 const state: ProcessorState = {
752 + asyncProcessingPromises: [],
753 batchUID: 0,
754 currentReactComponentMeasure: null,
755 measureStack: [],
@@ -773,5 +810,9 @@ export default function preprocessData(
810 },
811 );
812
813 + // Wait for any async processing to complete before returning.
814 + // Since processing is done in a worker, async work must complete before data is serialized and returned.
815 + await Promise.all(state.asyncProcessingPromises);
816 +
817 return profilerData;
818 }
packages/react-devtools-scheduling-profiler/src/types.js
+10
@@ -115,6 +115,14 @@ export type UserTimingMark = {|
115 timestamp: Milliseconds,
116 |};
117
118 +export type Snapshot = {|
119 + height: number,
120 + image: Image | null,
121 + +imageSource: string,
122 + +timestamp: Milliseconds,
123 + width: number,
124 +|};
125 +
126 /**
127 * A "layer" of stack frames in the profiler UI, i.e. all stack frames of the
128 * same depth across all stack traces. Displayed as a flamechart row in the UI.
@@ -150,6 +158,7 @@ export type ReactProfilerData = {|
158 otherUserTimingMarks: UserTimingMark[],
159 reactVersion: string | null,
160 schedulingEvents: SchedulingEvent[],
161 + snapshots: Snapshot[],
162 startTime: number,
163 suspenseEvents: SuspenseEvent[],
164 |};
@@ -162,5 +171,6 @@ export type ReactHoverContextInfo = {|
171 nativeEvent: NativeEvent | null,
172 schedulingEvent: SchedulingEvent | null,
173 suspenseEvent: SuspenseEvent | null,
174 + snapshot: Snapshot | null,
175 userTimingMark: UserTimingMark | null,
176 |};