Skip to content

Commit

Permalink
Add TTI/TTFR tracking for Activities & Fragments (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
smbduknow authored Feb 12, 2024
1 parent bd3c5f4 commit 05b904b
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 1 deletion.
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ flexible with re-using the monitoring approaches already existing in your produc
Library supports collecting following performance metrics:
- App Cold Startup Time
- Rendering performance per Activity
- Time to Interactive & Time to First Render per screen

We recommend to read our blogpost ["Measuring mobile apps performance in production"](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f)
first to get some idea on how performance metrics work and why those were chosen.
first to get some idea on what are these performance metrics, how they work and why those were chosen.

> NOTE: You can also refer to the [SampleApp](sampleApp/src/main/java/com/booking/perfsuite/app)
> in this repo to see a simplified example of how the library can be used in the real app
### Dependency

Expand Down Expand Up @@ -98,6 +102,55 @@ Then metrics will be represented as [`RenderingMetrics`](src/main/java/com/booki

Even though we support collecting widely used slow & frozen frames we [strongly recommend relying on `totalFreezeTimeMs` as the main rendering metric](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f#2d5d)

### Collecting Screen Time to Interactive (TTI)

Implement the callbacks invoked every time when screen's
[Time To Interactive (TTI)](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f#ad4d) &
[Time To First Render (TTFR)](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f#f862)
metrics are collected:

```kotlin
object MyTtiListener : BaseTtiTracker.Listener {

override fun onScreenCreated(screen: String) {}

override fun onFirstFrameIsDrawn(screen: String, duration: Long) {
// Log or report TTFR metrics for specific screen in a preferable way
}
override fun onFirstUsableFrameIsDrawn(screen: String, duration: Long) {
// Log or report TTI metrics for specific screen in a preferable way
}
}
```

Then instantiate TTI tracker in `Application#onCreate` before any activity is created and using this listener:

```kotlin
// keep instances globally accessible or inject as singletons using any preferable DI framework
val ttiTracker = BaseTtiTracker(AppTtiListener)
val viewTtiTracker = ViewTtiTracker(ttiTracker)

class MyApplication : Application() {

override fun onCreate() {
super.onCreate()
ActivityTtfrHelper.register(this, viewTtiTracker)
}
}
```

That will enable automatic TTFR collection for every Activity in the app.
For TTI collection you'll need to call `viewTtiTracker.onScreenIsUsable(..)` manually from the Activity,
when the meaningful data is visible to the user e.g.:

```kotlin
// call this e.g. when the data is received from the backend,
// progress bar stops spinning and screen is fully ready for the user
viewTtiTracker.onScreenIsUsable(activity.componentName, rootContentView)
```

See the [SampleApp](sampleApp/src/main/java/com/booking/perfsuite/app) for a full working example

## Additional documentation
- [Measuring mobile apps performance in production](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f)
- [App Startup Time documentation by Google](https://developer.android.com/topic/performance/vitals/launch-time)
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ androidx-appcompat = "1.6.1"
[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" }

[plugins]
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Expand Down
1 change: 1 addition & 0 deletions perfsuite/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ android {

dependencies {
implementation(libs.androidx.ktx)
implementation(libs.androidx.fragment)
}
116 changes: 116 additions & 0 deletions perfsuite/src/main/java/com/booking/perfsuite/tti/BaseTtiTracker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.booking.perfsuite.tti

import androidx.annotation.UiThread
import com.booking.perfsuite.internal.nowMillis

/**
* The most basic TTFR/TTI tracking implementation.
* This class can be used with any possible screen implementation
* (Activities, Fragments, Views, Jetpack Compose and etc.).
*
* To work properly it requires that methods are called respectively to the screen lifecycle events:
* 1. Call [onScreenCreated] at the earliest possible moment of the screen instantiation
* 2. Then call [onScreenViewIsReady] when the first screen frame is shown to the user,
* that will indicate that TTFR metric is collected
* 3. Optionally call [onScreenIsUsable] when the usable content is shown to the user,
* that will indicate that TTI metric is collected
*
* For more details please refer to the documentation:
* https://github.com/bookingcom/perfsuite-android?tab=readme-ov-file#additional-documentation
*
* @param listener implementation is used to handle screen TTI\TTFR metrics when they are ready
*/
@UiThread
public class BaseTtiTracker(
private val listener: Listener
) {

private val screenCreationTimestamp = HashMap<String, Long>()

/**
* Call this method immediately on screen creation as early as possible
*
* @param screen - unique screen identifier
* @param timestamp - the time the screen was created at.
*/
public fun onScreenCreated(screen: String, timestamp: Long = nowMillis()) {
screenCreationTimestamp[screen] = timestamp
listener.onScreenCreated(screen)
}

/**
* Call this method when screen is rendered for the first time
*
* @param screen - unique screen identifier
*/
public fun onScreenViewIsReady(screen: String) {
screenCreationTimestamp[screen]?.let { creationTimestamp ->
val duration = nowMillis() - creationTimestamp
listener.onFirstFrameIsDrawn(screen, duration)
}
}

/**
* Call this method when the screen is ready for user interaction
* (e.g. all data is ready and meaningful content is shown).
*
* The method is optional, whenever it is not called TTI won't be measured
*
* @param screen - unique screen identifier
*/
public fun onScreenIsUsable(screen: String) {
screenCreationTimestamp[screen]?.let { creationTimestamp ->
val duration = nowMillis() - creationTimestamp
listener.onFirstUsableFrameIsDrawn(screen, duration)
screenCreationTimestamp.remove(screen)
}
}

/**
* Call this when user leaves the screen.
*
* This prevent us from producing outliers and avoid tracking cheap screen transitions
* (e.g. back navigation, when the screen is already created in memory),
* so we're able to track only real screen creation performance
*/
public fun onScreenStopped(screen: String) {
screenCreationTimestamp.remove(screen)
}

/**
* Returns true if the screen is still in the state of collecting metrics.
* When result is false,that means that both TTFR/TTI metrics were already collected or
* discarded for any reason
*/
public fun isScreenEnabledForTracking(screen: String): Boolean =
screenCreationTimestamp.containsKey(screen)

/**
* Listener interface providing TTFR/TTI metrics when they're ready
*/
public interface Listener {

/**
* Called as early as possible after the screen [screen] is created.
*
* @param screen - screen key
*/
public fun onScreenCreated(screen: String)

/**
* Called when the very first screen frame is drawn
*
* @param screen - screen key
* @param duration - elapsed time since screen's creation till the first frame is drawn
*/
public fun onFirstFrameIsDrawn(screen: String, duration: Long)

/**
* Called when the first usable/meaningful screen frame is drawn
*
* @param screen - screen key
* @param duration - elapsed time since screen's creation till the usable frame is drawn
*/
public fun onFirstUsableFrameIsDrawn(screen: String, duration: Long)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.booking.perfsuite.tti

import android.view.View
import androidx.annotation.UiThread
import com.booking.perfsuite.internal.doOnNextDraw

/**
* Android View-based implementation of TTI\TTFR tracking. This class should be used with screens
* which are rendered using Android [View] class (Activities, Fragments, Views).
*
* For Android Views we always should measure time until the actual draw happens
* and [View.onDraw] is called.
* That's why when [onScreenViewIsReady] or [onScreenIsUsable] are called, the tracker actually
* waits until the next frame draw before finish collecting TTFR/TTI metrics.
*
* Technically this is a wrapper around [BaseTtiTracker] which helps to collect metrics respectively to
* how [View] rendering works.
* Therefore, please use [BaseTtiTracker] directly in case of using canvas drawing,
* Jetpack Compose or any other approach which is not based on Views.
*
* See also [com.booking.perfsuite.tti.helpers.ActivityTtfrHelper] and
* [com.booking.perfsuite.tti.helpers.FragmentTtfrHelper] for automatic TTFR collection
* in Activities and Fragments.
*/
@UiThread
public class ViewTtiTracker(private val tracker: BaseTtiTracker) {

/**
* Call this method immediately on screen creation as early as possible
*
* @param screen - unique screen identifier
*/
public fun onScreenCreated(screen: String) {
tracker.onScreenCreated(screen)
}

/**
* Call this when screen View is ready but it is not drawn yet
*
* @param screen - unique screen identifier
* @param rootView - root view of the screen, metric is ready when this view is next drawn
*/
public fun onScreenViewIsReady(screen: String, rootView: View) {
if (tracker.isScreenEnabledForTracking(screen)) {
rootView.doOnNextDraw { tracker.onScreenViewIsReady(screen) }
}
}

/**
* Call this when the screen View is ready for user interaction.
* Only the first call after screen creation is considered, repeat calls are ignored
*
* @see BaseTtiTracker.onScreenIsUsable
*
* @param screen - unique screen identifier
* @param rootView - root view of the screen, metric is ready when this view is next drawn
*
*
*/
public fun onScreenIsUsable(screen: String, rootView: View) {
if (tracker.isScreenEnabledForTracking(screen)) {
rootView.doOnNextDraw { tracker.onScreenIsUsable(screen) }
}
}

/**
* Call this when user leaves the screen.
*
* This prevent us from tracking cheap screen transitions (e.g. back navigation,
* when the screen is already created in memory), so we're able to track
* only real screen creation performance, removing outliers
*/
public fun onScreenStopped(screen: String) {
tracker.onScreenStopped(screen)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.booking.perfsuite.tti.helpers

import android.app.Activity
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import com.booking.perfsuite.tti.ViewTtiTracker

/**
* This class helps to automatically track TTFR metric for every activity by handling
* [android.app.Application.ActivityLifecycleCallbacks]
*
* @param tracker TTI tracker instance
* @param screenNameProvider function used to generate unique screen name/identifier for activity.
* If it returns null, then activity won't be tracked.
* By default it uses the implementation based on Activity's class name
*/
public class ActivityTtfrHelper(
private val tracker: ViewTtiTracker,
private val screenNameProvider: (Activity) -> String? = { it.javaClass.name }
) : ActivityLifecycleCallbacks {

public companion object {

/**
* Registers [ActivityTtfrHelper] instance with the app as
* [android.app.Application.ActivityLifecycleCallbacks] to collect TTFR metrics for
* every activity
*
* Call this method at the app startup, before the first activity is created
*
* @param application current [Application] instance
* @param tracker configured for the app [ViewTtiTracker] instance
*/
@JvmStatic
public fun register(application: Application, tracker: ViewTtiTracker) {
val activityHelper = ActivityTtfrHelper(tracker)
application.registerActivityLifecycleCallbacks(activityHelper)
}
}

override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
val screenKey = screenNameProvider(activity) ?: return
tracker.onScreenCreated(screenKey)
}

override fun onActivityStarted(activity: Activity) {
val screenKey = screenNameProvider(activity) ?: return
val rootView = activity.window.decorView
tracker.onScreenViewIsReady(screenKey, rootView)
}

override fun onActivityStopped(activity: Activity) {
val screenKey = screenNameProvider(activity) ?: return
tracker.onScreenStopped(screenKey)
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { }
override fun onActivityResumed(activity: Activity) { }
override fun onActivityPaused(activity: Activity) { }
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { }
override fun onActivityDestroyed(activity: Activity) { }


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.booking.perfsuite.tti.helpers

import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.booking.perfsuite.tti.ViewTtiTracker

/**
* This class helps to automatically track TTFR metric for every fragment
* within the particular activity or particular parent fragment by handling
* [androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks]
*
* @param tracker TTI tracker instance
* @param screenNameProvider function used to generate unique screen name/identifier for fragment.
* If it returns null, then fragment won't be tracked.
* By default it uses the implementation based on Fragment's class name
*/
public class FragmentTtfrHelper(
private val tracker: ViewTtiTracker,
private val screenNameProvider: (Fragment) -> String? = { it.javaClass.name }
) : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentPreCreated(
fm: FragmentManager,
fragment: Fragment,
savedInstanceState: Bundle?
) {
val screenKey = screenNameProvider(fragment) ?: return
tracker.onScreenCreated(screenKey)
}

override fun onFragmentStarted(fm: FragmentManager, fragment: Fragment) {
val screenKey = screenNameProvider(fragment) ?: return
val rootView = fragment.view ?: return
tracker.onScreenViewIsReady(screenKey, rootView)
}

override fun onFragmentStopped(fm: FragmentManager, fragment: Fragment) {
val screenKey = screenNameProvider(fragment) ?: return
tracker.onScreenStopped(screenKey)
}
}
Loading

0 comments on commit 05b904b

Please sign in to comment.