diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 6a8ae6000..58c9b521f 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -30,7 +30,8 @@ class DdSdkImplementation( private val reactContext: ReactApplicationContext, private val datadog: DatadogWrapper = DatadogSDKWrapper(), private val ddTelemetry: DdTelemetry = DdTelemetry(), - private val uiThreadExecutor: UiThreadExecutor = ReactUiThreadExecutor() + private val uiThreadExecutor: UiThreadExecutor = ReactUiThreadExecutor(), + private val jsThreadExecutor: JsThreadExecutor = ReactJsThreadExecutor(reactContext) ) { internal val appContext: Context = reactContext.applicationContext internal val initialized = AtomicBoolean(false) @@ -327,10 +328,8 @@ class DdSdkImplementation( ddSdkConfiguration: DdSdkConfiguration ): FrameRateProvider? { val frameTimeCallback = buildFrameTimeCallback(ddSdkConfiguration) ?: return null - val frameRateProvider = FrameRateProvider(frameTimeCallback, uiThreadExecutor) - reactContext.runOnJSQueueThread { - frameRateProvider.start() - } + val frameRateProvider = FrameRateProvider(frameTimeCallback, jsThreadExecutor) + frameRateProvider.start() return frameRateProvider } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/FrameRateProvider.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/FrameRateProvider.kt index 99a8c1f7f..7366f2c8d 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/FrameRateProvider.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/FrameRateProvider.kt @@ -10,11 +10,11 @@ import android.view.Choreographer internal class FrameRateProvider( reactFrameRateCallback: ((Double) -> Unit), - uiThreadExecutor: UiThreadExecutor + jsThreadExecutor: JsThreadExecutor ) { private val frameCallback: FpsFrameCallback = FpsFrameCallback( reactFrameRateCallback, - uiThreadExecutor + jsThreadExecutor ) fun start() { @@ -29,7 +29,7 @@ internal class FrameRateProvider( internal class FpsFrameCallback( private val reactFrameRateCallback: ((Double) -> Unit), - private val uiThreadExecutor: UiThreadExecutor + private val jsThreadExecutor: JsThreadExecutor ) : Choreographer.FrameCallback { private var choreographer: Choreographer? = null @@ -43,16 +43,24 @@ internal class FpsFrameCallback( choreographer?.postFrameCallback(this) } + @Suppress("SwallowedException") fun start() { - uiThreadExecutor.runOnUiThread { - choreographer = Choreographer.getInstance() - choreographer?.postFrameCallback(this@FpsFrameCallback) + // Choreographer is thread-local: we register on the JS thread so frame callbacks + // measure JS frame timings, matching the iOS CADisplayLink-on-JS-RunLoop approach. + jsThreadExecutor.runOnJsThread { + try { + val instance = Choreographer.getInstance() + instance.removeFrameCallback(this@FpsFrameCallback) + choreographer = instance + instance.postFrameCallback(this@FpsFrameCallback) + } catch (e: IllegalStateException) { + // Choreographer requires a Looper; guard defensively in case the JS thread lacks one. + } } } fun stop() { - uiThreadExecutor.runOnUiThread { - choreographer = Choreographer.getInstance() + jsThreadExecutor.runOnJsThread { choreographer?.removeFrameCallback(this@FpsFrameCallback) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/JsThreadExecutor.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/JsThreadExecutor.kt new file mode 100644 index 000000000..651ec5453 --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/JsThreadExecutor.kt @@ -0,0 +1,27 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.facebook.react.bridge.ReactApplicationContext + +/** + * Simple JS Thread Executor. By default it is based on [ReactApplicationContext.runOnJSQueueThread]. + */ +interface JsThreadExecutor { + /** + * Runs the given runnable on the JS Thread. + */ + fun runOnJsThread(runnable: Runnable) +} + +internal class ReactJsThreadExecutor( + private val reactContext: ReactApplicationContext +) : JsThreadExecutor { + override fun runOnJsThread(runnable: Runnable) { + reactContext.runOnJSQueueThread(runnable) + } +} diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 758856060..cae3539c7 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -37,6 +37,7 @@ import com.datadog.android.trace.TraceConfiguration import com.datadog.android.trace.TracingHeaderType import com.datadog.tools.unit.GenericAssert.Companion.assertThat import com.datadog.tools.unit.MockRumMonitor +import com.datadog.tools.unit.TestJsThreadExecutor import com.datadog.tools.unit.TestUiThreadExecutor import com.datadog.tools.unit.forge.BaseConfigurator import com.datadog.tools.unit.setStaticValue @@ -165,15 +166,12 @@ internal class DdSdkTest { 0 ) ) doReturn mockPackageInfo - whenever(mockReactContext.runOnJSQueueThread(any())).thenAnswer { answer -> - answer.getArgument(0).run() - true - } testedBridgeSdk = DdSdkImplementation( mockReactContext, mockDatadog, mockDdTelemetry, - TestUiThreadExecutor() + TestUiThreadExecutor(), + TestJsThreadExecutor() ) DatadogSDKWrapperStorage.onInitializedListeners.clear() diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/TestJsThreadExecutor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/TestJsThreadExecutor.kt new file mode 100644 index 000000000..2ba91cb5d --- /dev/null +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/TestJsThreadExecutor.kt @@ -0,0 +1,16 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.tools.unit + +import com.datadog.reactnative.JsThreadExecutor + +internal class TestJsThreadExecutor : JsThreadExecutor { + override fun runOnJsThread(runnable: Runnable) { + // Run immediately in the same thread for tests + runnable.run() + } +}