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 {
    
  11.   Flamechart,
    
  12.   FlamechartStackFrame,
    
  13.   FlamechartStackLayer,
    
  14.   InternalModuleSourceToRanges,
    
  15. } from '../types';
    
  16. import type {
    
  17.   Interaction,
    
  18.   MouseMoveInteraction,
    
  19.   Rect,
    
  20.   Size,
    
  21.   ViewRefs,
    
  22. } from '../view-base';
    
  23. 
    
  24. import {
    
  25.   BackgroundColorView,
    
  26.   Surface,
    
  27.   View,
    
  28.   layeredLayout,
    
  29.   rectContainsPoint,
    
  30.   intersectionOfRects,
    
  31.   rectIntersectsRect,
    
  32.   verticallyStackedLayout,
    
  33. } from '../view-base';
    
  34. import {isInternalModule} from './utils/moduleFilters';
    
  35. import {
    
  36.   durationToWidth,
    
  37.   positioningScaleFactor,
    
  38.   timestampToPosition,
    
  39. } from './utils/positioning';
    
  40. import {drawText} from './utils/text';
    
  41. import {
    
  42.   COLORS,
    
  43.   FLAMECHART_FRAME_HEIGHT,
    
  44.   COLOR_HOVER_DIM_DELTA,
    
  45.   BORDER_SIZE,
    
  46. } from './constants';
    
  47. import {ColorGenerator, dimmedColor, hslaColorToString} from './utils/colors';
    
  48. 
    
  49. // Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/timeline/TimelineUIUtils.js;l=2109;drc=fb32e928d79707a693351b806b8710b2f6b7d399
    
  50. const colorGenerator = new ColorGenerator(
    
  51.   {min: 30, max: 330},
    
  52.   {min: 50, max: 80, count: 3},
    
  53.   85,
    
  54. );
    
  55. colorGenerator.setColorForID('', {h: 43.6, s: 45.8, l: 90.6, a: 100});
    
  56. 
    
  57. function defaultHslaColorForStackFrame({scriptUrl}: FlamechartStackFrame) {
    
  58.   return colorGenerator.colorForID(scriptUrl ?? '');
    
  59. }
    
  60. 
    
  61. function defaultColorForStackFrame(stackFrame: FlamechartStackFrame): string {
    
  62.   const color = defaultHslaColorForStackFrame(stackFrame);
    
  63.   return hslaColorToString(color);
    
  64. }
    
  65. 
    
  66. function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
    
  67.   const color = dimmedColor(
    
  68.     defaultHslaColorForStackFrame(stackFrame),
    
  69.     COLOR_HOVER_DIM_DELTA,
    
  70.   );
    
  71.   return hslaColorToString(color);
    
  72. }
    
  73. 
    
  74. class FlamechartStackLayerView extends View {
    
  75.   /** Layer to display */
    
  76.   _stackLayer: FlamechartStackLayer;
    
  77. 
    
  78.   /** A set of `stackLayer`'s frames, for efficient lookup. */
    
  79.   _stackFrameSet: Set<FlamechartStackFrame>;
    
  80. 
    
  81.   _internalModuleSourceToRanges: InternalModuleSourceToRanges;
    
  82. 
    
  83.   _intrinsicSize: Size;
    
  84. 
    
  85.   _hoveredStackFrame: FlamechartStackFrame | null = null;
    
  86.   _onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
    
  87. 
    
  88.   constructor(
    
  89.     surface: Surface,
    
  90.     frame: Rect,
    
  91.     stackLayer: FlamechartStackLayer,
    
  92.     internalModuleSourceToRanges: InternalModuleSourceToRanges,
    
  93.     duration: number,
    
  94.   ) {
    
  95.     super(surface, frame);
    
  96.     this._stackLayer = stackLayer;
    
  97.     this._stackFrameSet = new Set(stackLayer);
    
  98.     this._internalModuleSourceToRanges = internalModuleSourceToRanges;
    
  99.     this._intrinsicSize = {
    
  100.       width: duration,
    
  101.       height: FLAMECHART_FRAME_HEIGHT,
    
  102.     };
    
  103.   }
    
  104. 
    
  105.   desiredSize(): Size {
    
  106.     return this._intrinsicSize;
    
  107.   }
    
  108. 
    
  109.   setHoveredFlamechartStackFrame(
    
  110.     hoveredStackFrame: FlamechartStackFrame | null,
    
  111.   ) {
    
  112.     if (this._hoveredStackFrame === hoveredStackFrame) {
    
  113.       return; // We're already hovering over this frame
    
  114.     }
    
  115. 
    
  116.     // Only care about frames displayed by this view.
    
  117.     const stackFrameToSet =
    
  118.       hoveredStackFrame && this._stackFrameSet.has(hoveredStackFrame)
    
  119.         ? hoveredStackFrame
    
  120.         : null;
    
  121.     if (this._hoveredStackFrame === stackFrameToSet) {
    
  122.       return; // Resulting state is unchanged
    
  123.     }
    
  124.     this._hoveredStackFrame = stackFrameToSet;
    
  125.     this.setNeedsDisplay();
    
  126.   }
    
  127. 
    
  128.   draw(context: CanvasRenderingContext2D) {
    
  129.     const {
    
  130.       frame,
    
  131.       _stackLayer,
    
  132.       _hoveredStackFrame,
    
  133.       _intrinsicSize,
    
  134.       visibleArea,
    
  135.     } = this;
    
  136. 
    
  137.     context.fillStyle = COLORS.PRIORITY_BACKGROUND;
    
  138.     context.fillRect(
    
  139.       visibleArea.origin.x,
    
  140.       visibleArea.origin.y,
    
  141.       visibleArea.size.width,
    
  142.       visibleArea.size.height,
    
  143.     );
    
  144. 
    
  145.     const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    
  146. 
    
  147.     for (let i = 0; i < _stackLayer.length; i++) {
    
  148.       const stackFrame = _stackLayer[i];
    
  149.       const {name, timestamp, duration} = stackFrame;
    
  150. 
    
  151.       const width = durationToWidth(duration, scaleFactor);
    
  152.       if (width < 1) {
    
  153.         continue; // Too small to render at this zoom level
    
  154.       }
    
  155. 
    
  156.       const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
    
  157.       const nodeRect: Rect = {
    
  158.         origin: {x, y: frame.origin.y},
    
  159.         size: {
    
  160.           width: Math.floor(width - BORDER_SIZE),
    
  161.           height: Math.floor(FLAMECHART_FRAME_HEIGHT - BORDER_SIZE),
    
  162.         },
    
  163.       };
    
  164.       if (!rectIntersectsRect(nodeRect, visibleArea)) {
    
  165.         continue; // Not in view
    
  166.       }
    
  167. 
    
  168.       const showHoverHighlight = _hoveredStackFrame === _stackLayer[i];
    
  169. 
    
  170.       let textFillStyle;
    
  171.       if (isInternalModule(this._internalModuleSourceToRanges, stackFrame)) {
    
  172.         context.fillStyle = showHoverHighlight
    
  173.           ? COLORS.INTERNAL_MODULE_FRAME_HOVER
    
  174.           : COLORS.INTERNAL_MODULE_FRAME;
    
  175.         textFillStyle = COLORS.INTERNAL_MODULE_FRAME_TEXT;
    
  176.       } else {
    
  177.         context.fillStyle = showHoverHighlight
    
  178.           ? hoverColorForStackFrame(stackFrame)
    
  179.           : defaultColorForStackFrame(stackFrame);
    
  180.         textFillStyle = COLORS.TEXT_COLOR;
    
  181.       }
    
  182. 
    
  183.       const drawableRect = intersectionOfRects(nodeRect, visibleArea);
    
  184.       context.fillRect(
    
  185.         drawableRect.origin.x,
    
  186.         drawableRect.origin.y,
    
  187.         drawableRect.size.width,
    
  188.         drawableRect.size.height,
    
  189.       );
    
  190. 
    
  191.       drawText(name, context, nodeRect, drawableRect, {
    
  192.         fillStyle: textFillStyle,
    
  193.       });
    
  194.     }
    
  195. 
    
  196.     // Render bottom border.
    
  197.     const borderFrame: Rect = {
    
  198.       origin: {
    
  199.         x: frame.origin.x,
    
  200.         y: frame.origin.y + FLAMECHART_FRAME_HEIGHT - BORDER_SIZE,
    
  201.       },
    
  202.       size: {
    
  203.         width: frame.size.width,
    
  204.         height: BORDER_SIZE,
    
  205.       },
    
  206.     };
    
  207.     if (rectIntersectsRect(borderFrame, visibleArea)) {
    
  208.       const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
    
  209.       context.fillStyle = COLORS.PRIORITY_BORDER;
    
  210.       context.fillRect(
    
  211.         borderDrawableRect.origin.x,
    
  212.         borderDrawableRect.origin.y,
    
  213.         borderDrawableRect.size.width,
    
  214.         borderDrawableRect.size.height,
    
  215.       );
    
  216.     }
    
  217.   }
    
  218. 
    
  219.   /**
    
  220.    * @private
    
  221.    */
    
  222.   _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    
  223.     const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this;
    
  224.     const {location} = interaction.payload;
    
  225.     if (!_onHover || !rectContainsPoint(location, visibleArea)) {
    
  226.       return;
    
  227.     }
    
  228. 
    
  229.     // Find the node being hovered over.
    
  230.     const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    
  231.     let startIndex = 0;
    
  232.     let stopIndex = _stackLayer.length - 1;
    
  233.     while (startIndex <= stopIndex) {
    
  234.       const currentIndex = Math.floor((startIndex + stopIndex) / 2);
    
  235.       const flamechartStackFrame = _stackLayer[currentIndex];
    
  236.       const {timestamp, duration} = flamechartStackFrame;
    
  237. 
    
  238.       const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
    
  239.       const width = durationToWidth(duration, scaleFactor);
    
  240. 
    
  241.       // Don't show tooltips for nodes that are too small to render at this zoom level.
    
  242.       if (Math.floor(width - BORDER_SIZE) >= 1) {
    
  243.         if (x <= location.x && x + width >= location.x) {
    
  244.           this.currentCursor = 'context-menu';
    
  245.           viewRefs.hoveredView = this;
    
  246.           _onHover(flamechartStackFrame);
    
  247.           return;
    
  248.         }
    
  249.       }
    
  250. 
    
  251.       if (x > location.x) {
    
  252.         stopIndex = currentIndex - 1;
    
  253.       } else {
    
  254.         startIndex = currentIndex + 1;
    
  255.       }
    
  256.     }
    
  257. 
    
  258.     _onHover(null);
    
  259.   }
    
  260. 
    
  261.   _didGrab: boolean = false;
    
  262. 
    
  263.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  264.     switch (interaction.type) {
    
  265.       case 'mousemove':
    
  266.         this._handleMouseMove(interaction, viewRefs);
    
  267.         break;
    
  268.     }
    
  269.   }
    
  270. }
    
  271. 
    
  272. export class FlamechartView extends View {
    
  273.   _flamechartRowViews: FlamechartStackLayerView[] = [];
    
  274. 
    
  275.   /** Container view that vertically stacks flamechart rows */
    
  276.   _verticalStackView: View;
    
  277. 
    
  278.   _hoveredStackFrame: FlamechartStackFrame | null = null;
    
  279.   _onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
    
  280. 
    
  281.   constructor(
    
  282.     surface: Surface,
    
  283.     frame: Rect,
    
  284.     flamechart: Flamechart,
    
  285.     internalModuleSourceToRanges: InternalModuleSourceToRanges,
    
  286.     duration: number,
    
  287.   ) {
    
  288.     super(surface, frame, layeredLayout);
    
  289.     this.setDataAndUpdateSubviews(
    
  290.       flamechart,
    
  291.       internalModuleSourceToRanges,
    
  292.       duration,
    
  293.     );
    
  294.   }
    
  295. 
    
  296.   setDataAndUpdateSubviews(
    
  297.     flamechart: Flamechart,
    
  298.     internalModuleSourceToRanges: InternalModuleSourceToRanges,
    
  299.     duration: number,
    
  300.   ) {
    
  301.     const {surface, frame, _onHover, _hoveredStackFrame} = this;
    
  302. 
    
  303.     // Clear existing rows on data update
    
  304.     if (this._verticalStackView) {
    
  305.       this.removeAllSubviews();
    
  306.       this._flamechartRowViews = [];
    
  307.     }
    
  308. 
    
  309.     this._verticalStackView = new View(surface, frame, verticallyStackedLayout);
    
  310.     this._flamechartRowViews = flamechart.map(stackLayer => {
    
  311.       const rowView = new FlamechartStackLayerView(
    
  312.         surface,
    
  313.         frame,
    
  314.         stackLayer,
    
  315.         internalModuleSourceToRanges,
    
  316.         duration,
    
  317.       );
    
  318.       this._verticalStackView.addSubview(rowView);
    
  319. 
    
  320.       // Update states
    
  321.       rowView._onHover = _onHover;
    
  322.       rowView.setHoveredFlamechartStackFrame(_hoveredStackFrame);
    
  323.       return rowView;
    
  324.     });
    
  325. 
    
  326.     // Add a plain background view to prevent gaps from appearing between flamechartRowViews.
    
  327.     this.addSubview(new BackgroundColorView(surface, frame));
    
  328.     this.addSubview(this._verticalStackView);
    
  329.   }
    
  330. 
    
  331.   setHoveredFlamechartStackFrame(
    
  332.     hoveredStackFrame: FlamechartStackFrame | null,
    
  333.   ) {
    
  334.     this._hoveredStackFrame = hoveredStackFrame;
    
  335.     this._flamechartRowViews.forEach(rowView =>
    
  336.       rowView.setHoveredFlamechartStackFrame(hoveredStackFrame),
    
  337.     );
    
  338.   }
    
  339. 
    
  340.   setOnHover(onHover: (node: FlamechartStackFrame | null) => void) {
    
  341.     this._onHover = onHover;
    
  342.     this._flamechartRowViews.forEach(rowView => (rowView._onHover = onHover));
    
  343.   }
    
  344. 
    
  345.   desiredSize(): {
    
  346.     height: number,
    
  347.     hideScrollBarIfLessThanHeight?: number,
    
  348.     maxInitialHeight?: number,
    
  349.     width: number,
    
  350.   } {
    
  351.     // Ignore the wishes of the background color view
    
  352.     const intrinsicSize = this._verticalStackView.desiredSize();
    
  353.     return {
    
  354.       ...intrinsicSize,
    
  355.       // Collapsed by default
    
  356.       maxInitialHeight: 0,
    
  357.     };
    
  358.   }
    
  359. 
    
  360.   /**
    
  361.    * @private
    
  362.    */
    
  363.   _handleMouseMove(interaction: MouseMoveInteraction) {
    
  364.     const {_onHover, visibleArea} = this;
    
  365.     if (!_onHover) {
    
  366.       return;
    
  367.     }
    
  368. 
    
  369.     const {location} = interaction.payload;
    
  370.     if (!rectContainsPoint(location, visibleArea)) {
    
  371.       // Clear out any hovered flamechart stack frame
    
  372.       _onHover(null);
    
  373.     }
    
  374.   }
    
  375. 
    
  376.   handleInteraction(interaction: Interaction) {
    
  377.     switch (interaction.type) {
    
  378.       case 'mousemove':
    
  379.         this._handleMouseMove(interaction);
    
  380.         break;
    
  381.     }
    
  382.   }
    
  383. }