main
js 164 lines 4.31 KB
Raw
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 {extractHOCNames, getNestedBoundingClientRect} from '../utils';
13
14 import type {HostInstance} 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 // Markers for different types of HOCs
28 const HOC_MARKERS = new Map([
29 ['Forget', ''],
30 ['Memo', '🧠'],
31 ]);
32
33 // Some environments (e.g. React Native / Hermes) don't support the performance API yet.
34 const getCurrentTime =
35 // $FlowFixMe[method-unbinding]
36 typeof performance === 'object' && typeof performance.now === 'function'
37 ? () => performance.now()
38 : () => Date.now();
39
40 export type Data = {
41 count: number,
42 expirationTime: number,
43 lastMeasuredAt: number,
44 rect: Rect | null,
45 displayName: string | null,
46 };
47
48 const nodeToData: Map<HostInstance, Data> = new Map();
49
50 let agent: Agent = null as any as Agent;
51 let drawAnimationFrameID: AnimationFrameID | null = null;
52 let isEnabled: boolean = false;
53 let redrawTimeoutID: TimeoutID | null = null;
54
55 export function initialize(injectedAgent: Agent): void {
56 agent = injectedAgent;
57 agent.addListener('traceUpdates', traceUpdates);
58 }
59
60 export function toggleEnabled(value: boolean): void {
61 isEnabled = value;
62
63 if (!isEnabled) {
64 nodeToData.clear();
65
66 if (drawAnimationFrameID !== null) {
67 cancelAnimationFrame(drawAnimationFrameID);
68 drawAnimationFrameID = null;
69 }
70
71 if (redrawTimeoutID !== null) {
72 clearTimeout(redrawTimeoutID);
73 redrawTimeoutID = null;
74 }
75
76 destroyCanvas(agent);
77 }
78 }
79
80 function traceUpdates(nodes: Set<HostInstance>): void {
81 if (!isEnabled) return;
82
83 nodes.forEach(node => {
84 const data = nodeToData.get(node);
85 const now = getCurrentTime();
86
87 let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0;
88 let rect = data != null ? data.rect : null;
89
90 if (rect === null || lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) {
91 lastMeasuredAt = now;
92 rect = measureNode(node);
93 }
94
95 let displayName = agent.getComponentNameForHostInstance(node);
96 if (displayName) {
97 const {baseComponentName, hocNames} = extractHOCNames(displayName);
98
99 const markers = hocNames.map(hoc => HOC_MARKERS.get(hoc) || '').join('');
100
101 const enhancedDisplayName = markers
102 ? `${markers}${baseComponentName}`
103 : baseComponentName;
104
105 displayName = enhancedDisplayName;
106 }
107
108 nodeToData.set(node, {
109 count: data != null ? data.count + 1 : 1,
110 expirationTime:
111 data != null
112 ? Math.min(
113 now + MAX_DISPLAY_DURATION,
114 data.expirationTime + DISPLAY_DURATION,
115 )
116 : now + DISPLAY_DURATION,
117 lastMeasuredAt,
118 rect,
119 displayName,
120 });
121 });
122
123 if (redrawTimeoutID !== null) {
124 clearTimeout(redrawTimeoutID);
125 redrawTimeoutID = null;
126 }
127
128 if (drawAnimationFrameID === null) {
129 drawAnimationFrameID = requestAnimationFrame(prepareToDraw);
130 }
131 }
132
133 function prepareToDraw(): void {
134 drawAnimationFrameID = null;
135 redrawTimeoutID = null;
136
137 const now = getCurrentTime();
138 let earliestExpiration = Number.MAX_VALUE;
139
140 // Remove any items that have already expired.
141 nodeToData.forEach((data, node) => {
142 if (data.expirationTime < now) {
143 nodeToData.delete(node);
144 } else {
145 earliestExpiration = Math.min(earliestExpiration, data.expirationTime);
146 }
147 });
148
149 draw(nodeToData, agent);
150
151 if (earliestExpiration !== Number.MAX_VALUE) {
152 redrawTimeoutID = setTimeout(prepareToDraw, earliestExpiration - now);
153 }
154 }
155
156 function measureNode(node: Object): Rect | null {
157 if (!node || typeof node.getBoundingClientRect !== 'function') {
158 return null;
159 }
160
161 const currentWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window;
162
163 return getNestedBoundingClientRect(node, currentWindow);
164 }