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 {Snapshot, TimelineData} from '../types';
    
  11. import type {
    
  12.   Interaction,
    
  13.   Point,
    
  14.   Rect,
    
  15.   Size,
    
  16.   Surface,
    
  17.   ViewRefs,
    
  18. } from '../view-base';
    
  19. 
    
  20. import {positioningScaleFactor, timestampToPosition} from './utils/positioning';
    
  21. import {
    
  22.   intersectionOfRects,
    
  23.   rectContainsPoint,
    
  24.   rectEqualToRect,
    
  25.   View,
    
  26. } from '../view-base';
    
  27. import {BORDER_SIZE, COLORS, SNAPSHOT_SCRUBBER_SIZE} from './constants';
    
  28. 
    
  29. type OnHover = (node: Snapshot | null) => void;
    
  30. 
    
  31. export class SnapshotsView extends View {
    
  32.   _hoverLocation: Point | null = null;
    
  33.   _intrinsicSize: Size;
    
  34.   _profilerData: TimelineData;
    
  35. 
    
  36.   onHover: OnHover | null = null;
    
  37. 
    
  38.   constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
    
  39.     super(surface, frame);
    
  40. 
    
  41.     this._intrinsicSize = {
    
  42.       width: profilerData.duration,
    
  43.       height: profilerData.snapshotHeight,
    
  44.     };
    
  45.     this._profilerData = profilerData;
    
  46.   }
    
  47. 
    
  48.   desiredSize(): Size {
    
  49.     return this._intrinsicSize;
    
  50.   }
    
  51. 
    
  52.   draw(context: CanvasRenderingContext2D) {
    
  53.     const snapshotHeight = this._profilerData.snapshotHeight;
    
  54.     const {visibleArea} = this;
    
  55. 
    
  56.     context.fillStyle = COLORS.BACKGROUND;
    
  57.     context.fillRect(
    
  58.       visibleArea.origin.x,
    
  59.       visibleArea.origin.y,
    
  60.       visibleArea.size.width,
    
  61.       visibleArea.size.height,
    
  62.     );
    
  63. 
    
  64.     const y = visibleArea.origin.y;
    
  65. 
    
  66.     let x = visibleArea.origin.x;
    
  67. 
    
  68.     // Rather than drawing each snapshot where it occurred,
    
  69.     // draw them at fixed intervals and just show the nearest one.
    
  70.     while (x < visibleArea.origin.x + visibleArea.size.width) {
    
  71.       const snapshot = this._findClosestSnapshot(x);
    
  72.       if (snapshot === null) {
    
  73.         // This shold never happen.
    
  74.         break;
    
  75.       }
    
  76. 
    
  77.       const scaledHeight = snapshotHeight;
    
  78.       const scaledWidth = (snapshot.width * snapshotHeight) / snapshot.height;
    
  79. 
    
  80.       const imageRect: Rect = {
    
  81.         origin: {
    
  82.           x,
    
  83.           y,
    
  84.         },
    
  85.         size: {width: scaledWidth, height: scaledHeight},
    
  86.       };
    
  87. 
    
  88.       // Lazily create and cache Image objects as we render a snapsho for the first time.
    
  89.       if (snapshot.image === null) {
    
  90.         const img = (snapshot.image = new Image());
    
  91.         img.onload = () => {
    
  92.           this._drawSnapshotImage(context, snapshot, imageRect);
    
  93.         };
    
  94.         img.src = snapshot.imageSource;
    
  95.       } else {
    
  96.         this._drawSnapshotImage(context, snapshot, imageRect);
    
  97.       }
    
  98. 
    
  99.       x += scaledWidth + BORDER_SIZE;
    
  100.     }
    
  101. 
    
  102.     const hoverLocation = this._hoverLocation;
    
  103.     if (hoverLocation !== null) {
    
  104.       const scrubberWidth = SNAPSHOT_SCRUBBER_SIZE + BORDER_SIZE * 2;
    
  105.       const scrubberOffset = scrubberWidth / 2;
    
  106. 
    
  107.       context.fillStyle = COLORS.SCRUBBER_BORDER;
    
  108.       context.fillRect(
    
  109.         hoverLocation.x - scrubberOffset,
    
  110.         visibleArea.origin.y,
    
  111.         scrubberWidth,
    
  112.         visibleArea.size.height,
    
  113.       );
    
  114. 
    
  115.       context.fillStyle = COLORS.SCRUBBER_BACKGROUND;
    
  116.       context.fillRect(
    
  117.         hoverLocation.x - scrubberOffset + BORDER_SIZE,
    
  118.         visibleArea.origin.y,
    
  119.         SNAPSHOT_SCRUBBER_SIZE,
    
  120.         visibleArea.size.height,
    
  121.       );
    
  122.     }
    
  123.   }
    
  124. 
    
  125.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  126.     switch (interaction.type) {
    
  127.       case 'mousemove':
    
  128.       case 'wheel-control':
    
  129.       case 'wheel-meta':
    
  130.       case 'wheel-plain':
    
  131.       case 'wheel-shift':
    
  132.         this._updateHover(interaction.payload.location, viewRefs);
    
  133.         break;
    
  134.     }
    
  135.   }
    
  136. 
    
  137.   _drawSnapshotImage(
    
  138.     context: CanvasRenderingContext2D,
    
  139.     snapshot: Snapshot,
    
  140.     imageRect: Rect,
    
  141.   ) {
    
  142.     const visibleArea = this.visibleArea;
    
  143. 
    
  144.     // Prevent snapshot from visibly overflowing its container when clipped.
    
  145.     // View clips by default, but since this view may draw async (on Image load) we re-clip.
    
  146.     const shouldClip = !rectEqualToRect(imageRect, visibleArea);
    
  147.     if (shouldClip) {
    
  148.       const clippedRect = intersectionOfRects(imageRect, visibleArea);
    
  149.       context.save();
    
  150.       context.beginPath();
    
  151.       context.rect(
    
  152.         clippedRect.origin.x,
    
  153.         clippedRect.origin.y,
    
  154.         clippedRect.size.width,
    
  155.         clippedRect.size.height,
    
  156.       );
    
  157.       context.closePath();
    
  158.       context.clip();
    
  159.     }
    
  160. 
    
  161.     context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
    
  162.     context.fillRect(
    
  163.       imageRect.origin.x,
    
  164.       imageRect.origin.y,
    
  165.       imageRect.size.width,
    
  166.       imageRect.size.height,
    
  167.     );
    
  168. 
    
  169.     // $FlowFixMe[incompatible-call] Flow doesn't know about the 9 argument variant of drawImage()
    
  170.     context.drawImage(
    
  171.       snapshot.image,
    
  172. 
    
  173.       // Image coordinates
    
  174.       0,
    
  175.       0,
    
  176. 
    
  177.       // Native image size
    
  178.       snapshot.width,
    
  179.       snapshot.height,
    
  180. 
    
  181.       // Canvas coordinates
    
  182.       imageRect.origin.x + BORDER_SIZE,
    
  183.       imageRect.origin.y + BORDER_SIZE,
    
  184. 
    
  185.       // Scaled image size
    
  186.       imageRect.size.width - BORDER_SIZE * 2,
    
  187.       imageRect.size.height - BORDER_SIZE * 2,
    
  188.     );
    
  189. 
    
  190.     if (shouldClip) {
    
  191.       context.restore();
    
  192.     }
    
  193.   }
    
  194. 
    
  195.   _findClosestSnapshot(x: number): Snapshot | null {
    
  196.     const frame = this.frame;
    
  197.     const scaleFactor = positioningScaleFactor(
    
  198.       this._intrinsicSize.width,
    
  199.       frame,
    
  200.     );
    
  201. 
    
  202.     const snapshots = this._profilerData.snapshots;
    
  203. 
    
  204.     let startIndex = 0;
    
  205.     let stopIndex = snapshots.length - 1;
    
  206.     while (startIndex <= stopIndex) {
    
  207.       const currentIndex = Math.floor((startIndex + stopIndex) / 2);
    
  208.       const snapshot = snapshots[currentIndex];
    
  209.       const {timestamp} = snapshot;
    
  210. 
    
  211.       const snapshotX = Math.floor(
    
  212.         timestampToPosition(timestamp, scaleFactor, frame),
    
  213.       );
    
  214. 
    
  215.       if (x < snapshotX) {
    
  216.         stopIndex = currentIndex - 1;
    
  217.       } else {
    
  218.         startIndex = currentIndex + 1;
    
  219.       }
    
  220.     }
    
  221. 
    
  222.     return snapshots[stopIndex] || null;
    
  223.   }
    
  224. 
    
  225.   /**
    
  226.    * @private
    
  227.    */
    
  228.   _updateHover(location: Point, viewRefs: ViewRefs) {
    
  229.     const {onHover, visibleArea} = this;
    
  230.     if (!onHover) {
    
  231.       return;
    
  232.     }
    
  233. 
    
  234.     if (!rectContainsPoint(location, visibleArea)) {
    
  235.       if (this._hoverLocation !== null) {
    
  236.         this._hoverLocation = null;
    
  237. 
    
  238.         this.setNeedsDisplay();
    
  239.       }
    
  240. 
    
  241.       onHover(null);
    
  242.       return;
    
  243.     }
    
  244. 
    
  245.     const snapshot = this._findClosestSnapshot(location.x);
    
  246.     if (snapshot !== null) {
    
  247.       this._hoverLocation = location;
    
  248. 
    
  249.       onHover(snapshot);
    
  250.     } else {
    
  251.       this._hoverLocation = null;
    
  252. 
    
  253.       onHover(null);
    
  254.     }
    
  255. 
    
  256.     // Any time the mouse moves within the boundaries of this view, we need to re-render.
    
  257.     // This is because we draw a scrubbing bar that shows the location corresponding to the current tooltip.
    
  258.     this.setNeedsDisplay();
    
  259.   }
    
  260. }