Skip to content

Commit 05b904b

Browse files
authored
Add TTI/TTFR tracking for Activities & Fragments (#2)
Add missing metrics tracking: - [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) Usage example is also added to the `sampleApp`
1 parent bd3c5f4 commit 05b904b

File tree

10 files changed

+401
-1
lines changed

10 files changed

+401
-1
lines changed

README.md

+54-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ flexible with re-using the monitoring approaches already existing in your produc
1616
Library supports collecting following performance metrics:
1717
- App Cold Startup Time
1818
- Rendering performance per Activity
19+
- Time to Interactive & Time to First Render per screen
1920

2021
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)
21-
first to get some idea on how performance metrics work and why those were chosen.
22+
first to get some idea on what are these performance metrics, how they work and why those were chosen.
23+
24+
> NOTE: You can also refer to the [SampleApp](sampleApp/src/main/java/com/booking/perfsuite/app)
25+
> in this repo to see a simplified example of how the library can be used in the real app
2226
2327
### Dependency
2428

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

99103
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)
100104

105+
### Collecting Screen Time to Interactive (TTI)
106+
107+
Implement the callbacks invoked every time when screen's
108+
[Time To Interactive (TTI)](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f#ad4d) &
109+
[Time To First Render (TTFR)](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f#f862)
110+
metrics are collected:
111+
112+
```kotlin
113+
object MyTtiListener : BaseTtiTracker.Listener {
114+
115+
override fun onScreenCreated(screen: String) {}
116+
117+
override fun onFirstFrameIsDrawn(screen: String, duration: Long) {
118+
// Log or report TTFR metrics for specific screen in a preferable way
119+
}
120+
override fun onFirstUsableFrameIsDrawn(screen: String, duration: Long) {
121+
// Log or report TTI metrics for specific screen in a preferable way
122+
}
123+
}
124+
```
125+
126+
Then instantiate TTI tracker in `Application#onCreate` before any activity is created and using this listener:
127+
128+
```kotlin
129+
// keep instances globally accessible or inject as singletons using any preferable DI framework
130+
val ttiTracker = BaseTtiTracker(AppTtiListener)
131+
val viewTtiTracker = ViewTtiTracker(ttiTracker)
132+
133+
class MyApplication : Application() {
134+
135+
override fun onCreate() {
136+
super.onCreate()
137+
ActivityTtfrHelper.register(this, viewTtiTracker)
138+
}
139+
}
140+
```
141+
142+
That will enable automatic TTFR collection for every Activity in the app.
143+
For TTI collection you'll need to call `viewTtiTracker.onScreenIsUsable(..)` manually from the Activity,
144+
when the meaningful data is visible to the user e.g.:
145+
146+
```kotlin
147+
// call this e.g. when the data is received from the backend,
148+
// progress bar stops spinning and screen is fully ready for the user
149+
viewTtiTracker.onScreenIsUsable(activity.componentName, rootContentView)
150+
```
151+
152+
See the [SampleApp](sampleApp/src/main/java/com/booking/perfsuite/app) for a full working example
153+
101154
## Additional documentation
102155
- [Measuring mobile apps performance in production](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f)
103156
- [App Startup Time documentation by Google](https://developer.android.com/topic/performance/vitals/launch-time)

gradle/libs.versions.toml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ androidx-appcompat = "1.6.1"
99
[libraries]
1010
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-ktx" }
1111
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
12+
androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidx-appcompat" }
1213

1314
[plugins]
1415
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

perfsuite/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ android {
2424

2525
dependencies {
2626
implementation(libs.androidx.ktx)
27+
implementation(libs.androidx.fragment)
2728
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.booking.perfsuite.tti
2+
3+
import androidx.annotation.UiThread
4+
import com.booking.perfsuite.internal.nowMillis
5+
6+
/**
7+
* The most basic TTFR/TTI tracking implementation.
8+
* This class can be used with any possible screen implementation
9+
* (Activities, Fragments, Views, Jetpack Compose and etc.).
10+
*
11+
* To work properly it requires that methods are called respectively to the screen lifecycle events:
12+
* 1. Call [onScreenCreated] at the earliest possible moment of the screen instantiation
13+
* 2. Then call [onScreenViewIsReady] when the first screen frame is shown to the user,
14+
* that will indicate that TTFR metric is collected
15+
* 3. Optionally call [onScreenIsUsable] when the usable content is shown to the user,
16+
* that will indicate that TTI metric is collected
17+
*
18+
* For more details please refer to the documentation:
19+
* https://github.com/bookingcom/perfsuite-android?tab=readme-ov-file#additional-documentation
20+
*
21+
* @param listener implementation is used to handle screen TTI\TTFR metrics when they are ready
22+
*/
23+
@UiThread
24+
public class BaseTtiTracker(
25+
private val listener: Listener
26+
) {
27+
28+
private val screenCreationTimestamp = HashMap<String, Long>()
29+
30+
/**
31+
* Call this method immediately on screen creation as early as possible
32+
*
33+
* @param screen - unique screen identifier
34+
* @param timestamp - the time the screen was created at.
35+
*/
36+
public fun onScreenCreated(screen: String, timestamp: Long = nowMillis()) {
37+
screenCreationTimestamp[screen] = timestamp
38+
listener.onScreenCreated(screen)
39+
}
40+
41+
/**
42+
* Call this method when screen is rendered for the first time
43+
*
44+
* @param screen - unique screen identifier
45+
*/
46+
public fun onScreenViewIsReady(screen: String) {
47+
screenCreationTimestamp[screen]?.let { creationTimestamp ->
48+
val duration = nowMillis() - creationTimestamp
49+
listener.onFirstFrameIsDrawn(screen, duration)
50+
}
51+
}
52+
53+
/**
54+
* Call this method when the screen is ready for user interaction
55+
* (e.g. all data is ready and meaningful content is shown).
56+
*
57+
* The method is optional, whenever it is not called TTI won't be measured
58+
*
59+
* @param screen - unique screen identifier
60+
*/
61+
public fun onScreenIsUsable(screen: String) {
62+
screenCreationTimestamp[screen]?.let { creationTimestamp ->
63+
val duration = nowMillis() - creationTimestamp
64+
listener.onFirstUsableFrameIsDrawn(screen, duration)
65+
screenCreationTimestamp.remove(screen)
66+
}
67+
}
68+
69+
/**
70+
* Call this when user leaves the screen.
71+
*
72+
* This prevent us from producing outliers and avoid tracking cheap screen transitions
73+
* (e.g. back navigation, when the screen is already created in memory),
74+
* so we're able to track only real screen creation performance
75+
*/
76+
public fun onScreenStopped(screen: String) {
77+
screenCreationTimestamp.remove(screen)
78+
}
79+
80+
/**
81+
* Returns true if the screen is still in the state of collecting metrics.
82+
* When result is false,that means that both TTFR/TTI metrics were already collected or
83+
* discarded for any reason
84+
*/
85+
public fun isScreenEnabledForTracking(screen: String): Boolean =
86+
screenCreationTimestamp.containsKey(screen)
87+
88+
/**
89+
* Listener interface providing TTFR/TTI metrics when they're ready
90+
*/
91+
public interface Listener {
92+
93+
/**
94+
* Called as early as possible after the screen [screen] is created.
95+
*
96+
* @param screen - screen key
97+
*/
98+
public fun onScreenCreated(screen: String)
99+
100+
/**
101+
* Called when the very first screen frame is drawn
102+
*
103+
* @param screen - screen key
104+
* @param duration - elapsed time since screen's creation till the first frame is drawn
105+
*/
106+
public fun onFirstFrameIsDrawn(screen: String, duration: Long)
107+
108+
/**
109+
* Called when the first usable/meaningful screen frame is drawn
110+
*
111+
* @param screen - screen key
112+
* @param duration - elapsed time since screen's creation till the usable frame is drawn
113+
*/
114+
public fun onFirstUsableFrameIsDrawn(screen: String, duration: Long)
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.booking.perfsuite.tti
2+
3+
import android.view.View
4+
import androidx.annotation.UiThread
5+
import com.booking.perfsuite.internal.doOnNextDraw
6+
7+
/**
8+
* Android View-based implementation of TTI\TTFR tracking. This class should be used with screens
9+
* which are rendered using Android [View] class (Activities, Fragments, Views).
10+
*
11+
* For Android Views we always should measure time until the actual draw happens
12+
* and [View.onDraw] is called.
13+
* That's why when [onScreenViewIsReady] or [onScreenIsUsable] are called, the tracker actually
14+
* waits until the next frame draw before finish collecting TTFR/TTI metrics.
15+
*
16+
* Technically this is a wrapper around [BaseTtiTracker] which helps to collect metrics respectively to
17+
* how [View] rendering works.
18+
* Therefore, please use [BaseTtiTracker] directly in case of using canvas drawing,
19+
* Jetpack Compose or any other approach which is not based on Views.
20+
*
21+
* See also [com.booking.perfsuite.tti.helpers.ActivityTtfrHelper] and
22+
* [com.booking.perfsuite.tti.helpers.FragmentTtfrHelper] for automatic TTFR collection
23+
* in Activities and Fragments.
24+
*/
25+
@UiThread
26+
public class ViewTtiTracker(private val tracker: BaseTtiTracker) {
27+
28+
/**
29+
* Call this method immediately on screen creation as early as possible
30+
*
31+
* @param screen - unique screen identifier
32+
*/
33+
public fun onScreenCreated(screen: String) {
34+
tracker.onScreenCreated(screen)
35+
}
36+
37+
/**
38+
* Call this when screen View is ready but it is not drawn yet
39+
*
40+
* @param screen - unique screen identifier
41+
* @param rootView - root view of the screen, metric is ready when this view is next drawn
42+
*/
43+
public fun onScreenViewIsReady(screen: String, rootView: View) {
44+
if (tracker.isScreenEnabledForTracking(screen)) {
45+
rootView.doOnNextDraw { tracker.onScreenViewIsReady(screen) }
46+
}
47+
}
48+
49+
/**
50+
* Call this when the screen View is ready for user interaction.
51+
* Only the first call after screen creation is considered, repeat calls are ignored
52+
*
53+
* @see BaseTtiTracker.onScreenIsUsable
54+
*
55+
* @param screen - unique screen identifier
56+
* @param rootView - root view of the screen, metric is ready when this view is next drawn
57+
*
58+
*
59+
*/
60+
public fun onScreenIsUsable(screen: String, rootView: View) {
61+
if (tracker.isScreenEnabledForTracking(screen)) {
62+
rootView.doOnNextDraw { tracker.onScreenIsUsable(screen) }
63+
}
64+
}
65+
66+
/**
67+
* Call this when user leaves the screen.
68+
*
69+
* This prevent us from tracking cheap screen transitions (e.g. back navigation,
70+
* when the screen is already created in memory), so we're able to track
71+
* only real screen creation performance, removing outliers
72+
*/
73+
public fun onScreenStopped(screen: String) {
74+
tracker.onScreenStopped(screen)
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.booking.perfsuite.tti.helpers
2+
3+
import android.app.Activity
4+
import android.app.Application
5+
import android.app.Application.ActivityLifecycleCallbacks
6+
import android.os.Bundle
7+
import com.booking.perfsuite.tti.ViewTtiTracker
8+
9+
/**
10+
* This class helps to automatically track TTFR metric for every activity by handling
11+
* [android.app.Application.ActivityLifecycleCallbacks]
12+
*
13+
* @param tracker TTI tracker instance
14+
* @param screenNameProvider function used to generate unique screen name/identifier for activity.
15+
* If it returns null, then activity won't be tracked.
16+
* By default it uses the implementation based on Activity's class name
17+
*/
18+
public class ActivityTtfrHelper(
19+
private val tracker: ViewTtiTracker,
20+
private val screenNameProvider: (Activity) -> String? = { it.javaClass.name }
21+
) : ActivityLifecycleCallbacks {
22+
23+
public companion object {
24+
25+
/**
26+
* Registers [ActivityTtfrHelper] instance with the app as
27+
* [android.app.Application.ActivityLifecycleCallbacks] to collect TTFR metrics for
28+
* every activity
29+
*
30+
* Call this method at the app startup, before the first activity is created
31+
*
32+
* @param application current [Application] instance
33+
* @param tracker configured for the app [ViewTtiTracker] instance
34+
*/
35+
@JvmStatic
36+
public fun register(application: Application, tracker: ViewTtiTracker) {
37+
val activityHelper = ActivityTtfrHelper(tracker)
38+
application.registerActivityLifecycleCallbacks(activityHelper)
39+
}
40+
}
41+
42+
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
43+
val screenKey = screenNameProvider(activity) ?: return
44+
tracker.onScreenCreated(screenKey)
45+
}
46+
47+
override fun onActivityStarted(activity: Activity) {
48+
val screenKey = screenNameProvider(activity) ?: return
49+
val rootView = activity.window.decorView
50+
tracker.onScreenViewIsReady(screenKey, rootView)
51+
}
52+
53+
override fun onActivityStopped(activity: Activity) {
54+
val screenKey = screenNameProvider(activity) ?: return
55+
tracker.onScreenStopped(screenKey)
56+
}
57+
58+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { }
59+
override fun onActivityResumed(activity: Activity) { }
60+
override fun onActivityPaused(activity: Activity) { }
61+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { }
62+
override fun onActivityDestroyed(activity: Activity) { }
63+
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.booking.perfsuite.tti.helpers
2+
3+
import android.os.Bundle
4+
import androidx.fragment.app.Fragment
5+
import androidx.fragment.app.FragmentManager
6+
import com.booking.perfsuite.tti.ViewTtiTracker
7+
8+
/**
9+
* This class helps to automatically track TTFR metric for every fragment
10+
* within the particular activity or particular parent fragment by handling
11+
* [androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks]
12+
*
13+
* @param tracker TTI tracker instance
14+
* @param screenNameProvider function used to generate unique screen name/identifier for fragment.
15+
* If it returns null, then fragment won't be tracked.
16+
* By default it uses the implementation based on Fragment's class name
17+
*/
18+
public class FragmentTtfrHelper(
19+
private val tracker: ViewTtiTracker,
20+
private val screenNameProvider: (Fragment) -> String? = { it.javaClass.name }
21+
) : FragmentManager.FragmentLifecycleCallbacks() {
22+
23+
override fun onFragmentPreCreated(
24+
fm: FragmentManager,
25+
fragment: Fragment,
26+
savedInstanceState: Bundle?
27+
) {
28+
val screenKey = screenNameProvider(fragment) ?: return
29+
tracker.onScreenCreated(screenKey)
30+
}
31+
32+
override fun onFragmentStarted(fm: FragmentManager, fragment: Fragment) {
33+
val screenKey = screenNameProvider(fragment) ?: return
34+
val rootView = fragment.view ?: return
35+
tracker.onScreenViewIsReady(screenKey, rootView)
36+
}
37+
38+
override fun onFragmentStopped(fm: FragmentManager, fragment: Fragment) {
39+
val screenKey = screenNameProvider(fragment) ?: return
40+
tracker.onScreenStopped(screenKey)
41+
}
42+
}

0 commit comments

Comments
 (0)