mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
Refactor DevTools to record Timeline data (in memory) while profiling. Updated the Profiler UI to import/export Timeline data along with legacy profiler data. Relates to issue #22529
521 lines
14 KiB
JavaScript
521 lines
14 KiB
JavaScript
/**
|
|
* 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 {Point} from './view-base';
|
|
import type {
|
|
FlamechartStackFrame,
|
|
NativeEvent,
|
|
NetworkMeasure,
|
|
ReactComponentMeasure,
|
|
ReactHoverContextInfo,
|
|
ReactMeasure,
|
|
TimelineData,
|
|
SchedulingEvent,
|
|
Snapshot,
|
|
SuspenseEvent,
|
|
ThrownError,
|
|
UserTimingMark,
|
|
} from './types';
|
|
|
|
import * as React from 'react';
|
|
import {formatDuration, formatTimestamp, trimString} from './utils/formatting';
|
|
import {getBatchRange} from './utils/getBatchRange';
|
|
import useSmartTooltip from './utils/useSmartTooltip';
|
|
import styles from './EventTooltip.css';
|
|
|
|
const MAX_TOOLTIP_TEXT_LENGTH = 60;
|
|
|
|
type Props = {|
|
|
canvasRef: {|current: HTMLCanvasElement | null|},
|
|
data: TimelineData,
|
|
height: number,
|
|
hoveredEvent: ReactHoverContextInfo | null,
|
|
origin: Point,
|
|
width: number,
|
|
|};
|
|
|
|
function getSchedulingEventLabel(event: SchedulingEvent): string | null {
|
|
switch (event.type) {
|
|
case 'schedule-render':
|
|
return 'render scheduled';
|
|
case 'schedule-state-update':
|
|
return 'state update scheduled';
|
|
case 'schedule-force-update':
|
|
return 'force update scheduled';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getReactMeasureLabel(type): string | null {
|
|
switch (type) {
|
|
case 'commit':
|
|
return 'react commit';
|
|
case 'render-idle':
|
|
return 'react idle';
|
|
case 'render':
|
|
return 'react render';
|
|
case 'layout-effects':
|
|
return 'react layout effects';
|
|
case 'passive-effects':
|
|
return 'react passive effects';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default function EventTooltip({
|
|
canvasRef,
|
|
data,
|
|
height,
|
|
hoveredEvent,
|
|
origin,
|
|
width,
|
|
}: Props) {
|
|
const ref = useSmartTooltip({
|
|
canvasRef,
|
|
mouseX: origin.x,
|
|
mouseY: origin.y,
|
|
});
|
|
|
|
if (hoveredEvent === null) {
|
|
return null;
|
|
}
|
|
|
|
const {
|
|
componentMeasure,
|
|
flamechartStackFrame,
|
|
measure,
|
|
nativeEvent,
|
|
networkMeasure,
|
|
schedulingEvent,
|
|
snapshot,
|
|
suspenseEvent,
|
|
thrownError,
|
|
userTimingMark,
|
|
} = hoveredEvent;
|
|
|
|
let content = null;
|
|
if (componentMeasure !== null) {
|
|
content = (
|
|
<TooltipReactComponentMeasure componentMeasure={componentMeasure} />
|
|
);
|
|
} else if (nativeEvent !== null) {
|
|
content = <TooltipNativeEvent nativeEvent={nativeEvent} />;
|
|
} else if (networkMeasure !== null) {
|
|
content = <TooltipNetworkMeasure networkMeasure={networkMeasure} />;
|
|
} else if (schedulingEvent !== null) {
|
|
content = (
|
|
<TooltipSchedulingEvent data={data} schedulingEvent={schedulingEvent} />
|
|
);
|
|
} else if (snapshot !== null) {
|
|
content = (
|
|
<TooltipSnapshot height={height} snapshot={snapshot} width={width} />
|
|
);
|
|
} else if (suspenseEvent !== null) {
|
|
content = <TooltipSuspenseEvent suspenseEvent={suspenseEvent} />;
|
|
} else if (measure !== null) {
|
|
content = <TooltipReactMeasure data={data} measure={measure} />;
|
|
} else if (flamechartStackFrame !== null) {
|
|
content = <TooltipFlamechartNode stackFrame={flamechartStackFrame} />;
|
|
} else if (userTimingMark !== null) {
|
|
content = <TooltipUserTimingMark mark={userTimingMark} />;
|
|
} else if (thrownError !== null) {
|
|
content = <TooltipThrownError thrownError={thrownError} />;
|
|
}
|
|
|
|
if (content !== null) {
|
|
return (
|
|
<div className={styles.Tooltip} ref={ref}>
|
|
{content}
|
|
</div>
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const TooltipReactComponentMeasure = ({
|
|
componentMeasure,
|
|
}: {|
|
|
componentMeasure: ReactComponentMeasure,
|
|
|}) => {
|
|
const {componentName, duration, timestamp, type, warning} = componentMeasure;
|
|
|
|
let label = componentName;
|
|
switch (type) {
|
|
case 'render':
|
|
label += ' rendered';
|
|
break;
|
|
case 'layout-effect-mount':
|
|
label += ' mounted layout effect';
|
|
break;
|
|
case 'layout-effect-unmount':
|
|
label += ' unmounted layout effect';
|
|
break;
|
|
case 'passive-effect-mount':
|
|
label += ' mounted passive effect';
|
|
break;
|
|
case 'passive-effect-unmount':
|
|
label += ' unmounted passive effect';
|
|
break;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.TooltipSection}>
|
|
{trimString(label, 768)}
|
|
<div className={styles.Divider} />
|
|
<div className={styles.DetailsGrid}>
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
<div className={styles.DetailsGridLabel}>Duration:</div>
|
|
<div>{formatDuration(duration)}</div>
|
|
</div>
|
|
</div>
|
|
{warning !== null && (
|
|
<div className={styles.TooltipWarningSection}>
|
|
<div className={styles.WarningText}>{warning}</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const TooltipFlamechartNode = ({
|
|
stackFrame,
|
|
}: {|
|
|
stackFrame: FlamechartStackFrame,
|
|
|}) => {
|
|
const {name, timestamp, duration, locationLine, locationColumn} = stackFrame;
|
|
return (
|
|
<div className={styles.TooltipSection}>
|
|
<span className={styles.FlamechartStackFrameName}>{name}</span>
|
|
<div className={styles.DetailsGrid}>
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
<div className={styles.DetailsGridLabel}>Duration:</div>
|
|
<div>{formatDuration(duration)}</div>
|
|
{(locationLine !== undefined || locationColumn !== undefined) && (
|
|
<>
|
|
<div className={styles.DetailsGridLabel}>Location:</div>
|
|
<div>
|
|
line {locationLine}, column {locationColumn}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TooltipNativeEvent = ({nativeEvent}: {|nativeEvent: NativeEvent|}) => {
|
|
const {duration, timestamp, type, warning} = nativeEvent;
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.TooltipSection}>
|
|
<span className={styles.NativeEventName}>{trimString(type, 768)}</span>
|
|
event
|
|
<div className={styles.Divider} />
|
|
<div className={styles.DetailsGrid}>
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
<div className={styles.DetailsGridLabel}>Duration:</div>
|
|
<div>{formatDuration(duration)}</div>
|
|
</div>
|
|
</div>
|
|
{warning !== null && (
|
|
<div className={styles.TooltipWarningSection}>
|
|
<div className={styles.WarningText}>{warning}</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const TooltipNetworkMeasure = ({
|
|
networkMeasure,
|
|
}: {|
|
|
networkMeasure: NetworkMeasure,
|
|
|}) => {
|
|
const {
|
|
finishTimestamp,
|
|
lastReceivedDataTimestamp,
|
|
priority,
|
|
sendRequestTimestamp,
|
|
url,
|
|
} = networkMeasure;
|
|
|
|
let urlToDisplay = url;
|
|
if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) {
|
|
const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2);
|
|
urlToDisplay = url.substr(0, half) + '…' + url.substr(url.length - half);
|
|
}
|
|
|
|
const timestampBegin = sendRequestTimestamp;
|
|
const timestampEnd = finishTimestamp || lastReceivedDataTimestamp;
|
|
const duration =
|
|
timestampEnd > 0
|
|
? formatDuration(finishTimestamp - timestampBegin)
|
|
: '(incomplete)';
|
|
|
|
return (
|
|
<div className={styles.SingleLineTextSection}>
|
|
{duration} <span className={styles.DimText}>{priority}</span>{' '}
|
|
{urlToDisplay}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TooltipSchedulingEvent = ({
|
|
data,
|
|
schedulingEvent,
|
|
}: {|
|
|
data: TimelineData,
|
|
schedulingEvent: SchedulingEvent,
|
|
|}) => {
|
|
const label = getSchedulingEventLabel(schedulingEvent);
|
|
if (!label) {
|
|
if (__DEV__) {
|
|
console.warn(
|
|
'Unexpected schedulingEvent type "%s"',
|
|
schedulingEvent.type,
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
let laneLabels = null;
|
|
let lanes = null;
|
|
switch (schedulingEvent.type) {
|
|
case 'schedule-render':
|
|
case 'schedule-state-update':
|
|
case 'schedule-force-update':
|
|
lanes = schedulingEvent.lanes;
|
|
laneLabels = lanes.map(
|
|
lane => ((data.laneToLabelMap.get(lane): any): string),
|
|
);
|
|
break;
|
|
}
|
|
|
|
const {componentName, timestamp, warning} = schedulingEvent;
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.TooltipSection}>
|
|
{componentName && (
|
|
<span className={styles.ComponentName}>
|
|
{trimString(componentName, 100)}
|
|
</span>
|
|
)}
|
|
{label}
|
|
<div className={styles.Divider} />
|
|
<div className={styles.DetailsGrid}>
|
|
{laneLabels !== null && lanes !== null && (
|
|
<>
|
|
<div className={styles.DetailsGridLabel}>Lanes:</div>
|
|
<div>
|
|
{laneLabels.join(', ')} ({lanes.join(', ')})
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
</div>
|
|
</div>
|
|
{warning !== null && (
|
|
<div className={styles.TooltipWarningSection}>
|
|
<div className={styles.WarningText}>{warning}</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const TooltipSnapshot = ({
|
|
height,
|
|
snapshot,
|
|
width,
|
|
}: {|
|
|
height: number,
|
|
snapshot: Snapshot,
|
|
width: number,
|
|
|}) => {
|
|
const aspectRatio = snapshot.width / snapshot.height;
|
|
|
|
// Zoomed in view should not be any bigger than the DevTools viewport.
|
|
let safeWidth = snapshot.width;
|
|
let safeHeight = snapshot.height;
|
|
if (safeWidth > width) {
|
|
safeWidth = width;
|
|
safeHeight = safeWidth / aspectRatio;
|
|
}
|
|
if (safeHeight > height) {
|
|
safeHeight = height;
|
|
safeWidth = safeHeight * aspectRatio;
|
|
}
|
|
|
|
return (
|
|
<img
|
|
className={styles.Image}
|
|
src={snapshot.imageSource}
|
|
style={{height: safeHeight, width: safeWidth}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const TooltipSuspenseEvent = ({
|
|
suspenseEvent,
|
|
}: {|
|
|
suspenseEvent: SuspenseEvent,
|
|
|}) => {
|
|
const {
|
|
componentName,
|
|
duration,
|
|
phase,
|
|
promiseName,
|
|
resolution,
|
|
timestamp,
|
|
warning,
|
|
} = suspenseEvent;
|
|
|
|
let label = 'suspended';
|
|
if (phase !== null) {
|
|
label += ` during ${phase}`;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.TooltipSection}>
|
|
{componentName && (
|
|
<span className={styles.ComponentName}>
|
|
{trimString(componentName, 100)}
|
|
</span>
|
|
)}
|
|
{label}
|
|
<div className={styles.Divider} />
|
|
<div className={styles.DetailsGrid}>
|
|
{promiseName !== null && (
|
|
<>
|
|
<div className={styles.DetailsGridLabel}>Resource:</div>
|
|
<div className={styles.DetailsGridLongValue}>{promiseName}</div>
|
|
</>
|
|
)}
|
|
<div className={styles.DetailsGridLabel}>Status:</div>
|
|
<div>{resolution}</div>
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
{duration !== null && (
|
|
<>
|
|
<div className={styles.DetailsGridLabel}>Duration:</div>
|
|
<div>{formatDuration(duration)}</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{warning !== null && (
|
|
<div className={styles.TooltipWarningSection}>
|
|
<div className={styles.WarningText}>{warning}</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const TooltipReactMeasure = ({
|
|
data,
|
|
measure,
|
|
}: {|
|
|
data: TimelineData,
|
|
measure: ReactMeasure,
|
|
|}) => {
|
|
const label = getReactMeasureLabel(measure.type);
|
|
if (!label) {
|
|
if (__DEV__) {
|
|
console.warn('Unexpected measure type "%s"', measure.type);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const {batchUID, duration, timestamp, lanes} = measure;
|
|
const [startTime, stopTime] = getBatchRange(batchUID, data);
|
|
|
|
const laneLabels = lanes.map(
|
|
lane => ((data.laneToLabelMap.get(lane): any): string),
|
|
);
|
|
|
|
return (
|
|
<div className={styles.TooltipSection}>
|
|
<span className={styles.ReactMeasureLabel}>{label}</span>
|
|
<div className={styles.Divider} />
|
|
<div className={styles.DetailsGrid}>
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
{measure.type !== 'render-idle' && (
|
|
<>
|
|
<div className={styles.DetailsGridLabel}>Duration:</div>
|
|
<div>{formatDuration(duration)}</div>
|
|
</>
|
|
)}
|
|
<div className={styles.DetailsGridLabel}>Batch duration:</div>
|
|
<div>{formatDuration(stopTime - startTime)}</div>
|
|
<div className={styles.DetailsGridLabel}>
|
|
Lane{lanes.length === 1 ? '' : 's'}:
|
|
</div>
|
|
<div>
|
|
{laneLabels.length > 0
|
|
? `${laneLabels.join(', ')} (${lanes.join(', ')})`
|
|
: lanes.join(', ')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TooltipUserTimingMark = ({mark}: {|mark: UserTimingMark|}) => {
|
|
const {name, timestamp} = mark;
|
|
return (
|
|
<div className={styles.TooltipSection}>
|
|
<span className={styles.UserTimingLabel}>{name}</span>
|
|
<div className={styles.Divider} />
|
|
<div className={styles.DetailsGrid}>
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TooltipThrownError = ({thrownError}: {|thrownError: ThrownError|}) => {
|
|
const {componentName, message, phase, timestamp} = thrownError;
|
|
const label = `threw an error during ${phase}`;
|
|
return (
|
|
<div className={styles.TooltipSection}>
|
|
{componentName && (
|
|
<span className={styles.ComponentName}>
|
|
{trimString(componentName, 100)}
|
|
</span>
|
|
)}
|
|
<span className={styles.UserTimingLabel}>{label}</span>
|
|
<div className={styles.Divider} />
|
|
<div className={styles.DetailsGrid}>
|
|
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
|
<div>{formatTimestamp(timestamp)}</div>
|
|
{message !== '' && (
|
|
<>
|
|
<div className={styles.DetailsGridLabel}>Error:</div>
|
|
<div>{message}</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|