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 {NativeEvent, TimelineData} 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 {COLORS, NATIVE_EVENT_HEIGHT, BORDER_SIZE} from './constants';
    
  35. 
    
  36. const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE;
    
  37. 
    
  38. export class NativeEventsView extends View {
    
  39.   _depthToNativeEvent: Map<number, NativeEvent[]>;
    
  40.   _hoveredEvent: NativeEvent | null = null;
    
  41.   _intrinsicSize: IntrinsicSize;
    
  42.   _maxDepth: number = 0;
    
  43.   _profilerData: TimelineData;
    
  44. 
    
  45.   onHover: ((event: NativeEvent | null) => void) | null = null;
    
  46. 
    
  47.   constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
    
  48.     super(surface, frame);
    
  49. 
    
  50.     this._profilerData = profilerData;
    
  51. 
    
  52.     this._performPreflightComputations();
    
  53.   }
    
  54. 
    
  55.   _performPreflightComputations() {
    
  56.     this._depthToNativeEvent = new Map();
    
  57. 
    
  58.     const {duration, nativeEvents} = this._profilerData;
    
  59. 
    
  60.     nativeEvents.forEach(event => {
    
  61.       const depth = event.depth;
    
  62. 
    
  63.       this._maxDepth = Math.max(this._maxDepth, depth);
    
  64. 
    
  65.       if (!this._depthToNativeEvent.has(depth)) {
    
  66.         this._depthToNativeEvent.set(depth, [event]);
    
  67.       } else {
    
  68.         // $FlowFixMe[incompatible-use] This is unnecessary.
    
  69.         this._depthToNativeEvent.get(depth).push(event);
    
  70.       }
    
  71.     });
    
  72. 
    
  73.     this._intrinsicSize = {
    
  74.       width: duration,
    
  75.       height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
    
  76.       hideScrollBarIfLessThanHeight: ROW_WITH_BORDER_HEIGHT,
    
  77.     };
    
  78.   }
    
  79. 
    
  80.   desiredSize(): IntrinsicSize {
    
  81.     return this._intrinsicSize;
    
  82.   }
    
  83. 
    
  84.   setHoveredEvent(hoveredEvent: NativeEvent | null) {
    
  85.     if (this._hoveredEvent === hoveredEvent) {
    
  86.       return;
    
  87.     }
    
  88.     this._hoveredEvent = hoveredEvent;
    
  89.     this.setNeedsDisplay();
    
  90.   }
    
  91. 
    
  92.   /**
    
  93.    * Draw a single `NativeEvent` as a box/span with text inside of it.
    
  94.    */
    
  95.   _drawSingleNativeEvent(
    
  96.     context: CanvasRenderingContext2D,
    
  97.     rect: Rect,
    
  98.     event: NativeEvent,
    
  99.     baseY: number,
    
  100.     scaleFactor: number,
    
  101.     showHoverHighlight: boolean,
    
  102.   ) {
    
  103.     const {frame} = this;
    
  104.     const {depth, duration, timestamp, type, warning} = event;
    
  105. 
    
  106.     baseY += depth * ROW_WITH_BORDER_HEIGHT;
    
  107. 
    
  108.     const xStart = timestampToPosition(timestamp, scaleFactor, frame);
    
  109.     const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
    
  110.     const eventRect: Rect = {
    
  111.       origin: {
    
  112.         x: xStart,
    
  113.         y: baseY,
    
  114.       },
    
  115.       size: {width: xStop - xStart, height: NATIVE_EVENT_HEIGHT},
    
  116.     };
    
  117.     if (!rectIntersectsRect(eventRect, rect)) {
    
  118.       return; // Not in view
    
  119.     }
    
  120. 
    
  121.     const width = durationToWidth(duration, scaleFactor);
    
  122.     if (width < 1) {
    
  123.       return; // Too small to render at this zoom level
    
  124.     }
    
  125. 
    
  126.     const drawableRect = intersectionOfRects(eventRect, rect);
    
  127.     context.beginPath();
    
  128.     if (warning !== null) {
    
  129.       context.fillStyle = showHoverHighlight
    
  130.         ? COLORS.WARNING_BACKGROUND_HOVER
    
  131.         : COLORS.WARNING_BACKGROUND;
    
  132.     } else {
    
  133.       context.fillStyle = showHoverHighlight
    
  134.         ? COLORS.NATIVE_EVENT_HOVER
    
  135.         : COLORS.NATIVE_EVENT;
    
  136.     }
    
  137.     context.fillRect(
    
  138.       drawableRect.origin.x,
    
  139.       drawableRect.origin.y,
    
  140.       drawableRect.size.width,
    
  141.       drawableRect.size.height,
    
  142.     );
    
  143. 
    
  144.     const label = `${type} - ${formatDuration(duration)}`;
    
  145. 
    
  146.     drawText(label, context, eventRect, drawableRect);
    
  147.   }
    
  148. 
    
  149.   draw(context: CanvasRenderingContext2D) {
    
  150.     const {
    
  151.       frame,
    
  152.       _profilerData: {nativeEvents},
    
  153.       _hoveredEvent,
    
  154.       visibleArea,
    
  155.     } = this;
    
  156. 
    
  157.     context.fillStyle = COLORS.PRIORITY_BACKGROUND;
    
  158.     context.fillRect(
    
  159.       visibleArea.origin.x,
    
  160.       visibleArea.origin.y,
    
  161.       visibleArea.size.width,
    
  162.       visibleArea.size.height,
    
  163.     );
    
  164. 
    
  165.     // Draw events
    
  166.     const scaleFactor = positioningScaleFactor(
    
  167.       this._intrinsicSize.width,
    
  168.       frame,
    
  169.     );
    
  170. 
    
  171.     nativeEvents.forEach(event => {
    
  172.       this._drawSingleNativeEvent(
    
  173.         context,
    
  174.         visibleArea,
    
  175.         event,
    
  176.         frame.origin.y,
    
  177.         scaleFactor,
    
  178.         event === _hoveredEvent,
    
  179.       );
    
  180.     });
    
  181. 
    
  182.     // Render bottom borders.
    
  183.     for (let i = 0; i <= this._maxDepth; i++) {
    
  184.       const borderFrame: Rect = {
    
  185.         origin: {
    
  186.           x: frame.origin.x,
    
  187.           y: frame.origin.y + NATIVE_EVENT_HEIGHT,
    
  188.         },
    
  189.         size: {
    
  190.           width: frame.size.width,
    
  191.           height: BORDER_SIZE,
    
  192.         },
    
  193.       };
    
  194.       if (rectIntersectsRect(borderFrame, visibleArea)) {
    
  195.         const borderDrawableRect = intersectionOfRects(
    
  196.           borderFrame,
    
  197.           visibleArea,
    
  198.         );
    
  199.         context.fillStyle = COLORS.PRIORITY_BORDER;
    
  200.         context.fillRect(
    
  201.           borderDrawableRect.origin.x,
    
  202.           borderDrawableRect.origin.y,
    
  203.           borderDrawableRect.size.width,
    
  204.           borderDrawableRect.size.height,
    
  205.         );
    
  206.       }
    
  207.     }
    
  208.   }
    
  209. 
    
  210.   /**
    
  211.    * @private
    
  212.    */
    
  213.   _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    
  214.     const {frame, _intrinsicSize, onHover, visibleArea} = this;
    
  215.     if (!onHover) {
    
  216.       return;
    
  217.     }
    
  218. 
    
  219.     const {location} = interaction.payload;
    
  220.     if (!rectContainsPoint(location, visibleArea)) {
    
  221.       onHover(null);
    
  222.       return;
    
  223.     }
    
  224. 
    
  225.     const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    
  226.     const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
    
  227. 
    
  228.     const adjustedCanvasMouseY = location.y - frame.origin.y;
    
  229.     const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT);
    
  230.     const nativeEventsAtDepth = this._depthToNativeEvent.get(depth);
    
  231. 
    
  232.     if (nativeEventsAtDepth) {
    
  233.       // Find the event being hovered over.
    
  234.       for (let index = nativeEventsAtDepth.length - 1; index >= 0; index--) {
    
  235.         const nativeEvent = nativeEventsAtDepth[index];
    
  236.         const {duration, timestamp} = nativeEvent;
    
  237. 
    
  238.         if (
    
  239.           hoverTimestamp >= timestamp &&
    
  240.           hoverTimestamp <= timestamp + duration
    
  241.         ) {
    
  242.           viewRefs.hoveredView = this;
    
  243.           onHover(nativeEvent);
    
  244.           return;
    
  245.         }
    
  246.       }
    
  247.     }
    
  248. 
    
  249.     onHover(null);
    
  250.   }
    
  251. 
    
  252.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  253.     switch (interaction.type) {
    
  254.       case 'mousemove':
    
  255.         this._handleMouseMove(interaction, viewRefs);
    
  256.         break;
    
  257.     }
    
  258.   }
    
  259. }