Skip to content

Commit

Permalink
Add TraceUpdateOverlay native component to render highlights on trace…
Browse files Browse the repository at this point in the history
… updates

Summary:
This diff adds `TraceUpdateOverlay` native component to render highlights when trace update is detected from React JS. This allows a highlight border to be rendered outside of the component with re-renders.

- Created `TraceUpdateOverlay` native component and added to the `DebugCorePackage`
- Added to C++ registry so it's compatible with Fabric
- Added to `AppContainer` for all RN apps when global devtools hook is available

Changelog:
[Android][Internal] - Add trace update overlay to show re-render highlights

Reviewed By: javache

Differential Revision: D42831719

fbshipit-source-id: 30c2e24859a316c27700270087a0d7779d7ad8ed
  • Loading branch information
Xin Chen authored and facebook-github-bot committed Feb 14, 2023
1 parent ceb1d0d commit 6ac88a4
Show file tree
Hide file tree
Showing 7 changed files with 442 additions and 1 deletion.
163 changes: 163 additions & 0 deletions Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* 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 './TraceUpdateOverlayNativeComponent';

import processColor from '../../StyleSheet/processColor';
import StyleSheet from '../../StyleSheet/StyleSheet';
import View from '../View/View';
import TraceUpdateOverlayNativeComponent, {
Commands,
} from './TraceUpdateOverlayNativeComponent';
import * as React from 'react';

type AgentEvents = {
drawTraceUpdates: [Array<{node: TraceNode, color: string}>],
disableTraceUpdates: [],
};

interface Agent {
addListener<Event: $Keys<AgentEvents>>(
event: Event,
listener: (...AgentEvents[Event]) => void,
): void;
removeListener(event: $Keys<AgentEvents>, listener: () => void): void;
}

type TraceNode = {
canonical?: TraceNode,
measure?: (
(
x: number,
y: number,
width: number,
height: number,
left: number,
top: number,
) => void,
) => void,
};

type ReactDevToolsGlobalHook = {
on: (eventName: string, (agent: Agent) => void) => void,
off: (eventName: string, (agent: Agent) => void) => void,
reactDevtoolsAgent: Agent,
};

const {useEffect, useRef, useState} = React;
const hook: ReactDevToolsGlobalHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
let devToolsAgent: ?Agent;

export default function TraceUpdateOverlay(): React.Node {
const [overlayDisabled, setOverlayDisabled] = useState(false);
// This effect is designed to be explictly shown here to avoid re-subscribe from the same
// overlay component.
useEffect(() => {
function attachToDevtools(agent: Agent) {
devToolsAgent = agent;
agent.addListener('drawTraceUpdates', onAgentDrawTraceUpdates);
agent.addListener('disableTraceUpdates', onAgentDisableTraceUpdates);
}

function subscribe() {
hook?.on('react-devtools', attachToDevtools);
if (hook?.reactDevtoolsAgent) {
attachToDevtools(hook.reactDevtoolsAgent);
}
}

function unsubscribe() {
hook?.off('react-devtools', attachToDevtools);
const agent = devToolsAgent;
if (agent != null) {
agent.removeListener('drawTraceUpdates', onAgentDrawTraceUpdates);
agent.removeListener('disableTraceUpdates', onAgentDisableTraceUpdates);
devToolsAgent = null;
}
}

function onAgentDrawTraceUpdates(
nodesToDraw: Array<{node: TraceNode, color: string}> = [],
) {
// If overlay is disabled before, now it's enabled.
setOverlayDisabled(false);

const newFramesToDraw: Array<Promise<Overlay>> = [];
nodesToDraw.forEach(({node, color}) => {
const component = node.canonical ?? node;
if (!component || !component.measure) {
return;
}
const frameToDrawPromise = new Promise<Overlay>(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}`);
},
);
}

function onAgentDisableTraceUpdates() {
// 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);
}

subscribe();
return unsubscribe;
}, []); // Only run once when the overlay initially rendered

const nativeComponentRef =
useRef<?React.ElementRef<typeof TraceUpdateOverlayNativeComponent>>(null);

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

const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* 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 {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import type {ViewProps} from '../View/ViewPropTypes';

import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
import codegenNativeComponent from '../../Utilities/codegenNativeComponent';
import * as React from 'react';

type NativeProps = $ReadOnly<{|
...ViewProps,
|}>;
export type TraceUpdateOverlayNativeComponentType = HostComponent<NativeProps>;
export type Overlay = {
rect: {left: number, top: number, width: number, height: number},
color: ?ProcessedColorValue,
};

interface NativeCommands {
+draw: (
viewRef: React.ElementRef<TraceUpdateOverlayNativeComponentType>,
// TODO(T144046177): Ideally we can pass array of Overlay, but currently
// Array type is not supported in RN codegen for building native commands.
overlays: string,
) => void;
}

export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['draw'],
});

export default (codegenNativeComponent<NativeProps>(
'TraceUpdateOverlay',
): HostComponent<NativeProps>);
1 change: 1 addition & 0 deletions ReactAndroid/src/main/java/com/facebook/react/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ rn_android_library(
react_native_target("java/com/facebook/react/turbomodule/core:core"),
react_native_target("java/com/facebook/react/turbomodule/core/interfaces:interfaces"),
react_native_target("java/com/facebook/react/views/imagehelper:imagehelper"),
react_native_target("java/com/facebook/react/views/traceupdateoverlay:traceupdateoverlay"),
],
exported_deps = [
react_native_target("java/com/facebook/react/modules/core:core"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

package com.facebook.react;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.ModuleSpec;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.devsupport.JSCHeapCapture;
Expand All @@ -15,8 +17,15 @@
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.views.traceupdateoverlay.TraceUpdateOverlayManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Provider;

/**
* Package defining core framework modules (e.g. UIManager). It should be used for modules that
Expand All @@ -27,7 +36,9 @@
nativeModules = {
JSCHeapCapture.class,
})
public class DebugCorePackage extends TurboReactPackage {
public class DebugCorePackage extends TurboReactPackage implements ViewManagerOnDemandReactPackage {
private @Nullable Map<String, ModuleSpec> mViewManagers;

public DebugCorePackage() {}

@Override
Expand Down Expand Up @@ -81,4 +92,45 @@ public Map<String, ReactModuleInfo> getReactModuleInfos() {
"No ReactModuleInfoProvider for DebugCorePackage$$ReactModuleInfoProvider", e);
}
}

private static void appendMap(
Map<String, ModuleSpec> map, String name, Provider<? extends NativeModule> provider) {
map.put(name, ModuleSpec.viewManagerSpec(provider));
}

/** @return a map of view managers that should be registered with {@link UIManagerModule} */
private Map<String, ModuleSpec> getViewManagersMap(final ReactApplicationContext reactContext) {
if (mViewManagers == null) {
Map<String, ModuleSpec> viewManagers = new HashMap<>();
appendMap(
viewManagers,
TraceUpdateOverlayManager.REACT_CLASS,
new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new TraceUpdateOverlayManager();
}
});

mViewManagers = viewManagers;
}
return mViewManagers;
}

@Override
public List<ModuleSpec> getViewManagers(ReactApplicationContext reactContext) {
return new ArrayList<>(getViewManagersMap(reactContext).values());
}

@Override
public Collection<String> getViewManagerNames(ReactApplicationContext reactContext) {
return getViewManagersMap(reactContext).keySet();
}

@Override
public @Nullable ViewManager createViewManager(
ReactApplicationContext reactContext, String viewManagerName) {
ModuleSpec spec = getViewManagersMap(reactContext).get(viewManagerName);
return spec != null ? (ViewManager) spec.getProvider().get() : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library")

oncall("react_native")

rn_android_library(
name = "traceupdateoverlay",
srcs = glob(["*.java"]),
autoglob = False,
labels = [
"pfh:ReactNative_CommonInfrastructurePlaceholder",
],
language = "JAVA",
visibility = [
"PUBLIC",
],
deps = [
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"),
react_native_dep("third-party/android/androidx:annotation"),
react_native_dep("third-party/android/androidx:core"),
react_native_dep("third-party/android/androidx:fragment"),
react_native_dep("third-party/android/androidx:legacy-support-core-utils"),
react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/module/annotations:annotations"),
react_native_target("java/com/facebook/react/uimanager:uimanager"),
react_native_target("java/com/facebook/react/uimanager/annotations:annotations"),
react_native_target("java/com/facebook/react/util:util"),
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.
*/

package com.facebook.react.views.traceupdateoverlay;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.view.View;
import androidx.annotation.UiThread;
import com.facebook.react.uimanager.PixelUtil;
import java.util.ArrayList;
import java.util.List;

public class TraceUpdateOverlay extends View {
private final Paint mOverlayPaint = new Paint();
private List<Overlay> mOverlays = new ArrayList<Overlay>();

public static class Overlay {
private final int mColor;
private final RectF mRect;

public Overlay(int color, RectF rect) {
mColor = color;
mRect = rect;
}

public int getColor() {
return mColor;
}

public RectF getPixelRect() {
return new RectF(
PixelUtil.toPixelFromDIP(mRect.left),
PixelUtil.toPixelFromDIP(mRect.top),
PixelUtil.toPixelFromDIP(mRect.right),
PixelUtil.toPixelFromDIP(mRect.bottom));
}
}

public TraceUpdateOverlay(Context context) {
super(context);
mOverlayPaint.setStyle(Paint.Style.STROKE);
mOverlayPaint.setStrokeWidth(6);
}

@UiThread
public void setOverlays(List<Overlay> overlays) {
mOverlays = overlays;
invalidate();
}

@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);

if (!mOverlays.isEmpty()) {
// Draw border outside of the given overlays to be aligned with web trace highlights
for (Overlay overlay : mOverlays) {
mOverlayPaint.setColor(overlay.getColor());
canvas.drawRect(overlay.getPixelRect(), mOverlayPaint);
}
}
}
}
Loading

0 comments on commit 6ac88a4

Please sign in to comment.