├── .gitignore
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── sampleApp
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ └── mipmap-anydpi-v26
│ │ │ └── ic_launcher.xml
│ │ ├── java
│ │ └── com
│ │ │ └── booking
│ │ │ └── perfsuite
│ │ │ └── app
│ │ │ ├── monitoring
│ │ │ ├── AppStartupTimeListener.kt
│ │ │ ├── AppTtiListener.kt
│ │ │ └── ActivityFrameMetricsListener.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── SampleApp.kt
│ │ └── AndroidManifest.xml
└── build.gradle.kts
├── gradle.properties
├── settings.gradle.kts
├── .github
└── workflows
│ └── ci.yml
├── perfsuite
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── booking
│ │ └── perfsuite
│ │ ├── internal
│ │ ├── SystemUtil.kt
│ │ └── FrameDrawUtil.kt
│ │ ├── rendering
│ │ ├── RenderingMetrics.kt
│ │ ├── RenderingMetricsMapper.kt
│ │ └── ActivityFrameMetricsTracker.kt
│ │ ├── tti
│ │ ├── helpers
│ │ │ ├── FragmentTtfrHelper.kt
│ │ │ └── ActivityTtfrHelper.kt
│ │ ├── ViewTtiTracker.kt
│ │ └── BaseTtiTracker.kt
│ │ └── startup
│ │ ├── AppStartTimeProvider.kt
│ │ └── AppStartupTimeTracker.kt
└── build.gradle.kts
├── gradlew.bat
├── gradlew
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | *.iml
4 | .idea/
5 | .gradle
6 | /captures
7 |
8 | build/
9 | local.properties
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sampleApp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PerfSuite SampleApp
3 |
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bookingcom/perfsuite-android/HEAD/sampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri May 12 17:39:59 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | name=perfsute
2 | group=com.booking
3 | version=0.4
4 |
5 | #Gradle
6 | org.gradle.parallel=true
7 | org.gradle.caching=true
8 | org.gradle.configureondemand=true
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 |
11 | #Kotlin
12 | kotlin.code.style=official
13 |
14 | #Android
15 | android.useAndroidX=true
16 |
--------------------------------------------------------------------------------
/sampleApp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ffb700
4 | #006ce4
5 | #003b95
6 |
7 | #FF000000
8 | #FFFFFFFF
9 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "PerfSuite"
2 |
3 | pluginManagement {
4 | repositories {
5 | gradlePluginPortal()
6 | google()
7 | mavenCentral()
8 | }
9 | }
10 |
11 | @Suppress("UnstableApiUsage")
12 | dependencyResolutionManagement {
13 | repositories {
14 | google()
15 | mavenCentral()
16 | }
17 | }
18 |
19 | include(":sampleApp")
20 | include(":perfsuite")
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: set up JDK 17
15 | uses: actions/setup-java@v3
16 | with:
17 | java-version: '17'
18 | distribution: 'temurin'
19 | - name: Build the project
20 | run: ./gradlew clean build
21 |
--------------------------------------------------------------------------------
/perfsuite/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sampleApp/src/main/java/com/booking/perfsuite/app/monitoring/AppStartupTimeListener.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.app.monitoring
2 |
3 | import android.app.Activity
4 | import android.util.Log
5 | import com.booking.perfsuite.startup.AppStartupTimeTracker
6 |
7 | internal object AppStartupTimeListener : AppStartupTimeTracker.Listener {
8 |
9 | override fun onColdStartupTimeIsReady(
10 | startupTime: Long,
11 | firstActivity: Activity,
12 | isActualColdStart: Boolean
13 | ) {
14 | Log.d("PerfSuite", "Startup time = ${startupTime}ms")
15 | }
16 | }
--------------------------------------------------------------------------------
/sampleApp/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/sampleApp/src/main/java/com/booking/perfsuite/app/monitoring/AppTtiListener.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.app.monitoring
2 |
3 | import android.util.Log
4 | import com.booking.perfsuite.tti.BaseTtiTracker
5 |
6 | object AppTtiListener : BaseTtiTracker.Listener {
7 |
8 | override fun onScreenCreated(screen: String) {}
9 |
10 | override fun onFirstFrameIsDrawn(screen: String, duration: Long) {
11 | Log.d("PerfSuite", "$screen - TTFR = ${duration}ms")
12 | }
13 |
14 | override fun onFirstUsableFrameIsDrawn(screen: String, duration: Long) {
15 | Log.d("PerfSuite", "$screen - TTI = ${duration}ms")
16 | }
17 | }
--------------------------------------------------------------------------------
/sampleApp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/sampleApp/src/main/java/com/booking/perfsuite/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.app
2 |
3 | import android.os.Bundle
4 | import android.widget.TextView
5 | import androidx.appcompat.app.AppCompatActivity
6 |
7 | class MainActivity : AppCompatActivity() {
8 |
9 | private lateinit var contentView: TextView
10 |
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 |
14 | contentView = TextView(this)
15 | contentView.text = "Loading content..."
16 | setContentView(contentView)
17 |
18 | contentView.postDelayed({
19 | contentView.text = "Screen is usable"
20 | reportIsUsable()
21 | }, 1000)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "1.9.0"
3 |
4 | androidGradlePlugin = "8.0.2"
5 |
6 | androidx-ktx = "1.10.1"
7 | androidx-appcompat = "1.6.1"
8 |
9 | [libraries]
10 | androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-ktx" }
11 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
12 | androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidx-appcompat" }
13 |
14 | [plugins]
15 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
16 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
17 | android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
--------------------------------------------------------------------------------
/sampleApp/src/main/java/com/booking/perfsuite/app/monitoring/ActivityFrameMetricsListener.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.app.monitoring
2 |
3 | import android.app.Activity
4 | import android.util.Log
5 | import android.util.SparseIntArray
6 | import com.booking.perfsuite.rendering.ActivityFrameMetricsTracker
7 | import com.booking.perfsuite.rendering.RenderingMetricsMapper
8 |
9 | internal object ActivityFrameMetricsListener : ActivityFrameMetricsTracker.Listener {
10 |
11 | override fun onFramesMetricsReady(
12 | activity: Activity,
13 | frameMetrics: Array,
14 | foregroundTime: Long?
15 | ) {
16 | val activityName = activity.javaClass.simpleName
17 | val data = RenderingMetricsMapper.toRenderingMetrics(frameMetrics, foregroundTime) ?: return
18 |
19 | Log.d("PerfSuite", "Frame metrics for [$activityName] are collected: $data")
20 | }
21 | }
--------------------------------------------------------------------------------
/sampleApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin)
3 | alias(libs.plugins.android.application)
4 | }
5 |
6 | kotlin {
7 | jvmToolchain(17)
8 | }
9 |
10 | android {
11 | namespace = "com.booking.perfsuite.app"
12 | compileSdk = 33
13 |
14 | defaultConfig {
15 | applicationId = "com.booking.perfsuite.app"
16 | minSdk = 24
17 | targetSdk = 33
18 | versionCode = 1
19 | versionName = "1.0"
20 | }
21 |
22 | buildTypes {
23 | release {
24 | isMinifyEnabled = true
25 | }
26 | }
27 |
28 | compileOptions {
29 | sourceCompatibility = JavaVersion.VERSION_17
30 | targetCompatibility = JavaVersion.VERSION_17
31 | }
32 | }
33 |
34 | dependencies {
35 | implementation(project(":perfsuite"))
36 |
37 | implementation(libs.androidx.ktx)
38 | implementation(libs.androidx.appcompat)
39 | }
40 |
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/internal/SystemUtil.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.internal
2 |
3 | import android.app.ActivityManager
4 | import android.os.SystemClock
5 |
6 | /**
7 | * Returns current time for performance measurements by relying on [SystemClock.uptimeMillis],
8 | * since the measurements should not be affected by what happens in a deep sleep
9 | *
10 | * @return current time in milliseconds
11 | */
12 | internal fun nowMillis(): Long = SystemClock.uptimeMillis()
13 |
14 | /**
15 | * Detects if the process is currently in "foreground" state by checking
16 | * [android.app.ActivityManager.RunningAppProcessInfo.importance]
17 | *
18 | * @return `true` if the process is currently in "foreground" state
19 | */
20 | internal fun isForegroundProcess(): Boolean {
21 | val processInfo = ActivityManager.RunningAppProcessInfo()
22 | ActivityManager.getMyMemoryState(processInfo)
23 | return processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
24 | }
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/rendering/RenderingMetrics.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.rendering
2 |
3 | /**
4 | * Class aggregating raw frame metrics into more high level representation suitable for
5 | * reporting and simpler to analyze
6 | */
7 | public data class RenderingMetrics(
8 |
9 | /**
10 | * Total amount of frames rendered during the screen session
11 | */
12 | val totalFrames: Long,
13 |
14 | /**
15 | * Amount of frames that take more that 16ms to render (considered as "slow")
16 | */
17 | val slowFrames: Long,
18 |
19 | /**
20 | * Amount of frames that take more that 700ms to render (considered as "frozen")
21 | */
22 | val frozenFrames: Long,
23 |
24 | /**
25 | * Total time of freezing the UI due to rendering of the slow frames per screen session.
26 | * This metric accumulates all freeze durations during the screen session
27 | */
28 | val totalFreezeTimeMs: Long = 0,
29 |
30 | /**
31 | * Total time spent by user on current screen. Helpful as a supporting metrics, since sometimes
32 | * increase in Total Freeze Time might be caused by longer interactions with the screen
33 | */
34 | val foregroundTimeMs: Long? = null
35 | )
36 |
--------------------------------------------------------------------------------
/sampleApp/src/main/java/com/booking/perfsuite/app/SampleApp.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.app
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.view.View
6 | import com.booking.perfsuite.app.monitoring.ActivityFrameMetricsListener
7 | import com.booking.perfsuite.app.monitoring.AppStartupTimeListener
8 | import com.booking.perfsuite.app.monitoring.AppTtiListener
9 | import com.booking.perfsuite.rendering.ActivityFrameMetricsTracker
10 | import com.booking.perfsuite.startup.AppStartupTimeTracker
11 | import com.booking.perfsuite.tti.BaseTtiTracker
12 | import com.booking.perfsuite.tti.ViewTtiTracker
13 | import com.booking.perfsuite.tti.helpers.ActivityTtfrHelper
14 |
15 | class SampleApp : Application() {
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 |
20 | // setup startup time tracking
21 | AppStartupTimeTracker.register(this, AppStartupTimeListener)
22 |
23 | // setup rendering performance tracking
24 | ActivityFrameMetricsTracker.register(this, ActivityFrameMetricsListener)
25 |
26 | // setup Activity TTI tracking
27 | ActivityTtfrHelper.register(this, viewTtiTracker)
28 | }
29 | }
30 |
31 | val ttiTracker = BaseTtiTracker(AppTtiListener)
32 | val viewTtiTracker = ViewTtiTracker(ttiTracker)
33 |
34 | fun Activity.reportIsUsable(contentView: View = this.window.decorView) {
35 | viewTtiTracker.onScreenIsUsable(this.javaClass.name, contentView)
36 | }
37 |
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/tti/helpers/FragmentTtfrHelper.kt:
--------------------------------------------------------------------------------
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 | }
43 |
--------------------------------------------------------------------------------
/perfsuite/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.vanniktech.maven.publish.SonatypeHost
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin)
5 | alias(libs.plugins.android.library)
6 | id("com.vanniktech.maven.publish")
7 | }
8 |
9 | kotlin {
10 | jvmToolchain(17)
11 | explicitApi()
12 | }
13 |
14 | android {
15 | namespace = "com.booking.perfsuite"
16 | compileSdk = 33
17 |
18 | defaultConfig {
19 | minSdk = 24
20 | }
21 |
22 | compileOptions {
23 | sourceCompatibility = JavaVersion.VERSION_17
24 | targetCompatibility = JavaVersion.VERSION_17
25 | }
26 | }
27 |
28 | dependencies {
29 | implementation(libs.androidx.ktx)
30 | implementation(libs.androidx.fragment)
31 | }
32 |
33 | // publishing
34 |
35 | val libName = "PerfSuite"
36 | val libDescription = "Lightweight library for collecting app performance metrics"
37 | val libUrl = "https://github.com/bookingcom/perfsuite-android"
38 |
39 |
40 | val groupId = project.group as String
41 | val artifactId = project.name as String
42 | val version = project.version as String
43 |
44 | @Suppress("UnstableApiUsage")
45 | mavenPublishing {
46 | publishToMavenCentral(SonatypeHost.DEFAULT, true)
47 | signAllPublications()
48 |
49 | coordinates(groupId, artifactId, version)
50 |
51 | pom {
52 | name.set(libName)
53 | description.set(libDescription)
54 | url.set(libUrl)
55 |
56 | licenses {
57 | license {
58 | name.set("The Apache License, Version 2.0")
59 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
60 | }
61 | }
62 | scm {
63 | connection.set("scm:git:github.com/bookingcom/perfsuite-android.git")
64 | developerConnection.set("scm:git:ssh://github.com/bookingcom/perfsuite-android.git")
65 | url.set("https://github.com/bookingcom/perfsuite-android")
66 | }
67 | developers {
68 | developer {
69 | name.set("Vadim Chepovskii")
70 | email.set("smbduknow@gmail.com")
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/startup/AppStartTimeProvider.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.startup
2 |
3 | import android.content.ContentProvider
4 | import android.content.ContentValues
5 | import android.database.Cursor
6 | import android.net.Uri
7 | import android.os.Process
8 | import com.booking.perfsuite.internal.nowMillis
9 |
10 | /**
11 | * Implementation of [ContentProvider] providing the closest possible timestamp
12 | * to the app's startup process.
13 | *
14 | * **Note:** By default it is declared in AndroidManifest.xml with the `android:initOrder="9999"`
15 | * to ensure that this content provider is initialized first and able to properly collect start time
16 | */
17 | public class AppStartTimeProvider: ContentProvider() {
18 |
19 | public companion object {
20 |
21 | // Maximum limit for the time since process starts till Application.onCreate is called
22 | private const val MAX_APP_CREATION_TIME = 60_000L
23 |
24 | private var contentProviderOnCreateTime: Long = 0
25 |
26 | /**
27 | * Returns the timestamp of app's process creation (the earliest possible moment)
28 | *
29 | * @param onCreateTime timestamp of [android.app.Application.onCreate] invocation in millis
30 | * @return app's start time in millis
31 | */
32 | @JvmStatic
33 | public fun getAppStartTime(onCreateTime: Long): Long =
34 | Process.getStartUptimeMillis().let { processStartTime ->
35 | if (onCreateTime - processStartTime > MAX_APP_CREATION_TIME) {
36 | contentProviderOnCreateTime
37 | } else {
38 | processStartTime
39 | }
40 | }
41 | }
42 |
43 | override fun onCreate(): Boolean {
44 | contentProviderOnCreateTime = nowMillis()
45 | return true
46 | }
47 |
48 | override fun query(
49 | uri: Uri,
50 | projection: Array?,
51 | selection: String?,
52 | selectionArgs: Array?,
53 | sortOrder: String?
54 | ): Cursor? = null
55 |
56 | override fun insert(uri: Uri, values: ContentValues?): Uri? = null
57 |
58 | override fun update(
59 | uri: Uri,
60 | values: ContentValues?,
61 | selection: String?,
62 | selectionArgs: Array?
63 | ): Int = 0
64 |
65 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
66 |
67 | override fun getType(uri: Uri): String? = null
68 | }
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/tti/helpers/ActivityTtfrHelper.kt:
--------------------------------------------------------------------------------
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 | }
66 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/tti/ViewTtiTracker.kt:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/rendering/RenderingMetricsMapper.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.rendering
2 |
3 | import android.util.SparseIntArray
4 | import androidx.core.app.FrameMetricsAggregator
5 | import androidx.core.util.forEach
6 | import androidx.core.util.isEmpty
7 |
8 | public object RenderingMetricsMapper {
9 |
10 | /**
11 | * Aggregates raw frames data collected by [ActivityFrameMetricsTracker] into
12 | * [RenderingMetrics] data class, which is more suitable for reporting & metric analysis
13 | */
14 | public fun toRenderingMetrics(
15 | metrics: Array?,
16 | foregroundTime: Long?
17 | ): RenderingMetrics? {
18 | val totalMetrics = metrics?.getOrNull(FrameMetricsAggregator.TOTAL_INDEX)
19 |
20 | if (totalMetrics == null || totalMetrics.isEmpty()) return null
21 |
22 | var total = 0L
23 | var slow = 0L
24 | var frozen = 0L
25 | var totalFreezeTime = 0L
26 |
27 | totalMetrics.forEach { frameDuration, numberOfFrames ->
28 | if (!isValidDuration(frameDuration)) return@forEach
29 |
30 | if (frameDuration > FROZEN_FRAME_THRESHOLD_MS) {
31 | frozen += numberOfFrames.toLong()
32 | }
33 | if (frameDuration > SLOW_FRAME_THRESHOLD_MS) {
34 | slow += numberOfFrames.toLong()
35 | totalFreezeTime += frameDuration * numberOfFrames
36 | }
37 | total += numberOfFrames.toLong()
38 | }
39 |
40 | return RenderingMetrics(
41 | totalFrames = total,
42 | slowFrames = slow,
43 | frozenFrames = frozen,
44 | totalFreezeTimeMs = totalFreezeTime,
45 | foregroundTimeMs = foregroundTime
46 | )
47 | }
48 |
49 | /**
50 | * Due to potential bug in [FrameMetricsAggregator] in combination with
51 | * unsafe int to long cast there (see FrameMetricsApi24Impl.addDurationItem(..))
52 | * it produces sometimes non-realistic frames durations such as negatives and
53 | * extremely high values close to [Integer.MAX_VALUE].
54 | *
55 | * Since it's barely possible to identify and fix the issue inside the SDK we
56 | * exclude such frame durations from rendering reports
57 | */
58 | private fun isValidDuration(frameDuration: Int): Boolean =
59 | frameDuration in 0 until MAX_FRAME_DURATION_MS
60 |
61 | /**
62 | * All frames that takes >16ms are considered as "slow"
63 | *
64 | * For more details: https://support.google.com/googleplay/android-developer/answer/7385505
65 | */
66 | private const val SLOW_FRAME_THRESHOLD_MS = 16
67 |
68 | /**
69 | * All frames that takes >700ms are considered as "frozen"
70 | *
71 | * For more details: https://support.google.com/googleplay/android-developer/answer/7385505
72 | */
73 | private const val FROZEN_FRAME_THRESHOLD_MS = 700
74 |
75 | /**
76 | * All frames that takes >5s are considered as invalid and excluded from frames report
77 | * It should be safe to discard this data, since freezes longer than 5s are considered as ANRs
78 | */
79 | private const val MAX_FRAME_DURATION_MS = 5_000
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/rendering/ActivityFrameMetricsTracker.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.rendering
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 | import android.util.SparseIntArray
7 | import androidx.annotation.UiThread
8 | import androidx.core.app.FrameMetricsAggregator
9 | import com.booking.perfsuite.internal.nowMillis
10 | import java.util.WeakHashMap
11 |
12 | /**
13 | * Implementation of frames metric tracking based on
14 | * [android.app.Application.ActivityLifecycleCallbacks]
15 | * which automatically collects frame metrics for every Activity in the app
16 | */
17 | @UiThread
18 | public class ActivityFrameMetricsTracker private constructor(
19 | private val listener: Listener
20 | ) : Application.ActivityLifecycleCallbacks {
21 |
22 | private val aggregator = FrameMetricsAggregator()
23 | private val activityStartTimes = WeakHashMap()
24 |
25 | public companion object {
26 |
27 | /**
28 | * Registers [ActivityFrameMetricsTracker] instance with the app as
29 | * [android.app.Application.ActivityLifecycleCallbacks] to collect frame metrics for
30 | * every activity
31 | *
32 | * Call this method at the app startup, before the first activity is created
33 | *
34 | * @param application current [Application] instance
35 | * @param listener callback invoked every time when any activity's frame metrics are ready
36 | */
37 | @JvmStatic
38 | public fun register(application: Application, listener: Listener) {
39 | val frameMetricsTracker = ActivityFrameMetricsTracker(listener)
40 | application.registerActivityLifecycleCallbacks(frameMetricsTracker)
41 | }
42 | }
43 |
44 | override fun onActivityStarted(activity: Activity) {
45 | aggregator.add(activity)
46 | activityStartTimes[activity] = nowMillis()
47 | }
48 |
49 | override fun onActivityPaused(activity: Activity) {
50 | val metrics = aggregator.reset()
51 | if (metrics != null) {
52 | val foregroundTime = activityStartTimes.remove(activity)
53 | ?.let { nowMillis() - it }
54 | listener.onFramesMetricsReady(activity, metrics, foregroundTime)
55 | }
56 | }
57 |
58 | override fun onActivityStopped(activity: Activity) {
59 | try {
60 | aggregator.remove(activity)
61 | } catch (ignored: Exception) {
62 | // do nothing, aggregator.remove() may cause rare crashes on some devices
63 | }
64 | }
65 |
66 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
67 | override fun onActivityResumed(activity: Activity) {}
68 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
69 | override fun onActivityDestroyed(activity: Activity) {}
70 |
71 | /**
72 | * Listener interface providing notifications when the activity's frame metrics are ready
73 | */
74 | public interface Listener {
75 |
76 | /**
77 | * Called everytime when foreground activity goes to the "paused" state,
78 | * which means that frame metrics for this screen session are collected
79 | *
80 | * @param activity current activity
81 | * @param frameMetrics raw frame metrics collected by [FrameMetricsAggregator]
82 | * @param foregroundTime time in millis, spent by this activity in foreground state
83 | */
84 | public fun onFramesMetricsReady(
85 | activity: Activity,
86 | frameMetrics: Array,
87 | foregroundTime: Long?
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/startup/AppStartupTimeTracker.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.startup
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 | import androidx.annotation.UiThread
7 | import com.booking.perfsuite.internal.doOnFirstDraw
8 | import com.booking.perfsuite.internal.isForegroundProcess
9 | import com.booking.perfsuite.internal.nowMillis
10 |
11 | /**
12 | * This class is responsible for app startup tracking & measures the time since the earliest
13 | * possible moment of the app's process creation till the app's first frame is drawn.
14 | *
15 | * Startup time is measured according to the Google's definition of
16 | * [Cold App Startup Time](https://developer.android.com/topic/performance/vitals/launch-time)
17 | */
18 | public object AppStartupTimeTracker {
19 |
20 | /**
21 | * Register a callback invoked when the app's cold startup time is obtained.
22 | * The method must be called as early as possible from [Application.onCreate].
23 | *
24 | * @param application current [Application] instance
25 | * @param listener callback to be invoked as soon as startup time is ready
26 | */
27 | @JvmStatic
28 | public fun register(application: Application, listener: Listener) {
29 | if(!isForegroundProcess()) return
30 |
31 | val appOnCreateTime = nowMillis()
32 | val appStartTime = AppStartTimeProvider.getAppStartTime(appOnCreateTime)
33 |
34 | application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
35 |
36 | private var isInterrupted = false
37 |
38 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
39 | activity.doOnFirstDraw {
40 | if (!isInterrupted) {
41 | application.unregisterActivityLifecycleCallbacks(this)
42 |
43 | val duration = nowMillis() - appStartTime
44 | val isActualColdStart = savedInstanceState == null
45 | listener.onColdStartupTimeIsReady(duration, activity, isActualColdStart)
46 | }
47 | }
48 | }
49 |
50 | override fun onActivityPaused(activity: Activity) {
51 | // Drop the tracking if the activity paused even before its first frame is drawn
52 | isInterrupted = true
53 | }
54 | override fun onActivityStarted(activity: Activity) {}
55 | override fun onActivityDestroyed(activity: Activity) {}
56 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
57 | override fun onActivityStopped(activity: Activity) {}
58 | override fun onActivityResumed(activity: Activity) {}
59 |
60 | })
61 | }
62 |
63 | /**
64 | * Listener to be invoked when the app's startup time is ready
65 | */
66 | public interface Listener {
67 |
68 | /**
69 | * Called when the app cold startup time is ready
70 | *
71 | * @param startupTime app' cold startup time in milliseconds
72 | * @param firstActivity instance of the first activity launched after the app starts
73 | * @param isActualColdStart the flag informs if the real cold start was detected. If the flag
74 | * is false, then the activity is created with a saved instance state bundle, which mean that
75 | * it shouldn't be considered as purely cold start, but rather as a one of warm start scenarios
76 | */
77 | @UiThread
78 | public fun onColdStartupTimeIsReady(startupTime: Long, firstActivity: Activity, isActualColdStart: Boolean)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/internal/FrameDrawUtil.kt:
--------------------------------------------------------------------------------
1 | package com.booking.perfsuite.internal
2 |
3 | import android.app.Activity
4 | import android.view.View
5 | import android.view.ViewTreeObserver
6 | import android.view.Window
7 | import androidx.annotation.UiThread
8 | import androidx.core.view.doOnAttach
9 |
10 | /**
11 | * Performs the given action when the activity very first frame is drawn.
12 | *
13 | * This function can be called multiple times on the Same activity, and all the submitted actions
14 | * will be called.
15 | */
16 | @UiThread
17 | public fun Activity.doOnFirstDraw(action: () -> Unit) {
18 | window.onDecorViewReady { decorView ->
19 | decorView.doOnNextDraw {
20 | decorView.handler.postAtFrontOfQueue(action) // wait till the draw finished
21 | }
22 | }
23 | }
24 |
25 | /**
26 | * Performs the given action when the view is next drawn.
27 | *
28 | * The action will only be invoked once on the next draw and then removed.
29 | */
30 | @UiThread
31 | public fun View.doOnNextDraw(action: () -> Unit) {
32 | if (!viewTreeObserver.isAlive) return
33 | doOnAttach {
34 | viewTreeObserver.addOnDrawListener(
35 | NextDrawListener(this, action)
36 | )
37 | }
38 | }
39 |
40 | /**
41 | * Performs the given action when the decor view of the current Window is ready
42 | */
43 | private fun Window.onDecorViewReady(action: (decorView: View) -> Unit) {
44 | val decorView = peekDecorView()
45 | if (decorView == null) {
46 | doOnNextContentChanged { action(this.decorView) }
47 | } else {
48 | action(decorView)
49 | }
50 | }
51 |
52 | /**
53 | * Performs the given one-time action when [Window] content changes next time.
54 | *
55 | * This method adds one-shot [Window.Callback] wrapper, which restores the original
56 | * [Window.Callback] right before the callback is invoked for the first time
57 | */
58 | private fun Window.doOnNextContentChanged(action: () -> Unit) {
59 | callbackWrapper().addAction(action)
60 | }
61 |
62 | private fun Window.callbackWrapper(): WindowCallbackWrapper {
63 | return when (val originalCallback = callback) {
64 | is WindowCallbackWrapper -> originalCallback
65 | else -> WindowCallbackWrapper(originalCallback).also {
66 | // Inject custom wrapper which will propagate actions to nested Window.Callbacks.
67 | // No need to reset this later, since any following call will just append its action
68 | // to this instance and the action is removed right after being triggered
69 | callback = it
70 | }
71 | }
72 | }
73 |
74 | private class NextDrawListener(
75 | val view: View,
76 | val callback: () -> Unit
77 | ) : ViewTreeObserver.OnDrawListener {
78 |
79 | private var isInvoked = false
80 | private var viewTreeObserver = view.viewTreeObserver
81 |
82 | override fun onDraw() {
83 | if (isInvoked) return
84 | isInvoked = true
85 |
86 | callback()
87 |
88 | view.post {
89 | if (viewTreeObserver.isAlive) {
90 | viewTreeObserver.removeOnDrawListener(this)
91 | }
92 | }
93 | }
94 | }
95 |
96 | @UiThread
97 | private class WindowCallbackWrapper constructor(
98 | private val originalCallback: Window.Callback
99 | ) : Window.Callback by originalCallback {
100 |
101 | private val onContentChangedCallbacks = mutableListOf<() -> Unit>()
102 |
103 | override fun onContentChanged() {
104 | onContentChangedCallbacks.forEach { it.invoke() }
105 | // clear the callbacks since they only one-shot callbacks are supported here
106 | onContentChangedCallbacks.clear()
107 | originalCallback.onContentChanged()
108 | }
109 |
110 | fun addAction(action: () -> Unit) {
111 | onContentChangedCallbacks += action
112 | }
113 | }
--------------------------------------------------------------------------------
/perfsuite/src/main/java/com/booking/perfsuite/tti/BaseTtiTracker.kt:
--------------------------------------------------------------------------------
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()
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 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PerformanceSuite Android
2 |
3 | [](https://github.com/bookingcom/perfsuite-android/actions/workflows/ci.yml)
4 | [](https://github.com/bookingcom/perfsuite-android/blob/main/LICENSE)
5 | [](https://github.com/bookingcom/perfsuite-android/releases)
6 |
7 | Lightweight library designed to measure and collect performance metrics for Android applications
8 | in production.
9 |
10 | Unlike other SaaS solutions (like Firebase Performance) it focuses only on collecting pure metrics
11 | and not enforces you to use specific reporting channel and monitoring infrastructure, so you're
12 | flexible with re-using the monitoring approaches already existing in your product.
13 |
14 | ## Getting started
15 |
16 | Library supports collecting following performance metrics:
17 | - App Cold Startup Time
18 | - Rendering performance per Activity
19 | - Time to Interactive & Time to First Render per screen
20 |
21 | 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)
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
26 |
27 | ### Dependency
28 |
29 | The library is available on Maven Central:
30 | ```groovy
31 | implementation("com.booking:perfsuite:0.3")
32 | ```
33 |
34 | ### Collecting Startup Times
35 |
36 | Implement the callback invoked once [Startup Time](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f#e383) is collected:
37 |
38 | ```kotlin
39 | class MyStartupTimeListener : AppStartupTimeTracker.Listener {
40 |
41 | override fun onColdStartupTimeIsReady(
42 | startupTime: Long,
43 | firstActivity: Activity,
44 | isActualColdStart: Boolean
45 | ) {
46 | // Log or report Startup Time metric in a preferable way
47 | }
48 | }
49 | ```
50 |
51 | Then register your listener as early in `Application#onCreate` as possible:
52 |
53 | ```kotlin
54 | class MyApplication : Application() {
55 |
56 | override fun onCreate() {
57 | super.onCreate()
58 | AppStartupTimeTracker.register(this, MyStartupTimeListener())
59 | }
60 | }
61 | ```
62 |
63 | ### Collecting Frame Metrics
64 |
65 | Implement the callback invoked every time when the foreground `Activity` is paused
66 | (we can call it "the end of the screen session") and use `RenderingMetricsMapper` to
67 | represent [rendering performance metrics](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f#3eca)
68 | in a convenient aggregated format:
69 |
70 | ```kotlin
71 | class MyFrameMetricsListener : ActivityFrameMetricsTracker.Listener {
72 |
73 | override fun onFramesMetricsReady(
74 | activity: Activity,
75 | frameMetrics: Array,
76 | foregroundTime: Long?
77 | ) {
78 | val data = RenderingMetricsMapper.toRenderingMetrics(frameMetrics, foregroundTime) ?: return
79 | // Log or report Frame Metrics for current Activity's "session" in a preferable way
80 | }
81 | }
82 | ```
83 |
84 | Then register your listener in `Application#onCreate` before any activity is created:
85 |
86 | ```kotlin
87 | class MyApplication : Application() {
88 |
89 | override fun onCreate() {
90 | super.onCreate()
91 | ActivityFrameMetricsTracker.register(this, MyFrameMetricsListener)
92 | }
93 | }
94 | ```
95 |
96 | As per the code sample above you can use `RenderingMetricsMapper` to collect frames metics in the aggreated format which is convenient for reporting to the backend.
97 | Then metrics will be represented as [`RenderingMetrics`](src/main/java/com/booking/perfsuite/rendering/RenderingMetrics.kt) instance, which will provide data on:
98 | - `totalFrames` - total amount of frames rendered during the screen session
99 | - `totalFreezeTimeMs` - total accumulated time of the UI being frozen during the screen session
100 | - `slowFrames` - amount of [slow frames](https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#slow-rendering-frames) per screens session
101 | - `frozenFrames` - amount of [frozen frames](https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#frozen-frames) per screens session
102 |
103 | 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)
104 |
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 |
154 | #### Collecting TTI/TTFR for `Fragment`-based screens in single-Activity apps
155 |
156 | The example above works for `Activity`-based screens, however if you use the "Single-Activity" approach you also need
157 | to enable TTI/TTFR tracking for the Fragments inside you main Activity:
158 |
159 | ```kotlin
160 | class MyMainActivity : Activity() {
161 |
162 | override fun onCreate() {
163 | super.onCreate()
164 | val fragmentHelper = FragmentTtfrHelper(viewTtiTracker)
165 | supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentHelper, true)
166 | }
167 | }
168 | ```
169 | Then you can call `viewTtiTracker.onScreenIsUsable(..)` in Fragments the same way as described above.
170 |
171 | ## Additional documentation
172 | - [Measuring mobile apps performance in production](https://medium.com/booking-com-development/measuring-mobile-apps-performance-in-production-726e7e84072f)
173 | - [App Startup Time documentation by Google](https://developer.android.com/topic/performance/vitals/launch-time)
174 | - [Rendering Performance documentation by Google](https://developer.android.com/topic/performance/vitals/render)
175 | - [Android Vitals Articles by Pierre Yves Ricau](https://dev.to/pyricau/series/7827)
176 |
177 | ## ACKNOWLEDGMENT
178 |
179 | This software was originally developed at Booking.com.
180 | With approval from Booking.com, this software was released as open source,
181 | for which the authors would like to express their gratitude.
182 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | Copyright 2023 Booking.com
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
191 |
--------------------------------------------------------------------------------