Skip to content

Commit

Permalink
Implemented FrameRateProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-saia-datadog committed Jul 16, 2024
1 parent 8b325de commit be526a0
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 36 deletions.
3 changes: 3 additions & 0 deletions packages/core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ dependencies {
testImplementation "com.github.xgouchet.Elmyr:jvm:1.3.1"
testImplementation "org.mockito.kotlin:mockito-kotlin:5.1.0"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation "io.mockk:mockk:1.13.2" // For JVM unit tests
androidTestImplementation "io.mockk:mockk-android:1.13.2" // For Android instrumented tests

unmock 'org.robolectric:android-all:4.4_r1-robolectric-r2'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ package com.datadog.reactnative

import android.content.Context
import android.util.Log
import android.view.Choreographer
import com.datadog.android.privacy.TrackingConsent
import com.datadog.android.rum.configuration.VitalsUpdateFrequency
import com.datadog.android.rum.RumPerformanceMetric
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
Expand All @@ -21,12 +21,12 @@ import java.util.concurrent.atomic.AtomicBoolean

/** The entry point to initialize Datadog's features. */
class DdSdkImplementation(
reactContext: ReactApplicationContext,
private val reactContext: ReactApplicationContext,
private val datadog: DatadogWrapper = DatadogSDKWrapper()
) {
internal val appContext: Context = reactContext.applicationContext
internal val reactContext: ReactApplicationContext = reactContext
internal val initialized = AtomicBoolean(false)
private var frameRateProvider: FrameRateProvider? = null

// region DdSdk

Expand All @@ -39,7 +39,23 @@ class DdSdkImplementation(

val nativeInitialization = DdSdkNativeInitialization(appContext, datadog)
nativeInitialization.initialize(ddSdkConfiguration)
monitorJsRefreshRate(ddSdkConfiguration)

this.frameRateProvider = createFrameRateProvider(ddSdkConfiguration)

reactContext.addLifecycleEventListener(object : LifecycleEventListener {
override fun onHostResume() {
frameRateProvider?.start()
}

override fun onHostPause() {
frameRateProvider?.stop()
}

override fun onHostDestroy() {
frameRateProvider?.stop()
}
})

initialized.set(true)

promise.resolve(null)
Expand Down Expand Up @@ -150,26 +166,16 @@ class DdSdkImplementation(
}
}

private fun handlePostFrameCallbackError(e: IllegalStateException) {
datadog.telemetryError(e.message ?: MONITOR_JS_ERROR_MESSAGE, e)
}

private fun monitorJsRefreshRate(ddSdkConfiguration: DdSdkConfiguration) {
val frameTimeCallback = buildFrameTimeCallback(ddSdkConfiguration)
if (frameTimeCallback != null) {
reactContext.runOnJSQueueThread {
val vitalFrameCallback =
VitalFrameCallback(frameTimeCallback, ::handlePostFrameCallbackError) {
initialized.get()
}
try {
Choreographer.getInstance().postFrameCallback(vitalFrameCallback)
} catch (e: IllegalStateException) {
// This should never happen as the React Native thread always has a Looper
handlePostFrameCallbackError(e)
}
}
private fun createFrameRateProvider(
ddSdkConfiguration: DdSdkConfiguration
): FrameRateProvider? {
val frameTimeCallback = buildFrameTimeCallback(ddSdkConfiguration) ?: return null
val frameRateProvider = FrameRateProvider(frameTimeCallback)
reactContext.runOnJSQueueThread {
frameRateProvider.start()
}

return frameRateProvider
}

private fun buildFrameTimeCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.datadog.reactnative

import com.facebook.react.modules.core.ChoreographerCompat

internal class FrameRateProvider(
reactFrameRateCallback: ((Double) -> Unit)?
) {
private val frameCallback: FpsFrameCallback = FpsFrameCallback(reactFrameRateCallback)

fun start() {
frameCallback.reset()
frameCallback.start()
}

fun stop() {
frameCallback.stop()
}
}

internal class FpsFrameCallback(
private val reactFrameRateCallback: ((Double) -> Unit)?
) :
ChoreographerCompat.FrameCallback() {
private var mChoreographer: ChoreographerCompat? = null

private var mLastFrameTime = -1L

override fun doFrame(time: Long) {
if (mLastFrameTime != -1L) {
reactFrameRateCallback?.let { it((time - mLastFrameTime).toDouble()) }
}
mLastFrameTime = time
mChoreographer?.postFrameCallback(this)
}

fun start() {
UiThreadExecutor.Provider.uiThreadExecutor.runOnUiThread {
mChoreographer = ChoreographerCompat.getInstance()
mChoreographer?.postFrameCallback(this@FpsFrameCallback)
}
}

fun stop() {
UiThreadExecutor.Provider.uiThreadExecutor.runOnUiThread {
mChoreographer = ChoreographerCompat.getInstance()
mChoreographer?.removeFrameCallback(this@FpsFrameCallback)
}
}

fun reset() {
mLastFrameTime = -1L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.datadog.reactnative

import com.facebook.react.bridge.UiThreadUtil

internal interface UiThreadExecutor {
fun runOnUiThread(runnable: Runnable)

object Provider {
val uiThreadExecutor: UiThreadExecutor by lazy {
if (isRunningInTest()) {
TestUiThreadExecutor()
} else {
RealUiThreadExecutor()
}
}

private fun isRunningInTest(): Boolean {
return System.getProperty("IS_UNIT_TEST") == "true"
}
}
}

internal class RealUiThreadExecutor : UiThreadExecutor {
override fun runOnUiThread(runnable: Runnable) {
UiThreadUtil.runOnUiThread(runnable)
}
}

internal class TestUiThreadExecutor : UiThreadExecutor {
override fun runOnUiThread(runnable: Runnable) {
// Run immediately in the same thread for tests
runnable.run()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.datadog.reactnative

import android.content.pm.PackageInfo
import android.os.Looper
import android.util.Log
import android.view.Choreographer
import com.datadog.android.DatadogSite
Expand Down Expand Up @@ -36,6 +37,7 @@ import com.datadog.tools.unit.toReadableMap
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.modules.core.ChoreographerCompat
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.annotation.AdvancedForgery
import fr.xgouchet.elmyr.annotation.BoolForgery
Expand All @@ -47,6 +49,10 @@ import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.annotation.StringForgeryType
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import java.util.Locale
import java.util.stream.Stream
import org.assertj.core.api.Assertions.assertThat
Expand Down Expand Up @@ -78,6 +84,13 @@ import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness

fun mockChoreographerCompatInstance(mock: ChoreographerCompat = mock()) {
ChoreographerCompat::class.java.setStaticValue(
"sInstance",
mock
)
}

fun mockChoreographerInstance(mock: Choreographer = mock()) {
Choreographer::class.java.setStaticValue(
"sThreadInstance",
Expand All @@ -96,7 +109,6 @@ fun mockChoreographerInstance(mock: Choreographer = mock()) {
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(value = BaseConfigurator::class)
internal class DdSdkTest {

lateinit var testedBridgeSdk: DdSdkImplementation

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
Expand Down Expand Up @@ -126,13 +138,30 @@ internal class DdSdkTest {
@Mock
lateinit var mockChoreographer: Choreographer

@Mock
lateinit var mockChoreographerCompat: ChoreographerCompat

@BeforeEach
fun `set up`() {
mockkStatic(Looper::class)

val looper = mockk<Looper> {
every { thread } returns Thread.currentThread()
}

every { Looper.getMainLooper() } returns looper

System.setProperty("IS_UNIT_TEST", "true")

whenever(mockDatadog.getRumMonitor()) doReturn mockRumMonitor
whenever(mockRumMonitor._getInternal()) doReturn mockRumInternalProxy

doNothing().whenever(mockChoreographer).postFrameCallback(any())
doNothing().whenever(mockChoreographerCompat).postFrameCallback(any())

mockChoreographerInstance(mockChoreographer)
mockChoreographerCompatInstance(mockChoreographerCompat)

whenever(mockReactContext.applicationContext) doReturn mockContext
whenever(mockContext.packageName) doReturn "packageName"
whenever(
Expand All @@ -154,6 +183,7 @@ internal class DdSdkTest {
@AfterEach
fun `tear down`() {
GlobalState.globalAttributes.clear()
unmockkAll()
}

// region initialize / nativeCrashReportEnabled
Expand Down Expand Up @@ -1561,9 +1591,9 @@ internal class DdSdkTest {
.hasField("featureConfiguration") {
it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.RARE)
}
argumentCaptor<Choreographer.FrameCallback> {
verify(mockChoreographer).postFrameCallback(capture())
assertThat(firstValue).isInstanceOf(VitalFrameCallback::class.java)
argumentCaptor<ChoreographerCompat.FrameCallback> {
verify(mockChoreographerCompat).postFrameCallback(capture())
assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java)
}
}

Expand All @@ -1572,7 +1602,7 @@ internal class DdSdkTest {
@Forgery configuration: DdSdkConfiguration
) {
// Given
doThrow(IllegalStateException()).whenever(mockChoreographer).postFrameCallback(any())
doThrow(IllegalStateException()).whenever(mockChoreographerCompat).postFrameCallback(any())
val bridgeConfiguration = configuration.copy(
vitalsUpdateFrequency = "NEVER",
longTaskThresholdMs = 0.0
Expand Down Expand Up @@ -1600,7 +1630,7 @@ internal class DdSdkTest {
.hasField("featureConfiguration") {
it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.NEVER)
}
verifyNoInteractions(mockChoreographer)
verifyNoInteractions(mockChoreographerCompat)
}

@Test
Expand Down Expand Up @@ -1640,9 +1670,9 @@ internal class DdSdkTest {
.hasField("featureConfiguration") {
it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.AVERAGE)
}
argumentCaptor<Choreographer.FrameCallback> {
verify(mockChoreographer).postFrameCallback(capture())
assertThat(firstValue).isInstanceOf(VitalFrameCallback::class.java)
argumentCaptor<ChoreographerCompat.FrameCallback> {
verify(mockChoreographerCompat).postFrameCallback(capture())
assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java)

// When
firstValue.doFrame(timestampNs)
Expand Down Expand Up @@ -1678,8 +1708,8 @@ internal class DdSdkTest {
testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
argumentCaptor<Choreographer.FrameCallback> {
verify(mockChoreographer).postFrameCallback(capture())
argumentCaptor<ChoreographerCompat.FrameCallback> {
verify(mockChoreographerCompat).postFrameCallback(capture())

// When
firstValue.doFrame(timestampNs)
Expand Down Expand Up @@ -1715,8 +1745,8 @@ internal class DdSdkTest {
testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
argumentCaptor<Choreographer.FrameCallback> {
verify(mockChoreographer).postFrameCallback(capture())
argumentCaptor<ChoreographerCompat.FrameCallback> {
verify(mockChoreographerCompat).postFrameCallback(capture())

// When
firstValue.doFrame(timestampNs)
Expand Down

0 comments on commit be526a0

Please sign in to comment.