From be255cbc16ad4409afecbc6d4b983b03c0740d77 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 15 Jan 2024 14:11:00 -0800 Subject: [PATCH] refactor: DebuggingOverlayRegistry to handle trace updates (#41744) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/41744 Changelog: [Internal] With these changes: - DebuggingRegitry is responsible for listening to the events from React DevTools and - AppContainer renders DebuggingOverlay component and subscribes with its reference to the DebuggingRegistry - [Improvement] Since DebuggingRegistry is a singleton, it will only subscribe to the React DevTools events once and not *number-of-rendered-AppContainers* times All required functionality for highlighting elements on a single AppContainer will be added in one of the next diffs of this stack, changes are incremental. Reviewed By: sammy-SC Differential Revision: D51603860 fbshipit-source-id: 92b029eb54ef63b27af970770eb522915578a0b9 --- .../TraceUpdateOverlay/TraceUpdateOverlay.js | 137 ------------------ .../Libraries/Debugging/DebuggingOverlay.js | 88 +++++++++++ .../Debugging/DebuggingOverlayRegistry.js | 102 ++++++++++++- .../useSubscribeToDebuggingOverlayRegistry.js | 10 +- .../Libraries/ReactNative/AppContainer-dev.js | 18 ++- .../__snapshots__/public-api-test.js.snap | 29 ++-- 6 files changed, 229 insertions(+), 155 deletions(-) delete mode 100644 packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js create mode 100644 packages/react-native/Libraries/Debugging/DebuggingOverlay.js diff --git a/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js b/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js deleted file mode 100644 index 94ded259e12..00000000000 --- a/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * 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 strict-local - * @format - */ - -import type {Overlay} from '../../Debugging/DebuggingOverlayNativeComponent'; -import type { - InstanceFromReactDevTools, - ReactDevToolsAgent, -} from '../../Types/ReactDevToolsTypes'; - -import DebuggingOverlayNativeComponent, { - Commands, -} from '../../Debugging/DebuggingOverlayNativeComponent'; -import UIManager from '../../ReactNative/UIManager'; -import processColor from '../../StyleSheet/processColor'; -import StyleSheet from '../../StyleSheet/StyleSheet'; -import View from '../View/View'; -import * as React from 'react'; - -const {useEffect, useRef, useState} = React; -const isNativeComponentReady = - UIManager.hasViewManagerConfig('DebuggingOverlay'); - -type Props = { - reactDevToolsAgent: ReactDevToolsAgent, -}; - -export default function TraceUpdateOverlay({ - reactDevToolsAgent, -}: Props): React.Node { - const [overlayDisabled, setOverlayDisabled] = useState(false); - - useEffect(() => { - const drawTraceUpdates = ( - nodesToDraw: Array<{node: InstanceFromReactDevTools, color: string}> = [], - ) => { - if (!isNativeComponentReady) { - return; - } - - // If overlay is disabled before, now it's enabled. - setOverlayDisabled(false); - - const newFramesToDraw: Array> = []; - nodesToDraw.forEach(({node, color}) => { - // `canonical.publicInstance` => Fabric - // TODO: remove this check when syncing the new version of the renderer from React to React Native. - // `canonical` => Legacy Fabric - // `node` => Legacy renderer - const component = - (node.canonical && node.canonical.publicInstance) ?? - node.canonical ?? - node; - if (!component || !component.measure) { - return; - } - const frameToDrawPromise = new Promise(resolve => { - // The if statement here is to make flow happy - if (component.measure) { - // TODO(T145522797): We should refactor this to use `getBoundingClientRect` when Paper is no longer supported. - component.measure((x, y, width, height, left, top) => { - resolve({ - rect: {left, top, width, height}, - color: processColor(color), - }); - }); - } - }); - newFramesToDraw.push(frameToDrawPromise); - }); - Promise.all(newFramesToDraw).then( - results => { - if (nativeComponentRef.current != null) { - Commands.draw( - nativeComponentRef.current, - JSON.stringify( - results.filter( - ({rect, color}) => rect.width >= 0 && rect.height >= 0, - ), - ), - ); - } - }, - err => { - console.error(`Failed to measure updated traces. Error: ${err}`); - }, - ); - }; - - const disableTraceUpdates = () => { - // When trace updates are disabled from the backend, we won't receive draw events until it's enabled by the next draw. We can safely remove the overlay as it's not needed now. - setOverlayDisabled(true); - }; - - reactDevToolsAgent.addListener('drawTraceUpdates', drawTraceUpdates); - reactDevToolsAgent.addListener('disableTraceUpdates', drawTraceUpdates); - - return () => { - reactDevToolsAgent.removeListener('drawTraceUpdates', drawTraceUpdates); - reactDevToolsAgent.removeListener( - 'disableTraceUpdates', - disableTraceUpdates, - ); - }; - }, [reactDevToolsAgent]); - - const nativeComponentRef = - useRef>(null); - - return ( - !overlayDisabled && - isNativeComponentReady && ( - - - - ) - ); -} - -const styles = StyleSheet.create({ - overlay: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - }, -}); diff --git a/packages/react-native/Libraries/Debugging/DebuggingOverlay.js b/packages/react-native/Libraries/Debugging/DebuggingOverlay.js new file mode 100644 index 00000000000..8d834145d09 --- /dev/null +++ b/packages/react-native/Libraries/Debugging/DebuggingOverlay.js @@ -0,0 +1,88 @@ +/** + * 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 strict-local + * @format + */ + +import type {Overlay} from './DebuggingOverlayNativeComponent'; + +import View from '../Components/View/View'; +import UIManager from '../ReactNative/UIManager'; +import StyleSheet from '../StyleSheet/StyleSheet'; +import DebuggingOverlayNativeComponent, { + Commands, +} from './DebuggingOverlayNativeComponent'; +import * as React from 'react'; + +const {useRef, useImperativeHandle} = React; +const isNativeComponentReady = + UIManager.hasViewManagerConfig('DebuggingOverlay'); + +type DebuggingOverlayHandle = { + highlightTraceUpdates(updates: Overlay[]): void, +}; + +function DebuggingOverlay( + _props: {}, + ref: React.RefSetter, +): React.Node { + useImperativeHandle( + ref, + () => ({ + highlightTraceUpdates(updates) { + if (!isNativeComponentReady) { + return; + } + + const nonEmptyRectangles = updates.filter( + ({rect, color}) => rect.width >= 0 && rect.height >= 0, + ); + + if (nativeComponentRef.current != null) { + Commands.draw( + nativeComponentRef.current, + JSON.stringify(nonEmptyRectangles), + ); + } + }, + }), + [], + ); + + const nativeComponentRef = useRef | null>(null); + + return ( + isNativeComponentReady && ( + + + + ) + ); +} + +const styles = StyleSheet.create({ + overlay: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, +}); + +const DebuggingOverlayWithForwardedRef: React.AbstractComponent< + {}, + DebuggingOverlayHandle, + React.Node, +> = React.forwardRef(DebuggingOverlay); + +export default DebuggingOverlayWithForwardedRef; diff --git a/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js b/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js index fd858f928de..d888ea731c9 100644 --- a/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js +++ b/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js @@ -9,14 +9,46 @@ * @oncall react_native */ -import type {AppContainerRootViewRef} from '../ReactNative/AppContainer-dev'; +import type { + AppContainerRootViewRef, + DebuggingOverlayRef, +} from '../ReactNative/AppContainer-dev'; +import type {NativeMethods} from '../Renderer/shims/ReactNativeTypes'; +import type { + InstanceFromReactDevTools, + ReactDevToolsAgent, + ReactDevToolsAgentEvents, + ReactDevToolsGlobalHook, +} from '../Types/ReactDevToolsTypes'; +import type {Overlay} from './DebuggingOverlayNativeComponent'; + +import processColor from '../StyleSheet/processColor'; + +// TODO(T171193075): __REACT_DEVTOOLS_GLOBAL_HOOK__ is always injected in dev-bundles, +// but it is not mocked in some Jest tests. We should update Jest tests setup, so it would be the same as expected testing environment. +const reactDevToolsHook: ?ReactDevToolsGlobalHook = + window.__REACT_DEVTOOLS_GLOBAL_HOOK__; export type DebuggingOverlayRegistrySubscriberProtocol = { rootViewRef: AppContainerRootViewRef, + debuggingOverlayRef: DebuggingOverlayRef, }; class DebuggingOverlayRegistry { #registry: Set = new Set(); + #reactDevToolsAgent: ReactDevToolsAgent | null = null; + + constructor() { + if (reactDevToolsHook?.reactDevtoolsAgent != null) { + this.#onReactDevToolsAgentAttached(reactDevToolsHook.reactDevtoolsAgent); + return; + } + + reactDevToolsHook?.on?.( + 'react-devtools', + this.#onReactDevToolsAgentAttached, + ); + } subscribe(subscriber: DebuggingOverlayRegistrySubscriberProtocol) { this.#registry.add(subscriber); @@ -31,6 +63,74 @@ class DebuggingOverlayRegistry { ); } } + + #onReactDevToolsAgentAttached = (agent: ReactDevToolsAgent): void => { + this.#reactDevToolsAgent = agent; + + agent.addListener('drawTraceUpdates', this.#onDrawTraceUpdates); + }; + + #getPublicInstanceFromInstance( + instanceHandle: InstanceFromReactDevTools, + ): NativeMethods | null { + // `canonical.publicInstance` => Fabric + if (instanceHandle.canonical?.publicInstance != null) { + return instanceHandle.canonical?.publicInstance; + } + + // `canonical` => Legacy Fabric + if (instanceHandle.canonical != null) { + // $FlowFixMe[incompatible-return] + return instanceHandle.canonical; + } + + // `instanceHandle` => Legacy renderer + if (instanceHandle.measure != null) { + // $FlowFixMe[incompatible-return] + return instanceHandle; + } + + return null; + } + + #onDrawTraceUpdates: ( + ...ReactDevToolsAgentEvents['drawTraceUpdates'] + ) => void = traceUpdates => { + const promisesToResolve: Array> = []; + + traceUpdates.forEach(({node, color}) => { + const publicInstance = this.#getPublicInstanceFromInstance(node); + + if (publicInstance == null) { + return; + } + + const frameToDrawPromise = new Promise(resolve => { + // TODO(T171095283): We should refactor this to use `getBoundingClientRect` when Paper is no longer supported. + publicInstance.measure((x, y, width, height, left, top) => { + resolve({ + rect: {left, top, width, height}, + color: processColor(color), + }); + }); + }); + + promisesToResolve.push(frameToDrawPromise); + }); + + Promise.all(promisesToResolve).then( + updates => { + for (const subscriber of this.#registry) { + subscriber.debuggingOverlayRef.current?.highlightTraceUpdates( + updates, + ); + } + }, + err => { + console.error(`Failed to measure updated traces. Error: ${err}`); + }, + ); + }; } const debuggingOverlayRegistryInstance: DebuggingOverlayRegistry = diff --git a/packages/react-native/Libraries/Debugging/useSubscribeToDebuggingOverlayRegistry.js b/packages/react-native/Libraries/Debugging/useSubscribeToDebuggingOverlayRegistry.js index eae9392263c..931a2583fa5 100644 --- a/packages/react-native/Libraries/Debugging/useSubscribeToDebuggingOverlayRegistry.js +++ b/packages/react-native/Libraries/Debugging/useSubscribeToDebuggingOverlayRegistry.js @@ -9,20 +9,24 @@ * @oncall react_native */ -import type {AppContainerRootViewRef} from '../ReactNative/AppContainer-dev'; +import type { + AppContainerRootViewRef, + DebuggingOverlayRef, +} from '../ReactNative/AppContainer-dev'; import DebuggingOverlayRegistry from './DebuggingOverlayRegistry'; import {useEffect} from 'react'; const useSubscribeToDebuggingOverlayRegistry = ( rootViewRef: AppContainerRootViewRef, + debuggingOverlayRef: DebuggingOverlayRef, ) => { useEffect(() => { - const subscriber = {rootViewRef}; + const subscriber = {rootViewRef, debuggingOverlayRef}; DebuggingOverlayRegistry.subscribe(subscriber); return () => DebuggingOverlayRegistry.unsubscribe(subscriber); - }, [rootViewRef]); + }, [rootViewRef, debuggingOverlayRef]); }; export default useSubscribeToDebuggingOverlayRegistry; diff --git a/packages/react-native/Libraries/ReactNative/AppContainer-dev.js b/packages/react-native/Libraries/ReactNative/AppContainer-dev.js index e8f0c67be49..05c97b5d44d 100644 --- a/packages/react-native/Libraries/ReactNative/AppContainer-dev.js +++ b/packages/react-native/Libraries/ReactNative/AppContainer-dev.js @@ -15,9 +15,9 @@ import type { } from '../Types/ReactDevToolsTypes'; import type {Props} from './AppContainer'; -import TraceUpdateOverlay from '../Components/TraceUpdateOverlay/TraceUpdateOverlay'; import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes'; import View from '../Components/View/View'; +import DebuggingOverlay from '../Debugging/DebuggingOverlay'; import useSubscribeToDebuggingOverlayRegistry from '../Debugging/useSubscribeToDebuggingOverlayRegistry'; import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; import ReactDevToolsOverlay from '../Inspector/ReactDevToolsOverlay'; @@ -76,7 +76,12 @@ const AppContainer = ({ }: Props): React.Node => { const appContainerRootViewRef: AppContainerRootViewRef = React.useRef(null); const innerViewRef: InspectedViewRef = React.useRef(null); - useSubscribeToDebuggingOverlayRegistry(appContainerRootViewRef); + const debuggingOverlayRef: DebuggingOverlayRef = React.useRef(null); + + useSubscribeToDebuggingOverlayRegistry( + appContainerRootViewRef, + debuggingOverlayRef, + ); const [key, setKey] = useState(0); const [shouldRenderInspector, setShouldRenderInspector] = useState(false); @@ -147,9 +152,8 @@ const AppContainer = ({ pointerEvents="box-none"> {innerView} - {reactDevToolsAgent != null && ( - - )} + + {reactDevToolsAgent != null && ( | null>; +export type DebuggingOverlayRef = React.RefObject | null>; + export default AppContainer; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 0ba4cbb86ae..1a16fa29ec5 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -3758,14 +3758,6 @@ declare module.exports: Touchable; " `; -exports[`public API should not change unintentionally Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js 1`] = ` -"type Props = { - reactDevToolsAgent: ReactDevToolsAgent, -}; -declare export default function TraceUpdateOverlay(Props): React.Node; -" -`; - exports[`public API should not change unintentionally Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js 1`] = ` "type NativeProps = $ReadOnly<{| ...ViewProps, @@ -4757,6 +4749,19 @@ declare export function createNodeList( " `; +exports[`public API should not change unintentionally Libraries/Debugging/DebuggingOverlay.js 1`] = ` +"type DebuggingOverlayHandle = { + highlightTraceUpdates(updates: Overlay[]): void, +}; +declare const DebuggingOverlayWithForwardedRef: React.AbstractComponent< + {}, + DebuggingOverlayHandle, + React.Node, +>; +declare export default typeof DebuggingOverlayWithForwardedRef; +" +`; + exports[`public API should not change unintentionally Libraries/Debugging/DebuggingOverlayNativeComponent.js 1`] = ` "type NativeProps = $ReadOnly<{| ...ViewProps, @@ -4780,8 +4785,10 @@ declare export default HostComponent; exports[`public API should not change unintentionally Libraries/Debugging/DebuggingOverlayRegistry.js 1`] = ` "export type DebuggingOverlayRegistrySubscriberProtocol = { rootViewRef: AppContainerRootViewRef, + debuggingOverlayRef: DebuggingOverlayRef, }; declare class DebuggingOverlayRegistry { + constructor(): void; subscribe(subscriber: DebuggingOverlayRegistrySubscriberProtocol): void; unsubscribe(subscriber: DebuggingOverlayRegistrySubscriberProtocol): void; } @@ -4792,7 +4799,8 @@ declare export default typeof debuggingOverlayRegistryInstance; exports[`public API should not change unintentionally Libraries/Debugging/useSubscribeToDebuggingOverlayRegistry.js 1`] = ` "declare const useSubscribeToDebuggingOverlayRegistry: ( - rootViewRef: AppContainerRootViewRef + rootViewRef: AppContainerRootViewRef, + debuggingOverlayRef: DebuggingOverlayRef ) => void; declare export default typeof useSubscribeToDebuggingOverlayRegistry; " @@ -7546,6 +7554,9 @@ export type AppContainerRootViewRef = React.RefObject | null>; +export type DebuggingOverlayRef = React.RefObject | null>; declare export default typeof AppContainer; " `;