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 Agent from 'react-devtools-shared/src/backend/agent';
    
  11. import {destroy as destroyCanvas, draw} from './canvas';
    
  12. import {getNestedBoundingClientRect} from '../utils';
    
  13. 
    
  14. import type {NativeType} from '../../types';
    
  15. import type {Rect} from '../utils';
    
  16. 
    
  17. // How long the rect should be shown for?
    
  18. const DISPLAY_DURATION = 250;
    
  19. 
    
  20. // What's the longest we are willing to show the overlay for?
    
  21. // This can be important if we're getting a flurry of events (e.g. scroll update).
    
  22. const MAX_DISPLAY_DURATION = 3000;
    
  23. 
    
  24. // How long should a rect be considered valid for?
    
  25. const REMEASUREMENT_AFTER_DURATION = 250;
    
  26. 
    
  27. // Some environments (e.g. React Native / Hermes) don't support the performance API yet.
    
  28. const getCurrentTime =
    
  29.   // $FlowFixMe[method-unbinding]
    
  30.   typeof performance === 'object' && typeof performance.now === 'function'
    
  31.     ? () => performance.now()
    
  32.     : () => Date.now();
    
  33. 
    
  34. export type Data = {
    
  35.   count: number,
    
  36.   expirationTime: number,
    
  37.   lastMeasuredAt: number,
    
  38.   rect: Rect | null,
    
  39. };
    
  40. 
    
  41. const nodeToData: Map<NativeType, Data> = new Map();
    
  42. 
    
  43. let agent: Agent = ((null: any): Agent);
    
  44. let drawAnimationFrameID: AnimationFrameID | null = null;
    
  45. let isEnabled: boolean = false;
    
  46. let redrawTimeoutID: TimeoutID | null = null;
    
  47. 
    
  48. export function initialize(injectedAgent: Agent): void {
    
  49.   agent = injectedAgent;
    
  50.   agent.addListener('traceUpdates', traceUpdates);
    
  51. }
    
  52. 
    
  53. export function toggleEnabled(value: boolean): void {
    
  54.   isEnabled = value;
    
  55. 
    
  56.   if (!isEnabled) {
    
  57.     nodeToData.clear();
    
  58. 
    
  59.     if (drawAnimationFrameID !== null) {
    
  60.       cancelAnimationFrame(drawAnimationFrameID);
    
  61.       drawAnimationFrameID = null;
    
  62.     }
    
  63. 
    
  64.     if (redrawTimeoutID !== null) {
    
  65.       clearTimeout(redrawTimeoutID);
    
  66.       redrawTimeoutID = null;
    
  67.     }
    
  68. 
    
  69.     destroyCanvas(agent);
    
  70.   }
    
  71. }
    
  72. 
    
  73. function traceUpdates(nodes: Set<NativeType>): void {
    
  74.   if (!isEnabled) {
    
  75.     return;
    
  76.   }
    
  77. 
    
  78.   nodes.forEach(node => {
    
  79.     const data = nodeToData.get(node);
    
  80.     const now = getCurrentTime();
    
  81. 
    
  82.     let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0;
    
  83.     let rect = data != null ? data.rect : null;
    
  84.     if (rect === null || lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) {
    
  85.       lastMeasuredAt = now;
    
  86.       rect = measureNode(node);
    
  87.     }
    
  88. 
    
  89.     nodeToData.set(node, {
    
  90.       count: data != null ? data.count + 1 : 1,
    
  91.       expirationTime:
    
  92.         data != null
    
  93.           ? Math.min(
    
  94.               now + MAX_DISPLAY_DURATION,
    
  95.               data.expirationTime + DISPLAY_DURATION,
    
  96.             )
    
  97.           : now + DISPLAY_DURATION,
    
  98.       lastMeasuredAt,
    
  99.       rect,
    
  100.     });
    
  101.   });
    
  102. 
    
  103.   if (redrawTimeoutID !== null) {
    
  104.     clearTimeout(redrawTimeoutID);
    
  105.     redrawTimeoutID = null;
    
  106.   }
    
  107. 
    
  108.   if (drawAnimationFrameID === null) {
    
  109.     drawAnimationFrameID = requestAnimationFrame(prepareToDraw);
    
  110.   }
    
  111. }
    
  112. 
    
  113. function prepareToDraw(): void {
    
  114.   drawAnimationFrameID = null;
    
  115.   redrawTimeoutID = null;
    
  116. 
    
  117.   const now = getCurrentTime();
    
  118.   let earliestExpiration = Number.MAX_VALUE;
    
  119. 
    
  120.   // Remove any items that have already expired.
    
  121.   nodeToData.forEach((data, node) => {
    
  122.     if (data.expirationTime < now) {
    
  123.       nodeToData.delete(node);
    
  124.     } else {
    
  125.       earliestExpiration = Math.min(earliestExpiration, data.expirationTime);
    
  126.     }
    
  127.   });
    
  128. 
    
  129.   draw(nodeToData, agent);
    
  130. 
    
  131.   if (earliestExpiration !== Number.MAX_VALUE) {
    
  132.     redrawTimeoutID = setTimeout(prepareToDraw, earliestExpiration - now);
    
  133.   }
    
  134. }
    
  135. 
    
  136. function measureNode(node: Object): Rect | null {
    
  137.   if (!node || typeof node.getBoundingClientRect !== 'function') {
    
  138.     return null;
    
  139.   }
    
  140. 
    
  141.   const currentWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window;
    
  142. 
    
  143.   return getNestedBoundingClientRect(node, currentWindow);
    
  144. }