/** * 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, ReactEvent, ReactHoverContextInfo, ReactMeasure, ReactProfilerData, Return, UserTimingMark, } from './types'; import * as React from 'react'; import {Fragment, useRef} from 'react'; import prettyMilliseconds from 'pretty-ms'; import {COLORS} from './content-views/constants'; import {getBatchRange} from './utils/getBatchRange'; import useSmartTooltip from './utils/useSmartTooltip'; import styles from './EventTooltip.css'; type Props = {| data: ReactProfilerData, hoveredEvent: ReactHoverContextInfo | null, origin: Point, |}; function formatTimestamp(ms) { return ms.toLocaleString(undefined, {minimumFractionDigits: 2}) + 'ms'; } function formatDuration(ms) { return prettyMilliseconds(ms, {millisecondsDecimalDigits: 3}); } function trimmedString(string: string, length: number): string { if (string.length > length) { return `${string.substr(0, length - 1)}…`; } return string; } function getReactEventLabel(type): string | null { switch (type) { case 'schedule-render': return 'render scheduled'; case 'schedule-state-update': return 'state update scheduled'; case 'schedule-force-update': return 'force update scheduled'; case 'suspense-suspend': return 'suspended'; case 'suspense-resolved': return 'suspense resolved'; case 'suspense-rejected': return 'suspense rejected'; default: return null; } } function getReactEventColor(event: ReactEvent): string | null { switch (event.type) { case 'schedule-render': return COLORS.REACT_SCHEDULE_HOVER; case 'schedule-state-update': case 'schedule-force-update': return event.isCascading ? COLORS.REACT_SCHEDULE_CASCADING_HOVER : COLORS.REACT_SCHEDULE_HOVER; case 'suspense-suspend': case 'suspense-resolved': case 'suspense-rejected': return COLORS.REACT_SUSPEND_HOVER; default: return null; } } function getReactMeasureLabel(type): string | null { switch (type) { case 'commit': return 'commit'; case 'render-idle': return 'idle'; case 'render': return 'render'; case 'layout-effects': return 'layout effects'; case 'passive-effects': return 'passive effects'; default: return null; } } export default function EventTooltip({data, hoveredEvent, origin}: Props) { const tooltipRef = useSmartTooltip({ mouseX: origin.x, mouseY: origin.y, }); if (hoveredEvent === null) { return null; } const {event, measure, flamechartStackFrame, userTimingMark} = hoveredEvent; if (event !== null) { return ; } else if (measure !== null) { return ( ); } else if (flamechartStackFrame !== null) { return ( ); } else if (userTimingMark !== null) { return ( ); } return null; } function formatComponentStack(componentStack: string): string { const lines = componentStack.split('\n').map(line => line.trim()); lines.shift(); if (lines.length > 5) { return lines.slice(0, 5).join('\n') + '\n...'; } return lines.join('\n'); } const TooltipFlamechartNode = ({ stackFrame, tooltipRef, }: { stackFrame: FlamechartStackFrame, tooltipRef: Return, }) => { const { name, timestamp, duration, scriptUrl, locationLine, locationColumn, } = stackFrame; return ( {formatDuration(duration)} {name} Timestamp: {formatTimestamp(timestamp)} {scriptUrl && ( <> Script URL: {scriptUrl} > )} {(locationLine !== undefined || locationColumn !== undefined) && ( <> Location: line {locationLine}, column {locationColumn} > )} ); }; const TooltipReactEvent = ({ event, tooltipRef, }: { event: ReactEvent, tooltipRef: Return, }) => { const label = getReactEventLabel(event.type); const color = getReactEventColor(event); if (!label || !color) { if (__DEV__) { console.warn('Unexpected event type "%s"', event.type); } return null; } const {componentName, componentStack, timestamp} = event; return ( {componentName && ( {trimmedString(componentName, 768)} )} {label} Timestamp: {formatTimestamp(timestamp)} {componentStack && ( Component stack: {formatComponentStack(componentStack)} )} ); }; const TooltipReactMeasure = ({ data, measure, tooltipRef, }: { data: ReactProfilerData, measure: ReactMeasure, tooltipRef: Return, }) => { const label = getReactMeasureLabel(measure.type); if (!label) { if (__DEV__) { console.warn('Unexpected measure type "%s"', measure.type); } return null; } const {batchUID, duration, timestamp, lanes, laneLabels} = measure; const [startTime, stopTime] = getBatchRange(batchUID, data); return ( {formatDuration(duration)} {label} Timestamp: {formatTimestamp(timestamp)} Batch duration: {formatDuration(stopTime - startTime)} Lane{lanes.length === 1 ? '' : 's'}: {laneLabels.length > 0 ? `${laneLabels.join(', ')} (${lanes.join(', ')})` : lanes.join(', ')} ); }; const TooltipUserTimingMark = ({ mark, tooltipRef, }: { mark: UserTimingMark, tooltipRef: Return, }) => { const {name, timestamp} = mark; return ( {name} Timestamp: {formatTimestamp(timestamp)} ); };
{formatComponentStack(componentStack)}