main
js 253 lines 6.74 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 type {Data} from './index';
11 import type {Rect} from '../utils';
12 import type {HostInstance} from '../../types';
13 import type Agent from '../../agent';
14
15 import {isReactNativeEnvironment} from 'react-devtools-shared/src/backend/utils';
16
17 // Note these colors are in sync with DevTools Profiler chart colors.
18 const COLORS = [
19 '#37afa9',
20 '#63b19e',
21 '#80b393',
22 '#97b488',
23 '#abb67d',
24 '#beb771',
25 '#cfb965',
26 '#dfba57',
27 '#efbb49',
28 '#febc38',
29 ];
30
31 let canvas: HTMLCanvasElement | null = null;
32
33 function drawNative(nodeToData: Map<HostInstance, Data>, agent: Agent) {
34 const nodesToDraw = [];
35 iterateNodes(nodeToData, ({color, node}) => {
36 nodesToDraw.push({node, color});
37 });
38
39 agent.emit('drawTraceUpdates', nodesToDraw);
40
41 const mergedNodes = groupAndSortNodes(nodeToData);
42 agent.emit('drawGroupedTraceUpdatesWithNames', mergedNodes);
43 }
44
45 function drawWeb(nodeToData: Map<HostInstance, Data>) {
46 if (canvas === null) {
47 initialize();
48 }
49
50 const dpr = window.devicePixelRatio || 1;
51 const canvasFlow: HTMLCanvasElement = canvas as any as HTMLCanvasElement;
52 canvasFlow.width = window.innerWidth * dpr;
53 canvasFlow.height = window.innerHeight * dpr;
54 canvasFlow.style.width = `${window.innerWidth}px`;
55 canvasFlow.style.height = `${window.innerHeight}px`;
56
57 const context = canvasFlow.getContext('2d');
58 context.scale(dpr, dpr);
59
60 context.clearRect(0, 0, canvasFlow.width / dpr, canvasFlow.height / dpr);
61
62 const mergedNodes = groupAndSortNodes(nodeToData);
63
64 mergedNodes.forEach(group => {
65 drawGroupBorders(context, group);
66 drawGroupLabel(context, group);
67 });
68
69 if (canvas !== null) {
70 if (nodeToData.size === 0 && canvas.matches(':popover-open')) {
71 // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
72 // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
73 canvas.hidePopover();
74 return;
75 }
76 // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
77 if (canvas.matches(':popover-open')) {
78 // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
79 // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
80 canvas.hidePopover();
81 }
82 // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
83 // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
84 canvas.showPopover();
85 }
86 }
87
88 type GroupItem = {
89 rect: Rect,
90 color: string,
91 displayName: string | null,
92 count: number,
93 };
94
95 export type {GroupItem};
96
97 export function groupAndSortNodes(
98 nodeToData: Map<HostInstance, Data>,
99 ): Array<Array<GroupItem>> {
100 const positionGroups: Map<string, Array<GroupItem>> = new Map();
101
102 iterateNodes(nodeToData, ({rect, color, displayName, count}) => {
103 if (!rect) return;
104 const key = `${rect.left},${rect.top}`;
105 if (!positionGroups.has(key)) positionGroups.set(key, []);
106 positionGroups.get(key)?.push({rect, color, displayName, count});
107 });
108
109 return Array.from(positionGroups.values()).sort((groupA, groupB) => {
110 const maxCountA = Math.max(...groupA.map(item => item.count));
111 const maxCountB = Math.max(...groupB.map(item => item.count));
112 return maxCountA - maxCountB;
113 });
114 }
115
116 function drawGroupBorders(
117 context: CanvasRenderingContext2D,
118 group: Array<GroupItem>,
119 ) {
120 group.forEach(({color, rect}) => {
121 context.beginPath();
122 context.strokeStyle = color;
123 context.rect(rect.left, rect.top, rect.width - 1, rect.height - 1);
124 context.stroke();
125 });
126 }
127
128 function drawGroupLabel(
129 context: CanvasRenderingContext2D,
130 group: Array<GroupItem>,
131 ) {
132 const mergedName = group
133 .map(({displayName, count}) =>
134 displayName ? `${displayName}${count > 1 ? ` x${count}` : ''}` : '',
135 )
136 .filter(Boolean)
137 .join(', ');
138
139 if (mergedName) {
140 drawLabel(context, group[0].rect, mergedName, group[0].color);
141 }
142 }
143
144 export function draw(nodeToData: Map<HostInstance, Data>, agent: Agent): void {
145 return isReactNativeEnvironment()
146 ? drawNative(nodeToData, agent)
147 : drawWeb(nodeToData);
148 }
149
150 type DataWithColorAndNode = {
151 ...Data,
152 color: string,
153 node: HostInstance,
154 };
155
156 function iterateNodes(
157 nodeToData: Map<HostInstance, Data>,
158 execute: (data: DataWithColorAndNode) => void,
159 ) {
160 nodeToData.forEach((data, node) => {
161 const colorIndex = Math.min(COLORS.length - 1, data.count - 1);
162 const color = COLORS[colorIndex];
163 execute({
164 color,
165 node,
166 count: data.count,
167 displayName: data.displayName,
168 expirationTime: data.expirationTime,
169 lastMeasuredAt: data.lastMeasuredAt,
170 rect: data.rect,
171 });
172 });
173 }
174
175 function drawLabel(
176 context: CanvasRenderingContext2D,
177 rect: Rect,
178 text: string,
179 color: string,
180 ): void {
181 const {left, top} = rect;
182 context.font = '10px monospace';
183 context.textBaseline = 'middle';
184 context.textAlign = 'center';
185
186 const padding = 2;
187 const textHeight = 14;
188
189 const metrics = context.measureText(text);
190 const backgroundWidth = metrics.width + padding * 2;
191 const backgroundHeight = textHeight;
192 const labelX = left;
193 const labelY = top - backgroundHeight;
194
195 context.fillStyle = color;
196 context.fillRect(labelX, labelY, backgroundWidth, backgroundHeight);
197
198 context.fillStyle = '#000000';
199 context.fillText(
200 text,
201 labelX + backgroundWidth / 2,
202 labelY + backgroundHeight / 2,
203 );
204 }
205
206 function destroyNative(agent: Agent) {
207 agent.emit('disableTraceUpdates');
208 }
209
210 function destroyWeb() {
211 if (canvas !== null) {
212 if (canvas.matches(':popover-open')) {
213 // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
214 // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
215 canvas.hidePopover();
216 }
217
218 // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API and loses canvas nullability tracking
219 if (canvas.parentNode != null) {
220 // $FlowFixMe[incompatible-type]: Flow doesn't track that canvas is non-null here
221 canvas.parentNode.removeChild(canvas);
222 }
223 canvas = null;
224 }
225 }
226
227 export function destroy(agent: Agent): void {
228 return isReactNativeEnvironment() ? destroyNative(agent) : destroyWeb();
229 }
230
231 function initialize(): void {
232 canvas = window.document.createElement('canvas');
233 canvas.setAttribute('popover', 'manual');
234
235 // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
236 canvas.style.cssText = `
237 xx-background-color: red;
238 xx-opacity: 0.5;
239 bottom: 0;
240 left: 0;
241 pointer-events: none;
242 position: fixed;
243 right: 0;
244 top: 0;
245 background-color: transparent;
246 outline: none;
247 box-shadow: none;
248 border: none;
249 `;
250
251 const root = window.document.documentElement;
252 root.insertBefore(canvas, root.firstChild);
253 }