1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and 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 {Point} from './view-base';
    
  11. import type {
    
  12.   FlamechartStackFrame,
    
  13.   NativeEvent,
    
  14.   NetworkMeasure,
    
  15.   ReactComponentMeasure,
    
  16.   ReactEventInfo,
    
  17.   ReactMeasure,
    
  18.   ReactMeasureType,
    
  19.   SchedulingEvent,
    
  20.   Snapshot,
    
  21.   SuspenseEvent,
    
  22.   ThrownError,
    
  23.   TimelineData,
    
  24.   UserTimingMark,
    
  25. } from './types';
    
  26. 
    
  27. import * as React from 'react';
    
  28. import {
    
  29.   formatDuration,
    
  30.   formatTimestamp,
    
  31.   trimString,
    
  32.   getSchedulingEventLabel,
    
  33. } from './utils/formatting';
    
  34. import {getBatchRange} from './utils/getBatchRange';
    
  35. import useSmartTooltip from './utils/useSmartTooltip';
    
  36. import styles from './EventTooltip.css';
    
  37. 
    
  38. const MAX_TOOLTIP_TEXT_LENGTH = 60;
    
  39. 
    
  40. type Props = {
    
  41.   canvasRef: {current: HTMLCanvasElement | null},
    
  42.   data: TimelineData,
    
  43.   height: number,
    
  44.   hoveredEvent: ReactEventInfo | null,
    
  45.   origin: Point,
    
  46.   width: number,
    
  47. };
    
  48. 
    
  49. function getReactMeasureLabel(type: ReactMeasureType): string | null {
    
  50.   switch (type) {
    
  51.     case 'commit':
    
  52.       return 'react commit';
    
  53.     case 'render-idle':
    
  54.       return 'react idle';
    
  55.     case 'render':
    
  56.       return 'react render';
    
  57.     case 'layout-effects':
    
  58.       return 'react layout effects';
    
  59.     case 'passive-effects':
    
  60.       return 'react passive effects';
    
  61.     default:
    
  62.       return null;
    
  63.   }
    
  64. }
    
  65. 
    
  66. export default function EventTooltip({
    
  67.   canvasRef,
    
  68.   data,
    
  69.   height,
    
  70.   hoveredEvent,
    
  71.   origin,
    
  72.   width,
    
  73. }: Props): React.Node {
    
  74.   const ref = useSmartTooltip({
    
  75.     canvasRef,
    
  76.     mouseX: origin.x,
    
  77.     mouseY: origin.y,
    
  78.   });
    
  79. 
    
  80.   if (hoveredEvent === null) {
    
  81.     return null;
    
  82.   }
    
  83. 
    
  84.   const {
    
  85.     componentMeasure,
    
  86.     flamechartStackFrame,
    
  87.     measure,
    
  88.     nativeEvent,
    
  89.     networkMeasure,
    
  90.     schedulingEvent,
    
  91.     snapshot,
    
  92.     suspenseEvent,
    
  93.     thrownError,
    
  94.     userTimingMark,
    
  95.   } = hoveredEvent;
    
  96. 
    
  97.   let content = null;
    
  98.   if (componentMeasure !== null) {
    
  99.     content = (
    
  100.       <TooltipReactComponentMeasure componentMeasure={componentMeasure} />
    
  101.     );
    
  102.   } else if (nativeEvent !== null) {
    
  103.     content = <TooltipNativeEvent nativeEvent={nativeEvent} />;
    
  104.   } else if (networkMeasure !== null) {
    
  105.     content = <TooltipNetworkMeasure networkMeasure={networkMeasure} />;
    
  106.   } else if (schedulingEvent !== null) {
    
  107.     content = (
    
  108.       <TooltipSchedulingEvent data={data} schedulingEvent={schedulingEvent} />
    
  109.     );
    
  110.   } else if (snapshot !== null) {
    
  111.     content = (
    
  112.       <TooltipSnapshot height={height} snapshot={snapshot} width={width} />
    
  113.     );
    
  114.   } else if (suspenseEvent !== null) {
    
  115.     content = <TooltipSuspenseEvent suspenseEvent={suspenseEvent} />;
    
  116.   } else if (measure !== null) {
    
  117.     content = <TooltipReactMeasure data={data} measure={measure} />;
    
  118.   } else if (flamechartStackFrame !== null) {
    
  119.     content = <TooltipFlamechartNode stackFrame={flamechartStackFrame} />;
    
  120.   } else if (userTimingMark !== null) {
    
  121.     content = <TooltipUserTimingMark mark={userTimingMark} />;
    
  122.   } else if (thrownError !== null) {
    
  123.     content = <TooltipThrownError thrownError={thrownError} />;
    
  124.   }
    
  125. 
    
  126.   if (content !== null) {
    
  127.     return (
    
  128.       <div className={styles.Tooltip} ref={ref}>
    
  129.         {content}
    
  130.       </div>
    
  131.     );
    
  132.   } else {
    
  133.     return null;
    
  134.   }
    
  135. }
    
  136. 
    
  137. const TooltipReactComponentMeasure = ({
    
  138.   componentMeasure,
    
  139. }: {
    
  140.   componentMeasure: ReactComponentMeasure,
    
  141. }) => {
    
  142.   const {componentName, duration, timestamp, type, warning} = componentMeasure;
    
  143. 
    
  144.   let label = componentName;
    
  145.   switch (type) {
    
  146.     case 'render':
    
  147.       label += ' rendered';
    
  148.       break;
    
  149.     case 'layout-effect-mount':
    
  150.       label += ' mounted layout effect';
    
  151.       break;
    
  152.     case 'layout-effect-unmount':
    
  153.       label += ' unmounted layout effect';
    
  154.       break;
    
  155.     case 'passive-effect-mount':
    
  156.       label += ' mounted passive effect';
    
  157.       break;
    
  158.     case 'passive-effect-unmount':
    
  159.       label += ' unmounted passive effect';
    
  160.       break;
    
  161.   }
    
  162. 
    
  163.   return (
    
  164.     <>
    
  165.       <div className={styles.TooltipSection}>
    
  166.         {trimString(label, 768)}
    
  167.         <div className={styles.Divider} />
    
  168.         <div className={styles.DetailsGrid}>
    
  169.           <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  170.           <div>{formatTimestamp(timestamp)}</div>
    
  171.           <div className={styles.DetailsGridLabel}>Duration:</div>
    
  172.           <div>{formatDuration(duration)}</div>
    
  173.         </div>
    
  174.       </div>
    
  175.       {warning !== null && (
    
  176.         <div className={styles.TooltipWarningSection}>
    
  177.           <div className={styles.WarningText}>{warning}</div>
    
  178.         </div>
    
  179.       )}
    
  180.     </>
    
  181.   );
    
  182. };
    
  183. 
    
  184. const TooltipFlamechartNode = ({
    
  185.   stackFrame,
    
  186. }: {
    
  187.   stackFrame: FlamechartStackFrame,
    
  188. }) => {
    
  189.   const {name, timestamp, duration, locationLine, locationColumn} = stackFrame;
    
  190.   return (
    
  191.     <div className={styles.TooltipSection}>
    
  192.       <span className={styles.FlamechartStackFrameName}>{name}</span>
    
  193.       <div className={styles.DetailsGrid}>
    
  194.         <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  195.         <div>{formatTimestamp(timestamp)}</div>
    
  196.         <div className={styles.DetailsGridLabel}>Duration:</div>
    
  197.         <div>{formatDuration(duration)}</div>
    
  198.         {(locationLine !== undefined || locationColumn !== undefined) && (
    
  199.           <>
    
  200.             <div className={styles.DetailsGridLabel}>Location:</div>
    
  201.             <div>
    
  202.               line {locationLine}, column {locationColumn}
    
  203.             </div>
    
  204.           </>
    
  205.         )}
    
  206.       </div>
    
  207.     </div>
    
  208.   );
    
  209. };
    
  210. 
    
  211. const TooltipNativeEvent = ({nativeEvent}: {nativeEvent: NativeEvent}) => {
    
  212.   const {duration, timestamp, type, warning} = nativeEvent;
    
  213. 
    
  214.   return (
    
  215.     <>
    
  216.       <div className={styles.TooltipSection}>
    
  217.         <span className={styles.NativeEventName}>{trimString(type, 768)}</span>
    
  218.         event
    
  219.         <div className={styles.Divider} />
    
  220.         <div className={styles.DetailsGrid}>
    
  221.           <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  222.           <div>{formatTimestamp(timestamp)}</div>
    
  223.           <div className={styles.DetailsGridLabel}>Duration:</div>
    
  224.           <div>{formatDuration(duration)}</div>
    
  225.         </div>
    
  226.       </div>
    
  227.       {warning !== null && (
    
  228.         <div className={styles.TooltipWarningSection}>
    
  229.           <div className={styles.WarningText}>{warning}</div>
    
  230.         </div>
    
  231.       )}
    
  232.     </>
    
  233.   );
    
  234. };
    
  235. 
    
  236. const TooltipNetworkMeasure = ({
    
  237.   networkMeasure,
    
  238. }: {
    
  239.   networkMeasure: NetworkMeasure,
    
  240. }) => {
    
  241.   const {
    
  242.     finishTimestamp,
    
  243.     lastReceivedDataTimestamp,
    
  244.     priority,
    
  245.     sendRequestTimestamp,
    
  246.     url,
    
  247.   } = networkMeasure;
    
  248. 
    
  249.   let urlToDisplay = url;
    
  250.   if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) {
    
  251.     const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2);
    
  252.     urlToDisplay = url.slice(0, half) + '…' + url.slice(url.length - half);
    
  253.   }
    
  254. 
    
  255.   const timestampBegin = sendRequestTimestamp;
    
  256.   const timestampEnd = finishTimestamp || lastReceivedDataTimestamp;
    
  257.   const duration =
    
  258.     timestampEnd > 0
    
  259.       ? formatDuration(finishTimestamp - timestampBegin)
    
  260.       : '(incomplete)';
    
  261. 
    
  262.   return (
    
  263.     <div className={styles.SingleLineTextSection}>
    
  264.       {duration} <span className={styles.DimText}>{priority}</span>{' '}
    
  265.       {urlToDisplay}
    
  266.     </div>
    
  267.   );
    
  268. };
    
  269. 
    
  270. const TooltipSchedulingEvent = ({
    
  271.   data,
    
  272.   schedulingEvent,
    
  273. }: {
    
  274.   data: TimelineData,
    
  275.   schedulingEvent: SchedulingEvent,
    
  276. }) => {
    
  277.   const label = getSchedulingEventLabel(schedulingEvent);
    
  278.   if (!label) {
    
  279.     if (__DEV__) {
    
  280.       console.warn(
    
  281.         'Unexpected schedulingEvent type "%s"',
    
  282.         schedulingEvent.type,
    
  283.       );
    
  284.     }
    
  285.     return null;
    
  286.   }
    
  287. 
    
  288.   let laneLabels = null;
    
  289.   let lanes = null;
    
  290.   switch (schedulingEvent.type) {
    
  291.     case 'schedule-render':
    
  292.     case 'schedule-state-update':
    
  293.     case 'schedule-force-update':
    
  294.       lanes = schedulingEvent.lanes;
    
  295.       laneLabels = lanes.map(
    
  296.         lane => ((data.laneToLabelMap.get(lane): any): string),
    
  297.       );
    
  298.       break;
    
  299.   }
    
  300. 
    
  301.   const {componentName, timestamp, warning} = schedulingEvent;
    
  302. 
    
  303.   return (
    
  304.     <>
    
  305.       <div className={styles.TooltipSection}>
    
  306.         {componentName && (
    
  307.           <span className={styles.ComponentName}>
    
  308.             {trimString(componentName, 100)}
    
  309.           </span>
    
  310.         )}
    
  311.         {label}
    
  312.         <div className={styles.Divider} />
    
  313.         <div className={styles.DetailsGrid}>
    
  314.           {laneLabels !== null && lanes !== null && (
    
  315.             <>
    
  316.               <div className={styles.DetailsGridLabel}>Lanes:</div>
    
  317.               <div>
    
  318.                 {laneLabels.join(', ')} ({lanes.join(', ')})
    
  319.               </div>
    
  320.             </>
    
  321.           )}
    
  322.           <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  323.           <div>{formatTimestamp(timestamp)}</div>
    
  324.         </div>
    
  325.       </div>
    
  326.       {warning !== null && (
    
  327.         <div className={styles.TooltipWarningSection}>
    
  328.           <div className={styles.WarningText}>{warning}</div>
    
  329.         </div>
    
  330.       )}
    
  331.     </>
    
  332.   );
    
  333. };
    
  334. 
    
  335. const TooltipSnapshot = ({
    
  336.   height,
    
  337.   snapshot,
    
  338.   width,
    
  339. }: {
    
  340.   height: number,
    
  341.   snapshot: Snapshot,
    
  342.   width: number,
    
  343. }) => {
    
  344.   const aspectRatio = snapshot.width / snapshot.height;
    
  345. 
    
  346.   // Zoomed in view should not be any bigger than the DevTools viewport.
    
  347.   let safeWidth = snapshot.width;
    
  348.   let safeHeight = snapshot.height;
    
  349.   if (safeWidth > width) {
    
  350.     safeWidth = width;
    
  351.     safeHeight = safeWidth / aspectRatio;
    
  352.   }
    
  353.   if (safeHeight > height) {
    
  354.     safeHeight = height;
    
  355.     safeWidth = safeHeight * aspectRatio;
    
  356.   }
    
  357. 
    
  358.   return (
    
  359.     <img
    
  360.       className={styles.Image}
    
  361.       src={snapshot.imageSource}
    
  362.       style={{height: safeHeight, width: safeWidth}}
    
  363.     />
    
  364.   );
    
  365. };
    
  366. 
    
  367. const TooltipSuspenseEvent = ({
    
  368.   suspenseEvent,
    
  369. }: {
    
  370.   suspenseEvent: SuspenseEvent,
    
  371. }) => {
    
  372.   const {
    
  373.     componentName,
    
  374.     duration,
    
  375.     phase,
    
  376.     promiseName,
    
  377.     resolution,
    
  378.     timestamp,
    
  379.     warning,
    
  380.   } = suspenseEvent;
    
  381. 
    
  382.   let label = 'suspended';
    
  383.   if (phase !== null) {
    
  384.     label += ` during ${phase}`;
    
  385.   }
    
  386. 
    
  387.   return (
    
  388.     <>
    
  389.       <div className={styles.TooltipSection}>
    
  390.         {componentName && (
    
  391.           <span className={styles.ComponentName}>
    
  392.             {trimString(componentName, 100)}
    
  393.           </span>
    
  394.         )}
    
  395.         {label}
    
  396.         <div className={styles.Divider} />
    
  397.         <div className={styles.DetailsGrid}>
    
  398.           {promiseName !== null && (
    
  399.             <>
    
  400.               <div className={styles.DetailsGridLabel}>Resource:</div>
    
  401.               <div className={styles.DetailsGridLongValue}>{promiseName}</div>
    
  402.             </>
    
  403.           )}
    
  404.           <div className={styles.DetailsGridLabel}>Status:</div>
    
  405.           <div>{resolution}</div>
    
  406.           <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  407.           <div>{formatTimestamp(timestamp)}</div>
    
  408.           {duration !== null && (
    
  409.             <>
    
  410.               <div className={styles.DetailsGridLabel}>Duration:</div>
    
  411.               <div>{formatDuration(duration)}</div>
    
  412.             </>
    
  413.           )}
    
  414.         </div>
    
  415.       </div>
    
  416.       {warning !== null && (
    
  417.         <div className={styles.TooltipWarningSection}>
    
  418.           <div className={styles.WarningText}>{warning}</div>
    
  419.         </div>
    
  420.       )}
    
  421.     </>
    
  422.   );
    
  423. };
    
  424. 
    
  425. const TooltipReactMeasure = ({
    
  426.   data,
    
  427.   measure,
    
  428. }: {
    
  429.   data: TimelineData,
    
  430.   measure: ReactMeasure,
    
  431. }) => {
    
  432.   const label = getReactMeasureLabel(measure.type);
    
  433.   if (!label) {
    
  434.     if (__DEV__) {
    
  435.       console.warn('Unexpected measure type "%s"', measure.type);
    
  436.     }
    
  437.     return null;
    
  438.   }
    
  439. 
    
  440.   const {batchUID, duration, timestamp, lanes} = measure;
    
  441.   const [startTime, stopTime] = getBatchRange(batchUID, data);
    
  442. 
    
  443.   const laneLabels = lanes.map(
    
  444.     lane => ((data.laneToLabelMap.get(lane): any): string),
    
  445.   );
    
  446. 
    
  447.   return (
    
  448.     <div className={styles.TooltipSection}>
    
  449.       <span className={styles.ReactMeasureLabel}>{label}</span>
    
  450.       <div className={styles.Divider} />
    
  451.       <div className={styles.DetailsGrid}>
    
  452.         <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  453.         <div>{formatTimestamp(timestamp)}</div>
    
  454.         {measure.type !== 'render-idle' && (
    
  455.           <>
    
  456.             <div className={styles.DetailsGridLabel}>Duration:</div>
    
  457.             <div>{formatDuration(duration)}</div>
    
  458.           </>
    
  459.         )}
    
  460.         <div className={styles.DetailsGridLabel}>Batch duration:</div>
    
  461.         <div>{formatDuration(stopTime - startTime)}</div>
    
  462.         <div className={styles.DetailsGridLabel}>
    
  463.           Lane{lanes.length === 1 ? '' : 's'}:
    
  464.         </div>
    
  465.         <div>
    
  466.           {laneLabels.length > 0
    
  467.             ? `${laneLabels.join(', ')} (${lanes.join(', ')})`
    
  468.             : lanes.join(', ')}
    
  469.         </div>
    
  470.       </div>
    
  471.     </div>
    
  472.   );
    
  473. };
    
  474. 
    
  475. const TooltipUserTimingMark = ({mark}: {mark: UserTimingMark}) => {
    
  476.   const {name, timestamp} = mark;
    
  477.   return (
    
  478.     <div className={styles.TooltipSection}>
    
  479.       <span className={styles.UserTimingLabel}>{name}</span>
    
  480.       <div className={styles.Divider} />
    
  481.       <div className={styles.DetailsGrid}>
    
  482.         <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  483.         <div>{formatTimestamp(timestamp)}</div>
    
  484.       </div>
    
  485.     </div>
    
  486.   );
    
  487. };
    
  488. 
    
  489. const TooltipThrownError = ({thrownError}: {thrownError: ThrownError}) => {
    
  490.   const {componentName, message, phase, timestamp} = thrownError;
    
  491.   const label = `threw an error during ${phase}`;
    
  492.   return (
    
  493.     <div className={styles.TooltipSection}>
    
  494.       {componentName && (
    
  495.         <span className={styles.ComponentName}>
    
  496.           {trimString(componentName, 100)}
    
  497.         </span>
    
  498.       )}
    
  499.       <span className={styles.UserTimingLabel}>{label}</span>
    
  500.       <div className={styles.Divider} />
    
  501.       <div className={styles.DetailsGrid}>
    
  502.         <div className={styles.DetailsGridLabel}>Timestamp:</div>
    
  503.         <div>{formatTimestamp(timestamp)}</div>
    
  504.         {message !== '' && (
    
  505.           <>
    
  506.             <div className={styles.DetailsGridLabel}>Error:</div>
    
  507.             <div>{message}</div>
    
  508.           </>
    
  509.         )}
    
  510.       </div>
    
  511.     </div>
    
  512.   );
    
  513. };