Skip to content

Commit

Permalink
Adding basic monitor log
Browse files Browse the repository at this point in the history
  • Loading branch information
FranAguilera committed Feb 13, 2025
1 parent 35bf6ee commit 5f932c9
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 1 deletion.
1 change: 1 addition & 0 deletions platform/jvm/capture/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation(libs.androidx.startup.runtime)
implementation(libs.jsr305)
implementation(libs.gson)
implementation(libs.performance)

testImplementation(libs.junit)
testImplementation(libs.assertj.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import io.bitdrift.capture.events.lifecycle.EventsListenerTarget
import io.bitdrift.capture.events.performance.AppMemoryPressureListenerLogger
import io.bitdrift.capture.events.performance.BatteryMonitor
import io.bitdrift.capture.events.performance.DiskUsageMonitor
import io.bitdrift.capture.events.performance.JankStatsMonitor
import io.bitdrift.capture.events.performance.MemoryMetricsProvider
import io.bitdrift.capture.events.performance.ResourceUtilizationTarget
import io.bitdrift.capture.events.span.Span
Expand Down Expand Up @@ -207,6 +208,8 @@ internal class LoggerImpl(
ProcessLifecycleOwner.get(),
runtime,
eventListenerDispatcher.executorService,
errorHandler = errorHandler,
windowListener = JankStatsMonitor(this, runtime),
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@

package io.bitdrift.capture.events.lifecycle

import android.view.Window
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import io.bitdrift.capture.LogLevel
import io.bitdrift.capture.LogType
import io.bitdrift.capture.LoggerImpl
import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.common.MainThreadHandler
import io.bitdrift.capture.common.Runtime
import io.bitdrift.capture.common.RuntimeFeature
import io.bitdrift.capture.common.WindowManager
import io.bitdrift.capture.common.phoneWindow
import io.bitdrift.capture.events.IEventListenerLogger
import java.util.concurrent.ExecutorService

Expand All @@ -25,8 +29,12 @@ internal class AppLifecycleListenerLogger(
private val runtime: Runtime,
private val executor: ExecutorService,
private val mainThreadHandler: MainThreadHandler = MainThreadHandler(),
errorHandler: ErrorHandler,
private val windowListener: IWindowListener,
) : IEventListenerLogger,
LifecycleEventObserver {
private val windowManager = WindowManager(errorHandler)

private val lifecycleEventNames =
hashMapOf(
Lifecycle.Event.ON_CREATE to "AppCreate",
Expand Down Expand Up @@ -58,6 +66,7 @@ internal class AppLifecycleListenerLogger(
if (!runtime.isEnabled(RuntimeFeature.APP_LIFECYCLE_EVENTS)) {
return@execute
}

// refer to lifecycle states https://developer.android.com/topic/libraries/architecture/lifecycle#lc
logger.log(
LogType.LIFECYCLE,
Expand All @@ -67,6 +76,33 @@ internal class AppLifecycleListenerLogger(
if (event == Lifecycle.Event.ON_STOP) {
logger.flush(false)
}

emitWindowChanges(event)
}
}

private fun emitWindowChanges(event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_CREATE,
Lifecycle.Event.ON_START,
Lifecycle.Event.ON_RESUME,
-> {
getCurrentWindow()?.let {
mainThreadHandler.run { windowListener.onWindowAvailable(it) }
}
}

Lifecycle.Event.ON_DESTROY,
Lifecycle.Event.ON_STOP,
-> {
mainThreadHandler.run { windowListener.onWindowRemoved() }
}

else -> {
// ignore rest of events
}
}
}

private fun getCurrentWindow(): Window? = windowManager.findRootViews().firstOrNull()?.phoneWindow
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.bitdrift.capture.events.lifecycle

import android.view.Window

/**
* Emits when a [android.view.Window] is available or removed
*/
interface IWindowListener {
/**
* Reports when [android.view.Window] is available
*/
fun onWindowAvailable(window: Window)

/**
* Reports when the current window is not around
*/
fun onWindowRemoved()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.bitdrift.capture.events.performance

import android.view.Window
import androidx.metrics.performance.FrameData
import androidx.metrics.performance.JankStats
import io.bitdrift.capture.LogLevel
import io.bitdrift.capture.LogType
import io.bitdrift.capture.LoggerImpl
import io.bitdrift.capture.common.Runtime
import io.bitdrift.capture.common.RuntimeFeature
import io.bitdrift.capture.events.lifecycle.IWindowListener
import io.bitdrift.capture.providers.toFields

/**
* Reports Jank Frames and its duration in ms
*
* This will be a no-op when `client_feature.android.jank_stats_reporting` kill switch is disabled
*/
internal class JankStatsMonitor(
private val logger: LoggerImpl,
runtime: Runtime,
) : IWindowListener,
JankStats.OnFrameListener {
private val shouldDisableMonitorViaKillSwitch = !runtime.isEnabled(RuntimeFeature.JANK_STATS_EVENTS)

private var jankStats: JankStats? = null
private var currentWindow: Window? = null

override fun onFrame(volatileFrameData: FrameData) {
if (volatileFrameData.shouldReportJankyFrame()) {
sendJankStatData(volatileFrameData)
}
}

/**
* Called when we have a new window available
*/
override fun onWindowAvailable(window: Window) {
if (shouldDisableMonitorViaKillSwitch || currentWindow == window) {
return
}
currentWindow = window
jankStats = JankStats.createAndTrack(window, this)
}

/**
* Called when Application is Stopped/Destroyed
*/
override fun onWindowRemoved() {
if (shouldDisableMonitorViaKillSwitch) {
return
}
jankStats?.isTrackingEnabled = false
jankStats = null
}

private fun sendJankStatData(frameData: FrameData) {
val message = "$ROW_MESSAGE ${frameData.durationToMilli()} ms"
val fields = mapOf(JANKY_FRAME_DURATION_KEY to "${frameData.durationToMilli()}")
logger.log(LogType.LIFECYCLE, LogLevel.WARNING, fields.toFields()) { message }
}

private fun FrameData.durationToMilli(): Long = this.frameDurationUiNanos / TO_MILLI

private fun FrameData.shouldReportJankyFrame(): Boolean =
this.isJank && this.durationToMilli() >= MIN_THRESHOLD_JANKY_FRAME_DURATION_IN_MILLI

private companion object {
private const val TO_MILLI = 1000000L
private const val MIN_THRESHOLD_JANKY_FRAME_DURATION_IN_MILLI = 16L
private const val JANKY_FRAME_DURATION_KEY = "_jank_frame_duration"
private const val ROW_MESSAGE = "Frozen frame with a duration of"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ sealed class RuntimeFeature(
* Whether the logger should be flushed on crash.
*/
data object LOGGER_FLUSHING_ON_CRASH : RuntimeFeature("client_feature.android.logger_flushing_on_force_quit", defaultValue = true)

/**
* Whether Jank Stats reporting is enabled
*/
data object JANK_STATS_EVENTS : RuntimeFeature("client_feature.android.jank_stats_reporting")
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import android.os.Build
import android.view.View
import android.view.inspector.WindowInspector

// Used for retrieving the view hierarchies
/**
* Used for retrieving the view hierarchies
*/
class WindowManager(
private val errorHandler: ErrorHandler,
) {
Expand Down

0 comments on commit 5f932c9

Please sign in to comment.