/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import Agent from 'react-devtools-shared/src/backend/agent';
import {destroy as destroyCanvas, draw} from './canvas';
import {getNestedBoundingClientRect} from '../utils';
import type {NativeType} from '../../types';
import type {Rect} from '../utils';
// How long the rect should be shown for?
const DISPLAY_DURATION = 250;
// What's the longest we are willing to show the overlay for?
// This can be important if we're getting a flurry of events (e.g. scroll update).
const MAX_DISPLAY_DURATION = 3000;
// How long should a rect be considered valid for?
const REMEASUREMENT_AFTER_DURATION = 250;
// Some environments (e.g. React Native / Hermes) don't support the performance API yet.
const getCurrentTime =
// $FlowFixMe[method-unbinding]
typeof performance === 'object' && typeof performance.now === 'function'
? () => performance.now()
: () => Date.now();
export type Data = {
count: number,
expirationTime: number,
lastMeasuredAt: number,
rect: Rect | null,
};
const nodeToData: Map<NativeType, Data> = new Map();
let agent: Agent = ((null: any): Agent);
let drawAnimationFrameID: AnimationFrameID | null = null;
let isEnabled: boolean = false;
let redrawTimeoutID: TimeoutID | null = null;
export function initialize(injectedAgent: Agent): void {
agent = injectedAgent;
agent.addListener('traceUpdates', traceUpdates);
}
export function toggleEnabled(value: boolean): void {
isEnabled = value;
if (!isEnabled) {
nodeToData.clear();
if (drawAnimationFrameID !== null) {
cancelAnimationFrame(drawAnimationFrameID);
drawAnimationFrameID = null;
}
if (redrawTimeoutID !== null) {
clearTimeout(redrawTimeoutID);
redrawTimeoutID = null;
}
destroyCanvas(agent);
}
}
function traceUpdates(nodes: Set<NativeType>): void {
if (!isEnabled) {
return;
}
nodes.forEach(node => {
const data = nodeToData.get(node);
const now = getCurrentTime();
let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0;
let rect = data != null ? data.rect : null;
if (rect === null || lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) {
lastMeasuredAt = now;
rect = measureNode(node);
}
nodeToData.set(node, {
count: data != null ? data.count + 1 : 1,
expirationTime:
data != null
? Math.min(
now + MAX_DISPLAY_DURATION,
data.expirationTime + DISPLAY_DURATION,
)
: now + DISPLAY_DURATION,
lastMeasuredAt,
rect,
});
});
if (redrawTimeoutID !== null) {
clearTimeout(redrawTimeoutID);
redrawTimeoutID = null;
}
if (drawAnimationFrameID === null) {
drawAnimationFrameID = requestAnimationFrame(prepareToDraw);
}
}
function prepareToDraw(): void {
drawAnimationFrameID = null;
redrawTimeoutID = null;
const now = getCurrentTime();
let earliestExpiration = Number.MAX_VALUE;
// Remove any items that have already expired.
nodeToData.forEach((data, node) => {
if (data.expirationTime < now) {
nodeToData.delete(node);
} else {
earliestExpiration = Math.min(earliestExpiration, data.expirationTime);
}
});
draw(nodeToData, agent);
if (earliestExpiration !== Number.MAX_VALUE) {
redrawTimeoutID = setTimeout(prepareToDraw, earliestExpiration - now);
}
}
function measureNode(node: Object): Rect | null {
if (!node || typeof node.getBoundingClientRect !== 'function') {
return null;
}
const currentWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window;
return getNestedBoundingClientRect(node, currentWindow);
}