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

This commit is contained in:
Brian Vaughn
2021-08-13 17:14:29 -04:00
committed by GitHub
parent e4e8226c62
commit 54e170c794
9 changed files with 375 additions and 41 deletions
@@ -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,
@@ -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';
@@ -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
});
@@ -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) {
@@ -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,
|};