From 15ba1aaee4320972aaadc1365f9d827b09feed44 Mon Sep 17 00:00:00 2001 From: Ertugrul Date: Mon, 2 Dec 2024 10:58:00 +0300 Subject: [PATCH 1/2] Create FragmentFrameMetricsTracker and implement callbacks --- .../rendering/FragmentFrameMetricsTracker.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 perfsuite/src/main/java/com/booking/perfsuite/rendering/FragmentFrameMetricsTracker.kt diff --git a/perfsuite/src/main/java/com/booking/perfsuite/rendering/FragmentFrameMetricsTracker.kt b/perfsuite/src/main/java/com/booking/perfsuite/rendering/FragmentFrameMetricsTracker.kt new file mode 100644 index 0000000..a1a2c86 --- /dev/null +++ b/perfsuite/src/main/java/com/booking/perfsuite/rendering/FragmentFrameMetricsTracker.kt @@ -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() + + 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, + foregroundTime: Long? + ) + } +} From 38aa06440cbf25b632de56578d87329d5460c8db Mon Sep 17 00:00:00 2001 From: Ertugrul Date: Mon, 2 Dec 2024 10:58:28 +0300 Subject: [PATCH 2/2] Add message list fragment to sample app --- gradle/libs.versions.toml | 2 ++ sampleApp/build.gradle.kts | 1 + .../com/booking/perfsuite/app/MainActivity.kt | 28 ++++++++++++---- .../perfsuite/app/MessageListAdapter.kt | 33 +++++++++++++++++++ .../perfsuite/app/MessageListFragment.kt | 29 ++++++++++++++++ .../FragmentFrameMetricsListener.kt | 19 +++++++++++ .../src/main/res/layout/activity_main.xml | 18 ++++++++++ 7 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 sampleApp/src/main/java/com/booking/perfsuite/app/MessageListAdapter.kt create mode 100644 sampleApp/src/main/java/com/booking/perfsuite/app/MessageListFragment.kt create mode 100644 sampleApp/src/main/java/com/booking/perfsuite/app/monitoring/FragmentFrameMetricsListener.kt create mode 100644 sampleApp/src/main/res/layout/activity_main.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c9e29c..49451f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/sampleApp/build.gradle.kts b/sampleApp/build.gradle.kts index 964fea8..af6b0fa 100644 --- a/sampleApp/build.gradle.kts +++ b/sampleApp/build.gradle.kts @@ -36,4 +36,5 @@ dependencies { implementation(libs.androidx.ktx) implementation(libs.androidx.appcompat) + implementation(libs.androidx.recyclerview) } diff --git a/sampleApp/src/main/java/com/booking/perfsuite/app/MainActivity.kt b/sampleApp/src/main/java/com/booking/perfsuite/app/MainActivity.kt index 2c20a98..23bc04f 100644 --- a/sampleApp/src/main/java/com/booking/perfsuite/app/MainActivity.kt +++ b/sampleApp/src/main/java/com/booking/perfsuite/app/MainActivity.kt @@ -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(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) } } diff --git a/sampleApp/src/main/java/com/booking/perfsuite/app/MessageListAdapter.kt b/sampleApp/src/main/java/com/booking/perfsuite/app/MessageListAdapter.kt new file mode 100644 index 0000000..c0334ff --- /dev/null +++ b/sampleApp/src/main/java/com/booking/perfsuite/app/MessageListAdapter.kt @@ -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 +) : RecyclerView.Adapter() { + + 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 + } +} diff --git a/sampleApp/src/main/java/com/booking/perfsuite/app/MessageListFragment.kt b/sampleApp/src/main/java/com/booking/perfsuite/app/MessageListFragment.kt new file mode 100644 index 0000000..12e46fc --- /dev/null +++ b/sampleApp/src/main/java/com/booking/perfsuite/app/MessageListFragment.kt @@ -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 + } +} diff --git a/sampleApp/src/main/java/com/booking/perfsuite/app/monitoring/FragmentFrameMetricsListener.kt b/sampleApp/src/main/java/com/booking/perfsuite/app/monitoring/FragmentFrameMetricsListener.kt new file mode 100644 index 0000000..ba91e2e --- /dev/null +++ b/sampleApp/src/main/java/com/booking/perfsuite/app/monitoring/FragmentFrameMetricsListener.kt @@ -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, + foregroundTime: Long? + ) { + val data = RenderingMetricsMapper.toRenderingMetrics(frameMetrics, foregroundTime) ?: return + + Log.d("PerfSuite", "Frame metrics for [$fragment::class.simpleName] are collected: $data") + } +} diff --git a/sampleApp/src/main/res/layout/activity_main.xml b/sampleApp/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..faaf79c --- /dev/null +++ b/sampleApp/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + + +