Skip to content

Commit

Permalink
Implemented FrameRateProvider (W.I.P)
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-saia-datadog committed Jul 4, 2024
1 parent 8b325de commit c3e62cb
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 31 deletions.
1 change: 0 additions & 1 deletion example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {startReactNativeNavigation} from './src/WixApp';
import {name as appName} from './app.json';
import {navigation as navigationLib} from './app.json';
import {initializeDatadog} from './src/ddUtils';
import {TrackingConsent} from '@datadog/mobile-react-native';
import {Navigation} from 'react-native-navigation';

console.log('Starting Application with navigation library: ' + navigationLib);
Expand Down
8 changes: 6 additions & 2 deletions example/src/ddUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) {
config.sessionSamplingRate = 100
config.serviceName = "com.datadoghq.reactnative.sample"
config.verbosity = SdkVerbosity.DEBUG;
config.resourceEventMapper = (event) => {
console.log(event);
console.log(JSON.stringify(event, null, 4));
return event
}

return config
}
Expand All @@ -35,7 +40,7 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) {
}

// Legacy SDK Setup
export function initializeDatadog(trackingConsent: TrackingConsent) {
export function initializeDatadog() {

const config = new DdSdkReactNativeConfiguration(
CLIENT_TOKEN,
Expand All @@ -44,7 +49,6 @@ export function initializeDatadog(trackingConsent: TrackingConsent) {
true,
true,
true,
trackingConsent
)
config.nativeCrashReportEnabled = true
config.sampleRate = 100
Expand Down
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 @@ -150,24 +150,12 @@ 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)
val frameRateProvider = FrameRateProvider(frameTimeCallback, reactContext)
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)
}
frameRateProvider.start()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package com.datadog.reactnative

import com.facebook.infer.annotation.Assertions
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.modules.core.ChoreographerCompat
import com.facebook.react.modules.debug.DidJSUpdateUiDuringFrameDetector
import com.facebook.react.modules.debug.FpsDebugFrameCallback
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener
import java.lang.ClassCastException
import java.util.TreeMap
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService

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

private var backgroundExecutor: ScheduledExecutorService? = null

private val monitor = Monitor(frameCallback, reactFrameRateCallback)

fun start() {
// Create an executor that executes tasks in a background thread.
val executor = Executors.newSingleThreadScheduledExecutor()
backgroundExecutor = executor
frameCallback.reset()
frameCallback.start()
monitor.start(executor)
}

fun stop() {
backgroundExecutor?.shutdown()
backgroundExecutor = null
frameCallback.stop()
monitor.stop()
}

internal class Monitor(
private val frameCallback: FpsFrameCallback,
private val reactFrameRateCallback: ((Double) -> Unit)?
): Runnable {

private var mShouldStop = false

override fun run() {
if (mShouldStop) {
return
}

// Send JS FPS info
reactFrameRateCallback?.let { it(frameCallback.jSFPS) }
}

fun start(backgroundExecutor: ScheduledExecutorService) {
mShouldStop = false
backgroundExecutor.execute(this)
}

fun stop() {
mShouldStop = true
}
}
}

internal class FpsFrameCallback(private val mReactContext: ReactContext) :
ChoreographerCompat.FrameCallback() {
private var mChoreographer: ChoreographerCompat? = null

private val mUIManagerModule: UIManagerModule? get() {
var uiManagerModule: UIManagerModule? = null
try {
uiManagerModule = mReactContext.getNativeModule(
UIManagerModule::class.java
)
} catch (_: ClassCastException) {}
return uiManagerModule
}

private val mDidJSUpdateUiDuringFrameDetector: DidJSUpdateUiDuringFrameDetector =
DidJSUpdateUiDuringFrameDetector()
private var mFirstFrameTime = -1L
private var mLastFrameTime = -1L
private var mNumFrameCallbacks = 0
private var mExpectedNumFramesPrev = 0
private var m4PlusFrameStutters = 0
private var mNumFrameCallbacksWithBatchDispatches = 0
private var mIsRecordingFpsInfoAtEachFrame = false
private var mTimeToFps: TreeMap<Long?, FpsDebugFrameCallback.FpsInfo?>? = null

val fPS: Double
get() = if (mLastFrameTime == mFirstFrameTime) 0.0 else numFrames.toDouble() * 1.0E9 / (mLastFrameTime - mFirstFrameTime).toDouble()
val jSFPS: Double
get() = if (mLastFrameTime == mFirstFrameTime) 0.0 else numJSFrames.toDouble() * 1.0E9 / (mLastFrameTime - mFirstFrameTime).toDouble()
val numFrames: Int
get() = mNumFrameCallbacks - 1
val numJSFrames: Int
get() = mNumFrameCallbacksWithBatchDispatches - 1
val expectedNumFrames: Int
get() {
val totalTimeMS = totalTimeMS.toDouble()
return (totalTimeMS / 16.9 + 1.0).toInt()
}
val totalTimeMS: Int
get() = (mLastFrameTime.toDouble() - mFirstFrameTime.toDouble()).toInt() / 1000000


override fun doFrame(l: Long) {
if (mFirstFrameTime == -1L) {
mFirstFrameTime = l
}
val lastFrameStartTime = mLastFrameTime
mLastFrameTime = l
if (mDidJSUpdateUiDuringFrameDetector.getDidJSHitFrameAndCleanup(lastFrameStartTime, l)) {
++mNumFrameCallbacksWithBatchDispatches
}
++mNumFrameCallbacks
val expectedNumFrames = expectedNumFrames
val framesDropped = expectedNumFrames - mExpectedNumFramesPrev - 1
if (framesDropped >= 4) {
++m4PlusFrameStutters
}

mExpectedNumFramesPrev = expectedNumFrames
if (mChoreographer != null) {
mChoreographer!!.postFrameCallback(this)
}
}

fun start() {
mReactContext.catalystInstance.addBridgeIdleDebugListener(mDidJSUpdateUiDuringFrameDetector)
mUIManagerModule?.setViewHierarchyUpdateDebugListener(mDidJSUpdateUiDuringFrameDetector)
UiThreadExecutor.Provider.uiThreadExecutor.runOnUiThread {
mChoreographer = ChoreographerCompat.getInstance()
mChoreographer?.postFrameCallback(this@FpsFrameCallback)
}
}

fun stop() {
mReactContext.catalystInstance.removeBridgeIdleDebugListener(
mDidJSUpdateUiDuringFrameDetector
)
mUIManagerModule?.setViewHierarchyUpdateDebugListener(null as NotThreadSafeViewHierarchyUpdateDebugListener?)
UiThreadExecutor.Provider.uiThreadExecutor.runOnUiThread {
mChoreographer = ChoreographerCompat.getInstance()
mChoreographer?.removeFrameCallback(this@FpsFrameCallback)
}
}

fun reset() {
mFirstFrameTime = -1L
mLastFrameTime = -1L
mNumFrameCallbacks = 0
m4PlusFrameStutters = 0
mNumFrameCallbacksWithBatchDispatches = 0
mIsRecordingFpsInfoAtEachFrame = false
mTimeToFps = null
}
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.datadog.reactnative

import com.facebook.react.bridge.UiThreadUtil

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"
}
}
}

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

class TestUiThreadExecutor : UiThreadExecutor {
override fun runOnUiThread(runnable: Runnable) {
// Run immediately in the same thread for tests
runnable.run()
}
}
Loading

0 comments on commit c3e62cb

Please sign in to comment.