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 {ReactLane, ReactMeasure, TimelineData} from '../types';
    
  11. import type {
    
  12.   Interaction,
    
  13.   IntrinsicSize,
    
  14.   MouseMoveInteraction,
    
  15.   Rect,
    
  16.   ViewRefs,
    
  17. } from '../view-base';
    
  18. 
    
  19. import {formatDuration} from '../utils/formatting';
    
  20. import {drawText} from './utils/text';
    
  21. import {
    
  22.   durationToWidth,
    
  23.   positioningScaleFactor,
    
  24.   positionToTimestamp,
    
  25.   timestampToPosition,
    
  26. } from './utils/positioning';
    
  27. import {
    
  28.   View,
    
  29.   Surface,
    
  30.   rectContainsPoint,
    
  31.   rectIntersectsRect,
    
  32.   intersectionOfRects,
    
  33. } from '../view-base';
    
  34. 
    
  35. import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants';
    
  36. 
    
  37. const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
    
  38. const MAX_ROWS_TO_SHOW_INITIALLY = 5;
    
  39. 
    
  40. export class ReactMeasuresView extends View {
    
  41.   _intrinsicSize: IntrinsicSize;
    
  42.   _lanesToRender: ReactLane[];
    
  43.   _profilerData: TimelineData;
    
  44.   _hoveredMeasure: ReactMeasure | null = null;
    
  45. 
    
  46.   onHover: ((measure: ReactMeasure | null) => void) | null = null;
    
  47. 
    
  48.   constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
    
  49.     super(surface, frame);
    
  50.     this._profilerData = profilerData;
    
  51.     this._performPreflightComputations();
    
  52.   }
    
  53. 
    
  54.   _performPreflightComputations() {
    
  55.     this._lanesToRender = [];
    
  56. 
    
  57.     // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    
  58.     for (const [lane, measuresForLane] of this._profilerData
    
  59.       .laneToReactMeasureMap) {
    
  60.       // Only show lanes with measures
    
  61.       if (measuresForLane.length > 0) {
    
  62.         this._lanesToRender.push(lane);
    
  63.       }
    
  64.     }
    
  65. 
    
  66.     this._intrinsicSize = {
    
  67.       width: this._profilerData.duration,
    
  68.       height: this._lanesToRender.length * REACT_LANE_HEIGHT,
    
  69.       hideScrollBarIfLessThanHeight: REACT_LANE_HEIGHT,
    
  70.       maxInitialHeight: MAX_ROWS_TO_SHOW_INITIALLY * REACT_LANE_HEIGHT,
    
  71.     };
    
  72.   }
    
  73. 
    
  74.   desiredSize(): IntrinsicSize {
    
  75.     return this._intrinsicSize;
    
  76.   }
    
  77. 
    
  78.   setHoveredMeasure(hoveredMeasure: ReactMeasure | null) {
    
  79.     if (this._hoveredMeasure === hoveredMeasure) {
    
  80.       return;
    
  81.     }
    
  82.     this._hoveredMeasure = hoveredMeasure;
    
  83.     this.setNeedsDisplay();
    
  84.   }
    
  85. 
    
  86.   /**
    
  87.    * Draw a single `ReactMeasure` as a bar in the canvas.
    
  88.    */
    
  89.   _drawSingleReactMeasure(
    
  90.     context: CanvasRenderingContext2D,
    
  91.     rect: Rect,
    
  92.     measure: ReactMeasure,
    
  93.     nextMeasure: ReactMeasure | null,
    
  94.     baseY: number,
    
  95.     scaleFactor: number,
    
  96.     showGroupHighlight: boolean,
    
  97.     showHoverHighlight: boolean,
    
  98.   ): void {
    
  99.     const {frame, visibleArea} = this;
    
  100.     const {timestamp, type, duration} = measure;
    
  101. 
    
  102.     let fillStyle = null;
    
  103.     let hoveredFillStyle = null;
    
  104.     let groupSelectedFillStyle = null;
    
  105.     let textFillStyle = null;
    
  106. 
    
  107.     // We could change the max to 0 and just skip over rendering anything that small,
    
  108.     // but this has the effect of making the chart look very empty when zoomed out.
    
  109.     // So long as perf is okay- it might be best to err on the side of showing things.
    
  110.     const width = durationToWidth(duration, scaleFactor);
    
  111.     if (width <= 0) {
    
  112.       return; // Too small to render at this zoom level
    
  113.     }
    
  114. 
    
  115.     const x = timestampToPosition(timestamp, scaleFactor, frame);
    
  116.     const measureRect: Rect = {
    
  117.       origin: {x, y: baseY},
    
  118.       size: {width, height: REACT_MEASURE_HEIGHT},
    
  119.     };
    
  120.     if (!rectIntersectsRect(measureRect, rect)) {
    
  121.       return; // Not in view
    
  122.     }
    
  123. 
    
  124.     const drawableRect = intersectionOfRects(measureRect, rect);
    
  125.     let textRect = measureRect;
    
  126. 
    
  127.     switch (type) {
    
  128.       case 'commit':
    
  129.         fillStyle = COLORS.REACT_COMMIT;
    
  130.         hoveredFillStyle = COLORS.REACT_COMMIT_HOVER;
    
  131.         groupSelectedFillStyle = COLORS.REACT_COMMIT_HOVER;
    
  132.         textFillStyle = COLORS.REACT_COMMIT_TEXT;
    
  133. 
    
  134.         // Commit phase rects are overlapped by layout and passive rects,
    
  135.         // and it looks bad if text flows underneath/behind these overlayed rects.
    
  136.         if (nextMeasure != null) {
    
  137.           // This clipping shouldn't apply for measures that don't overlap though,
    
  138.           // like passive effects that are processed after a delay,
    
  139.           // or if there are now layout or passive effects and the next measure is render or idle.
    
  140.           if (nextMeasure.timestamp < measure.timestamp + measure.duration) {
    
  141.             textRect = {
    
  142.               ...measureRect,
    
  143.               size: {
    
  144.                 width:
    
  145.                   timestampToPosition(
    
  146.                     nextMeasure.timestamp,
    
  147.                     scaleFactor,
    
  148.                     frame,
    
  149.                   ) - x,
    
  150.                 height: REACT_MEASURE_HEIGHT,
    
  151.               },
    
  152.             };
    
  153.           }
    
  154.         }
    
  155.         break;
    
  156.       case 'render-idle':
    
  157.         // We could render idle time as diagonal hashes.
    
  158.         // This looks nicer when zoomed in, but not so nice when zoomed out.
    
  159.         // color = context.createPattern(getIdlePattern(), 'repeat');
    
  160.         fillStyle = COLORS.REACT_IDLE;
    
  161.         hoveredFillStyle = COLORS.REACT_IDLE_HOVER;
    
  162.         groupSelectedFillStyle = COLORS.REACT_IDLE_HOVER;
    
  163.         break;
    
  164.       case 'render':
    
  165.         fillStyle = COLORS.REACT_RENDER;
    
  166.         hoveredFillStyle = COLORS.REACT_RENDER_HOVER;
    
  167.         groupSelectedFillStyle = COLORS.REACT_RENDER_HOVER;
    
  168.         textFillStyle = COLORS.REACT_RENDER_TEXT;
    
  169.         break;
    
  170.       case 'layout-effects':
    
  171.         fillStyle = COLORS.REACT_LAYOUT_EFFECTS;
    
  172.         hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
    
  173.         groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
    
  174.         textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
    
  175.         break;
    
  176.       case 'passive-effects':
    
  177.         fillStyle = COLORS.REACT_PASSIVE_EFFECTS;
    
  178.         hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
    
  179.         groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
    
  180.         textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
    
  181.         break;
    
  182.       default:
    
  183.         throw new Error(`Unexpected measure type "${type}"`);
    
  184.     }
    
  185. 
    
  186.     context.fillStyle = showHoverHighlight
    
  187.       ? hoveredFillStyle
    
  188.       : showGroupHighlight
    
  189.       ? groupSelectedFillStyle
    
  190.       : fillStyle;
    
  191.     context.fillRect(
    
  192.       drawableRect.origin.x,
    
  193.       drawableRect.origin.y,
    
  194.       drawableRect.size.width,
    
  195.       drawableRect.size.height,
    
  196.     );
    
  197. 
    
  198.     if (textFillStyle !== null) {
    
  199.       drawText(formatDuration(duration), context, textRect, visibleArea, {
    
  200.         fillStyle: textFillStyle,
    
  201.       });
    
  202.     }
    
  203.   }
    
  204. 
    
  205.   draw(context: CanvasRenderingContext2D): void {
    
  206.     const {frame, _hoveredMeasure, _lanesToRender, _profilerData, visibleArea} =
    
  207.       this;
    
  208. 
    
  209.     context.fillStyle = COLORS.PRIORITY_BACKGROUND;
    
  210.     context.fillRect(
    
  211.       visibleArea.origin.x,
    
  212.       visibleArea.origin.y,
    
  213.       visibleArea.size.width,
    
  214.       visibleArea.size.height,
    
  215.     );
    
  216. 
    
  217.     const scaleFactor = positioningScaleFactor(
    
  218.       this._intrinsicSize.width,
    
  219.       frame,
    
  220.     );
    
  221. 
    
  222.     for (let i = 0; i < _lanesToRender.length; i++) {
    
  223.       const lane = _lanesToRender[i];
    
  224.       const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
    
  225.       const measuresForLane = _profilerData.laneToReactMeasureMap.get(lane);
    
  226. 
    
  227.       if (!measuresForLane) {
    
  228.         throw new Error(
    
  229.           'No measures found for a React lane! This is a bug in this profiler tool. Please file an issue.',
    
  230.         );
    
  231.       }
    
  232. 
    
  233.       // Render lane labels
    
  234.       const label = _profilerData.laneToLabelMap.get(lane);
    
  235.       if (label == null) {
    
  236.         console.warn(`Could not find label for lane ${lane}.`);
    
  237.       } else {
    
  238.         const labelRect = {
    
  239.           origin: {
    
  240.             x: visibleArea.origin.x,
    
  241.             y: baseY,
    
  242.           },
    
  243.           size: {
    
  244.             width: visibleArea.size.width,
    
  245.             height: REACT_LANE_HEIGHT,
    
  246.           },
    
  247.         };
    
  248. 
    
  249.         drawText(label, context, labelRect, visibleArea, {
    
  250.           fillStyle: COLORS.TEXT_DIM_COLOR,
    
  251.         });
    
  252.       }
    
  253. 
    
  254.       // Draw measures
    
  255.       for (let j = 0; j < measuresForLane.length; j++) {
    
  256.         const measure = measuresForLane[j];
    
  257.         const showHoverHighlight = _hoveredMeasure === measure;
    
  258.         const showGroupHighlight =
    
  259.           !!_hoveredMeasure && _hoveredMeasure.batchUID === measure.batchUID;
    
  260. 
    
  261.         this._drawSingleReactMeasure(
    
  262.           context,
    
  263.           visibleArea,
    
  264.           measure,
    
  265.           measuresForLane[j + 1] || null,
    
  266.           baseY,
    
  267.           scaleFactor,
    
  268.           showGroupHighlight,
    
  269.           showHoverHighlight,
    
  270.         );
    
  271.       }
    
  272. 
    
  273.       // Render bottom border
    
  274.       const borderFrame: Rect = {
    
  275.         origin: {
    
  276.           x: frame.origin.x,
    
  277.           y: frame.origin.y + (i + 1) * REACT_LANE_HEIGHT - BORDER_SIZE,
    
  278.         },
    
  279.         size: {
    
  280.           width: frame.size.width,
    
  281.           height: BORDER_SIZE,
    
  282.         },
    
  283.       };
    
  284.       if (rectIntersectsRect(borderFrame, visibleArea)) {
    
  285.         const borderDrawableRect = intersectionOfRects(
    
  286.           borderFrame,
    
  287.           visibleArea,
    
  288.         );
    
  289.         context.fillStyle = COLORS.PRIORITY_BORDER;
    
  290.         context.fillRect(
    
  291.           borderDrawableRect.origin.x,
    
  292.           borderDrawableRect.origin.y,
    
  293.           borderDrawableRect.size.width,
    
  294.           borderDrawableRect.size.height,
    
  295.         );
    
  296.       }
    
  297.     }
    
  298.   }
    
  299. 
    
  300.   /**
    
  301.    * @private
    
  302.    */
    
  303.   _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    
  304.     const {
    
  305.       frame,
    
  306.       _intrinsicSize,
    
  307.       _lanesToRender,
    
  308.       onHover,
    
  309.       _profilerData,
    
  310.       visibleArea,
    
  311.     } = this;
    
  312.     if (!onHover) {
    
  313.       return;
    
  314.     }
    
  315. 
    
  316.     const {location} = interaction.payload;
    
  317.     if (!rectContainsPoint(location, visibleArea)) {
    
  318.       onHover(null);
    
  319.       return;
    
  320.     }
    
  321. 
    
  322.     // Identify the lane being hovered over
    
  323.     const adjustedCanvasMouseY = location.y - frame.origin.y;
    
  324.     const renderedLaneIndex = Math.floor(
    
  325.       adjustedCanvasMouseY / REACT_LANE_HEIGHT,
    
  326.     );
    
  327.     if (renderedLaneIndex < 0 || renderedLaneIndex >= _lanesToRender.length) {
    
  328.       onHover(null);
    
  329.       return;
    
  330.     }
    
  331.     const lane = _lanesToRender[renderedLaneIndex];
    
  332. 
    
  333.     // Find the measure in `lane` being hovered over.
    
  334.     //
    
  335.     // Because data ranges may overlap, we want to find the last intersecting item.
    
  336.     // This will always be the one on "top" (the one the user is hovering over).
    
  337.     const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    
  338.     const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
    
  339.     const measures = _profilerData.laneToReactMeasureMap.get(lane);
    
  340.     if (!measures) {
    
  341.       onHover(null);
    
  342.       return;
    
  343.     }
    
  344. 
    
  345.     for (let index = measures.length - 1; index >= 0; index--) {
    
  346.       const measure = measures[index];
    
  347.       const {duration, timestamp} = measure;
    
  348. 
    
  349.       if (
    
  350.         hoverTimestamp >= timestamp &&
    
  351.         hoverTimestamp <= timestamp + duration
    
  352.       ) {
    
  353.         this.currentCursor = 'context-menu';
    
  354.         viewRefs.hoveredView = this;
    
  355.         onHover(measure);
    
  356.         return;
    
  357.       }
    
  358.     }
    
  359. 
    
  360.     onHover(null);
    
  361.   }
    
  362. 
    
  363.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  364.     switch (interaction.type) {
    
  365.       case 'mousemove':
    
  366.         this._handleMouseMove(interaction, viewRefs);
    
  367.         break;
    
  368.     }
    
  369.   }
    
  370. }