-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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`
- Loading branch information
Showing
10 changed files
with
401 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,4 +24,5 @@ android { | |
|
||
dependencies { | ||
implementation(libs.androidx.ktx) | ||
implementation(libs.androidx.fragment) | ||
} |
116 changes: 116 additions & 0 deletions
116
perfsuite/src/main/java/com/booking/perfsuite/tti/BaseTtiTracker.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
perfsuite/src/main/java/com/booking/perfsuite/tti/ViewTtiTracker.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
perfsuite/src/main/java/com/booking/perfsuite/tti/helpers/ActivityTtfrHelper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { } | ||
|
||
|
||
} |
42 changes: 42 additions & 0 deletions
42
perfsuite/src/main/java/com/booking/perfsuite/tti/helpers/FragmentTtfrHelper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.