Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fragment frame metrics tracker #5

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ androidGradlePlugin = "8.0.2"

androidx-ktx = "1.10.1"
androidx-appcompat = "1.6.1"
recyclerview = "1.3.2"

[libraries]
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-ktx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidx-appcompat" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }

[plugins]
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.booking.perfsuite.rendering

import android.app.Activity
import android.os.Bundle
import android.util.SparseIntArray
import androidx.annotation.UiThread
import androidx.core.app.FrameMetricsAggregator
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
import com.booking.perfsuite.internal.nowMillis
import java.util.WeakHashMap

/**
* Implementation of frames metric tracking based on
* [androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks]
* which automatically collects frame metrics
*/
@UiThread
public class FragmentFrameMetricsTracker(
private val listener: Listener,
) : FragmentLifecycleCallbacks() {

private val aggregator = FrameMetricsAggregator()
private val fragmentStartTimes = WeakHashMap<Fragment, Long>()

override fun onFragmentPreCreated(
fm: FragmentManager,
f: Fragment,
savedInstanceState: Bundle?
) {
aggregator.add(f.activity as Activity)
}

override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
fragmentStartTimes[f] = nowMillis()
}

override fun onFragmentPaused(fm: FragmentManager, f: Fragment) {
val metrics = aggregator.reset()
if (metrics != null) {
val foregroundTime = fragmentStartTimes.remove(f)?.let { nowMillis() - it }
listener.onFramesMetricsReady(f, metrics, foregroundTime)
}
}

override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
try {
aggregator.remove(f.activity as Activity)
} catch (_: Exception) {
// do nothing, aggregator.remove() may cause rare crashes on some devices
}
}

/**
* Listener interface providing notifications when the fragment's frame metrics are ready
*/
public interface Listener {

/**
* Called everytime when foreground fragment goes to the "paused" state,
* which means that frame metrics for this screen session are collected
*
* @param fragment current fragment
* @param frameMetrics raw frame metrics collected by [FrameMetricsAggregator]
* @param foregroundTime time in millis, spent by this activity in foreground state
*/
public fun onFramesMetricsReady(
fragment: Fragment,
frameMetrics: Array<SparseIntArray>,
foregroundTime: Long?
)
}
}
1 change: 1 addition & 0 deletions sampleApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ dependencies {

implementation(libs.androidx.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview)
}
28 changes: 21 additions & 7 deletions sampleApp/src/main/java/com/booking/perfsuite/app/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
package com.booking.perfsuite.app

import android.os.Bundle
import android.view.View.GONE
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.booking.perfsuite.app.monitoring.FragmentFrameMetricsListener
import com.booking.perfsuite.rendering.FragmentFrameMetricsTracker

class MainActivity : AppCompatActivity() {

private lateinit var contentView: TextView
val fragmentFrameMetricsTracker = FragmentFrameMetricsTracker(FragmentFrameMetricsListener)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Register the frame metric tracker
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentFrameMetricsTracker, true)
val textViewLoadingMessage = findViewById<TextView>(R.id.textViewLoadingMessage)

contentView = TextView(this)
contentView.text = "Loading content..."
setContentView(contentView)

contentView.postDelayed({
contentView.text = "Screen is usable"
textViewLoadingMessage.postDelayed({
textViewLoadingMessage.visibility = GONE
supportFragmentManager.beginTransaction().replace(
R.id.fragmentContainerView,
MessageListFragment(),
).commit()
reportIsUsable()
}, 1000)

}

override fun onDestroy() {
super.onDestroy()
supportFragmentManager
.unregisterFragmentLifecycleCallbacks(fragmentFrameMetricsTracker)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.booking.perfsuite.app

import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlin.random.Random.Default.nextLong

class MessageListAdapter(
private val data: List<String>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val textView = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
return object : RecyclerView.ViewHolder(textView) {}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// Simulate expensive operations on the main thread with random delays
val randomDelay = nextLong(10, 300)
try {
Thread.sleep(randomDelay)
} catch (_: InterruptedException) {
// do nothing
}
(holder.itemView as TextView).text = data[position]
}

override fun getItemCount(): Int {
return data.size
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.booking.perfsuite.app

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MessageListFragment : Fragment() {

private val messageList = List(100) { "Message #$it" }

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = FrameLayout(requireContext())
val recyclerView = RecyclerView(requireContext()).apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = MessageListAdapter(messageList)
setHasFixedSize(true)
}
rootView.addView(recyclerView)
return rootView
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.booking.perfsuite.app.monitoring

import android.util.Log
import android.util.SparseIntArray
import androidx.fragment.app.Fragment
import com.booking.perfsuite.rendering.FragmentFrameMetricsTracker
import com.booking.perfsuite.rendering.RenderingMetricsMapper

object FragmentFrameMetricsListener : FragmentFrameMetricsTracker.Listener {
override fun onFramesMetricsReady(
fragment: Fragment,
frameMetrics: Array<SparseIntArray>,
foregroundTime: Long?
) {
val data = RenderingMetricsMapper.toRenderingMetrics(frameMetrics, foregroundTime) ?: return

Log.d("PerfSuite", "Frame metrics for [$fragment::class.simpleName] are collected: $data")
}
}
18 changes: 18 additions & 0 deletions sampleApp/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<TextView
android:id="@+id/textViewLoadingMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Loading content..." />

</RelativeLayout>