From d66c67718152e32975585f406e849411a9b09be1 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Wed, 8 May 2024 02:32:37 -0700 Subject: [PATCH] Complete conversion of modules.debug module (#44476) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44476 # Changelog: [Internal] - This is the last step to converting the whole `modules.debug` module to Kotlin. Differential Revision: D57095966 --- .../ReactAndroid/api/ReactAndroid.api | 3 +- .../DidJSUpdateUiDuringFrameDetector.java | 169 ------------------ .../debug/DidJSUpdateUiDuringFrameDetector.kt | 158 ++++++++++++++++ .../react/modules/debug/SourceCodeModule.java | 39 ---- .../react/modules/debug/SourceCodeModule.kt | 27 +++ 5 files changed, 186 insertions(+), 210 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.kt delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index b3a607c6d931fd..323c140269f4e3 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3219,9 +3219,8 @@ public final class com/facebook/react/modules/debug/FpsDebugFrameCallback$FpsInf public final fun getTotalTimeMs ()I } -public class com/facebook/react/modules/debug/SourceCodeModule : com/facebook/fbreact/specs/NativeSourceCodeSpec { +public final class com/facebook/react/modules/debug/SourceCodeModule : com/facebook/fbreact/specs/NativeSourceCodeSpec { public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V - protected fun getTypedExportedConstants ()Ljava/util/Map; } public abstract interface class com/facebook/react/modules/debug/interfaces/DeveloperSettings { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java deleted file mode 100644 index bda0972fae0ea5..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java +++ /dev/null @@ -1,169 +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. - */ - -package com.facebook.react.modules.debug; - -import android.view.Choreographer; -import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; -import com.facebook.react.bridge.ReactBridge; -import com.facebook.react.common.LongArray; -import com.facebook.react.uimanager.UIManagerModule; -import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener; - -/** - * Debug object that listens to bridge busy/idle events and UiManagerModule dispatches and uses it - * to calculate whether JS was able to update the UI during a given frame. After being installed on - * a {@link ReactBridge} and a {@link UIManagerModule}, {@link #getDidJSHitFrameAndCleanup} should - * be called once per frame via a {@link Choreographer.FrameCallback}. - */ -class DidJSUpdateUiDuringFrameDetector - implements NotThreadSafeBridgeIdleDebugListener, NotThreadSafeViewHierarchyUpdateDebugListener { - - private final LongArray mTransitionToIdleEvents = LongArray.createWithInitialCapacity(20); - private final LongArray mTransitionToBusyEvents = LongArray.createWithInitialCapacity(20); - private final LongArray mViewHierarchyUpdateEnqueuedEvents = - LongArray.createWithInitialCapacity(20); - private final LongArray mViewHierarchyUpdateFinishedEvents = - LongArray.createWithInitialCapacity(20); - private volatile boolean mWasIdleAtEndOfLastFrame = true; - - @Override - public synchronized void onTransitionToBridgeIdle() { - mTransitionToIdleEvents.add(System.nanoTime()); - } - - @Override - public synchronized void onTransitionToBridgeBusy() { - mTransitionToBusyEvents.add(System.nanoTime()); - } - - @Override - public synchronized void onBridgeDestroyed() { - // do nothing - } - - @Override - public synchronized void onViewHierarchyUpdateEnqueued() { - mViewHierarchyUpdateEnqueuedEvents.add(System.nanoTime()); - } - - @Override - public synchronized void onViewHierarchyUpdateFinished() { - mViewHierarchyUpdateFinishedEvents.add(System.nanoTime()); - } - - /** - * Designed to be called from a {@link Choreographer.FrameCallback#doFrame} call. - * - *

There are two 'success' cases that will cause {@link #getDidJSHitFrameAndCleanup} to return - * true for a given frame: - * - *

    - *
  1. UIManagerModule finished dispatching a batched UI update on the UI thread during the - * frame. This means that during the next hierarchy traversal, new UI will be drawn if - * needed (good). - *
  2. The bridge ended the frame idle (meaning there were no JS nor native module calls still - * in flight) AND there was no UiManagerModule update enqueued that didn't also finish. NB: - * if there was one enqueued that actually finished, we'd have case 1), so effectively we - * just look for whether one was enqueued. - *
- * - *

NB: This call can only be called once for a given frame time range because it cleans up - * events it recorded for that frame. - * - *

NB2: This makes the assumption that onViewHierarchyUpdateEnqueued is called from the {@link - * UIManagerModule#onBatchComplete()}, e.g. while the bridge is still considered busy, which means - * there is no race condition where the bridge has gone idle but a hierarchy update is waiting to - * be enqueued. - * - * @param frameStartTimeNanos the time in nanos that the last frame started - * @param frameEndTimeNanos the time in nanos that the last frame ended - */ - public synchronized boolean getDidJSHitFrameAndCleanup( - long frameStartTimeNanos, long frameEndTimeNanos) { - // Case 1: We dispatched a UI update - boolean finishedUiUpdate = - hasEventBetweenTimestamps( - mViewHierarchyUpdateFinishedEvents, frameStartTimeNanos, frameEndTimeNanos); - boolean didEndFrameIdle = didEndFrameIdle(frameStartTimeNanos, frameEndTimeNanos); - - boolean hitFrame; - if (finishedUiUpdate) { - hitFrame = true; - } else { - // Case 2: Ended idle but no UI was enqueued during that frame - hitFrame = - didEndFrameIdle - && !hasEventBetweenTimestamps( - mViewHierarchyUpdateEnqueuedEvents, frameStartTimeNanos, frameEndTimeNanos); - } - - cleanUp(mTransitionToIdleEvents, frameEndTimeNanos); - cleanUp(mTransitionToBusyEvents, frameEndTimeNanos); - cleanUp(mViewHierarchyUpdateEnqueuedEvents, frameEndTimeNanos); - cleanUp(mViewHierarchyUpdateFinishedEvents, frameEndTimeNanos); - - mWasIdleAtEndOfLastFrame = didEndFrameIdle; - - return hitFrame; - } - - private static boolean hasEventBetweenTimestamps( - LongArray eventArray, long startTime, long endTime) { - for (int i = 0; i < eventArray.size(); i++) { - long time = eventArray.get(i); - if (time >= startTime && time < endTime) { - return true; - } - } - return false; - } - - private static long getLastEventBetweenTimestamps( - LongArray eventArray, long startTime, long endTime) { - long lastEvent = -1; - for (int i = 0; i < eventArray.size(); i++) { - long time = eventArray.get(i); - if (time >= startTime && time < endTime) { - lastEvent = time; - } else if (time >= endTime) { - break; - } - } - return lastEvent; - } - - private boolean didEndFrameIdle(long startTime, long endTime) { - long lastIdleTransition = - getLastEventBetweenTimestamps(mTransitionToIdleEvents, startTime, endTime); - long lastBusyTransition = - getLastEventBetweenTimestamps(mTransitionToBusyEvents, startTime, endTime); - - if (lastIdleTransition == -1 && lastBusyTransition == -1) { - return mWasIdleAtEndOfLastFrame; - } - - return lastIdleTransition > lastBusyTransition; - } - - private static void cleanUp(LongArray eventArray, long endTime) { - int size = eventArray.size(); - int indicesToRemove = 0; - for (int i = 0; i < size; i++) { - if (eventArray.get(i) < endTime) { - indicesToRemove++; - } - } - - if (indicesToRemove > 0) { - for (int i = 0; i < size - indicesToRemove; i++) { - eventArray.set(i, eventArray.get(i + indicesToRemove)); - } - eventArray.dropTail(indicesToRemove); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.kt new file mode 100644 index 00000000000000..01424ae7d3bc76 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.kt @@ -0,0 +1,158 @@ +/* + * 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.modules.debug + +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener +import com.facebook.react.common.LongArray +import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener + +/** + * Debug object that listens to bridge busy/idle events and UiManagerModule dispatches and uses it + * to calculate whether JS was able to update the UI during a given frame. After being installed on + * a [ReactBridge] and a [UIManagerModule], [.getDidJSHitFrameAndCleanup] should be called once per + * frame via a [Choreographer.FrameCallback]. + */ +internal class DidJSUpdateUiDuringFrameDetector : + NotThreadSafeBridgeIdleDebugListener, NotThreadSafeViewHierarchyUpdateDebugListener { + private val transitionToIdleEvents = LongArray.createWithInitialCapacity(20) + private val transitionToBusyEvents = LongArray.createWithInitialCapacity(20) + private val viewHierarchyUpdateEnqueuedEvents = LongArray.createWithInitialCapacity(20) + private val viewHierarchyUpdateFinishedEvents = LongArray.createWithInitialCapacity(20) + @Volatile private var wasIdleAtEndOfLastFrame = true + + @Synchronized + override fun onTransitionToBridgeIdle() { + transitionToIdleEvents.add(System.nanoTime()) + } + + @Synchronized + override fun onTransitionToBridgeBusy() { + transitionToBusyEvents.add(System.nanoTime()) + } + + @Synchronized + override fun onBridgeDestroyed() { + // do nothing + } + + @Synchronized + override fun onViewHierarchyUpdateEnqueued() { + viewHierarchyUpdateEnqueuedEvents.add(System.nanoTime()) + } + + @Synchronized + override fun onViewHierarchyUpdateFinished() { + viewHierarchyUpdateFinishedEvents.add(System.nanoTime()) + } + + /** + * Designed to be called from a [Choreographer.FrameCallback.doFrame] call. + * + * There are two 'success' cases that will cause [.getDidJSHitFrameAndCleanup] to return true for + * a given frame: + * 1. UIManagerModule finished dispatching a batched UI update on the UI thread during the frame. + * This means that during the next hierarchy traversal, new UI will be drawn if needed (good). + * 1. The bridge ended the frame idle (meaning there were no JS nor native module calls still in + * flight) AND there was no UiManagerModule update enqueued that didn't also finish. NB: if + * there was one enqueued that actually finished, we'd have case 1), so effectively we just + * look for whether one was enqueued. + * + * NB: This call can only be called once for a given frame time range because it cleans up events + * it recorded for that frame. + * + * NB2: This makes the assumption that onViewHierarchyUpdateEnqueued is called from the + * [ ][UIManagerModule.onBatchComplete], e.g. while the bridge is still considered busy, which + * means there is no race condition where the bridge has gone idle but a hierarchy update is + * waiting to be enqueued. + * + * @param frameStartTimeNanos the time in nanos that the last frame started + * @param frameEndTimeNanos the time in nanos that the last frame ended + */ + @Synchronized + fun getDidJSHitFrameAndCleanup(frameStartTimeNanos: Long, frameEndTimeNanos: Long): Boolean { + // Case 1: We dispatched a UI update + val finishedUiUpdate = + hasEventBetweenTimestamps( + viewHierarchyUpdateFinishedEvents, frameStartTimeNanos, frameEndTimeNanos) + val didEndFrameIdle = didEndFrameIdle(frameStartTimeNanos, frameEndTimeNanos) + val hitFrame = + if (finishedUiUpdate) { + true + } else { + // Case 2: Ended idle but no UI was enqueued during that frame + (didEndFrameIdle && + !hasEventBetweenTimestamps( + viewHierarchyUpdateEnqueuedEvents, frameStartTimeNanos, frameEndTimeNanos)) + } + cleanUp(transitionToIdleEvents, frameEndTimeNanos) + cleanUp(transitionToBusyEvents, frameEndTimeNanos) + cleanUp(viewHierarchyUpdateEnqueuedEvents, frameEndTimeNanos) + cleanUp(viewHierarchyUpdateFinishedEvents, frameEndTimeNanos) + wasIdleAtEndOfLastFrame = didEndFrameIdle + return hitFrame + } + + private fun didEndFrameIdle(startTime: Long, endTime: Long): Boolean { + val lastIdleTransition = + getLastEventBetweenTimestamps(transitionToIdleEvents, startTime, endTime) + val lastBusyTransition = + getLastEventBetweenTimestamps(transitionToBusyEvents, startTime, endTime) + return if (lastIdleTransition == -1L && lastBusyTransition == -1L) { + wasIdleAtEndOfLastFrame + } else lastIdleTransition > lastBusyTransition + } + + companion object { + private fun hasEventBetweenTimestamps( + eventArray: LongArray, + startTime: Long, + endTime: Long + ): Boolean { + for (i in 0 until eventArray.size()) { + val time = eventArray[i] + if (time in startTime until endTime) { + return true + } + } + return false + } + + private fun getLastEventBetweenTimestamps( + eventArray: LongArray, + startTime: Long, + endTime: Long + ): Long { + var lastEvent: Long = -1 + for (i in 0 until eventArray.size()) { + val time = eventArray[i] + if (time in startTime until endTime) { + lastEvent = time + } else if (time >= endTime) { + break + } + } + return lastEvent + } + + private fun cleanUp(eventArray: LongArray, endTime: Long) { + val size = eventArray.size() + var indicesToRemove = 0 + for (i in 0 until size) { + if (eventArray[i] < endTime) { + indicesToRemove++ + } + } + if (indicesToRemove > 0) { + for (i in 0 until size - indicesToRemove) { + eventArray[i] = eventArray[i + indicesToRemove] + } + eventArray.dropTail(indicesToRemove) + } + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java deleted file mode 100644 index 137791b0ecbc72..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java +++ /dev/null @@ -1,39 +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. - */ - -package com.facebook.react.modules.debug; - -import com.facebook.fbreact.specs.NativeSourceCodeSpec; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.module.annotations.ReactModule; -import java.util.HashMap; -import java.util.Map; - -/** - * Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS - */ -@ReactModule(name = NativeSourceCodeSpec.NAME) -public class SourceCodeModule extends NativeSourceCodeSpec { - - public SourceCodeModule(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - protected Map getTypedExportedConstants() { - HashMap constants = new HashMap<>(); - - String sourceURL = - Assertions.assertNotNull( - getReactApplicationContext().getSourceURL(), - "No source URL loaded, have you initialised the instance?"); - - constants.put("scriptURL", sourceURL); - return constants; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.kt new file mode 100644 index 00000000000000..d3c03e782e24ed --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.kt @@ -0,0 +1,27 @@ +/* + * 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.modules.debug + +import com.facebook.fbreact.specs.NativeSourceCodeSpec +import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule + +/** + * Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS + */ +@ReactModule(name = NativeSourceCodeSpec.NAME) +public class SourceCodeModule(reactContext: ReactApplicationContext) : + NativeSourceCodeSpec(reactContext) { + override protected fun getTypedExportedConstants(): Map = + mapOf( + "scriptURL" to + Assertions.assertNotNull( + getReactApplicationContext().getSourceURL(), + "No source URL loaded, have you initialised the instance?")) +}