diff --git a/.gitignore b/.gitignore index 4752469dc1c..3c98670067a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .idea/copilot .idea/libraries .idea/inspectionProfiles +.idea/runConfigurations .idea/sonarlint .idea/*.xml .DS_Store diff --git a/baselineprofile/.gitignore b/baselineprofile/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/baselineprofile/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/baselineprofile/build.gradle b/baselineprofile/build.gradle new file mode 100644 index 00000000000..df8705fd7fc --- /dev/null +++ b/baselineprofile/build.gradle @@ -0,0 +1,75 @@ +import com.android.build.api.dsl.ManagedVirtualDevice + +plugins { + id 'com.android.test' + id 'org.jetbrains.kotlin.android' + id 'androidx.baselineprofile' +} + +android { + namespace 'im.vector.app.baselineprofile' + compileSdk versions.compileSdk + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } + + kotlinOptions { + jvmTarget = versions.jvmTarget + } + + defaultConfig { + minSdk 28 + targetSdk versions.targetSdk + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + targetProjectPath = ":vector-app" + + flavorDimensions += ["store"] + productFlavors { + gplay { dimension = "store" } + fdroid { dimension = "store" } + } + + // This code creates the gradle managed device used to generate baseline profiles. + // To use GMD please invoke generation through the command line: + // ./gradlew :vector-app:generateBaselineProfile + testOptions.managedDevices.devices { + pixel6Api34(ManagedVirtualDevice) { + device = "Pixel 6" + apiLevel = versions.targetSdk + // Since play services are used for gplay build, using a google image + systemImageSource = "google" + } + } +} + +// This is the configuration block for the Baseline Profile plugin. +// You can specify to run the generators on a managed devices or connected devices. +baselineProfile { + // Option 1: A Gradle Managed Device for profile generating + managedDevices += "pixel6Api34" + useConnectedDevices = false + // Option 2: For benchmarking the app with and without the profiles applied on a real device +// useConnectedDevices = true +} + +dependencies { + implementation libs.androidx.junit + implementation libs.androidx.testRunner + implementation libs.androidx.testUiAutomator + implementation libs.androidx.benchmarkMacroJunit +} + +androidComponents { + onVariants(selector().all()) { v -> + def artifactsLoader = v.artifacts.getBuiltArtifactsLoader() + v.instrumentationRunnerArguments.put( + "targetAppId", + v.testedApks.map { artifactsLoader.load(it)?.applicationId } + ) + } +} diff --git a/baselineprofile/src/main/AndroidManifest.xml b/baselineprofile/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..227314eeb7d --- /dev/null +++ b/baselineprofile/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/baselineprofile/src/main/java/im/vector/app/baselineprofile/BaselineProfileGenerator.kt b/baselineprofile/src/main/java/im/vector/app/baselineprofile/BaselineProfileGenerator.kt new file mode 100644 index 00000000000..d4cf18bcd69 --- /dev/null +++ b/baselineprofile/src/main/java/im/vector/app/baselineprofile/BaselineProfileGenerator.kt @@ -0,0 +1,68 @@ +package im.vector.app.baselineprofile + +import androidx.benchmark.macro.junit4.BaselineProfileRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This test class generates a basic startup baseline profile for the target package. + * + * We recommend you start with this but add important user flows to the profile to improve their performance. + * Refer to the [baseline profile documentation](https://d.android.com/topic/performance/baselineprofiles) + * for more information. + * + * You can run the generator with the "Generate Baseline Profile" run configuration in Android Studio or + * the equivalent `generateBaselineProfile` gradle task: + * ``` + * ./gradlew :vector-app:generateReleaseBaselineProfile + * ``` + * The run configuration runs the Gradle task and applies filtering to run only the generators. + * + * Check [documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args) + * for more information about available instrumentation arguments. + * + * After you run the generator, you can verify the improvements running the [StartupBenchmarks] benchmark. + * + * When using this class to generate a baseline profile, only API 33+ or rooted API 28+ are supported. + * + * The minimum required version of androidx.benchmark to generate a baseline profile is 1.2.0. + **/ +@RunWith(AndroidJUnit4::class) +@LargeTest +class BaselineProfileGenerator { + + @get:Rule + val rule = BaselineProfileRule() + + @Test + fun generate() { + // The application id for the running build variant is read from the instrumentation arguments. + rule.collect( + packageName = InstrumentationRegistry.getArguments().getString("targetAppId") + ?: throw Exception("targetAppId not passed as instrumentation runner arg"), + + // See: https://d.android.com/topic/performance/baselineprofiles/dex-layout-optimizations + includeInStartupProfile = true + ) { + // This block defines the app's critical user journey. Here we are interested in + // optimizing for app startup. But you can also navigate and scroll through your most important UI. + + // Start default activity for your app + pressHome() + startActivityAndWait() + + // TODO Write more interactions to optimize advanced journeys of your app. + // For example: + // 1. Wait until the content is asynchronously loaded + // 2. Scroll the feed content + // 3. Navigate to detail screen + + // Check UiAutomator documentation for more information how to interact with the app. + // https://d.android.com/training/testing/other-components/ui-automator + } + } +} diff --git a/baselineprofile/src/main/java/im/vector/app/baselineprofile/StartupBenchmarks.kt b/baselineprofile/src/main/java/im/vector/app/baselineprofile/StartupBenchmarks.kt new file mode 100644 index 00000000000..dab52201d23 --- /dev/null +++ b/baselineprofile/src/main/java/im/vector/app/baselineprofile/StartupBenchmarks.kt @@ -0,0 +1,78 @@ +package im.vector.app.baselineprofile + +import androidx.benchmark.macro.BaselineProfileMode +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This test class benchmarks the speed of app startup. + * Run this benchmark to verify how effective a Baseline Profile is. + * It does this by comparing [CompilationMode.None], which represents the app with no Baseline + * Profiles optimizations, and [CompilationMode.Partial], which uses Baseline Profiles. + * + * Run this benchmark to see startup measurements and captured system traces for verifying + * the effectiveness of your Baseline Profiles. You can run it directly from Android + * Studio as an instrumentation test, or run all benchmarks for a variant, for example benchmarkRelease, + * with this Gradle task: + * ``` + * ./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest + * ``` + * + * You should run the benchmarks on a physical device, not an Android emulator, because the + * emulator doesn't represent real world performance and shares system resources with its host. + * + * For more information, see the [Macrobenchmark documentation](https://d.android.com/macrobenchmark#create-macrobenchmark) + * and the [instrumentation arguments documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args). + * + * Before starting the benchmarks from Android Studio's gutter, select the *BenchmarkRelease or release variants in Build Variants panel. + **/ +@RunWith(AndroidJUnit4::class) +@LargeTest +class StartupBenchmarks { + + @get:Rule + val rule = MacrobenchmarkRule() + + @Test + fun startupCompilationNone() = + benchmark(CompilationMode.None()) + + @Test + fun startupCompilationBaselineProfiles() = + benchmark(CompilationMode.Partial(BaselineProfileMode.Require)) + + private fun benchmark(compilationMode: CompilationMode) { + // The application id for the running build variant is read from the instrumentation arguments. + rule.measureRepeated( + packageName = InstrumentationRegistry.getArguments().getString("targetAppId") + ?: throw Exception("targetAppId not passed as instrumentation runner arg"), + metrics = listOf(StartupTimingMetric()), + compilationMode = compilationMode, + startupMode = StartupMode.COLD, + iterations = 10, + setupBlock = { + pressHome() + }, + measureBlock = { + startActivityAndWait() + + // TODO Add interactions to wait for when your app is fully drawn. + // The app is fully drawn when Activity.reportFullyDrawn is called. + // For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter + // from the AndroidX Activity library. + + // Check the UiAutomator documentation for more information on how to + // interact with the app. + // https://d.android.com/training/testing/other-components/ui-automator + } + ) + } +} diff --git a/build.gradle b/build.gradle index 2e4b5b58171..4d78289b4b9 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ buildscript { classpath libs.gradle.gradlePlugin classpath libs.gradle.kotlinPlugin classpath libs.gradle.hiltPlugin + classpath libs.gradle.baselineProfilePlugin classpath 'com.google.firebase:firebase-appdistribution-gradle:4.0.0' classpath 'com.google.gms:google-services:4.3.15' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929' diff --git a/dependencies.gradle b/dependencies.gradle index 0f18443f224..23f5c0f3e77 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,7 +7,7 @@ ext.versions = [ 'jvmTarget' : "17", ] -def gradle = "8.4.2" +def gradle = "8.7.1" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.9.24" def kotlinCoroutines = "1.8.1" @@ -32,6 +32,8 @@ def fragment = "1.8.1" def mockk = "1.13.11" def espresso = "3.6.1" def androidxTest = "1.6.1" +def benchmark = "1.3.3" +def uiAutomator = "2.3.0" def androidxOrchestrator = "1.5.0" def paparazzi = "1.3.4" @@ -39,7 +41,8 @@ ext.libs = [ gradle : [ 'gradlePlugin' : "com.android.tools.build:gradle:$gradle", 'kotlinPlugin' : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin", - 'hiltPlugin' : "com.google.dagger:hilt-android-gradle-plugin:$dagger" + 'hiltPlugin' : "com.google.dagger:hilt-android-gradle-plugin:$dagger", + 'baselineProfilePlugin' : "androidx.baselineprofile:androidx.baselineprofile.gradle.plugin:$benchmark" ], jetbrains : [ 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", @@ -49,6 +52,7 @@ ext.libs = [ androidx : [ 'activity' : "androidx.activity:activity-ktx:1.9.0", 'appCompat' : "androidx.appcompat:appcompat:1.7.0", + 'benchmarkMacroJunit' : "androidx.benchmark:benchmark-macro-junit4:$benchmark", 'biometric' : "androidx.biometric:biometric:1.1.0", 'core' : "androidx.core:core-ktx:1.10.1", 'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0", @@ -68,11 +72,13 @@ ext.libs = [ 'datastore' : "androidx.datastore:datastore:1.0.0", 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", + 'profileinstaller' : "androidx.profileinstaller:profileinstaller:1.4.1", 'coreTesting' : "androidx.arch.core:core-testing:2.2.0", 'testCore' : "androidx.test:core:$androidxTest", 'orchestrator' : "androidx.test:orchestrator:$androidxOrchestrator", 'testRunner' : "androidx.test:runner:$androidxTest", 'testRules' : "androidx.test:rules:$androidxTest", + 'testUiAutomator' : "androidx.test.uiautomator:uiautomator:$uiAutomator", 'espressoCore' : "androidx.test.espresso:espresso-core:$espresso", 'espressoContrib' : "androidx.test.espresso:espresso-contrib:$espresso", 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso", @@ -175,5 +181,3 @@ ext.libs = [ 'robolectric' : "org.robolectric:robolectric:4.13", ] ] - - diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 4b69468e062..e9cff005dbb 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -132,6 +132,7 @@ ext.groups = [ 'com.squareup.okhttp3', 'com.squareup.okio', 'com.squareup.retrofit2', + 'com.squareup.wire', 'com.sun.activation', 'com.sun.istack', 'com.sun.xml.bind', diff --git a/gradle.properties b/gradle.properties index 8cde2fc6875..01637137669 100644 --- a/gradle.properties +++ b/gradle.properties @@ -42,4 +42,4 @@ signing.element.nightly.keyPassword=Secret # Customise the Lint version to use a more recent version than the one bundled with AGP # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html -android.experimental.lint.version=8.6.0-alpha08 +#android.experimental.lint.version=8.6.0-alpha08 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 515ab9d5f18..4094e92b2da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=f8b4f4772d302c8ff580bc40d0f56e715de69b163546944f787c87abf209c961 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/library/external/realmfieldnameshelper/build.gradle b/library/external/realmfieldnameshelper/build.gradle index 79e66702ddf..b6949e7cdb9 100644 --- a/library/external/realmfieldnameshelper/build.gradle +++ b/library/external/realmfieldnameshelper/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'kotlin' apply plugin: 'java' sourceCompatibility = versions.sourceCompat -targetCompatibility = versions.sourceCompat +targetCompatibility = versions.targetCompat dependencies { implementation 'com.squareup:javapoet:1.13.0' @@ -21,3 +21,8 @@ sourceSets { main.java.srcDirs += 'src/main/kotlin' } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(versions.jvmTarget) + } +} diff --git a/settings.gradle b/settings.gradle index a0b9ce65ed3..6b82dac7910 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,3 +21,5 @@ include ':library:external:barcodescanner:zxing' include ':library:rustCrypto' include ':matrix-sdk-android' include ':matrix-sdk-android-flow' + +include ':baselineprofile' diff --git a/vector-app/build.gradle b/vector-app/build.gradle index e96f6ff1f63..bba669b66df 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -10,6 +10,7 @@ apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'kotlinx-knit' apply plugin: 'com.likethesalad.stem' +apply plugin: 'androidx.baselineprofile' if (project.hasProperty("coverage")) { apply plugin: 'jacoco' @@ -299,6 +300,22 @@ android { appId = "${getFirebaseAppId()}" } } + + // Used for baseline and startup profile generating. + nonMinifiedRelease { + // The google-services.json is copied from debug build type + applicationIdSuffix ".debug" + signingConfig = signingConfigs.debug + resValue "string", "app_name", "Element - nmr" + } + + // Used for measuring the performance of the app with and without baseline profiles applied + benchmarkRelease { + // The google-services.json is copied from debug build type + applicationIdSuffix ".debug" + signingConfig = signingConfigs.debug + resValue "string", "app_name", "Element - br" + } } sourceSets { @@ -391,6 +408,9 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation "androidx.sharetarget:sharetarget:1.2.0" + baselineProfile project(':baselineprofile') + implementation libs.androidx.profileinstaller + // Flipper, debug builds only debugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' diff --git a/vector-app/src/gplay/benchmarkRelease/google-services.json b/vector-app/src/gplay/benchmarkRelease/google-services.json new file mode 100644 index 00000000000..713c1d4e039 --- /dev/null +++ b/vector-app/src/gplay/benchmarkRelease/google-services.json @@ -0,0 +1,40 @@ +{ + "project_info": { + "project_number": "912726360885", + "firebase_url": "https://vector-alpha.firebaseio.com", + "project_id": "vector-alpha", + "storage_bucket": "vector-alpha.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:912726360885:android:4ef8f3a0021e774d", + "android_client_info": { + "package_name": "im.vector.app.debug" + } + }, + "oauth_client": [ + { + "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/vector-app/src/gplay/nonMinifiedRelease/google-services.json b/vector-app/src/gplay/nonMinifiedRelease/google-services.json new file mode 100644 index 00000000000..713c1d4e039 --- /dev/null +++ b/vector-app/src/gplay/nonMinifiedRelease/google-services.json @@ -0,0 +1,40 @@ +{ + "project_info": { + "project_number": "912726360885", + "firebase_url": "https://vector-alpha.firebaseio.com", + "project_id": "vector-alpha", + "storage_bucket": "vector-alpha.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:912726360885:android:4ef8f3a0021e774d", + "android_client_info": { + "package_name": "im.vector.app.debug" + } + }, + "oauth_client": [ + { + "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file