Skip to content

Commit

Permalink
refactor: DebuggingRegistry to handle trace updates (facebook#41744)
Browse files Browse the repository at this point in the history
Summary:

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
  • Loading branch information
Ruslan Lesiutin authored and facebook-github-bot committed Dec 6, 2023
1 parent 98bc5dc commit 85b04b7
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 146 deletions.

This file was deleted.

88 changes: 88 additions & 0 deletions packages/react-native/Libraries/Debugging/DebuggingOverlay.js
Original file line number Diff line number Diff line change
@@ -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<DebuggingOverlayHandle>,
): 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<React.ElementRef<
typeof DebuggingOverlayNativeComponent,
> | null>(null);

return (
isNativeComponentReady && (
<View pointerEvents="none" style={styles.overlay}>
<DebuggingOverlayNativeComponent
ref={nativeComponentRef}
style={styles.overlay}
/>
</View>
)
);
}

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;
102 changes: 101 additions & 1 deletion packages/react-native/Libraries/Debugging/DebuggingRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 DebuggingRegistrySubscriberProtocol = {
rootViewRef: AppContainerRootViewRef,
debuggingOverlayRef: DebuggingOverlayRef,
};

class DebuggingRegistry {
#registry: Set<DebuggingRegistrySubscriberProtocol> = new Set();
#reactDevToolsAgent: ReactDevToolsAgent | null = null;

constructor() {
if (reactDevToolsHook?.reactDevtoolsAgent != null) {
this.#onReactDevToolsAgentAttached(reactDevToolsHook.reactDevtoolsAgent);
return;
}

reactDevToolsHook?.on?.(
'react-devtools',
this.#onReactDevToolsAgentAttached,
);
}

subscribe(subscriber: DebuggingRegistrySubscriberProtocol) {
this.#registry.add(subscriber);
Expand All @@ -31,6 +63,74 @@ class DebuggingRegistry {
);
}
}

#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<Promise<Overlay>> = [];

traceUpdates.forEach(({node, color}) => {
const publicInstance = this.#getPublicInstanceFromInstance(node);

if (publicInstance == null) {
return;
}

const frameToDrawPromise = new Promise<Overlay>(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 debuggingRegistryInstance: DebuggingRegistry = new DebuggingRegistry();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@
* @oncall react_native
*/

import type {AppContainerRootViewRef} from '../ReactNative/AppContainer-dev';
import type {
AppContainerRootViewRef,
DebuggingOverlayRef,
} from '../ReactNative/AppContainer-dev';

import DebuggingRegistry from './DebuggingRegistry';
import {useEffect} from 'react';

const useSubscribeToDebuggingRegistry = (
rootViewRef: AppContainerRootViewRef,
debuggingOverlayRef: DebuggingOverlayRef,
) => {
useEffect(() => {
const subscriber = {rootViewRef};
const subscriber = {rootViewRef, debuggingOverlayRef};

DebuggingRegistry.subscribe(subscriber);
return () => DebuggingRegistry.unsubscribe(subscriber);
}, [rootViewRef]);
}, [rootViewRef, debuggingOverlayRef]);
};

export default useSubscribeToDebuggingRegistry;
Loading

0 comments on commit 85b04b7

Please sign in to comment.