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 1e6ed09aa6ef..543ed13f9376 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 @@ -72,7 +72,6 @@ import com.facebook.react.devsupport.perfmonitor.PerfMonitorDevHelper import com.facebook.react.devsupport.perfmonitor.PerfMonitorOverlayManager import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags -import com.facebook.react.internal.tracing.PerformanceTracer import com.facebook.react.modules.core.RCTNativeAppEventEmitter import com.facebook.react.modules.debug.interfaces.DeveloperSettings import com.facebook.react.packagerconnection.RequestHandler @@ -212,8 +211,6 @@ public abstract class DevSupportManagerBase( private var perfMonitorOverlayManager: PerfMonitorOverlayManager? = null private var perfMonitorInitialized = false private var tracingStateProvider: TracingStateProvider? = null - private var tracingStateSubscriptionId: Int? = null - private var frameTiming: FrameTiming? = null public override var keyboardShortcutsEnabled: Boolean = true public override var devMenuEnabled: Boolean = true @@ -972,37 +969,12 @@ public abstract class DevSupportManagerBase( isPackagerConnected = true perfMonitorOverlayManager?.enable() perfMonitorOverlayManager?.startBackgroundTrace() - - // Subscribe to tracing state changes - tracingStateSubscriptionId = - PerformanceTracer.subscribeToTracingStateChanges( - object : PerformanceTracer.TracingStateCallback { - override fun onTracingStateChanged(isTracing: Boolean) { - if (isTracing) { - if (frameTiming == null) { - currentActivity?.window?.let { window -> - frameTiming = FrameTiming(window) - } - } - frameTiming?.startMonitoring() - } else { - frameTiming?.stopMonitoring() - } - } - } - ) } override fun onPackagerDisconnected() { isPackagerConnected = false perfMonitorOverlayManager?.disable() perfMonitorOverlayManager?.stopBackgroundTrace() - - // Unsubscribe from tracing state changes - tracingStateSubscriptionId?.let { subscriptionId -> - PerformanceTracer.unsubscribeFromTracingStateChanges(subscriptionId) - tracingStateSubscriptionId = null - } } override fun onPackagerReloadCommand() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/FrameTiming.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/FrameTiming.kt deleted file mode 100644 index 9639e7b22cde..000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/FrameTiming.kt +++ /dev/null @@ -1,68 +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.devsupport - -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.view.FrameMetrics -import android.view.Window -import com.facebook.proguard.annotations.DoNotStripAny -import com.facebook.soloader.SoLoader - -@DoNotStripAny -internal class FrameTiming(private val window: Window) { - init { - SoLoader.loadLibrary("react_devsupportjni") - } - - private var frameCounter: Int = 0 - - private external fun setLayerTreeId(frame: String, layerTreeId: Int) - - private val frameMetricsListener = - Window.OnFrameMetricsAvailableListener { _, frameMetrics, dropCount -> - val metrics = FrameMetrics(frameMetrics) - - val paintStartTime = metrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP) - val totalDuration = metrics.getMetric(FrameMetrics.TOTAL_DURATION) - - val currentFrame = frameCounter++ - reportFrameTiming( - frame = currentFrame, - paintStartNanos = paintStartTime, - paintEndNanos = paintStartTime + totalDuration, - ) - } - - companion object { - @JvmStatic - private external fun reportFrameTiming(frame: Int, paintStartNanos: Long, paintEndNanos: Long) - } - - private val handler = Handler(Looper.getMainLooper()) - - internal fun startMonitoring() { - frameCounter = 0 - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return - } - window.addOnFrameMetricsAvailableListener(frameMetricsListener, handler) - - // Hardcoded frame identfier and layerTreeId. Needed for DevTools to - // begin parsing frame events. - setLayerTreeId("", 1) - } - - internal fun stopMonitoring() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return - } - window.removeOnFrameMetricsAvailableListener(frameMetricsListener) - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt new file mode 100644 index 000000000000..3415a4bc12ce --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt @@ -0,0 +1,16 @@ +/* + * 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 + +internal data class FrameTimingSequence( + val id: Int, + val threadId: Int, + val beginDrawingTimestamp: Long, + val commitTimestamp: Long, + val endDrawingTimestamp: Long, +) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt new file mode 100644 index 000000000000..f4afc9ff894d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt @@ -0,0 +1,78 @@ +/* + * 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 android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.Process +import android.view.FrameMetrics +import android.view.Window +import com.facebook.proguard.annotations.DoNotStripAny +import com.facebook.soloader.SoLoader + +@DoNotStripAny +internal class FrameTimingsObserver( + private val window: Window, + onFrameTimingSequence: (sequence: FrameTimingSequence) -> Unit, +) { + private val handler = Handler(Looper.getMainLooper()) + private var frameCounter: Int = 0 + + private external fun setLayerTreeId(frame: String, layerTreeId: Int) + + private val frameMetricsListener = + Window.OnFrameMetricsAvailableListener { _, frameMetrics, _dropCount -> + val beginDrawingTimestamp = frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP) + val commitTimestamp = + beginDrawingTimestamp + frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION) + +frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION) + +frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) + +frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) + +frameMetrics.getMetric(FrameMetrics.SYNC_DURATION) + val endDrawingTimestamp = + beginDrawingTimestamp + frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION) + + onFrameTimingSequence( + FrameTimingSequence( + frameCounter++, + Process.myTid(), + beginDrawingTimestamp, + commitTimestamp, + endDrawingTimestamp, + ) + ) + } + + fun start() { + frameCounter = 0 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return + } + + window.addOnFrameMetricsAvailableListener(frameMetricsListener, handler) + } + + fun stop() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return + } + + window.removeOnFrameMetricsAvailableListener(frameMetricsListener) + handler.removeCallbacksAndMessages(null) + } + + private companion object { + init { + SoLoader.loadLibrary("react_devsupportjni") + } + + @JvmStatic + private external fun reportFrameTiming(frame: Int, paintStartNanos: Long, paintEndNanos: Long) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 09978ecbf5f4..ca38c4f2432c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -43,8 +43,11 @@ import com.facebook.react.devsupport.DevMenuConfiguration import com.facebook.react.devsupport.DevSupportManagerBase import com.facebook.react.devsupport.DevSupportManagerFactory import com.facebook.react.devsupport.InspectorFlags +import com.facebook.react.devsupport.inspector.FrameTimingsObserver import com.facebook.react.devsupport.inspector.InspectorNetworkHelper import com.facebook.react.devsupport.inspector.InspectorNetworkRequestListener +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateListener import com.facebook.react.devsupport.interfaces.BundleLoadCallback import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.devsupport.interfaces.DevSupportManager.PausedInDebuggerOverlayCommandListener @@ -147,6 +150,7 @@ public class ReactHostImpl( private val beforeDestroyListeners: MutableList<() -> Unit> = CopyOnWriteArrayList() internal var reactHostInspectorTarget: ReactHostInspectorTarget? = null + private var frameTimingsObserver: FrameTimingsObserver? = null @Volatile private var hostInvalidated = false @@ -1442,8 +1446,7 @@ public class ReactHostImpl( // If the host has been invalidated, now that the current context/instance // has been unregistered, we can safely destroy the host's inspector // target. - reactHostInspectorTarget?.close() - reactHostInspectorTarget = null + destroyReactHostInspectorTarget() } // Step 1: Destroy DevSupportManager @@ -1554,13 +1557,48 @@ public class ReactHostImpl( internal fun getOrCreateReactHostInspectorTarget(): ReactHostInspectorTarget? { if (reactHostInspectorTarget == null && InspectorFlags.getFuseboxEnabled()) { - // NOTE: ReactHostInspectorTarget only retains a weak reference to `this`. - reactHostInspectorTarget = ReactHostInspectorTarget(this) + reactHostInspectorTarget = createReactHostInspectorTarget() } return reactHostInspectorTarget } + private fun createReactHostInspectorTarget(): ReactHostInspectorTarget { + // NOTE: ReactHostInspectorTarget only retains a weak reference to `this`. + val inspectorTarget = ReactHostInspectorTarget(this) + inspectorTarget.registerTracingStateListener( + TracingStateListener { state: TracingState, _screenshotsEnabled: Boolean -> + when (state) { + TracingState.ENABLED_IN_BACKGROUND_MODE, + TracingState.ENABLED_IN_CDP_MODE -> { + currentActivity?.window?.let { window -> + val observer = + FrameTimingsObserver(window) { frameTimingsSequence -> + inspectorTarget.recordFrameTimings(frameTimingsSequence) + } + observer.start() + frameTimingsObserver = observer + } + } + TracingState.DISABLED -> { + frameTimingsObserver?.stop() + frameTimingsObserver = null + } + } + } + ) + + return inspectorTarget + } + + private fun destroyReactHostInspectorTarget() { + frameTimingsObserver?.stop() + frameTimingsObserver = null + + reactHostInspectorTarget?.close() + reactHostInspectorTarget = null + } + @ThreadConfined(ThreadConfined.UI) internal fun unregisterInstanceFromInspector(reactInstance: ReactInstance?) { if (reactInstance != null) { 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 218d9c6ecf47..68fb8a8945d9 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,6 +12,7 @@ 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.inspector.FrameTimingSequence import com.facebook.react.devsupport.inspector.TracingState import com.facebook.react.devsupport.inspector.TracingStateListener import com.facebook.react.devsupport.perfmonitor.PerfMonitorInspectorTarget @@ -48,6 +49,8 @@ internal class ReactHostInspectorTarget(reactHostImpl: ReactHostImpl) : external fun unregisterTracingStateListener(subscriptionId: Long) + external fun recordFrameTimings(frameTimingSequence: FrameTimingSequence) + override fun addPerfMonitorListener(listener: PerfMonitorUpdateListener) { perfMonitorListeners.add(listener) } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JFrameTiming.h b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JFrameTiming.h index f160db534f78..182041bc1ca7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JFrameTiming.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JFrameTiming.h @@ -16,7 +16,7 @@ namespace facebook::react::jsinspector_modern { */ class JFrameTiming : public jni::JavaClass { public: - static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/FrameTiming;"; + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/FrameTimingsObserver;"; static void reportFrameTiming(jni::alias_ref /*unused*/, jint frame, jlong paintStartNanos, jlong paintEndNanos); 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 983b7b64d5a3..94ddf5e84fd6 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 @@ -192,7 +192,7 @@ bool JReactHostInspectorTarget::startBackgroundTrace() { } } -tracing::TraceRecordingState JReactHostInspectorTarget::stopTracing() { +tracing::HostTracingProfile JReactHostInspectorTarget::stopTracing() { if (inspectorTarget_) { return inspectorTarget_->stopTracing(); } else { @@ -205,12 +205,12 @@ tracing::TraceRecordingState JReactHostInspectorTarget::stopTracing() { jboolean JReactHostInspectorTarget::stopAndMaybeEmitBackgroundTrace() { auto capturedTrace = inspectorTarget_->stopTracing(); if (inspectorTarget_->hasActiveSessionWithFuseboxClient()) { - inspectorTarget_->emitTraceRecordingForFirstFuseboxClient( + inspectorTarget_->emitTracingProfileForFirstFuseboxClient( std::move(capturedTrace)); return jboolean(true); } - stashTraceRecordingState(std::move(capturedTrace)); + stashTracingProfile(std::move(capturedTrace)); return jboolean(false); } @@ -218,16 +218,16 @@ void JReactHostInspectorTarget::stopAndDiscardBackgroundTrace() { inspectorTarget_->stopTracing(); } -void JReactHostInspectorTarget::stashTraceRecordingState( - tracing::TraceRecordingState&& state) { - stashedTraceRecordingState_ = std::move(state); +void JReactHostInspectorTarget::stashTracingProfile( + tracing::HostTracingProfile&& hostTracingProfile) { + stashedTracingProfile_ = std::move(hostTracingProfile); } -std::optional JReactHostInspectorTarget:: - unstable_getTraceRecordingThatWillBeEmittedOnInitialization() { - auto state = std::move(stashedTraceRecordingState_); - stashedTraceRecordingState_.reset(); - return state; +std::optional JReactHostInspectorTarget:: + unstable_getHostTracingProfileThatWillBeEmittedOnInitialization() { + auto tracingProfile = std::move(stashedTracingProfile_); + stashedTracingProfile_.reset(); + return tracingProfile; } void JReactHostInspectorTarget::registerNatives() { @@ -253,6 +253,8 @@ void JReactHostInspectorTarget::registerNatives() { makeNativeMethod( "unregisterTracingStateListener", JReactHostInspectorTarget::unregisterTracingStateListener), + makeNativeMethod( + "recordFrameTimings", JReactHostInspectorTarget::recordFrameTimings), }); } @@ -290,6 +292,17 @@ HostTargetTracingDelegate* JReactHostInspectorTarget::getTracingDelegate() { return tracingDelegate_.get(); } +void JReactHostInspectorTarget::recordFrameTimings( + jni::alias_ref frameTimingSequence) { + inspectorTarget_->recordFrameTimings({ + frameTimingSequence->getId(), + frameTimingSequence->getThreadId(), + frameTimingSequence->getBeginDrawingTimestamp(), + frameTimingSequence->getCommitTimestamp(), + frameTimingSequence->getEndDrawingTimestamp(), + }); +} + void TracingDelegate::onTracingStarted( tracing::Mode tracingMode, bool screenshotsCategoryEnabled) { 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 1eba8ae45a50..68a43b732ba9 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 @@ -32,6 +32,43 @@ struct JTracingStateListener : public jni::JavaClass { static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/TracingStateListener;"; }; +struct JFrameTimingSequence : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/FrameTimingSequence;"; + + uint64_t getId() const + { + auto field = javaClassStatic()->getField("id"); + return static_cast(getFieldValue(field)); + } + + uint64_t getThreadId() const + { + auto field = javaClassStatic()->getField("threadId"); + return static_cast(getFieldValue(field)); + } + + HighResTimeStamp getBeginDrawingTimestamp() const + { + auto field = javaClassStatic()->getField("beginDrawingTimestamp"); + return HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::nanoseconds(getFieldValue(field)))); + } + + HighResTimeStamp getCommitTimestamp() const + { + auto field = javaClassStatic()->getField("commitTimestamp"); + return HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::nanoseconds(getFieldValue(field)))); + } + + HighResTimeStamp getEndDrawingTimestamp() const + { + auto field = javaClassStatic()->getField("endDrawingTimestamp"); + return HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::nanoseconds(getFieldValue(field)))); + } +}; + struct JReactHostImpl : public jni::JavaClass { static constexpr auto kJavaDescriptor = "Lcom/facebook/react/runtime/ReactHostImpl;"; @@ -185,6 +222,11 @@ class JReactHostInspectorTarget : public jni::HybridClass frameTimingSequence); + // HostTargetDelegate methods jsinspector_modern::HostTargetMetadata getMetadata() override; void onReload(const PageReloadRequest &request) override; @@ -193,8 +235,8 @@ class JReactHostInspectorTarget : public jni::HybridClass executor) override; - std::optional - unstable_getTraceRecordingThatWillBeEmittedOnInitialization() override; + std::optional + unstable_getHostTracingProfileThatWillBeEmittedOnInitialization() override; jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override; private: @@ -213,20 +255,19 @@ class JReactHostInspectorTarget : public jni::HybridClass inspectorPageId_; /** - * Stops previously started trace recording and returns the captured trace. + * Stops previously started trace recording and returns the captured HostTracingProfile. */ - jsinspector_modern::tracing::TraceRecordingState stopTracing(); + jsinspector_modern::tracing::HostTracingProfile stopTracing(); /** - * Stashes previously recorded trace recording state that will be emitted when + * Stashes previously recorded HostTracingProfile that will be emitted when * CDP session is created. Once emitted, the value will be cleared from this * instance. */ - void stashTraceRecordingState(jsinspector_modern::tracing::TraceRecordingState &&state); + void stashTracingProfile(jsinspector_modern::tracing::HostTracingProfile &&hostTracingProfile); /** - * Previously recorded trace recording state that will be emitted when - * CDP session is created. + * Previously recorded HostTracingProfile that will be emitted when CDP session is created. */ - std::optional stashedTraceRecordingState_; + std::optional stashedTracingProfile_; /** * Encapsulates the logic around tracing for this HostInspectorTarget. */ diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 813770d51ad5..a4e5b254a00c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -238,9 +238,9 @@ class HostAgent::Impl final { auto stashedTraceRecording = targetController_.getDelegate() - .unstable_getTraceRecordingThatWillBeEmittedOnInitialization(); + .unstable_getHostTracingProfileThatWillBeEmittedOnInitialization(); if (stashedTraceRecording.has_value()) { - tracingAgent_.emitExternalTraceRecording( + tracingAgent_.emitExternalHostTracingProfile( std::move(stashedTraceRecording.value())); } @@ -385,12 +385,12 @@ class HostAgent::Impl final { return fuseboxClientType_ == FuseboxClientType::Fusebox; } - void emitExternalTraceRecording( - tracing::TraceRecordingState traceRecording) const { + void emitExternalTracingProfile( + tracing::HostTracingProfile tracingProfile) const { assert( hasFuseboxClientConnected() && "Attempted to emit a trace recording to a non-Fusebox client"); - tracingAgent_.emitExternalTraceRecording(std::move(traceRecording)); + tracingAgent_.emitExternalHostTracingProfile(std::move(tracingProfile)); } void emitSystemStateChanged(bool isSingleHost) { @@ -506,8 +506,7 @@ class HostAgent::Impl final { bool hasFuseboxClientConnected() const { return false; } - void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) { - } + void emitExternalTracingProfile(tracing::HostTracingProfile tracingProfile) {} void emitSystemStateChanged(bool isSingleHost) {} }; @@ -543,9 +542,9 @@ bool HostAgent::hasFuseboxClientConnected() const { return impl_->hasFuseboxClientConnected(); } -void HostAgent::emitExternalTraceRecording( - tracing::TraceRecordingState traceRecording) const { - impl_->emitExternalTraceRecording(std::move(traceRecording)); +void HostAgent::emitExternalTracingProfile( + tracing::HostTracingProfile tracingProfile) const { + impl_->emitExternalTracingProfile(std::move(tracingProfile)); } void HostAgent::emitSystemStateChanged(bool isSingleHost) const { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h index ab4478816562..8789cb5d32c1 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h @@ -74,10 +74,10 @@ class HostAgent final { bool hasFuseboxClientConnected() const; /** - * Emits the trace recording that was captured externally, not via the + * Emits the HostTracingProfile that was captured externally, not via the * CDP-initiated request. */ - void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) const; + void emitExternalTracingProfile(tracing::HostTracingProfile tracingProfile) const; /** * Emits a system state changed event when the number of ReactHost instances diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp index 6639614b8220..e34ca99bfad2 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp @@ -111,8 +111,9 @@ class HostTargetSession { return hostAgent_.hasFuseboxClientConnected(); } - void emitTraceRecording(tracing::TraceRecordingState traceRecording) const { - hostAgent_.emitExternalTraceRecording(std::move(traceRecording)); + void emitHostTracingProfile( + tracing::HostTracingProfile tracingProfile) const { + hostAgent_.emitExternalTracingProfile(std::move(tracingProfile)); } private: @@ -382,13 +383,13 @@ bool HostTarget::hasActiveSessionWithFuseboxClient() const { return hasActiveFuseboxSession; } -void HostTarget::emitTraceRecordingForFirstFuseboxClient( - tracing::TraceRecordingState traceRecording) const { +void HostTarget::emitTracingProfileForFirstFuseboxClient( + tracing::HostTracingProfile tracingProfile) const { bool emitted = false; sessions_.forEach([&](HostTargetSession& session) { if (emitted) { /** - * TraceRecordingState object is not copiable for performance reasons, + * HostTracingProfile object is not copiable for performance reasons, * because it could contain large Runtime sampling profile object. * * This approach would not work with multi-client debugger setup. @@ -396,7 +397,7 @@ void HostTarget::emitTraceRecordingForFirstFuseboxClient( return; } if (session.hasFuseboxClient()) { - session.emitTraceRecording(std::move(traceRecording)); + session.emitHostTracingProfile(std::move(tracingProfile)); emitted = true; } }); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index a946704c575d..aed1396b14e6 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #include #include @@ -183,10 +186,10 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate { * trace recording that may have been stashed by the Host from the previous * background session. * - * \return the trace recording state if there is one that needs to be + * \return the HostTracingProfile if there is one that needs to be * displayed, otherwise std::nullopt. */ - virtual std::optional unstable_getTraceRecordingThatWillBeEmittedOnInitialization() + virtual std::optional unstable_getHostTracingProfileThatWillBeEmittedOnInitialization() { return std::nullopt; } @@ -251,7 +254,7 @@ class HostTargetController final { /** * Stops previously started trace recording. */ - tracing::TraceRecordingState stopTracing(); + tracing::HostTracingProfile stopTracing(); private: HostTarget &target_; @@ -323,6 +326,7 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis */ void sendCommand(HostCommand command); +#pragma region Tracing /** * Creates a new HostTracingAgent. * This Agent is not owned by the HostTarget. The Agent will be destroyed at @@ -345,7 +349,7 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis /** * Stops previously started trace recording. */ - tracing::TraceRecordingState stopTracing(); + tracing::HostTracingProfile stopTracing(); /** * Returns whether there is an active session with the Fusebox client, i.e. @@ -354,12 +358,19 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis bool hasActiveSessionWithFuseboxClient() const; /** - * Emits the trace recording for the first active session with the Fusebox + * Emits the HostTracingProfile for the first active session with the Fusebox * client. * * @see \c hasActiveFrontendSession */ - void emitTraceRecordingForFirstFuseboxClient(tracing::TraceRecordingState traceRecording) const; + void emitTracingProfileForFirstFuseboxClient(tracing::HostTracingProfile tracingProfile) const; + + /** + * An endpoint for the Host to report frame timings that will be recorded if and only if there is currently an active + * tracing session. + */ + void recordFrameTimings(tracing::FrameTimingSequence frameTimingSequence); +#pragma endregion private: /** @@ -384,6 +395,7 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis std::unique_ptr perfMonitorUpdateHandler_; std::unique_ptr perfMetricsBinding_; +#pragma region Tracing /** * Current pending trace recording, which encapsulates the configuration of * the tracing session and the state. @@ -391,6 +403,14 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis * Should only be allocated when there is an active tracing session. */ std::unique_ptr traceRecording_{nullptr}; + /** + * Protects the state inside traceRecording_. + * + * Calls to tracing subsystem could happen from different threads, depending on the mode (Background or CDP) and + * the method: the Host could report frame timings from any arbitrary thread. + */ + std::mutex tracingMutex_; +#pragma endregion inline HostTargetDelegate &getDelegate() { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp index baea82c6cd42..38224d4f6973 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp @@ -8,15 +8,27 @@ #include "HostTargetTraceRecording.h" #include "HostTarget.h" +#include + namespace facebook::react::jsinspector_modern { HostTargetTraceRecording::HostTargetTraceRecording( HostTarget& hostTarget, tracing::Mode tracingMode, - std::set enabledCategories) + std::set enabledCategories, + std::optional windowSize) : hostTarget_(hostTarget), tracingMode_(tracingMode), - enabledCategories_(std::move(enabledCategories)) {} + enabledCategories_(std::move(enabledCategories)), + windowSize_(windowSize) { + if (windowSize) { + frameTimings_ = tracing::TimeWindowedBuffer( + [](auto& sequence) { return sequence.beginDrawingTimestamp; }, + *windowSize); + } else { + frameTimings_ = tracing::TimeWindowedBuffer(); + }; +} void HostTargetTraceRecording::setTracedInstance( InstanceTarget* instanceTarget) { @@ -32,15 +44,13 @@ void HostTargetTraceRecording::start() { hostTracingAgent_ == nullptr && "Tracing Agent for the HostTarget was already initialized."); - state_ = tracing::TraceRecordingState{ - .mode = tracingMode_, - .startTime = HighResTimeStamp::now(), - .enabledCategories = enabledCategories_, - }; + startTime_ = HighResTimeStamp::now(); + state_ = tracing::TraceRecordingState( + tracingMode_, enabledCategories_, windowSize_); hostTracingAgent_ = hostTarget_.createTracingAgent(*state_); } -tracing::TraceRecordingState HostTargetTraceRecording::stop() { +tracing::HostTracingProfile HostTargetTraceRecording::stop() { assert( hostTracingAgent_ != nullptr && "TracingAgent for the HostTarget has not been initialized."); @@ -52,7 +62,25 @@ tracing::TraceRecordingState HostTargetTraceRecording::stop() { auto state = std::move(*state_); state_.reset(); - return state; + auto startTime = *startTime_; + startTime_.reset(); + + return tracing::HostTracingProfile{ + .processId = oscompat::getCurrentProcessId(), + .startTime = startTime, + .frameTimings = frameTimings_.pruneExpiredAndExtract(), + .instanceTracingProfiles = std::move(state.instanceTracingProfiles), + .runtimeSamplingProfiles = std::move(state.runtimeSamplingProfiles), + }; +} + +void HostTargetTraceRecording::recordFrameTimings( + tracing::FrameTimingSequence frameTimingSequence) { + assert( + state_.has_value() && + "The state for this tracing session has not been initialized."); + + frameTimings_.push(frameTimingSequence); } } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h index 21fd88ffa71d..193d9e2aa970 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h @@ -11,8 +11,12 @@ #include "HostTarget.h" #include "InstanceTarget.h" +#include +#include +#include #include #include +#include #include #include @@ -30,10 +34,11 @@ namespace facebook::react::jsinspector_modern { */ class HostTargetTraceRecording { public: - explicit HostTargetTraceRecording( + HostTargetTraceRecording( HostTarget &hostTarget, tracing::Mode tracingMode, - std::set enabledCategories); + std::set enabledCategories, + std::optional windowSize = std::nullopt); inline bool isBackgroundInitiated() const { @@ -58,11 +63,18 @@ class HostTargetTraceRecording { void start(); /** - * Stops the recording and drops the recording state. + * Stops the recording and returns the recorded HostTracingProfile. * * Will deallocate all Tracing Agents. */ - tracing::TraceRecordingState stop(); + tracing::HostTracingProfile stop(); + + /** + * Adds the frame timing sequence to the current state of this trace recording. + * + * The caller guarantees the protection from data races. This is protected by the tracing mutex in HostTarget. + */ + void recordFrameTimings(tracing::FrameTimingSequence frameTimingSequence); private: /** @@ -75,6 +87,11 @@ class HostTargetTraceRecording { */ tracing::Mode tracingMode_; + /** + * The timestamp at which this Trace Recording started. + */ + std::optional startTime_; + /** * The state of the current Trace Recording. * Only allocated if the recording is enabled. @@ -91,6 +108,16 @@ class HostTargetTraceRecording { * The list of categories that are enabled for this recording. */ std::set enabledCategories_; + + /** + * The size of the time window for this recording. + */ + std::optional windowSize_; + + /** + * Frame timings captured on the Host side. + */ + tracing::TimeWindowedBuffer frameTimings_; }; } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp index bdf4033dee4b..9fba17e555f2 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp @@ -10,13 +10,22 @@ namespace facebook::react::jsinspector_modern { +namespace { + +// The size of the timeline for the trace recording that happened in the +// background. +constexpr HighResDuration kBackgroundTraceWindowSize = + HighResDuration::fromMilliseconds(20000); + +} // namespace + bool HostTargetController::startTracing( tracing::Mode tracingMode, std::set enabledCategories) { return target_.startTracing(tracingMode, std::move(enabledCategories)); } -tracing::TraceRecordingState HostTargetController::stopTracing() { +tracing::HostTracingProfile HostTargetController::stopTracing() { return target_.stopTracing(); } @@ -30,20 +39,30 @@ std::shared_ptr HostTarget::createTracingAgent( bool HostTarget::startTracing( tracing::Mode tracingMode, std::set enabledCategories) { + std::lock_guard lock(tracingMutex_); + if (traceRecording_ != nullptr) { if (traceRecording_->isBackgroundInitiated() && tracingMode == tracing::Mode::CDP) { - stopTracing(); + if (auto tracingDelegate = delegate_.getTracingDelegate()) { + tracingDelegate->onTracingStopped(); + } + + traceRecording_->stop(); + traceRecording_.reset(); } else { return false; } } + auto timeWindow = tracingMode == tracing::Mode::Background + ? std::make_optional(kBackgroundTraceWindowSize) + : std::nullopt; auto screenshotsCategoryEnabled = enabledCategories.contains(tracing::Category::Screenshot); traceRecording_ = std::make_unique( - *this, tracingMode, std::move(enabledCategories)); + *this, tracingMode, std::move(enabledCategories), timeWindow); traceRecording_->setTracedInstance(currentInstance_.get()); traceRecording_->start(); @@ -54,17 +73,32 @@ bool HostTarget::startTracing( return true; } -tracing::TraceRecordingState HostTarget::stopTracing() { +tracing::HostTracingProfile HostTarget::stopTracing() { + std::lock_guard lock(tracingMutex_); + assert(traceRecording_ != nullptr && "No tracing in progress"); if (auto tracingDelegate = delegate_.getTracingDelegate()) { tracingDelegate->onTracingStopped(); } - auto state = traceRecording_->stop(); + auto profile = traceRecording_->stop(); traceRecording_.reset(); - return state; + return profile; +} + +void HostTarget::recordFrameTimings( + tracing::FrameTimingSequence frameTimingSequence) { + std::lock_guard lock(tracingMutex_); + + if (traceRecording_) { + traceRecording_->recordFrameTimings(frameTimingSequence); + } else { + assert( + false && + "The HostTarget is not being profiled. Did you call recordFrameTimings() from the native Host side when there is no tracing in progress?"); + } } } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp index d868160c07b1..f1eae37bb82f 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp @@ -15,15 +15,6 @@ namespace facebook::react::jsinspector_modern { -namespace { - -// The size of the timeline for the trace recording that happened in the -// background. -constexpr HighResDuration kBackgroundTracePerformanceTracerWindowSize = - HighResDuration::fromMilliseconds(20000); - -} // namespace - InstanceAgent::InstanceAgent( FrontendChannel frontendChannel, InstanceTarget& target, @@ -171,8 +162,8 @@ void InstanceAgent::maybeSendPendingConsoleMessages() { InstanceTracingAgent::InstanceTracingAgent(tracing::TraceRecordingState& state) : tracing::TargetTracingAgent(state) { auto& performanceTracer = tracing::PerformanceTracer::getInstance(); - if (state.mode == tracing::Mode::Background) { - performanceTracer.startTracing(kBackgroundTracePerformanceTracerWindowSize); + if (state.windowSize) { + performanceTracer.startTracing(*state.windowSize); } else { performanceTracer.startTracing(); } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp index 9ed49254f63d..237f625ef547 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp @@ -7,10 +7,10 @@ #include "TracingAgent.h" +#include #include #include #include -#include #include namespace facebook::react::jsinspector_modern { @@ -104,36 +104,36 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) { return true; } else if (req.method == "Tracing.end") { - auto state = hostTargetController_.stopTracing(); + auto tracingProfile = hostTargetController_.stopTracing(); sessionState_.hasPendingTraceRecording = false; // Send response to Tracing.end request. frontendChannel_(cdp::jsonResult(req.id)); - emitTraceRecording(std::move(state)); + emitHostTracingProfile(std::move(tracingProfile)); return true; } return false; } -void TracingAgent::emitExternalTraceRecording( - tracing::TraceRecordingState traceRecording) const { +void TracingAgent::emitExternalHostTracingProfile( + tracing::HostTracingProfile tracingProfile) const { frontendChannel_( cdp::jsonNotification("ReactNativeApplication.traceRequested")); - emitTraceRecording(std::move(traceRecording)); + emitHostTracingProfile(std::move(tracingProfile)); } -void TracingAgent::emitTraceRecording( - tracing::TraceRecordingState traceRecording) const { +void TracingAgent::emitHostTracingProfile( + tracing::HostTracingProfile tracingProfile) const { auto dataCollectedCallback = [this](folly::dynamic&& eventsChunk) { frontendChannel_( cdp::jsonNotification( "Tracing.dataCollected", folly::dynamic::object("value", std::move(eventsChunk)))); }; - tracing::TraceRecordingStateSerializer::emitAsDataCollectedChunks( - std::move(traceRecording), + tracing::HostTracingProfileSerializer::emitAsDataCollectedChunks( + std::move(tracingProfile), dataCollectedCallback, TRACE_EVENT_CHUNK_SIZE, PROFILE_TRACE_EVENT_CHUNK_SIZE); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h index 9a27dcbe718f..d0f474f1234b 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h @@ -11,6 +11,7 @@ #include "InspectorInterfaces.h" #include +#include #include #include @@ -41,9 +42,9 @@ class TracingAgent { bool handleRequest(const cdp::PreparsedRequest &req); /** - * Emits the Trace Recording that was stashed externally by the HostTarget. + * Emits the HostTracingProfile that was stashed externally by the HostTarget. */ - void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) const; + void emitExternalHostTracingProfile(tracing::HostTracingProfile tracingProfile) const; private: /** @@ -56,10 +57,10 @@ class TracingAgent { HostTargetController &hostTargetController_; /** - * Emits the captured Trace Recording state in a series of + * Emits captured HostTracingProfile in a series of * Tracing.dataCollected events, followed by a Tracing.tracingComplete event. */ - void emitTraceRecording(tracing::TraceRecordingState traceRecording) const; + void emitHostTracingProfile(tracing::HostTracingProfile tracingProfile) const; }; } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp index 422e5088bc0a..1c07fbdc0df4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp @@ -56,4 +56,43 @@ TEST_F(TracingTest, EnablesSamplingProfilerOnlyCategoryIsSpecified) { AtJsonPtr("/cat", "disabled-by-default-v8.cpu_profiler")))); } +TEST_F(TracingTest, RecordsFrameTimings) { + InSequence s; + + page_->startTracing(tracing::Mode::Background, {tracing::Category::Timeline}); + + auto now = HighResTimeStamp::now(); + auto frameTimingSequence = tracing::FrameTimingSequence( + 1, // id + 11, // threadId + now, + now + HighResDuration::fromNanoseconds(10), + now + HighResDuration::fromNanoseconds(50)); + + page_->recordFrameTimings(frameTimingSequence); + + auto tracingProfile = page_->stopTracing(); + EXPECT_EQ(tracingProfile.frameTimings.size(), 1u); + EXPECT_EQ(tracingProfile.frameTimings[0].id, frameTimingSequence.id); +} + +TEST_F(TracingTest, EmitsRecordedFrameTimingSequences) { + InSequence s; + + startTracing(); + auto now = HighResTimeStamp::now(); + page_->recordFrameTimings( + tracing::FrameTimingSequence( + 1, // id + 11, // threadId + now, + now + HighResDuration::fromNanoseconds(10), + now + HighResDuration::fromNanoseconds(50))); + + auto allTraceEvents = endTracingAndCollectEvents(); + EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "BeginFrame"))); + EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "Commit"))); + EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "DrawFrame"))); +} + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h new file mode 100644 index 000000000000..8de97c37f324 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h @@ -0,0 +1,51 @@ +/* + * 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 "TraceEvent.h" + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * A struct representing a sequence of frame timings that happened on the Host side. + */ +struct FrameTimingSequence { + FrameTimingSequence() = delete; + + FrameTimingSequence( + uint64_t id, + ThreadId threadId, + HighResTimeStamp beginDrawingTimestamp, + HighResTimeStamp commitTimestamp, + HighResTimeStamp endDrawingTimestamp) + : id(id), + threadId(threadId), + beginDrawingTimestamp(beginDrawingTimestamp), + commitTimestamp(commitTimestamp), + endDrawingTimestamp(endDrawingTimestamp) + { + } + + /** + * Unique ID of the sequence, used by Chrome DevTools Frontend to identify the events that form one sequence. + */ + uint64_t id; + + /** + * The ID of the native thread that is associated with the frame. + */ + ThreadId threadId; + + HighResTimeStamp beginDrawingTimestamp; + HighResTimeStamp commitTimestamp; + HighResTimeStamp endDrawingTimestamp; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfile.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfile.h new file mode 100644 index 000000000000..fd32cb3c2257 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfile.h @@ -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. + */ + +#pragma once + +#include "FrameTimingSequence.h" +#include "InstanceTracingProfile.h" +#include "RuntimeSamplingProfile.h" + +#include + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * The final tracing profile for the given HostTarget. + * Contains all necessary information that is required to be be emitted as a series of Tracing.dataCollected CDP + * messages. + */ +struct HostTracingProfile { + // The ID of the OS-level process that this Trace Recording is associated + // with. + ProcessId processId; + + // The timestamp at which this Trace Recording started. + HighResTimeStamp startTime; + + // Frame timings captured on the Host side. + std::vector frameTimings; + + // All captured Instance Tracing Profiles during this Trace Recording. + std::vector instanceTracingProfiles; + + // All captured Runtime Sampling Profiles during this Trace Recording. + std::vector runtimeSamplingProfiles; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp new file mode 100644 index 000000000000..29f2d43633d2 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp @@ -0,0 +1,131 @@ +/* + * 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. + */ + +#include "HostTracingProfileSerializer.h" +#include "RuntimeSamplingProfileTraceEventSerializer.h" +#include "TraceEventGenerator.h" +#include "TraceEventSerializer.h" + +namespace facebook::react::jsinspector_modern::tracing { + +namespace { + +folly::dynamic generateNewChunk(uint16_t chunkSize) { + folly::dynamic chunk = folly::dynamic::array(); + chunk.reserve(chunkSize); + + return chunk; +} + +/** + * Hardcoded layer tree ID for all recorded frames. + * https://chromedevtools.github.io/devtools-protocol/tot/LayerTree/ + */ +constexpr int FALLBACK_LAYER_TREE_ID = 1; + +} // namespace + +/* static */ void HostTracingProfileSerializer::emitAsDataCollectedChunks( + HostTracingProfile&& hostTracingProfile, + const std::function& chunkCallback, + uint16_t traceEventsChunkSize, + uint16_t profileTraceEventsChunkSize) { + emitFrameTimings( + std::move(hostTracingProfile.frameTimings), + hostTracingProfile.processId, + hostTracingProfile.startTime, + chunkCallback, + traceEventsChunkSize); + + auto instancesProfiles = + std::move(hostTracingProfile.instanceTracingProfiles); + IdGenerator profileIdGenerator; + + for (auto& instanceProfile : instancesProfiles) { + emitPerformanceTraceEvents( + std::move(instanceProfile.performanceTraceEvents), + chunkCallback, + traceEventsChunkSize); + } + + RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( + std::move(hostTracingProfile.runtimeSamplingProfiles), + profileIdGenerator, + hostTracingProfile.startTime, + chunkCallback, + profileTraceEventsChunkSize); +} + +/* static */ void HostTracingProfileSerializer::emitPerformanceTraceEvents( + std::vector&& events, + const std::function& chunkCallback, + uint16_t chunkSize) { + folly::dynamic chunk = generateNewChunk(chunkSize); + + for (auto& event : events) { + if (chunk.size() == chunkSize) { + chunkCallback(std::move(chunk)); + chunk = generateNewChunk(chunkSize); + } + + chunk.push_back(TraceEventSerializer::serialize(std::move(event))); + } + + if (!chunk.empty()) { + chunkCallback(std::move(chunk)); + } +} + +/* static */ void HostTracingProfileSerializer::emitFrameTimings( + std::vector&& frameTimings, + ProcessId processId, + HighResTimeStamp recordingStartTimestamp, + const std::function& chunkCallback, + uint16_t chunkSize) { + if (frameTimings.empty()) { + return; + } + + folly::dynamic chunk = generateNewChunk(chunkSize); + auto setLayerTreeIdEvent = TraceEventGenerator::createSetLayerTreeIdEvent( + "", // Hardcoded frame name for the default (and only) layer. + FALLBACK_LAYER_TREE_ID, + processId, + frameTimings.front().threadId, + recordingStartTimestamp); + chunk.push_back( + TraceEventSerializer::serialize(std::move(setLayerTreeIdEvent))); + + for (const auto& frameTimingSequence : frameTimings) { + if (chunk.size() >= chunkSize) { + chunkCallback(std::move(chunk)); + chunk = generateNewChunk(chunkSize); + } + + auto [beginDrawingEvent, commitEvent, endDrawingEvent] = + TraceEventGenerator::createFrameTimingsEvents( + frameTimingSequence.id, + FALLBACK_LAYER_TREE_ID, + frameTimingSequence.beginDrawingTimestamp, + frameTimingSequence.commitTimestamp, + frameTimingSequence.endDrawingTimestamp, + processId, + frameTimingSequence.threadId); + + chunk.push_back( + TraceEventSerializer::serialize(std::move(beginDrawingEvent))); + chunk.push_back(TraceEventSerializer::serialize(std::move(commitEvent))); + chunk.push_back( + TraceEventSerializer::serialize(std::move(endDrawingEvent))); + } + + if (!chunk.empty()) { + chunkCallback(std::move(chunk)); + } +} + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h similarity index 54% rename from packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.h rename to packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h index 3936eab663b1..1007898ff912 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h @@ -7,8 +7,9 @@ #pragma once +#include "FrameTimingSequence.h" +#include "HostTracingProfile.h" #include "TraceEvent.h" -#include "TraceRecordingState.h" #include #include @@ -16,27 +17,34 @@ namespace facebook::react::jsinspector_modern::tracing { /** - * A serializer for TraceRecordingState that can be used for tranforming the - * recording into sequence of serialized Trace Events. + * A serializer for HostTracingProfile that can be used for transforming the + * profile into sequence of serialized Trace Events. */ -class TraceRecordingStateSerializer { +class HostTracingProfileSerializer { public: /** - * Transforms the recording into a sequence of serialized Trace Events, which - * is split in chunks of sizes \p performanceTraceEventsChunkSize or + * Transforms the profile into a sequence of serialized Trace Events, which + * is split in chunks of sizes \p traceEventsChunkSize or * \p profileTraceEventsChunkSize, depending on type, and sent with \p * chunkCallback. */ static void emitAsDataCollectedChunks( - TraceRecordingState &&recording, + HostTracingProfile &&hostTracingProfile, const std::function &chunkCallback, - uint16_t performanceTraceEventsChunkSize, + uint16_t traceEventsChunkSize, uint16_t profileTraceEventsChunkSize); static void emitPerformanceTraceEvents( std::vector &&events, const std::function &chunkCallback, uint16_t chunkSize); + + static void emitFrameTimings( + std::vector &&frameTimings, + ProcessId processId, + HighResTimeStamp recordingStartTimestamp, + const std::function &chunkCallback, + uint16_t chunkSize); }; } // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp index 06a404547acc..67c1a1df644c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp @@ -7,6 +7,7 @@ #include "PerformanceTracer.h" #include "Timing.h" +#include "TraceEventGenerator.h" #include "TraceEventSerializer.h" #include "TracingCategory.h" @@ -794,20 +795,13 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( }); }, [&](PerformanceTracerSetLayerTreeIdEvent&& event) { - folly::dynamic data = folly::dynamic::object("frame", event.frame)( - "layerTreeId", event.layerTreeId); - events.emplace_back( - TraceEvent{ - .name = "SetLayerTreeId", - .cat = {Category::Timeline}, - .ph = 'I', - .ts = event.start, - .pid = processId_, - .s = 't', - .tid = event.threadId, - .args = folly::dynamic::object("data", std::move(data)), - }); + TraceEventGenerator::createSetLayerTreeIdEvent( + std::move(event.frame), + event.layerTreeId, + processId_, + event.threadId, + event.start)); }, [&](PerformanceTracerFrameBeginDrawEvent&& event) { folly::dynamic data = folly::dynamic::object( diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TimeWindowedBuffer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TimeWindowedBuffer.h new file mode 100644 index 000000000000..6951c815f48e --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TimeWindowedBuffer.h @@ -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. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * The currentBufferStartTime_ is initialized once first element is pushed. + */ +constexpr HighResTimeStamp kCurrentBufferStartTimeUninitialized = HighResTimeStamp::min(); + +template +class TimeWindowedBuffer { + public: + using TimestampAccessor = std::function; + + TimeWindowedBuffer() : timestampAccessor_(std::nullopt), windowSize_(std::nullopt) {} + + TimeWindowedBuffer(TimestampAccessor timestampAccessor, HighResDuration windowSize) + : timestampAccessor_(std::move(timestampAccessor)), windowSize_(windowSize) + { + } + + void push(const T &element) + { + if (timestampAccessor_) { + auto timestamp = (*timestampAccessor_)(element); + enqueueElement(element, timestamp); + } else { + enqueueElement(element, HighResTimeStamp::now()); + } + } + + void push(T &&element) + { + if (timestampAccessor_) { + auto timestamp = (*timestampAccessor_)(element); + enqueueElement(std::move(element), timestamp); + } else { + enqueueElement(std::move(element), HighResTimeStamp::now()); + } + } + + void clear() + { + primaryBuffer_.clear(); + alternativeBuffer_.clear(); + currentBufferIndex_ = BufferIndex::Primary; + currentBufferStartTime_ = kCurrentBufferStartTimeUninitialized; + } + + /** + * Forces immediate removal of elements that are outside the time window. + * The right boundary of the window is the reference timestamp passed as an argument. + */ + std::vector pruneExpiredAndExtract(HighResTimeStamp windowRightBoundary = HighResTimeStamp::now()) + { + std::vector result; + + for (auto &wrappedElement : getPreviousBuffer()) { + if (isInsideTimeWindow(wrappedElement, windowRightBoundary)) { + result.push_back(std::move(wrappedElement.element)); + } + } + + for (auto &wrappedElement : getCurrentBuffer()) { + if (isInsideTimeWindow(wrappedElement, windowRightBoundary)) { + result.push_back(std::move(wrappedElement.element)); + } + } + + clear(); + return result; + } + + private: + enum class BufferIndex { Primary, Alternative }; + + struct TimestampedElement { + T element; + HighResTimeStamp timestamp; + }; + + std::vector &getCurrentBuffer() + { + return currentBufferIndex_ == BufferIndex::Primary ? primaryBuffer_ : alternativeBuffer_; + } + + std::vector &getPreviousBuffer() + { + return currentBufferIndex_ == BufferIndex::Primary ? alternativeBuffer_ : primaryBuffer_; + } + + void enqueueElement(const T &element, HighResTimeStamp timestamp) + { + if (windowSize_) { + if (currentBufferStartTime_ == kCurrentBufferStartTimeUninitialized) { + currentBufferStartTime_ = timestamp; + } else if (timestamp > currentBufferStartTime_ + *windowSize_) { + // We moved past the current buffer. We need to switch the other buffer as current. + currentBufferIndex_ = + currentBufferIndex_ == BufferIndex::Primary ? BufferIndex::Alternative : BufferIndex::Primary; + getCurrentBuffer().clear(); + currentBufferStartTime_ = timestamp; + } + } + + getCurrentBuffer().push_back({element, timestamp}); + } + + void enqueueElement(T &&element, HighResTimeStamp timestamp) + { + if (windowSize_) { + if (currentBufferStartTime_ == kCurrentBufferStartTimeUninitialized) { + currentBufferStartTime_ = timestamp; + } else if (timestamp > currentBufferStartTime_ + *windowSize_) { + // We moved past the current buffer. We need to switch the other buffer as current. + currentBufferIndex_ = + currentBufferIndex_ == BufferIndex::Primary ? BufferIndex::Alternative : BufferIndex::Primary; + getCurrentBuffer().clear(); + currentBufferStartTime_ = timestamp; + } + } + + getCurrentBuffer().push_back({std::move(element), timestamp}); + } + + bool isInsideTimeWindow(const TimestampedElement &element, HighResTimeStamp windowRightBoundary) const + { + if (!windowSize_) { + return true; + } + + return element.timestamp >= windowRightBoundary - *windowSize_ && element.timestamp <= windowRightBoundary; + } + + std::optional timestampAccessor_; + std::optional windowSize_; + + std::vector primaryBuffer_; + std::vector alternativeBuffer_; + BufferIndex currentBufferIndex_ = BufferIndex::Primary; + HighResTimeStamp currentBufferStartTime_{kCurrentBufferStartTimeUninitialized}; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp new file mode 100644 index 000000000000..d508b8507bc3 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp @@ -0,0 +1,80 @@ +/* + * 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. + */ + +#include "TraceEventGenerator.h" +#include "TracingCategory.h" + +namespace facebook::react::jsinspector_modern::tracing { + +/* static */ TraceEvent TraceEventGenerator::createSetLayerTreeIdEvent( + std::string frame, + int layerTreeId, + ProcessId processId, + ThreadId threadId, + HighResTimeStamp timestamp) { + folly::dynamic data = folly::dynamic::object("frame", std::move(frame))( + "layerTreeId", layerTreeId); + + return TraceEvent{ + .name = "SetLayerTreeId", + .cat = {Category::Timeline}, + .ph = 'I', + .ts = timestamp, + .pid = processId, + .s = 't', + .tid = threadId, + .args = folly::dynamic::object("data", std::move(data)), + }; +} + +/* static */ std::tuple +TraceEventGenerator::createFrameTimingsEvents( + uint64_t sequenceId, + int layerTreeId, + HighResTimeStamp beginDrawingTimestamp, + HighResTimeStamp commitTimestamp, + HighResTimeStamp endDrawingTimestamp, + ProcessId processId, + ThreadId threadId) { + folly::dynamic args = folly::dynamic::object("frameSeqId", sequenceId)( + "layerTreeId", layerTreeId); + + auto beginEvent = TraceEvent{ + .name = "BeginFrame", + .cat = {Category::Timeline}, + .ph = 'I', + .ts = beginDrawingTimestamp, + .pid = processId, + .s = 't', + .tid = threadId, + .args = args, + }; + auto commitEvent = TraceEvent{ + .name = "Commit", + .cat = {Category::Timeline}, + .ph = 'I', + .ts = commitTimestamp, + .pid = processId, + .s = 't', + .tid = threadId, + .args = args, + }; + auto drawEvent = TraceEvent{ + .name = "DrawFrame", + .cat = {Category::Timeline}, + .ph = 'I', + .ts = endDrawingTimestamp, + .pid = processId, + .s = 't', + .tid = threadId, + .args = args, + }; + + return {std::move(beginEvent), std::move(commitEvent), std::move(drawEvent)}; +} + +}; // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h new file mode 100644 index 000000000000..01fb14aa40ec --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h @@ -0,0 +1,47 @@ +/* + * 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 "TraceEvent.h" + +#include + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * This class encapsulates the logic for generating canonical trace events that will be serialized and sent as part of + * Tracing.dataCollected CDP message. + */ +class TraceEventGenerator { + public: + /** + * Creates canonical "SetLayerTreeId" trace event. + */ + static TraceEvent createSetLayerTreeIdEvent( + std::string frame, + int layerTreeId, + ProcessId processId, + ThreadId threadId, + HighResTimeStamp timestamp); + + /** + * Creates canonical "BeginFrame", "Commit", "DrawFrame" trace events. + */ + static std::tuple createFrameTimingsEvents( + uint64_t sequenceId, + int layerTreeId, + HighResTimeStamp beginDrawingTimestamp, + HighResTimeStamp commitTimestamp, + HighResTimeStamp endDrawingTimestamp, + ProcessId processId, + ThreadId threadId); +}; + +}; // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h index f46bae826efb..db0acc40b76c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h @@ -18,17 +18,22 @@ namespace facebook::react::jsinspector_modern::tracing { +/** + * The global state for the given Trace Recording. + * Shared with Tracing Agents, which could use it to stash the corresponding target profiles during reloads. + */ struct TraceRecordingState { + TraceRecordingState( + tracing::Mode tracingMode, + std::set enabledCategories, + std::optional windowSize = std::nullopt) + : mode(tracingMode), enabledCategories(std::move(enabledCategories)), windowSize(windowSize) + { + } + // The mode of this Trace Recording. tracing::Mode mode; - // The ID of the OS-level process that this Trace Recording is associated - // with. - ProcessId processId = oscompat::getCurrentProcessId(); - - // The timestamp at which this Trace Recording started. - HighResTimeStamp startTime; - // All captured Runtime Sampling Profiles during this Trace Recording. std::vector runtimeSamplingProfiles{}; @@ -37,6 +42,9 @@ struct TraceRecordingState { // The list of categories that are enabled for this recording. std::set enabledCategories; + + // The size of the time window for this recording. + std::optional windowSize; }; } // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.cpp deleted file mode 100644 index 09fe7368404c..000000000000 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.cpp +++ /dev/null @@ -1,68 +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. - */ - -#include "TraceRecordingStateSerializer.h" -#include "RuntimeSamplingProfileTraceEventSerializer.h" -#include "TraceEventSerializer.h" - -namespace facebook::react::jsinspector_modern::tracing { - -namespace { - -folly::dynamic generateNewChunk(uint16_t chunkSize) { - folly::dynamic chunk = folly::dynamic::array(); - chunk.reserve(chunkSize); - - return chunk; -} - -} // namespace - -/* static */ void TraceRecordingStateSerializer::emitAsDataCollectedChunks( - TraceRecordingState&& recording, - const std::function& chunkCallback, - uint16_t performanceTraceEventsChunkSize, - uint16_t profileTraceEventsChunkSize) { - auto instancesProfiles = std::move(recording.instanceTracingProfiles); - IdGenerator profileIdGenerator; - - for (auto& instanceProfile : instancesProfiles) { - emitPerformanceTraceEvents( - std::move(instanceProfile.performanceTraceEvents), - chunkCallback, - performanceTraceEventsChunkSize); - } - - RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( - std::move(recording.runtimeSamplingProfiles), - profileIdGenerator, - recording.startTime, - chunkCallback, - profileTraceEventsChunkSize); -} - -/* static */ void TraceRecordingStateSerializer::emitPerformanceTraceEvents( - std::vector&& events, - const std::function& chunkCallback, - uint16_t chunkSize) { - folly::dynamic chunk = generateNewChunk(chunkSize); - - for (auto& event : events) { - if (chunk.size() == chunkSize) { - chunkCallback(std::move(chunk)); - chunk = generateNewChunk(chunkSize); - } - - chunk.push_back(TraceEventSerializer::serialize(std::move(event))); - } - - if (!chunk.empty()) { - chunkCallback(std::move(chunk)); - } -} - -} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/TimeWindowedBufferTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/TimeWindowedBufferTest.cpp new file mode 100644 index 000000000000..03bed9a98303 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/TimeWindowedBufferTest.cpp @@ -0,0 +1,352 @@ +/* + * 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. + */ + +#include + +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +// Test structure with timestamp field +struct TestEvent { + int value; + HighResTimeStamp timestamp; + + bool operator==(const TestEvent& other) const { + return value == other.value; + } +}; + +// ============================================================================ +// Tests for unbounded buffer (no timestamp accessor) +// ============================================================================ + +TEST(TimeWindowedBufferTest, DefaultConstructorCreatesEmptyBuffer) { + TimeWindowedBuffer buffer; + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +TEST(TimeWindowedBufferTest, PushAddsElementsToUnboundedBuffer) { + TimeWindowedBuffer buffer; + buffer.push(1); + buffer.push(2); + buffer.push(3); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 3u); + EXPECT_EQ(result[0], 1); + EXPECT_EQ(result[1], 2); + EXPECT_EQ(result[2], 3); +} + +TEST(TimeWindowedBufferTest, UnboundedBufferPreservesAllElements) { + TimeWindowedBuffer buffer; + for (int i = 0; i < 100; ++i) { + buffer.push(i); + } + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 100u); + for (int i = 0; i < 100; ++i) { + EXPECT_EQ(result[i], i); + } +} + +TEST(TimeWindowedBufferTest, ClearEmptiesBuffer) { + TimeWindowedBuffer buffer; + buffer.push(1); + buffer.push(2); + buffer.push(3); + + buffer.clear(); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +TEST(TimeWindowedBufferTest, PushRvalueReference) { + TimeWindowedBuffer buffer; + std::string str = "test"; + buffer.push(std::move(str)); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], "test"); +} + +// ============================================================================ +// Tests for time-windowed buffer (with timestamp accessor) +// ============================================================================ + +TEST(TimeWindowedBufferTest, TimeWindowedBufferCreation) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +TEST(TimeWindowedBufferTest, TimeWindowedBufferAddsElements) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(1000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(100)}); + + auto result = buffer.pruneExpiredAndExtract(baseTime + windowSize); + EXPECT_EQ(result.size(), 2u); + EXPECT_EQ(result[0].value, 1); + EXPECT_EQ(result[1].value, 2); +} + +TEST(TimeWindowedBufferTest, ElementsWithinWindowArePreserved) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(500); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(100)}); + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(200)}); + buffer.push( + TestEvent{ + .value = 4, + .timestamp = baseTime + HighResDuration::fromMilliseconds(300)}); + + // Extract with window [300ms, 800ms] - only event at 300ms should be included + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(800)); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].value, 4); +} + +TEST(TimeWindowedBufferTest, BufferSwitchingWhenWindowExceeded) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add events within first window + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(50)}); + + // Add event that exceeds the window - should trigger buffer switch + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(150)}); + + // Extract events within the window using reference point at 250ms + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(250)); + + // Events from 150ms should be in the window (250 - 100 = 150) + EXPECT_GE(result.size(), 1u); +} + +TEST(TimeWindowedBufferTest, PruneExpiredFiltersOldElements) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add events in first window + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(50)}); + + // Move to second window + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(150)}); + + // Move to third window + buffer.push( + TestEvent{ + .value = 4, + .timestamp = baseTime + HighResDuration::fromMilliseconds(300)}); + + // Extract with reference at 300ms: window is [200ms, 300ms] + // Only event 4 at 300ms should be within window + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(300)); + + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].value, 4); +} + +TEST(TimeWindowedBufferTest, OutOfOrderTimestampsAreHandled) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(10000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add events out of order (by timestamp) + buffer.push( + TestEvent{ + .value = 1, + .timestamp = baseTime + HighResDuration::fromMilliseconds(100)}); + buffer.push( + TestEvent{.value = 2, .timestamp = baseTime}); // Earlier timestamp + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(200)}); + + // Extract with window [200ms, 10200ms] - only event at 200ms should be + // included + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(10200)); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].value, 3); +} + +TEST(TimeWindowedBufferTest, ClearResetsTimeWindowedBuffer) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(200)}); + + buffer.clear(); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +// ============================================================================ +// Tests for edge cases +// ============================================================================ + +TEST(TimeWindowedBufferTest, SingleElementBuffer) { + TimeWindowedBuffer buffer; + buffer.push(42); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 42); +} + +TEST(TimeWindowedBufferTest, LargeNumberOfElements) { + TimeWindowedBuffer buffer; + + const int count = 10000; + for (int i = 0; i < count; ++i) { + buffer.push(i); + } + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), static_cast(count)); + EXPECT_EQ(result[0], 0); + EXPECT_EQ(result[count - 1], count - 1); +} + +TEST(TimeWindowedBufferTest, VerySmallTimeWindow) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromNanoseconds(1000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + + // Next event with significant time difference should trigger switch + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(1)}); + + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(1)); + EXPECT_GE(result.size(), 1u); +} + +TEST(TimeWindowedBufferTest, VeryLargeTimeWindow) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(3600000); // 1 hour + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add many events spread over time + for (int i = 0; i < 100; ++i) { + buffer.push( + TestEvent{ + .value = i, + .timestamp = + baseTime + HighResDuration::fromMilliseconds(i * 10000)}); + } + + // All events should still be in the window + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(100 * 10000)); + EXPECT_EQ(result.size(), 100u); +} + +// ============================================================================ +// Tests for complex types +// ============================================================================ + +TEST(TimeWindowedBufferTest, WorksWithComplexTypes) { + struct ComplexType { + std::string name; + std::vector data; + HighResTimeStamp timestamp; + }; + + auto timestampAccessor = [](const ComplexType& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(1000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push( + ComplexType{.name = "test", .data = {1, 2, 3}, .timestamp = baseTime}); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].name, "test"); + EXPECT_EQ(result[0].data.size(), 3u); +} + +} // namespace facebook::react::jsinspector_modern::tracing