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 {ReactComponentMeasure, TimelineData, ViewState} from '../types';
    
  11. import type {
    
  12.   Interaction,
    
  13.   IntrinsicSize,
    
  14.   MouseMoveInteraction,
    
  15.   Rect,
    
  16.   ViewRefs,
    
  17. } from '../view-base';
    
  18. 
    
  19. import {
    
  20.   durationToWidth,
    
  21.   positioningScaleFactor,
    
  22.   positionToTimestamp,
    
  23.   timestampToPosition,
    
  24. } from './utils/positioning';
    
  25. import {drawText} from './utils/text';
    
  26. import {formatDuration} from '../utils/formatting';
    
  27. import {
    
  28.   View,
    
  29.   Surface,
    
  30.   rectContainsPoint,
    
  31.   rectIntersectsRect,
    
  32.   intersectionOfRects,
    
  33. } from '../view-base';
    
  34. import {BORDER_SIZE, COLORS, NATIVE_EVENT_HEIGHT} from './constants';
    
  35. 
    
  36. const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE;
    
  37. 
    
  38. export class ComponentMeasuresView extends View {
    
  39.   _cachedSearchMatches: Map<string, boolean>;
    
  40.   _cachedSearchRegExp: RegExp | null = null;
    
  41.   _hoveredComponentMeasure: ReactComponentMeasure | null = null;
    
  42.   _intrinsicSize: IntrinsicSize;
    
  43.   _profilerData: TimelineData;
    
  44.   _viewState: ViewState;
    
  45. 
    
  46.   onHover: ((event: ReactComponentMeasure | null) => void) | null = null;
    
  47. 
    
  48.   constructor(
    
  49.     surface: Surface,
    
  50.     frame: Rect,
    
  51.     profilerData: TimelineData,
    
  52.     viewState: ViewState,
    
  53.   ) {
    
  54.     super(surface, frame);
    
  55. 
    
  56.     this._profilerData = profilerData;
    
  57.     this._viewState = viewState;
    
  58. 
    
  59.     this._cachedSearchMatches = new Map();
    
  60.     this._cachedSearchRegExp = null;
    
  61. 
    
  62.     viewState.onSearchRegExpStateChange(() => {
    
  63.       this.setNeedsDisplay();
    
  64.     });
    
  65. 
    
  66.     this._intrinsicSize = {
    
  67.       width: profilerData.duration,
    
  68.       height: ROW_WITH_BORDER_HEIGHT,
    
  69.     };
    
  70.   }
    
  71. 
    
  72.   desiredSize(): IntrinsicSize {
    
  73.     return this._intrinsicSize;
    
  74.   }
    
  75. 
    
  76.   setHoveredEvent(hoveredEvent: ReactComponentMeasure | null) {
    
  77.     if (this._hoveredComponentMeasure === hoveredEvent) {
    
  78.       return;
    
  79.     }
    
  80.     this._hoveredComponentMeasure = hoveredEvent;
    
  81.     this.setNeedsDisplay();
    
  82.   }
    
  83. 
    
  84.   /**
    
  85.    * Draw a single `ReactComponentMeasure` as a box/span with text inside of it.
    
  86.    */
    
  87.   _drawSingleReactComponentMeasure(
    
  88.     context: CanvasRenderingContext2D,
    
  89.     rect: Rect,
    
  90.     componentMeasure: ReactComponentMeasure,
    
  91.     scaleFactor: number,
    
  92.     showHoverHighlight: boolean,
    
  93.   ): boolean {
    
  94.     const {frame} = this;
    
  95.     const {componentName, duration, timestamp, type, warning} =
    
  96.       componentMeasure;
    
  97. 
    
  98.     const xStart = timestampToPosition(timestamp, scaleFactor, frame);
    
  99.     const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
    
  100.     const componentMeasureRect: Rect = {
    
  101.       origin: {
    
  102.         x: xStart,
    
  103.         y: frame.origin.y,
    
  104.       },
    
  105.       size: {width: xStop - xStart, height: NATIVE_EVENT_HEIGHT},
    
  106.     };
    
  107.     if (!rectIntersectsRect(componentMeasureRect, rect)) {
    
  108.       return false; // Not in view
    
  109.     }
    
  110. 
    
  111.     const width = durationToWidth(duration, scaleFactor);
    
  112.     if (width < 1) {
    
  113.       return false; // Too small to render at this zoom level
    
  114.     }
    
  115. 
    
  116.     let textFillStyle = ((null: any): string);
    
  117.     let typeLabel = ((null: any): string);
    
  118. 
    
  119.     const drawableRect = intersectionOfRects(componentMeasureRect, rect);
    
  120.     context.beginPath();
    
  121.     if (warning !== null) {
    
  122.       context.fillStyle = showHoverHighlight
    
  123.         ? COLORS.WARNING_BACKGROUND_HOVER
    
  124.         : COLORS.WARNING_BACKGROUND;
    
  125.     } else {
    
  126.       switch (type) {
    
  127.         case 'render':
    
  128.           context.fillStyle = showHoverHighlight
    
  129.             ? COLORS.REACT_RENDER_HOVER
    
  130.             : COLORS.REACT_RENDER;
    
  131.           textFillStyle = COLORS.REACT_RENDER_TEXT;
    
  132.           typeLabel = 'rendered';
    
  133.           break;
    
  134.         case 'layout-effect-mount':
    
  135.           context.fillStyle = showHoverHighlight
    
  136.             ? COLORS.REACT_LAYOUT_EFFECTS_HOVER
    
  137.             : COLORS.REACT_LAYOUT_EFFECTS;
    
  138.           textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
    
  139.           typeLabel = 'mounted layout effect';
    
  140.           break;
    
  141.         case 'layout-effect-unmount':
    
  142.           context.fillStyle = showHoverHighlight
    
  143.             ? COLORS.REACT_LAYOUT_EFFECTS_HOVER
    
  144.             : COLORS.REACT_LAYOUT_EFFECTS;
    
  145.           textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
    
  146.           typeLabel = 'unmounted layout effect';
    
  147.           break;
    
  148.         case 'passive-effect-mount':
    
  149.           context.fillStyle = showHoverHighlight
    
  150.             ? COLORS.REACT_PASSIVE_EFFECTS_HOVER
    
  151.             : COLORS.REACT_PASSIVE_EFFECTS;
    
  152.           textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
    
  153.           typeLabel = 'mounted passive effect';
    
  154.           break;
    
  155.         case 'passive-effect-unmount':
    
  156.           context.fillStyle = showHoverHighlight
    
  157.             ? COLORS.REACT_PASSIVE_EFFECTS_HOVER
    
  158.             : COLORS.REACT_PASSIVE_EFFECTS;
    
  159.           textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
    
  160.           typeLabel = 'unmounted passive effect';
    
  161.           break;
    
  162.       }
    
  163.     }
    
  164. 
    
  165.     let isMatch = false;
    
  166.     const cachedSearchRegExp = this._cachedSearchRegExp;
    
  167.     if (cachedSearchRegExp !== null) {
    
  168.       const cachedSearchMatches = this._cachedSearchMatches;
    
  169.       const cachedValue = cachedSearchMatches.get(componentName);
    
  170.       if (cachedValue != null) {
    
  171.         isMatch = cachedValue;
    
  172.       } else {
    
  173.         isMatch = componentName.match(cachedSearchRegExp) !== null;
    
  174.         cachedSearchMatches.set(componentName, isMatch);
    
  175.       }
    
  176.     }
    
  177. 
    
  178.     if (isMatch) {
    
  179.       context.fillStyle = COLORS.SEARCH_RESULT_FILL;
    
  180.     }
    
  181. 
    
  182.     context.fillRect(
    
  183.       drawableRect.origin.x,
    
  184.       drawableRect.origin.y,
    
  185.       drawableRect.size.width,
    
  186.       drawableRect.size.height,
    
  187.     );
    
  188. 
    
  189.     const label = `${componentName} ${typeLabel} - ${formatDuration(duration)}`;
    
  190. 
    
  191.     drawText(label, context, componentMeasureRect, drawableRect, {
    
  192.       fillStyle: textFillStyle,
    
  193.     });
    
  194. 
    
  195.     return true;
    
  196.   }
    
  197. 
    
  198.   draw(context: CanvasRenderingContext2D) {
    
  199.     const {
    
  200.       frame,
    
  201.       _profilerData: {componentMeasures},
    
  202.       _hoveredComponentMeasure,
    
  203.       visibleArea,
    
  204.     } = this;
    
  205. 
    
  206.     const searchRegExp = this._viewState.searchRegExp;
    
  207.     if (this._cachedSearchRegExp !== searchRegExp) {
    
  208.       this._cachedSearchMatches = new Map();
    
  209.       this._cachedSearchRegExp = searchRegExp;
    
  210.     }
    
  211. 
    
  212.     context.fillStyle = COLORS.BACKGROUND;
    
  213.     context.fillRect(
    
  214.       visibleArea.origin.x,
    
  215.       visibleArea.origin.y,
    
  216.       visibleArea.size.width,
    
  217.       visibleArea.size.height,
    
  218.     );
    
  219. 
    
  220.     // Draw events
    
  221.     const scaleFactor = positioningScaleFactor(
    
  222.       this._intrinsicSize.width,
    
  223.       frame,
    
  224.     );
    
  225. 
    
  226.     let didDrawMeasure = false;
    
  227.     componentMeasures.forEach(componentMeasure => {
    
  228.       didDrawMeasure =
    
  229.         this._drawSingleReactComponentMeasure(
    
  230.           context,
    
  231.           visibleArea,
    
  232.           componentMeasure,
    
  233.           scaleFactor,
    
  234.           componentMeasure === _hoveredComponentMeasure,
    
  235.         ) || didDrawMeasure;
    
  236.     });
    
  237. 
    
  238.     if (!didDrawMeasure) {
    
  239.       drawText(
    
  240.         '(zoom or pan to see React components)',
    
  241.         context,
    
  242.         visibleArea,
    
  243.         visibleArea,
    
  244.         {fillStyle: COLORS.TEXT_DIM_COLOR, textAlign: 'center'},
    
  245.       );
    
  246.     }
    
  247. 
    
  248.     context.fillStyle = COLORS.PRIORITY_BORDER;
    
  249.     context.fillRect(
    
  250.       visibleArea.origin.x,
    
  251.       visibleArea.origin.y + ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
    
  252.       visibleArea.size.width,
    
  253.       BORDER_SIZE,
    
  254.     );
    
  255.   }
    
  256. 
    
  257.   /**
    
  258.    * @private
    
  259.    */
    
  260.   _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    
  261.     const {frame, _intrinsicSize, onHover, visibleArea} = this;
    
  262.     if (!onHover) {
    
  263.       return;
    
  264.     }
    
  265. 
    
  266.     const {location} = interaction.payload;
    
  267.     if (!rectContainsPoint(location, visibleArea)) {
    
  268.       onHover(null);
    
  269.       return;
    
  270.     }
    
  271. 
    
  272.     const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    
  273.     const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
    
  274. 
    
  275.     const componentMeasures = this._profilerData.componentMeasures;
    
  276.     for (let index = componentMeasures.length - 1; index >= 0; index--) {
    
  277.       const componentMeasure = componentMeasures[index];
    
  278.       const {duration, timestamp} = componentMeasure;
    
  279. 
    
  280.       if (
    
  281.         hoverTimestamp >= timestamp &&
    
  282.         hoverTimestamp <= timestamp + duration
    
  283.       ) {
    
  284.         this.currentCursor = 'context-menu';
    
  285.         viewRefs.hoveredView = this;
    
  286.         onHover(componentMeasure);
    
  287.         return;
    
  288.       }
    
  289.     }
    
  290. 
    
  291.     onHover(null);
    
  292.   }
    
  293. 
    
  294.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  295.     switch (interaction.type) {
    
  296.       case 'mousemove':
    
  297.         this._handleMouseMove(interaction, viewRefs);
    
  298.         break;
    
  299.     }
    
  300.   }
    
  301. }