mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[DevTools] Add screenshots to Scheduling Profiler (#22088)
This commit is contained in:
@@ -46,6 +46,7 @@ import {
|
||||
NativeEventsView,
|
||||
ReactMeasuresView,
|
||||
SchedulingEventsView,
|
||||
SnapshotsView,
|
||||
SuspenseEventsView,
|
||||
TimeAxisMarkersView,
|
||||
UserTimingMarksView,
|
||||
@@ -157,6 +158,7 @@ function AutoSizedCanvas({
|
||||
const componentMeasuresViewRef = useRef(null);
|
||||
const reactMeasuresViewRef = useRef(null);
|
||||
const flamechartViewRef = useRef(null);
|
||||
const snapshotsViewRef = useRef(null);
|
||||
|
||||
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
|
||||
|
||||
@@ -304,6 +306,18 @@ function AutoSizedCanvas({
|
||||
);
|
||||
}
|
||||
|
||||
let snapshotsViewWrapper = null;
|
||||
if (data.snapshots.length > 0) {
|
||||
const snapshotsView = new SnapshotsView(surface, defaultFrame, data);
|
||||
snapshotsViewRef.current = snapshotsView;
|
||||
snapshotsViewWrapper = createViewHelper(
|
||||
snapshotsView,
|
||||
'snapshots',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const flamechartView = new FlamechartView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
@@ -340,6 +354,9 @@ function AutoSizedCanvas({
|
||||
if (componentMeasuresViewWrapper !== null) {
|
||||
rootView.addSubview(componentMeasuresViewWrapper);
|
||||
}
|
||||
if (snapshotsViewWrapper !== null) {
|
||||
rootView.addSubview(snapshotsViewWrapper);
|
||||
}
|
||||
rootView.addSubview(flamechartViewWrapper);
|
||||
|
||||
const verticalScrollOverflowView = new VerticalScrollOverflowView(
|
||||
@@ -389,6 +406,7 @@ function AutoSizedCanvas({
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
};
|
||||
@@ -447,6 +465,7 @@ function AutoSizedCanvas({
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark,
|
||||
});
|
||||
@@ -465,6 +484,7 @@ function AutoSizedCanvas({
|
||||
measure: null,
|
||||
nativeEvent,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
@@ -483,6 +503,7 @@ function AutoSizedCanvas({
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
@@ -501,6 +522,7 @@ function AutoSizedCanvas({
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent,
|
||||
userTimingMark: null,
|
||||
});
|
||||
@@ -519,6 +541,7 @@ function AutoSizedCanvas({
|
||||
measure,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
@@ -540,6 +563,26 @@ function AutoSizedCanvas({
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const {current: snapshotsView} = snapshotsViewRef;
|
||||
if (snapshotsView) {
|
||||
snapshotsView.onHover = snapshot => {
|
||||
if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
snapshot,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
@@ -561,6 +604,7 @@ function AutoSizedCanvas({
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ReactProfilerData,
|
||||
Return,
|
||||
SchedulingEvent,
|
||||
Snapshot,
|
||||
SuspenseEvent,
|
||||
UserTimingMark,
|
||||
} from './types';
|
||||
@@ -87,6 +88,7 @@ export default function EventTooltip({
|
||||
measure,
|
||||
nativeEvent,
|
||||
schedulingEvent,
|
||||
snapshot,
|
||||
suspenseEvent,
|
||||
userTimingMark,
|
||||
} = hoveredEvent;
|
||||
@@ -110,6 +112,8 @@ export default function EventTooltip({
|
||||
tooltipRef={tooltipRef}
|
||||
/>
|
||||
);
|
||||
} else if (snapshot !== null) {
|
||||
return <TooltipSnapshot snapshot={snapshot} tooltipRef={tooltipRef} />;
|
||||
} else if (suspenseEvent !== null) {
|
||||
return (
|
||||
<TooltipSuspenseEvent
|
||||
@@ -301,6 +305,23 @@ const TooltipSchedulingEvent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipSnapshot = ({
|
||||
snapshot,
|
||||
tooltipRef,
|
||||
}: {
|
||||
snapshot: Snapshot,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
<img
|
||||
src={snapshot.imageSource}
|
||||
style={{width: snapshot.width / 2, height: snapshot.height / 2}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipSuspenseEvent = ({
|
||||
suspenseEvent,
|
||||
tooltipRef,
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {Snapshot, ReactProfilerData} from '../types';
|
||||
import type {
|
||||
Interaction,
|
||||
MouseMoveInteraction,
|
||||
Rect,
|
||||
Size,
|
||||
Surface,
|
||||
ViewRefs,
|
||||
} from '../view-base';
|
||||
|
||||
import {positioningScaleFactor, timestampToPosition} from './utils/positioning';
|
||||
import {
|
||||
intersectionOfRects,
|
||||
rectContainsPoint,
|
||||
rectEqualToRect,
|
||||
View,
|
||||
} from '../view-base';
|
||||
import {BORDER_SIZE, COLORS, SNAPSHOT_HEIGHT} from './constants';
|
||||
|
||||
type OnHover = (node: Snapshot | null) => void;
|
||||
|
||||
export class SnapshotsView extends View {
|
||||
_intrinsicSize: Size;
|
||||
_profilerData: ReactProfilerData;
|
||||
|
||||
onHover: OnHover | null = null;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
|
||||
super(surface, frame);
|
||||
|
||||
this._intrinsicSize = {
|
||||
width: profilerData.duration,
|
||||
height: SNAPSHOT_HEIGHT,
|
||||
};
|
||||
this._profilerData = profilerData;
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {visibleArea} = this;
|
||||
|
||||
context.fillStyle = COLORS.BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
visibleArea.size.width,
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
const y = visibleArea.origin.y;
|
||||
|
||||
let x = visibleArea.origin.x;
|
||||
|
||||
// Rather than drawing each snapshot where it occured,
|
||||
// draw them at fixed intervals and just show the nearest one.
|
||||
while (x < visibleArea.origin.x + visibleArea.size.width) {
|
||||
const snapshot = this._findClosestSnapshot(x);
|
||||
|
||||
const scaledHeight = SNAPSHOT_HEIGHT;
|
||||
const scaledWidth = (snapshot.width * SNAPSHOT_HEIGHT) / snapshot.height;
|
||||
|
||||
const imageRect: Rect = {
|
||||
origin: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
size: {width: scaledWidth, height: scaledHeight},
|
||||
};
|
||||
|
||||
// Lazily create and cache Image objects as we render a snapsho for the first time.
|
||||
if (snapshot.image === null) {
|
||||
const img = (snapshot.image = new Image());
|
||||
img.onload = () => {
|
||||
this._drawSnapshotImage(context, snapshot, imageRect);
|
||||
};
|
||||
img.src = snapshot.imageSource;
|
||||
} else {
|
||||
this._drawSnapshotImage(context, snapshot, imageRect);
|
||||
}
|
||||
|
||||
x += scaledWidth + BORDER_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_drawSnapshotImage(
|
||||
context: CanvasRenderingContext2D,
|
||||
snapshot: Snapshot,
|
||||
imageRect: Rect,
|
||||
) {
|
||||
const visibleArea = this.visibleArea;
|
||||
|
||||
// Prevent snapshot from visibly overflowing its container when clipped.
|
||||
const shouldClip = !rectEqualToRect(imageRect, visibleArea);
|
||||
if (shouldClip) {
|
||||
const clippedRect = intersectionOfRects(imageRect, visibleArea);
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.rect(
|
||||
clippedRect.origin.x,
|
||||
clippedRect.origin.y,
|
||||
clippedRect.size.width,
|
||||
clippedRect.size.height,
|
||||
);
|
||||
context.closePath();
|
||||
context.clip();
|
||||
}
|
||||
|
||||
// $FlowFixMe Flow doesn't know about the 9 argument variant of drawImage()
|
||||
context.drawImage(
|
||||
snapshot.image,
|
||||
|
||||
// Image coordinates
|
||||
0,
|
||||
0,
|
||||
|
||||
// Native image size
|
||||
snapshot.width,
|
||||
snapshot.height,
|
||||
|
||||
// Canvas coordinates
|
||||
imageRect.origin.x,
|
||||
imageRect.origin.y,
|
||||
|
||||
// Scaled image size
|
||||
imageRect.size.width,
|
||||
imageRect.size.height,
|
||||
);
|
||||
|
||||
if (shouldClip) {
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
|
||||
_findClosestSnapshot(x: number): Snapshot {
|
||||
const frame = this.frame;
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
|
||||
const snapshots = this._profilerData.snapshots;
|
||||
|
||||
let startIndex = 0;
|
||||
let stopIndex = snapshots.length - 1;
|
||||
while (startIndex <= stopIndex) {
|
||||
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
|
||||
const snapshot = snapshots[currentIndex];
|
||||
const {timestamp} = snapshot;
|
||||
|
||||
const snapshotX = Math.floor(
|
||||
timestampToPosition(timestamp, scaleFactor, frame),
|
||||
);
|
||||
|
||||
if (x < snapshotX) {
|
||||
stopIndex = currentIndex - 1;
|
||||
} else {
|
||||
startIndex = currentIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return snapshots[stopIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const {onHover, visibleArea} = this;
|
||||
if (!onHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, visibleArea)) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = this._findClosestSnapshot(location.x);
|
||||
if (snapshot) {
|
||||
this.currentCursor = 'context-menu';
|
||||
viewRefs.hoveredView = this;
|
||||
onHover(snapshot);
|
||||
} else {
|
||||
onHover(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export const REACT_MEASURE_HEIGHT = 14;
|
||||
export const BORDER_SIZE = 1;
|
||||
export const FLAMECHART_FRAME_HEIGHT = 14;
|
||||
export const TEXT_PADDING = 3;
|
||||
export const SNAPSHOT_HEIGHT = 50;
|
||||
|
||||
export const INTERVAL_TIMES = [
|
||||
1,
|
||||
|
||||
@@ -12,6 +12,7 @@ export * from './FlamechartView';
|
||||
export * from './NativeEventsView';
|
||||
export * from './ReactMeasuresView';
|
||||
export * from './SchedulingEventsView';
|
||||
export * from './SnapshotsView';
|
||||
export * from './SuspenseEventsView';
|
||||
export * from './TimeAxisMarkersView';
|
||||
export * from './UserTimingMarksView';
|
||||
|
||||
+43
-37
@@ -17,7 +17,7 @@ import {
|
||||
} from '../../constants';
|
||||
import REACT_VERSION from 'shared/ReactVersion';
|
||||
|
||||
describe(getLanesFromTransportDecimalBitmask, () => {
|
||||
describe('getLanesFromTransportDecimalBitmask', () => {
|
||||
it('should return array of lane numbers from bitmask string', () => {
|
||||
expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]);
|
||||
expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]);
|
||||
@@ -57,7 +57,7 @@ describe(getLanesFromTransportDecimalBitmask, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(preprocessData, () => {
|
||||
describe('preprocessData', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let Scheduler;
|
||||
@@ -217,11 +217,11 @@ describe(preprocessData, () => {
|
||||
delete global.performance;
|
||||
});
|
||||
|
||||
it('should throw given an empty timeline', () => {
|
||||
expect(() => preprocessData([])).toThrow();
|
||||
it('should throw given an empty timeline', async () => {
|
||||
await expect(async () => preprocessData([])).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw given a timeline with no Profile event', () => {
|
||||
it('should throw given a timeline with no Profile event', async () => {
|
||||
const randomSample = createUserTimingEntry({
|
||||
dur: 100,
|
||||
tdur: 200,
|
||||
@@ -231,10 +231,10 @@ describe(preprocessData, () => {
|
||||
args: {},
|
||||
});
|
||||
|
||||
expect(() => preprocessData([randomSample])).toThrow();
|
||||
await expect(async () => preprocessData([randomSample])).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should return empty data given a timeline with no React scheduling profiling marks', () => {
|
||||
it('should return empty data given a timeline with no React scheduling profiling marks', async () => {
|
||||
const cpuProfilerSample = creactCpuProfilerSample();
|
||||
const randomSample = createUserTimingEntry({
|
||||
dur: 100,
|
||||
@@ -246,7 +246,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
...createBoilerplateEntries(),
|
||||
cpuProfilerSample,
|
||||
randomSample,
|
||||
@@ -327,6 +327,7 @@ describe(preprocessData, () => {
|
||||
"otherUserTimingMarks": Array [],
|
||||
"reactVersion": "17.0.3",
|
||||
"schedulingEvents": Array [],
|
||||
"snapshots": Array [],
|
||||
"startTime": 1,
|
||||
"suspenseEvents": Array [],
|
||||
}
|
||||
@@ -334,13 +335,13 @@ describe(preprocessData, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should process legacy data format (before lane labels were added)', () => {
|
||||
it('should process legacy data format (before lane labels were added)', async () => {
|
||||
const cpuProfilerSample = creactCpuProfilerSample();
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
// Data below is hard-coded based on an older profile sample.
|
||||
// Should be fine since this is explicitly a legacy-format test.
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
...createBoilerplateEntries(),
|
||||
cpuProfilerSample,
|
||||
createUserTimingEntry({
|
||||
@@ -541,6 +542,7 @@ describe(preprocessData, () => {
|
||||
"warning": null,
|
||||
},
|
||||
],
|
||||
"snapshots": Array [],
|
||||
"startTime": 1,
|
||||
"suspenseEvents": Array [],
|
||||
}
|
||||
@@ -548,11 +550,11 @@ describe(preprocessData, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should process a sample legacy render sequence', () => {
|
||||
it('should process a sample legacy render sequence', async () => {
|
||||
ReactDOM.render(<div />, document.createElement('div'));
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
...createBoilerplateEntries(),
|
||||
...createUserTimingData(clearedMarks),
|
||||
]);
|
||||
@@ -730,6 +732,7 @@ describe(preprocessData, () => {
|
||||
"warning": null,
|
||||
},
|
||||
],
|
||||
"snapshots": Array [],
|
||||
"startTime": 4,
|
||||
"suspenseEvents": Array [],
|
||||
}
|
||||
@@ -737,7 +740,7 @@ describe(preprocessData, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should process a sample createRoot render sequence', () => {
|
||||
it('should process a sample createRoot render sequence', async () => {
|
||||
function App() {
|
||||
const [didMount, setDidMount] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
@@ -752,7 +755,7 @@ describe(preprocessData, () => {
|
||||
const root = ReactDOM.createRoot(document.createElement('div'));
|
||||
act(() => root.render(<App />));
|
||||
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
...createBoilerplateEntries(),
|
||||
...createUserTimingData(clearedMarks),
|
||||
]);
|
||||
@@ -1074,6 +1077,7 @@ describe(preprocessData, () => {
|
||||
"warning": null,
|
||||
},
|
||||
],
|
||||
"snapshots": Array [],
|
||||
"startTime": 4,
|
||||
"suspenseEvents": Array [],
|
||||
}
|
||||
@@ -1082,7 +1086,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
|
||||
// @gate enableSchedulingProfiler
|
||||
it('should error if events and measures are incomplete', () => {
|
||||
it('should error if events and measures are incomplete', async () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(<div />, container);
|
||||
|
||||
@@ -1097,7 +1101,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
|
||||
// @gate enableSchedulingProfiler
|
||||
it('should error if work is completed without being started', () => {
|
||||
it('should error if work is completed without being started', async () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(<div />, container);
|
||||
|
||||
@@ -1111,7 +1115,7 @@ describe(preprocessData, () => {
|
||||
expect(error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should populate other user timing marks', () => {
|
||||
it('should populate other user timing marks', async () => {
|
||||
const userTimingData = createUserTimingData([]);
|
||||
userTimingData.push(
|
||||
createUserTimingEntry({
|
||||
@@ -1138,7 +1142,7 @@ describe(preprocessData, () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
...createBoilerplateEntries(),
|
||||
...userTimingData,
|
||||
]);
|
||||
@@ -1162,7 +1166,7 @@ describe(preprocessData, () => {
|
||||
|
||||
describe('warnings', () => {
|
||||
describe('long event handlers', () => {
|
||||
it('should not warn when React scedules a (sync) update inside of a short event handler', () => {
|
||||
it('should not warn when React scedules a (sync) update inside of a short event handler', async () => {
|
||||
function App() {
|
||||
return null;
|
||||
}
|
||||
@@ -1180,13 +1184,13 @@ describe(preprocessData, () => {
|
||||
|
||||
testMarks.push(...createUserTimingData(clearedMarks));
|
||||
|
||||
const data = preprocessData(testMarks);
|
||||
const data = await preprocessData(testMarks);
|
||||
const event = data.nativeEvents.find(({type}) => type === 'click');
|
||||
expect(event.warning).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not warn about long events if the cause was non-React JavaScript', () => {
|
||||
it('should not warn about long events if the cause was non-React JavaScript', async () => {
|
||||
function App() {
|
||||
return null;
|
||||
}
|
||||
@@ -1206,13 +1210,13 @@ describe(preprocessData, () => {
|
||||
|
||||
testMarks.push(...createUserTimingData(clearedMarks));
|
||||
|
||||
const data = preprocessData(testMarks);
|
||||
const data = await preprocessData(testMarks);
|
||||
const event = data.nativeEvents.find(({type}) => type === 'click');
|
||||
expect(event.warning).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
it('should warn when React scedules a long (sync) update inside of an event', () => {
|
||||
it('should warn when React scedules a long (sync) update inside of an event', async () => {
|
||||
function App() {
|
||||
return null;
|
||||
}
|
||||
@@ -1245,7 +1249,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
});
|
||||
|
||||
const data = preprocessData(testMarks);
|
||||
const data = await preprocessData(testMarks);
|
||||
const event = data.nativeEvents.find(({type}) => type === 'click');
|
||||
expect(event.warning).toMatchInlineSnapshot(
|
||||
`"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, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', () => {
|
||||
it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', async () => {
|
||||
function Yield({id, value}) {
|
||||
Scheduler.unstable_yieldValue(`${id}:${value}`);
|
||||
return null;
|
||||
@@ -1303,7 +1307,7 @@ describe(preprocessData, () => {
|
||||
|
||||
testMarks.push(...createUserTimingData(clearedMarks));
|
||||
|
||||
const data = preprocessData(testMarks);
|
||||
const data = await preprocessData(testMarks);
|
||||
const event = data.nativeEvents.find(({type}) => type === 'click');
|
||||
expect(event.warning).toBe(null);
|
||||
}
|
||||
@@ -1311,7 +1315,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
|
||||
describe('nested updates', () => {
|
||||
it('should not warn about short nested (state) updates during layout effects', () => {
|
||||
it('should not warn about short nested (state) updates during layout effects', async () => {
|
||||
function Component() {
|
||||
const [didMount, setDidMount] = React.useState(false);
|
||||
Scheduler.unstable_yieldValue(
|
||||
@@ -1334,7 +1338,7 @@ describe(preprocessData, () => {
|
||||
'Component update',
|
||||
]);
|
||||
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
...createBoilerplateEntries(),
|
||||
...createUserTimingData(clearedMarks),
|
||||
]);
|
||||
@@ -1346,7 +1350,7 @@ describe(preprocessData, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should not warn about short (forced) updates during layout effects', () => {
|
||||
it('should not warn about short (forced) updates during layout effects', async () => {
|
||||
class Component extends React.Component {
|
||||
_didMount: boolean = false;
|
||||
componentDidMount() {
|
||||
@@ -1372,7 +1376,7 @@ describe(preprocessData, () => {
|
||||
'Component update',
|
||||
]);
|
||||
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
...createBoilerplateEntries(),
|
||||
...createUserTimingData(clearedMarks),
|
||||
]);
|
||||
@@ -1384,7 +1388,7 @@ describe(preprocessData, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should warn about long nested (state) updates during layout effects', () => {
|
||||
it('should warn about long nested (state) updates during layout effects', async () => {
|
||||
function Component() {
|
||||
const [didMount, setDidMount] = React.useState(false);
|
||||
Scheduler.unstable_yieldValue(
|
||||
@@ -1429,7 +1433,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
});
|
||||
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
cpuProfilerSample,
|
||||
...createBoilerplateEntries(),
|
||||
...testMarks,
|
||||
@@ -1444,7 +1448,7 @@ describe(preprocessData, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should warn about long nested (forced) updates during layout effects', () => {
|
||||
it('should warn about long nested (forced) updates during layout effects', async () => {
|
||||
class Component extends React.Component {
|
||||
_didMount: boolean = false;
|
||||
componentDidMount() {
|
||||
@@ -1490,7 +1494,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
});
|
||||
|
||||
const data = preprocessData([
|
||||
const data = await preprocessData([
|
||||
cpuProfilerSample,
|
||||
...createBoilerplateEntries(),
|
||||
...testMarks,
|
||||
@@ -1509,7 +1513,7 @@ describe(preprocessData, () => {
|
||||
describe('suspend during an update', () => {
|
||||
// This also tests an edge case where the a component suspends while profiling
|
||||
// before the first commit is logged (so the lane-to-labels map will not yet exist).
|
||||
it('should warn about suspending during an udpate', () => {
|
||||
it('should warn about suspending during an udpate', async () => {
|
||||
let promise = null;
|
||||
let resolvedValue = null;
|
||||
function readValue(value) {
|
||||
@@ -1557,7 +1561,7 @@ describe(preprocessData, () => {
|
||||
|
||||
testMarks.push(...createUserTimingData(clearedMarks));
|
||||
|
||||
const data = preprocessData(testMarks);
|
||||
const data = await preprocessData(testMarks);
|
||||
expect(data.suspenseEvents).toHaveLength(1);
|
||||
expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot(
|
||||
`"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, () => {
|
||||
|
||||
testMarks.push(...createUserTimingData(clearedMarks));
|
||||
|
||||
const data = preprocessData(testMarks);
|
||||
const data = await preprocessData(testMarks);
|
||||
expect(data.suspenseEvents).toHaveLength(1);
|
||||
expect(data.suspenseEvents[0].warning).toBe(null);
|
||||
}
|
||||
@@ -1623,5 +1627,7 @@ describe(preprocessData, () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add test for snapshot base64 parsing
|
||||
|
||||
// TODO: Add test for flamechart parsing
|
||||
});
|
||||
|
||||
+3
-1
@@ -26,9 +26,11 @@ export async function importFile(file: File): Promise<ImportWorkerOutputData> {
|
||||
throw new InvalidProfileError('No profiling data found in file.');
|
||||
}
|
||||
|
||||
const processedData = await preprocessData(events);
|
||||
|
||||
return {
|
||||
status: 'SUCCESS',
|
||||
processedData: preprocessData(events),
|
||||
processedData,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidProfileError) {
|
||||
|
||||
+44
-3
@@ -26,7 +26,6 @@ import type {
|
||||
SchedulingEvent,
|
||||
SuspenseEvent,
|
||||
} from '../types';
|
||||
|
||||
import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants';
|
||||
import InvalidProfileError from './InvalidProfileError';
|
||||
import {getBatchRange} from '../utils/getBatchRange';
|
||||
@@ -40,6 +39,7 @@ type MeasureStackElement = {|
|
||||
|};
|
||||
|
||||
type ProcessorState = {|
|
||||
asyncProcessingPromises: Promise<any>[],
|
||||
batchUID: BatchUID,
|
||||
currentReactComponentMeasure: ReactComponentMeasure | null,
|
||||
measureStack: MeasureStackElement[],
|
||||
@@ -224,6 +224,41 @@ function processTimelineEvent(
|
||||
) {
|
||||
const {args, cat, name, ts, ph} = event;
|
||||
switch (cat) {
|
||||
case 'disabled-by-default-devtools.screenshot':
|
||||
const encodedSnapshot = args.snapshot; // Base 64 encoded
|
||||
|
||||
const snapshot = {
|
||||
height: 0,
|
||||
image: null,
|
||||
imageSource: `data:image/png;base64,${encodedSnapshot}`,
|
||||
timestamp: (ts - currentProfilerData.startTime) / 1000,
|
||||
width: 0,
|
||||
};
|
||||
|
||||
// Delay processing until we've extracted snapshot dimensions.
|
||||
let resolveFn = ((null: any): Function);
|
||||
state.asyncProcessingPromises.push(
|
||||
new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
// Parse the Base64 image data to determine native size.
|
||||
// This will be used later to scale for display within the thumbnail strip.
|
||||
fetch(snapshot.imageSource)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
// $FlowFixMe createImageBitmap
|
||||
createImageBitmap(blob).then(bitmap => {
|
||||
snapshot.height = bitmap.height;
|
||||
snapshot.width = bitmap.width;
|
||||
|
||||
resolveFn();
|
||||
});
|
||||
});
|
||||
|
||||
currentProfilerData.snapshots.push(snapshot);
|
||||
break;
|
||||
case 'devtools.timeline':
|
||||
if (name === 'EventDispatch') {
|
||||
const type = args.data.type;
|
||||
@@ -661,9 +696,9 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
|
||||
return flamechart;
|
||||
}
|
||||
|
||||
export default function preprocessData(
|
||||
export default async function preprocessData(
|
||||
timeline: TimelineEvent[],
|
||||
): ReactProfilerData {
|
||||
): Promise<ReactProfilerData> {
|
||||
const flamechart = preprocessFlamechart(timeline);
|
||||
|
||||
const laneToReactMeasureMap = new Map();
|
||||
@@ -682,6 +717,7 @@ export default function preprocessData(
|
||||
otherUserTimingMarks: [],
|
||||
reactVersion: null,
|
||||
schedulingEvents: [],
|
||||
snapshots: [],
|
||||
startTime: 0,
|
||||
suspenseEvents: [],
|
||||
};
|
||||
@@ -713,6 +749,7 @@ export default function preprocessData(
|
||||
(timeline[timeline.length - 1].ts - profilerData.startTime) / 1000;
|
||||
|
||||
const state: ProcessorState = {
|
||||
asyncProcessingPromises: [],
|
||||
batchUID: 0,
|
||||
currentReactComponentMeasure: null,
|
||||
measureStack: [],
|
||||
@@ -773,5 +810,9 @@ export default function preprocessData(
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for any async processing to complete before returning.
|
||||
// Since processing is done in a worker, async work must complete before data is serialized and returned.
|
||||
await Promise.all(state.asyncProcessingPromises);
|
||||
|
||||
return profilerData;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,14 @@ export type UserTimingMark = {|
|
||||
timestamp: Milliseconds,
|
||||
|};
|
||||
|
||||
export type Snapshot = {|
|
||||
height: number,
|
||||
image: Image | null,
|
||||
+imageSource: string,
|
||||
+timestamp: Milliseconds,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
/**
|
||||
* A "layer" of stack frames in the profiler UI, i.e. all stack frames of the
|
||||
* same depth across all stack traces. Displayed as a flamechart row in the UI.
|
||||
@@ -150,6 +158,7 @@ export type ReactProfilerData = {|
|
||||
otherUserTimingMarks: UserTimingMark[],
|
||||
reactVersion: string | null,
|
||||
schedulingEvents: SchedulingEvent[],
|
||||
snapshots: Snapshot[],
|
||||
startTime: number,
|
||||
suspenseEvents: SuspenseEvent[],
|
||||
|};
|
||||
@@ -162,5 +171,6 @@ export type ReactHoverContextInfo = {|
|
||||
nativeEvent: NativeEvent | null,
|
||||
schedulingEvent: SchedulingEvent | null,
|
||||
suspenseEvent: SuspenseEvent | null,
|
||||
snapshot: Snapshot | null,
|
||||
userTimingMark: UserTimingMark | null,
|
||||
|};
|
||||
|
||||
Reference in New Issue
Block a user