diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index af72c70b059b..fc10b5cf6604 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2233,15 +2233,6 @@ public abstract interface class com/facebook/react/devsupport/interfaces/StackFr public abstract fun toJSON ()Lorg/json/JSONObject; } -public final class com/facebook/react/devsupport/interfaces/TracingState : java/lang/Enum { - public static final field DISABLED Lcom/facebook/react/devsupport/interfaces/TracingState; - public static final field ENABLEDINBACKGROUNDMODE Lcom/facebook/react/devsupport/interfaces/TracingState; - public static final field ENABLEDINCDPMODE Lcom/facebook/react/devsupport/interfaces/TracingState; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/devsupport/interfaces/TracingState; - public static fun values ()[Lcom/facebook/react/devsupport/interfaces/TracingState; -} - public final class com/facebook/react/fabric/ComponentFactory { public fun ()V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt index e0a56992ec17..d6e2c1a06b55 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt @@ -10,12 +10,12 @@ package com.facebook.react.devsupport import android.content.Context import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.common.SurfaceDelegateFactory +import com.facebook.react.devsupport.inspector.TracingState import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener import com.facebook.react.devsupport.interfaces.DevLoadingViewManager import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager import com.facebook.react.devsupport.interfaces.RedBoxHandler -import com.facebook.react.devsupport.interfaces.TracingState import com.facebook.react.packagerconnection.RequestHandler /** diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt index bf68f229322a..87d3cd6852c9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt @@ -52,6 +52,8 @@ import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener import com.facebook.react.devsupport.InspectorFlags.getFuseboxEnabled import com.facebook.react.devsupport.StackTraceHelper.convertJavaStackTrace import com.facebook.react.devsupport.StackTraceHelper.convertJsStackTrace +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateProvider import com.facebook.react.devsupport.interfaces.BundleLoadCallback import com.facebook.react.devsupport.interfaces.DebuggerFrontendPanelName import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener @@ -66,8 +68,6 @@ import com.facebook.react.devsupport.interfaces.PackagerStatusCallback import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager import com.facebook.react.devsupport.interfaces.RedBoxHandler import com.facebook.react.devsupport.interfaces.StackFrame -import com.facebook.react.devsupport.interfaces.TracingState -import com.facebook.react.devsupport.interfaces.TracingStateProvider import com.facebook.react.devsupport.perfmonitor.PerfMonitorDevHelper import com.facebook.react.devsupport.perfmonitor.PerfMonitorOverlayManager import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingState.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingState.kt similarity index 70% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingState.kt rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingState.kt index 91a7b73ea231..9683c8b48a54 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingState.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingState.kt @@ -5,14 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.devsupport.interfaces +package com.facebook.react.devsupport.inspector import com.facebook.proguard.annotations.DoNotStripAny -// Keep in sync with `TracingState.h` -// JNI wrapper for `jsinspector_modern::Tracing::TracingState`. @DoNotStripAny -public enum class TracingState { +internal enum class TracingState { DISABLED, // There is no active trace ENABLEDINBACKGROUNDMODE, // Trace is currently running in background mode ENABLEDINCDPMODE, // Trace is currently running in CDP mode diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateListener.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateListener.kt new file mode 100644 index 000000000000..0f54753a42da --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateListener.kt @@ -0,0 +1,15 @@ +/* + * 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.devsupport.inspector + +import com.facebook.proguard.annotations.DoNotStripAny + +@DoNotStripAny +internal fun interface TracingStateListener { + public fun onStateChanged(state: TracingState, screenshotsEnabled: Boolean) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingStateProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateProvider.kt similarity index 84% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingStateProvider.kt rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateProvider.kt index 1e9b2fead9d0..b9e7e53964ac 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingStateProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateProvider.kt @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.devsupport.interfaces +package com.facebook.react.devsupport.inspector internal interface TracingStateProvider { fun getTracingState(): TracingState diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt index 6df3bdeda737..dcd48ffb7b28 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt @@ -7,7 +7,7 @@ package com.facebook.react.devsupport.perfmonitor -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState /** * [Experimental] Interface implemented by [com.facebook.react.runtime.ReactHostInspectorTarget] diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt index 8e506f5f1a90..69b1e8eea141 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt @@ -10,7 +10,7 @@ package com.facebook.react.devsupport.perfmonitor import android.os.Handler import android.os.Looper import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState internal class PerfMonitorOverlayManager( private val devHelper: PerfMonitorDevHelper, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt index 3dbc7a89f41f..e33f037bbeb3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt @@ -21,7 +21,7 @@ import android.widget.TextView import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.facebook.react.R -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.uimanager.PixelUtil diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt index 417064fbd1f1..0da5d115fafc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt @@ -7,7 +7,7 @@ package com.facebook.react.devsupport.perfmonitor -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState /** [Experimental] An interface for subscribing to updates for the V2 Perf Monitor. */ internal interface PerfMonitorUpdateListener { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt index cc4598e7c98f..b61095904d47 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt @@ -16,8 +16,8 @@ import com.facebook.react.bridge.ReactContext import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.devsupport.ReactInstanceDevHelper -import com.facebook.react.devsupport.interfaces.TracingState -import com.facebook.react.devsupport.interfaces.TracingStateProvider +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateProvider import com.facebook.react.devsupport.perfmonitor.PerfMonitorDevHelper import com.facebook.react.devsupport.perfmonitor.PerfMonitorInspectorTarget import com.facebook.react.interfaces.TaskInterface diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt index 07397b85cf0d..22ac0d8b7ce9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt @@ -12,7 +12,8 @@ import com.facebook.proguard.annotations.DoNotStripAny import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.common.annotations.UnstableReactNativeAPI -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateListener import com.facebook.react.devsupport.perfmonitor.PerfMonitorInspectorTarget import com.facebook.react.devsupport.perfmonitor.PerfMonitorUpdateListener import com.facebook.soloader.SoLoader @@ -41,11 +42,11 @@ internal class ReactHostInspectorTarget(reactHostImpl: ReactHostImpl) : external fun stopAndDiscardBackgroundTrace() - external fun tracingStateAsInt(): Int + external override fun getTracingState(): TracingState - override fun getTracingState(): TracingState { - return TracingState.entries[tracingStateAsInt()] - } + external fun registerTracingStateListener(listener: TracingStateListener): Long + + external fun unregisterTracingStateListener(subscriptionId: Long) override fun addPerfMonitorListener(listener: PerfMonitorUpdateListener) { perfMonitorListeners.add(listener) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp index ad4c53308356..d978d5c16ba3 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp @@ -16,6 +16,36 @@ using namespace facebook::jni; using namespace facebook::react::jsinspector_modern; namespace facebook::react { + +namespace { +jni::local_ref convertCPPTracingStateToJava( + TracingState tracingState) { + auto tracingStateClass = jni::findClassLocal( + "com/facebook/react/devsupport/inspector/TracingState"); + auto valueOfMethod = + tracingStateClass->getStaticMethod("valueOf"); + + switch (tracingState) { + case TracingState::Disabled: + return valueOfMethod( + tracingStateClass, jni::make_jstring("DISABLED").get()); + + case TracingState::EnabledInBackgroundMode: + return valueOfMethod( + tracingStateClass, + jni::make_jstring("ENABLEDINBACKGROUNDMODE").get()); + + case TracingState::EnabledInCDPMode: + return valueOfMethod( + tracingStateClass, jni::make_jstring("ENABLEDINCDPMODE").get()); + + default: + jni::throwNewJavaException( + "java/lang/IllegalStateException", "Unexpected new TracingState."); + } +} +} // namespace + JReactHostInspectorTarget::JReactHostInspectorTarget( alias_ref jobj, alias_ref reactHostImpl, @@ -29,7 +59,8 @@ JReactHostInspectorTarget::JReactHostInspectorTarget( std::function&& callback) mutable { auto jrunnable = JNativeRunnable::newObjectCxxArgs(std::move(callback)); javaExecutor->execute(jrunnable); - }) { + }), + tracingDelegate_(std::make_unique()) { auto& inspectorFlags = InspectorFlags::getInstance(); if (inspectorFlags.getFuseboxEnabled()) { inspectorTarget_ = HostTarget::create(*this, inspectorExecutor_); @@ -215,13 +246,127 @@ void JReactHostInspectorTarget::registerNatives() { "stopAndDiscardBackgroundTrace", JReactHostInspectorTarget::stopAndDiscardBackgroundTrace), makeNativeMethod( - "tracingStateAsInt", JReactHostInspectorTarget::tracingState), + "getTracingState", JReactHostInspectorTarget::getTracingState), + makeNativeMethod( + "registerTracingStateListener", + JReactHostInspectorTarget::registerTracingStateListener), + makeNativeMethod( + "unregisterTracingStateListener", + JReactHostInspectorTarget::unregisterTracingStateListener), }); } -jint JReactHostInspectorTarget::tracingState() { - auto state = inspectorTarget_->tracingState(); - return static_cast(state); +jni::local_ref +JReactHostInspectorTarget::getTracingState() { + return convertCPPTracingStateToJava(tracingDelegate_->getTracingState()); +} + +jlong JReactHostInspectorTarget::registerTracingStateListener( + jni::alias_ref listener) { + auto cppListener = [globalRef = make_global(listener)]( + TracingState state, bool screenshotsEnabled) { + static auto method = + globalRef->getClass() + ->getMethod, jboolean)>( + "onStateChanged"); + + method( + globalRef, + convertCPPTracingStateToJava(state), + static_cast(screenshotsEnabled)); + }; + + return static_cast( + tracingDelegate_->registerTracingStateListener(std::move(cppListener))); +} + +void JReactHostInspectorTarget::unregisterTracingStateListener( + jlong subscriptionId) { + tracingDelegate_->unregisterTracingStateListener(subscriptionId); +} + +HostTargetTracingDelegate* JReactHostInspectorTarget::getTracingDelegate() { + return tracingDelegate_.get(); +} + +void TracingDelegate::onTracingStarted( + tracing::Mode tracingMode, + bool screenshotsCategoryEnabled) { + TracingState nextState = TracingState::Disabled; + switch (tracingMode) { + case tracing::Mode::CDP: + nextState = TracingState::EnabledInCDPMode; + break; + case tracing::Mode::Background: + nextState = TracingState::EnabledInBackgroundMode; + break; + default: + throw std::logic_error("Unexpected new Tracing Mode"); + } + + std::vector listeners; + { + std::lock_guard lock(mutex_); + + tracingState_ = nextState; + listeners = copySubscribedListeners(); + } + + notifyListeners(listeners, nextState, screenshotsCategoryEnabled); +} + +void TracingDelegate::onTracingStopped() { + std::vector listeners; + { + std::lock_guard lock(mutex_); + + tracingState_ = TracingState::Disabled; + listeners = copySubscribedListeners(); + } + + notifyListeners(listeners, TracingState::Disabled, false); +} + +TracingState TracingDelegate::getTracingState() { + std::lock_guard lock(mutex_); + + return tracingState_; +} + +size_t TracingDelegate::registerTracingStateListener( + TracingStateListener listener) { + std::lock_guard lock(mutex_); + + auto id = nextSubscriptionId_++; + subscriptions_[id] = std::move(listener); + return id; +} + +void TracingDelegate::unregisterTracingStateListener(size_t subscriptionId) { + std::lock_guard lock(mutex_); + + subscriptions_.erase(subscriptionId); +} + +std::vector TracingDelegate::copySubscribedListeners() { + std::vector listeners; + listeners.reserve(subscriptions_.size()); + + for (auto& [_, listener] : subscriptions_) { + listeners.push_back(listener); + } + + return listeners; +} + +void TracingDelegate::notifyListeners( + const std::vector& listeners, + TracingState state, + bool screenshotsCategoryEnabled) { + for (const auto& listener : listeners) { + listener(state, screenshotsCategoryEnabled); + } } } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h index 1807509a4609..1eba8ae45a50 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h @@ -8,10 +8,15 @@ #pragma once #include + #include #include #include + +#include +#include #include +#include namespace facebook::react { @@ -20,7 +25,11 @@ struct JTaskInterface : public jni::JavaClass { }; struct JTracingState : public jni::JavaClass { - static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/TracingState;"; + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/TracingState;"; +}; + +struct JTracingStateListener : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/TracingStateListener;"; }; struct JReactHostImpl : public jni::JavaClass { @@ -55,6 +64,71 @@ struct JReactHostImpl : public jni::JavaClass { } }; +enum class TracingState { + Disabled, + EnabledInBackgroundMode, + EnabledInCDPMode, +}; + +/** + * A callback that will be invoked when tracing state has changed. + */ +using TracingStateListener = std::function; + +class TracingDelegate : public jsinspector_modern::HostTargetTracingDelegate { + public: + void onTracingStarted(jsinspector_modern::tracing::Mode tracingMode, bool screenshotsCategoryEnabled) override; + void onTracingStopped() override; + + /** + * A synchronous way to get the current tracing state. + * Could be called from any thread. + */ + TracingState getTracingState(); + /** + * Register a listener that will be notified when the tracing state changes. + * Could be called from any thread. + */ + size_t registerTracingStateListener(TracingStateListener listener); + /** + * Unregister previously registered listener with the id returned from + * TracingDelegate::registerTracingStateListener(). + */ + void unregisterTracingStateListener(size_t subscriptionId); + + private: + /** + * Covers read / write operations on tracingState_ and subscriptions_. + */ + std::mutex mutex_; + /** + * Since HostInspectorTarget creates HostTarget, the default value is Disabled. + * However, the TracingDelegate is subscribed at the construction of HostTarget, so it will be notified as early as + * possible. + */ + TracingState tracingState_ = TracingState::Disabled; + /** + * Map of subscription ID to listener. + */ + std::unordered_map subscriptions_; + /** + * A counter for generating unique subscription IDs. + */ + uint64_t nextSubscriptionId_ = 0; + /** + * Returns a collection of listeners that are subscribed at the time of the call. + * Expected to be only called with mutex_ locked. + */ + std::vector copySubscribedListeners(); + /** + * Notifies specified listeners about the state change. + */ + void notifyListeners( + const std::vector &listeners, + TracingState state, + bool screenshotsCategoryEnabled); +}; + class JReactHostInspectorTarget : public jni::HybridClass, public jsinspector_modern::HostTargetDelegate { public: @@ -70,14 +144,6 @@ class JReactHostInspectorTarget : public jni::HybridClass getTracingState(); + + /** + * Register a listener that will be notified when the tracing state changes. + * Could be called from any thread. + * + * \return A unique subscription ID to use for unregistering the listener. + */ + jlong registerTracingStateListener(jni::alias_ref listener); + + /** + * Unregister a previously registered tracing state listener. + * + * \param subscriptionId The subscription ID returned from JReactHostInspectorTarget::registerTracingStateListener. + */ + void unregisterTracingStateListener(jlong subscriptionId); + // HostTargetDelegate methods jsinspector_modern::HostTargetMetadata getMetadata() override; void onReload(const PageReloadRequest &request) override; @@ -109,6 +195,7 @@ class JReactHostInspectorTarget : public jni::HybridClass executor) override; std::optional unstable_getTraceRecordingThatWillBeEmittedOnInitialization() override; + jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override; private: JReactHostInspectorTarget( @@ -140,6 +227,10 @@ class JReactHostInspectorTarget : public jni::HybridClass stashedTraceRecordingState_; + /** + * Encapsulates the logic around tracing for this HostInspectorTarget. + */ + std::unique_ptr tracingDelegate_; friend HybridBase; }; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index 72929a95a205..a946704c575d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -22,7 +22,6 @@ #include #include -#include #ifndef JSINSPECTOR_EXPORT #ifdef _MSC_VER @@ -55,6 +54,36 @@ struct HostTargetMetadata { std::optional reactNativeVersion{}; }; +/** + * Receives any performance-related events from a HostTarget: could be Tracing, Performance Monitor, etc. + */ +class HostTargetTracingDelegate { + public: + HostTargetTracingDelegate() = default; + virtual ~HostTargetTracingDelegate() = default; + + /** + * Fired when the corresponding HostTarget started recording a tracing session. + * The tracing state is expected to be initialized at this point and the delegate should be able to record events + * through HostTarget. + */ + virtual void onTracingStarted(tracing::Mode /* tracingMode */, bool /* screenshotsCategoryEnabled */) {} + + /** + * Fired when the corresponding HostTarget is about to end recording a tracing session. + * The tracing state is expected to be still initialized during the call and the delegate should be able to record + * events through HostTarget. + * + * Any attempts to record events after this callback is finished will fail. + */ + virtual void onTracingStopped() {} + + HostTargetTracingDelegate(const HostTargetTracingDelegate &) = delete; + HostTargetTracingDelegate(HostTargetTracingDelegate &&) = delete; + HostTargetTracingDelegate &operator=(const HostTargetTracingDelegate &) = delete; + HostTargetTracingDelegate &operator=(HostTargetTracingDelegate &&) = delete; +}; + /** * Receives events from a HostTarget. This is a shared interface that each * React Native platform needs to implement in order to integrate with the @@ -161,6 +190,14 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate { { return std::nullopt; } + + /** + * An optional delegate that will be used by HostTarget to notify about tracing-related events. + */ + virtual HostTargetTracingDelegate *getTracingDelegate() + { + return nullptr; + } }; /** @@ -230,12 +267,15 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis public: /** * Constructs a new HostTarget. + * * \param delegate The HostTargetDelegate that will * receive events from this HostTarget. The caller is responsible for ensuring * that the HostTargetDelegate outlives this object. + * * \param executor An executor that may be used to call methods on this * HostTarget while it exists. \c create additionally guarantees that the * executor will not be called after the HostTarget is destroyed. + * * \note Copies of the provided executor may be destroyed on arbitrary * threads, including after the HostTarget is destroyed. Callers must ensure * that such destructor calls are safe - e.g. if using a lambda as the @@ -307,11 +347,6 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis */ tracing::TraceRecordingState stopTracing(); - /** - * Returns the state of the background trace, running, stopped, or disabled - */ - tracing::TracingState tracingState() const; - /** * Returns whether there is an active session with the Fusebox client, i.e. * with Chrome DevTools Frontend fork for React Native. @@ -326,15 +361,11 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis */ void emitTraceRecordingForFirstFuseboxClient(tracing::TraceRecordingState traceRecording) const; - /** - * Emits a system state changed event to all active sessions. - */ - void emitSystemStateChanged(bool isSingleHost) const; - private: /** * Constructs a new HostTarget. * The caller must call setExecutor immediately afterwards. + * * \param delegate The HostTargetDelegate that will * receive events from this HostTarget. The caller is responsible for ensuring * that the HostTargetDelegate outlives this object. diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp index 7d9ec16a3ea2..bdf4033dee4b 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp @@ -5,8 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -#include - #include "HostTarget.h" #include "HostTargetTraceRecording.h" @@ -35,41 +33,38 @@ bool HostTarget::startTracing( if (traceRecording_ != nullptr) { if (traceRecording_->isBackgroundInitiated() && tracingMode == tracing::Mode::CDP) { - traceRecording_.reset(); + stopTracing(); } else { return false; } } + auto screenshotsCategoryEnabled = + enabledCategories.contains(tracing::Category::Screenshot); + traceRecording_ = std::make_unique( *this, tracingMode, std::move(enabledCategories)); traceRecording_->setTracedInstance(currentInstance_.get()); traceRecording_->start(); + if (auto tracingDelegate = delegate_.getTracingDelegate()) { + tracingDelegate->onTracingStarted(tracingMode, screenshotsCategoryEnabled); + } + return true; } tracing::TraceRecordingState HostTarget::stopTracing() { assert(traceRecording_ != nullptr && "No tracing in progress"); + if (auto tracingDelegate = delegate_.getTracingDelegate()) { + tracingDelegate->onTracingStopped(); + } + auto state = traceRecording_->stop(); traceRecording_.reset(); return state; } -tracing::TracingState HostTarget::tracingState() const { - if (traceRecording_ == nullptr) { - return tracing::TracingState::Disabled; - } - - if (traceRecording_->isBackgroundInitiated()) { - return tracing::TracingState::EnabledInBackgroundMode; - } - - // This means we have a traceRecording_, but not running in the background. - // CDP initiated this trace so we should report as disabled. - return tracing::TracingState::EnabledInCDPMode; -} - } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp index 88fef17ce041..5b600997ff89 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp @@ -1526,4 +1526,57 @@ TEST_F(HostTargetTest, IOReadSizeValidation) { })"); } +TEST_F(HostTargetTest, TracingDelegateIsNotifiedOnCDPRequest) { + connect(); + InSequence s; + + EXPECT_CALL( + hostTargetDelegate_.getTracingDelegateMock(), + onTracingStarted(Eq(tracing::Mode::CDP), Eq(false))) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 1, + "result": {} + })"))); + toPage_->sendMessage(R"({ + "id": 1, + "method": "Tracing.start" + })"); + + EXPECT_CALL(hostTargetDelegate_.getTracingDelegateMock(), onTracingStopped()) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 1, + "result": {} + })"))); + EXPECT_CALL( + fromPage(), + onMessage(JsonParsed( + testing::AllOf( + AtJsonPtr("/method", "Tracing.tracingComplete"), + AtJsonPtr("/params/dataLossOccurred", false))))); + toPage_->sendMessage(R"({ + "id": 1, + "method": "Tracing.end" + })"); +} + +TEST_F(HostTargetTest, TracingDelegateIsNotifiedOnDirectTracingCall) { + connect(); + + EXPECT_CALL( + hostTargetDelegate_.getTracingDelegateMock(), + onTracingStarted(Eq(tracing::Mode::Background), Eq(false))) + .Times(1) + .RetiresOnSaturation(); + page_->startTracing(tracing::Mode::Background, {}); + + EXPECT_CALL(hostTargetDelegate_.getTracingDelegateMock(), onTracingStopped()) + .Times(1) + .RetiresOnSaturation(); + page_->stopTracing(); +} + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h index d83ed8618846..122b233f24a3 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h @@ -113,6 +113,12 @@ class MockInspectorPackagerConnectionDelegate : public InspectorPackagerConnecti folly::Executor &executor_; }; +class MockHostTargetTracingDelegate : public HostTargetTracingDelegate { + public: + MOCK_METHOD(void, onTracingStarted, (tracing::Mode tracingMode, bool screenshotsCategoryEnabled), (override)); + MOCK_METHOD(void, onTracingStopped, (), (override)); +}; + class MockHostTargetDelegate : public HostTargetDelegate { public: // HostTargetDelegate methods @@ -131,6 +137,20 @@ class MockHostTargetDelegate : public HostTargetDelegate { loadNetworkResource, (const LoadNetworkResourceRequest ¶ms, ScopedExecutor executor), (override)); + + HostTargetTracingDelegate *getTracingDelegate() override + { + return mockTracingDelegate_.get(); + } + + MockHostTargetTracingDelegate &getTracingDelegateMock() + { + return *mockTracingDelegate_; + } + + private: + std::unique_ptr mockTracingDelegate_ = + std::make_unique(); }; class MockInstanceTargetDelegate : public InstanceTargetDelegate {}; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingState.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingState.h deleted file mode 100644 index 5914de8c5a47..000000000000 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingState.h +++ /dev/null @@ -1,24 +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. - */ - -#pragma once - -#include - -namespace facebook::react::jsinspector_modern::tracing { - -// Keep in sync with `TracingState.kt` -enum class TracingState : int32_t { - // There is no active trace - Disabled = 0, - // Trace is currently running in background mode - EnabledInBackgroundMode = 1, - // Trace is currently running in CDP mode - EnabledInCDPMode = 2, -}; - -} // namespace facebook::react::jsinspector_modern::tracing