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 {SchedulingEvent, TimelineData} from '../types';
    
  11. import type {
    
  12.   ClickInteraction,
    
  13.   Interaction,
    
  14.   MouseMoveInteraction,
    
  15.   Rect,
    
  16.   Size,
    
  17.   ViewRefs,
    
  18. } from '../view-base';
    
  19. 
    
  20. import {
    
  21.   positioningScaleFactor,
    
  22.   timestampToPosition,
    
  23.   positionToTimestamp,
    
  24.   widthToDuration,
    
  25. } from './utils/positioning';
    
  26. import {
    
  27.   View,
    
  28.   Surface,
    
  29.   rectContainsPoint,
    
  30.   rectIntersectsRect,
    
  31.   intersectionOfRects,
    
  32. } from '../view-base';
    
  33. import {
    
  34.   COLORS,
    
  35.   TOP_ROW_PADDING,
    
  36.   REACT_EVENT_DIAMETER,
    
  37.   BORDER_SIZE,
    
  38. } from './constants';
    
  39. 
    
  40. const EVENT_ROW_HEIGHT_FIXED =
    
  41.   TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING;
    
  42. 
    
  43. export class SchedulingEventsView extends View {
    
  44.   _profilerData: TimelineData;
    
  45.   _intrinsicSize: Size;
    
  46. 
    
  47.   _hoveredEvent: SchedulingEvent | null = null;
    
  48.   onHover: ((event: SchedulingEvent | null) => void) | null = null;
    
  49.   onClick:
    
  50.     | ((event: SchedulingEvent | null, eventIndex: number | null) => void)
    
  51.     | null = null;
    
  52. 
    
  53.   constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
    
  54.     super(surface, frame);
    
  55.     this._profilerData = profilerData;
    
  56. 
    
  57.     this._intrinsicSize = {
    
  58.       width: this._profilerData.duration,
    
  59.       height: EVENT_ROW_HEIGHT_FIXED,
    
  60.     };
    
  61.   }
    
  62. 
    
  63.   desiredSize(): Size {
    
  64.     return this._intrinsicSize;
    
  65.   }
    
  66. 
    
  67.   setHoveredEvent(hoveredEvent: SchedulingEvent | null) {
    
  68.     if (this._hoveredEvent === hoveredEvent) {
    
  69.       return;
    
  70.     }
    
  71.     this._hoveredEvent = hoveredEvent;
    
  72.     this.setNeedsDisplay();
    
  73.   }
    
  74. 
    
  75.   /**
    
  76.    * Draw a single `SchedulingEvent` as a circle in the canvas.
    
  77.    */
    
  78.   _drawSingleSchedulingEvent(
    
  79.     context: CanvasRenderingContext2D,
    
  80.     rect: Rect,
    
  81.     event: SchedulingEvent,
    
  82.     baseY: number,
    
  83.     scaleFactor: number,
    
  84.     showHoverHighlight: boolean,
    
  85.   ) {
    
  86.     const {frame} = this;
    
  87.     const {timestamp, type, warning} = event;
    
  88. 
    
  89.     const x = timestampToPosition(timestamp, scaleFactor, frame);
    
  90.     const radius = REACT_EVENT_DIAMETER / 2;
    
  91.     const eventRect: Rect = {
    
  92.       origin: {
    
  93.         x: x - radius,
    
  94.         y: baseY,
    
  95.       },
    
  96.       size: {width: REACT_EVENT_DIAMETER, height: REACT_EVENT_DIAMETER},
    
  97.     };
    
  98.     if (!rectIntersectsRect(eventRect, rect)) {
    
  99.       return; // Not in view
    
  100.     }
    
  101. 
    
  102.     let fillStyle = null;
    
  103. 
    
  104.     if (warning !== null) {
    
  105.       fillStyle = showHoverHighlight
    
  106.         ? COLORS.WARNING_BACKGROUND_HOVER
    
  107.         : COLORS.WARNING_BACKGROUND;
    
  108.     } else {
    
  109.       switch (type) {
    
  110.         case 'schedule-render':
    
  111.         case 'schedule-state-update':
    
  112.         case 'schedule-force-update':
    
  113.           fillStyle = showHoverHighlight
    
  114.             ? COLORS.REACT_SCHEDULE_HOVER
    
  115.             : COLORS.REACT_SCHEDULE;
    
  116.           break;
    
  117.         default:
    
  118.           if (__DEV__) {
    
  119.             console.warn('Unexpected event type "%s"', type);
    
  120.           }
    
  121.           break;
    
  122.       }
    
  123.     }
    
  124. 
    
  125.     if (fillStyle !== null) {
    
  126.       const y = eventRect.origin.y + radius;
    
  127. 
    
  128.       context.beginPath();
    
  129.       context.fillStyle = fillStyle;
    
  130.       context.arc(x, y, radius, 0, 2 * Math.PI);
    
  131.       context.fill();
    
  132.     }
    
  133.   }
    
  134. 
    
  135.   draw(context: CanvasRenderingContext2D) {
    
  136.     const {
    
  137.       frame,
    
  138.       _profilerData: {schedulingEvents},
    
  139.       _hoveredEvent,
    
  140.       visibleArea,
    
  141.     } = this;
    
  142. 
    
  143.     context.fillStyle = COLORS.BACKGROUND;
    
  144.     context.fillRect(
    
  145.       visibleArea.origin.x,
    
  146.       visibleArea.origin.y,
    
  147.       visibleArea.size.width,
    
  148.       visibleArea.size.height,
    
  149.     );
    
  150. 
    
  151.     // Draw events
    
  152.     const baseY = frame.origin.y + TOP_ROW_PADDING;
    
  153.     const scaleFactor = positioningScaleFactor(
    
  154.       this._intrinsicSize.width,
    
  155.       frame,
    
  156.     );
    
  157. 
    
  158.     const highlightedEvents: SchedulingEvent[] = [];
    
  159. 
    
  160.     schedulingEvents.forEach(event => {
    
  161.       if (event === _hoveredEvent) {
    
  162.         highlightedEvents.push(event);
    
  163.         return;
    
  164.       }
    
  165.       this._drawSingleSchedulingEvent(
    
  166.         context,
    
  167.         visibleArea,
    
  168.         event,
    
  169.         baseY,
    
  170.         scaleFactor,
    
  171.         false,
    
  172.       );
    
  173.     });
    
  174. 
    
  175.     // Draw the highlighted items on top so they stand out.
    
  176.     // This is helpful if there are multiple (overlapping) items close to each other.
    
  177.     highlightedEvents.forEach(event => {
    
  178.       this._drawSingleSchedulingEvent(
    
  179.         context,
    
  180.         visibleArea,
    
  181.         event,
    
  182.         baseY,
    
  183.         scaleFactor,
    
  184.         true,
    
  185.       );
    
  186.     });
    
  187. 
    
  188.     // Render bottom border.
    
  189.     // Propose border rect, check if intersects with `rect`, draw intersection.
    
  190.     const borderFrame: Rect = {
    
  191.       origin: {
    
  192.         x: frame.origin.x,
    
  193.         y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE,
    
  194.       },
    
  195.       size: {
    
  196.         width: frame.size.width,
    
  197.         height: BORDER_SIZE,
    
  198.       },
    
  199.     };
    
  200.     if (rectIntersectsRect(borderFrame, visibleArea)) {
    
  201.       const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
    
  202.       context.fillStyle = COLORS.REACT_WORK_BORDER;
    
  203.       context.fillRect(
    
  204.         borderDrawableRect.origin.x,
    
  205.         borderDrawableRect.origin.y,
    
  206.         borderDrawableRect.size.width,
    
  207.         borderDrawableRect.size.height,
    
  208.       );
    
  209.     }
    
  210.   }
    
  211. 
    
  212.   /**
    
  213.    * @private
    
  214.    */
    
  215.   _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    
  216.     const {frame, onHover, visibleArea} = this;
    
  217.     if (!onHover) {
    
  218.       return;
    
  219.     }
    
  220. 
    
  221.     const {location} = interaction.payload;
    
  222.     if (!rectContainsPoint(location, visibleArea)) {
    
  223.       onHover(null);
    
  224.       return;
    
  225.     }
    
  226. 
    
  227.     const {
    
  228.       _profilerData: {schedulingEvents},
    
  229.     } = this;
    
  230.     const scaleFactor = positioningScaleFactor(
    
  231.       this._intrinsicSize.width,
    
  232.       frame,
    
  233.     );
    
  234.     const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
    
  235.     const eventTimestampAllowance = widthToDuration(
    
  236.       REACT_EVENT_DIAMETER / 2,
    
  237.       scaleFactor,
    
  238.     );
    
  239. 
    
  240.     // Because data ranges may overlap, we want to find the last intersecting item.
    
  241.     // This will always be the one on "top" (the one the user is hovering over).
    
  242.     for (let index = schedulingEvents.length - 1; index >= 0; index--) {
    
  243.       const event = schedulingEvents[index];
    
  244.       const {timestamp} = event;
    
  245. 
    
  246.       if (
    
  247.         timestamp - eventTimestampAllowance <= hoverTimestamp &&
    
  248.         hoverTimestamp <= timestamp + eventTimestampAllowance
    
  249.       ) {
    
  250.         this.currentCursor = 'pointer';
    
  251.         viewRefs.hoveredView = this;
    
  252.         onHover(event);
    
  253.         return;
    
  254.       }
    
  255.     }
    
  256. 
    
  257.     onHover(null);
    
  258.   }
    
  259. 
    
  260.   /**
    
  261.    * @private
    
  262.    */
    
  263.   _handleClick(interaction: ClickInteraction) {
    
  264.     const {onClick} = this;
    
  265.     if (onClick) {
    
  266.       const {
    
  267.         _profilerData: {schedulingEvents},
    
  268.       } = this;
    
  269.       const eventIndex = schedulingEvents.findIndex(
    
  270.         event => event === this._hoveredEvent,
    
  271.       );
    
  272.       // onHover is going to take care of all the difficult logic here of
    
  273.       // figuring out which event when they're proximity is close.
    
  274.       onClick(this._hoveredEvent, eventIndex >= 0 ? eventIndex : null);
    
  275.     }
    
  276.   }
    
  277. 
    
  278.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  279.     switch (interaction.type) {
    
  280.       case 'mousemove':
    
  281.         this._handleMouseMove(interaction, viewRefs);
    
  282.         break;
    
  283.       case 'click':
    
  284.         this._handleClick(interaction);
    
  285.         break;
    
  286.     }
    
  287.   }
    
  288. }