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 {NetworkMeasure, 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 {BORDER_SIZE, COLORS, SUSPENSE_EVENT_HEIGHT} from './constants';
    
  35. 
    
  36. const HEIGHT = SUSPENSE_EVENT_HEIGHT; // TODO Constant name
    
  37. const ROW_WITH_BORDER_HEIGHT = HEIGHT + BORDER_SIZE;
    
  38. 
    
  39. const BASE_URL_REGEX = /([^:]+:\/\/[^\/]+)/;
    
  40. 
    
  41. export class NetworkMeasuresView extends View {
    
  42.   _depthToNetworkMeasure: Map<number, NetworkMeasure[]>;
    
  43.   _hoveredNetworkMeasure: NetworkMeasure | null = null;
    
  44.   _intrinsicSize: IntrinsicSize;
    
  45.   _maxDepth: number = 0;
    
  46.   _profilerData: TimelineData;
    
  47. 
    
  48.   onHover: ((event: NetworkMeasure | null) => void) | null = null;
    
  49. 
    
  50.   constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
    
  51.     super(surface, frame);
    
  52. 
    
  53.     this._profilerData = profilerData;
    
  54. 
    
  55.     this._performPreflightComputations();
    
  56.   }
    
  57. 
    
  58.   _performPreflightComputations() {
    
  59.     this._depthToNetworkMeasure = new Map();
    
  60. 
    
  61.     const {duration, networkMeasures} = this._profilerData;
    
  62. 
    
  63.     networkMeasures.forEach(event => {
    
  64.       const depth = event.depth;
    
  65. 
    
  66.       this._maxDepth = Math.max(this._maxDepth, depth);
    
  67. 
    
  68.       if (!this._depthToNetworkMeasure.has(depth)) {
    
  69.         this._depthToNetworkMeasure.set(depth, [event]);
    
  70.       } else {
    
  71.         // $FlowFixMe[incompatible-use] This is unnecessary.
    
  72.         this._depthToNetworkMeasure.get(depth).push(event);
    
  73.       }
    
  74.     });
    
  75. 
    
  76.     this._intrinsicSize = {
    
  77.       width: duration,
    
  78.       height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
    
  79.       // Collapsed by default
    
  80.       maxInitialHeight: 0,
    
  81.     };
    
  82.   }
    
  83. 
    
  84.   desiredSize(): IntrinsicSize {
    
  85.     return this._intrinsicSize;
    
  86.   }
    
  87. 
    
  88.   setHoveredEvent(networkMeasure: NetworkMeasure | null) {
    
  89.     if (this._hoveredNetworkMeasure === networkMeasure) {
    
  90.       return;
    
  91.     }
    
  92.     this._hoveredNetworkMeasure = networkMeasure;
    
  93.     this.setNeedsDisplay();
    
  94.   }
    
  95. 
    
  96.   /**
    
  97.    * Draw a single `NetworkMeasure` as a box/span with text inside of it.
    
  98.    */
    
  99.   _drawSingleNetworkMeasure(
    
  100.     context: CanvasRenderingContext2D,
    
  101.     networkMeasure: NetworkMeasure,
    
  102.     baseY: number,
    
  103.     scaleFactor: number,
    
  104.     showHoverHighlight: boolean,
    
  105.   ) {
    
  106.     const {frame, visibleArea} = this;
    
  107.     const {
    
  108.       depth,
    
  109.       finishTimestamp,
    
  110.       firstReceivedDataTimestamp,
    
  111.       lastReceivedDataTimestamp,
    
  112.       receiveResponseTimestamp,
    
  113.       sendRequestTimestamp,
    
  114.       url,
    
  115.     } = networkMeasure;
    
  116. 
    
  117.     // Account for requests that did not complete while we were profiling.
    
  118.     // As well as requests that did not receive data before finish (cached?).
    
  119.     const duration = this._profilerData.duration;
    
  120.     const timestampBegin = sendRequestTimestamp;
    
  121.     const timestampEnd =
    
  122.       finishTimestamp || lastReceivedDataTimestamp || duration;
    
  123.     const timestampMiddle =
    
  124.       receiveResponseTimestamp || firstReceivedDataTimestamp || timestampEnd;
    
  125. 
    
  126.     // Convert all timestamps to x coordinates.
    
  127.     const xStart = timestampToPosition(timestampBegin, scaleFactor, frame);
    
  128.     const xMiddle = timestampToPosition(timestampMiddle, scaleFactor, frame);
    
  129.     const xStop = timestampToPosition(timestampEnd, scaleFactor, frame);
    
  130. 
    
  131.     const width = durationToWidth(xStop - xStart, scaleFactor);
    
  132.     if (width < 1) {
    
  133.       return; // Too small to render at this zoom level
    
  134.     }
    
  135. 
    
  136.     baseY += depth * ROW_WITH_BORDER_HEIGHT;
    
  137. 
    
  138.     const outerRect: Rect = {
    
  139.       origin: {
    
  140.         x: xStart,
    
  141.         y: baseY,
    
  142.       },
    
  143.       size: {
    
  144.         width: xStop - xStart,
    
  145.         height: HEIGHT,
    
  146.       },
    
  147.     };
    
  148.     if (!rectIntersectsRect(outerRect, visibleArea)) {
    
  149.       return; // Not in view
    
  150.     }
    
  151. 
    
  152.     // Draw the secondary rect first (since it also shows as a thin border around the primary rect).
    
  153.     let rect = {
    
  154.       origin: {
    
  155.         x: xStart,
    
  156.         y: baseY,
    
  157.       },
    
  158.       size: {
    
  159.         width: xStop - xStart,
    
  160.         height: HEIGHT,
    
  161.       },
    
  162.     };
    
  163.     if (rectIntersectsRect(rect, visibleArea)) {
    
  164.       context.beginPath();
    
  165.       context.fillStyle =
    
  166.         this._hoveredNetworkMeasure === networkMeasure
    
  167.           ? COLORS.NETWORK_SECONDARY_HOVER
    
  168.           : COLORS.NETWORK_SECONDARY;
    
  169.       context.fillRect(
    
  170.         rect.origin.x,
    
  171.         rect.origin.y,
    
  172.         rect.size.width,
    
  173.         rect.size.height,
    
  174.       );
    
  175.     }
    
  176. 
    
  177.     rect = {
    
  178.       origin: {
    
  179.         x: xStart + BORDER_SIZE,
    
  180.         y: baseY + BORDER_SIZE,
    
  181.       },
    
  182.       size: {
    
  183.         width: xMiddle - xStart - BORDER_SIZE,
    
  184.         height: HEIGHT - BORDER_SIZE * 2,
    
  185.       },
    
  186.     };
    
  187.     if (rectIntersectsRect(rect, visibleArea)) {
    
  188.       context.beginPath();
    
  189.       context.fillStyle =
    
  190.         this._hoveredNetworkMeasure === networkMeasure
    
  191.           ? COLORS.NETWORK_PRIMARY_HOVER
    
  192.           : COLORS.NETWORK_PRIMARY;
    
  193.       context.fillRect(
    
  194.         rect.origin.x,
    
  195.         rect.origin.y,
    
  196.         rect.size.width,
    
  197.         rect.size.height,
    
  198.       );
    
  199.     }
    
  200. 
    
  201.     const baseUrl = url.match(BASE_URL_REGEX);
    
  202.     const displayUrl = baseUrl !== null ? baseUrl[1] : url;
    
  203. 
    
  204.     const durationLabel =
    
  205.       finishTimestamp !== 0
    
  206.         ? `${formatDuration(finishTimestamp - sendRequestTimestamp)} - `
    
  207.         : '';
    
  208. 
    
  209.     const label = durationLabel + displayUrl;
    
  210. 
    
  211.     drawText(label, context, outerRect, visibleArea);
    
  212.   }
    
  213. 
    
  214.   draw(context: CanvasRenderingContext2D) {
    
  215.     const {
    
  216.       frame,
    
  217.       _profilerData: {networkMeasures},
    
  218.       _hoveredNetworkMeasure,
    
  219.       visibleArea,
    
  220.     } = this;
    
  221. 
    
  222.     context.fillStyle = COLORS.PRIORITY_BACKGROUND;
    
  223.     context.fillRect(
    
  224.       visibleArea.origin.x,
    
  225.       visibleArea.origin.y,
    
  226.       visibleArea.size.width,
    
  227.       visibleArea.size.height,
    
  228.     );
    
  229. 
    
  230.     const scaleFactor = positioningScaleFactor(
    
  231.       this._intrinsicSize.width,
    
  232.       frame,
    
  233.     );
    
  234. 
    
  235.     networkMeasures.forEach(networkMeasure => {
    
  236.       this._drawSingleNetworkMeasure(
    
  237.         context,
    
  238.         networkMeasure,
    
  239.         frame.origin.y,
    
  240.         scaleFactor,
    
  241.         networkMeasure === _hoveredNetworkMeasure,
    
  242.       );
    
  243.     });
    
  244. 
    
  245.     // Render bottom borders.
    
  246.     for (let i = 0; i <= this._maxDepth; i++) {
    
  247.       const borderFrame: Rect = {
    
  248.         origin: {
    
  249.           x: frame.origin.x,
    
  250.           y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
    
  251.         },
    
  252.         size: {
    
  253.           width: frame.size.width,
    
  254.           height: BORDER_SIZE,
    
  255.         },
    
  256.       };
    
  257.       if (rectIntersectsRect(borderFrame, visibleArea)) {
    
  258.         const borderDrawableRect = intersectionOfRects(
    
  259.           borderFrame,
    
  260.           visibleArea,
    
  261.         );
    
  262.         context.fillStyle = COLORS.PRIORITY_BORDER;
    
  263.         context.fillRect(
    
  264.           borderDrawableRect.origin.x,
    
  265.           borderDrawableRect.origin.y,
    
  266.           borderDrawableRect.size.width,
    
  267.           borderDrawableRect.size.height,
    
  268.         );
    
  269.       }
    
  270.     }
    
  271.   }
    
  272. 
    
  273.   /**
    
  274.    * @private
    
  275.    */
    
  276.   _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    
  277.     const {frame, _intrinsicSize, onHover, visibleArea} = this;
    
  278.     if (!onHover) {
    
  279.       return;
    
  280.     }
    
  281. 
    
  282.     const {location} = interaction.payload;
    
  283.     if (!rectContainsPoint(location, visibleArea)) {
    
  284.       onHover(null);
    
  285.       return;
    
  286.     }
    
  287. 
    
  288.     const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    
  289.     const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
    
  290. 
    
  291.     const adjustedCanvasMouseY = location.y - frame.origin.y;
    
  292.     const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT);
    
  293.     const networkMeasuresAtDepth = this._depthToNetworkMeasure.get(depth);
    
  294. 
    
  295.     const duration = this._profilerData.duration;
    
  296. 
    
  297.     if (networkMeasuresAtDepth) {
    
  298.       // Find the event being hovered over.
    
  299.       for (let index = networkMeasuresAtDepth.length - 1; index >= 0; index--) {
    
  300.         const networkMeasure = networkMeasuresAtDepth[index];
    
  301.         const {
    
  302.           finishTimestamp,
    
  303.           lastReceivedDataTimestamp,
    
  304.           sendRequestTimestamp,
    
  305.         } = networkMeasure;
    
  306. 
    
  307.         const timestampBegin = sendRequestTimestamp;
    
  308.         const timestampEnd =
    
  309.           finishTimestamp || lastReceivedDataTimestamp || duration;
    
  310. 
    
  311.         if (
    
  312.           hoverTimestamp >= timestampBegin &&
    
  313.           hoverTimestamp <= timestampEnd
    
  314.         ) {
    
  315.           this.currentCursor = 'context-menu';
    
  316.           viewRefs.hoveredView = this;
    
  317.           onHover(networkMeasure);
    
  318.           return;
    
  319.         }
    
  320.       }
    
  321.     }
    
  322. 
    
  323.     if (viewRefs.hoveredView === this) {
    
  324.       viewRefs.hoveredView = null;
    
  325.     }
    
  326. 
    
  327.     onHover(null);
    
  328.   }
    
  329. 
    
  330.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  331.     switch (interaction.type) {
    
  332.       case 'mousemove':
    
  333.         this._handleMouseMove(interaction, viewRefs);
    
  334.         break;
    
  335.     }
    
  336.   }
    
  337. }