summaryrefslogtreecommitdiffhomepage
path: root/android/test
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2025-08-22 15:05:02 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-09-10 08:53:51 +0200
commitbdf63fb1bf50f16d1f8f3c8e9c4064aa20dd59a3 (patch)
tree34b311e6851bff63f4f5f21e666340b82debdd8c /android/test
parentefca82e53a62237fb7fe327ad24e9dc2ae50ddeb (diff)
downloadmullvadvpn-bdf63fb1bf50f16d1f8f3c8e9c4064aa20dd59a3.tar.xz
mullvadvpn-bdf63fb1bf50f16d1f8f3c8e9c4064aa20dd59a3.zip
Add inital baseline profile generation
To improve startup performance this PR adds a baseline profile generation module in test/baselineprofile. The baseline profile plugin requires Junit4 so that is also added as a dependency. A baseline-prof.txt was also generated by running `./gradlew generatePlayProdReleaseBaselineProfile` and checked in. The tests that generate the baselineprofile currently only start the app and accepts the privacy policy. This should be improved later on to improve the startup performance.
Diffstat (limited to 'android/test')
-rw-r--r--android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt8
-rw-r--r--android/test/baselineprofile/build.gradle.kts96
-rw-r--r--android/test/baselineprofile/src/main/AndroidManifest.xml1
-rw-r--r--android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/BaselineProfileGenerator.kt50
-rw-r--r--android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/StartupBenchmarks.kt76
5 files changed, 229 insertions, 2 deletions
diff --git a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt
index a2c743b360..4e1faf5e19 100644
--- a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt
+++ b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt
@@ -10,7 +10,7 @@ class JUnitTest {
@Test
fun `ensure only junit5 annotations are used for functions`() =
- Konsist.scopeFromProject()
+ projectScopeExceptBaseline()
.functions()
.filter {
it.annotations.any { annotation ->
@@ -22,7 +22,7 @@ class JUnitTest {
@Test
fun `ensure only junit5 annotations are used for classes`() =
- Konsist.scopeFromProject()
+ projectScopeExceptBaseline()
.classes()
.filter {
it.annotations.any { annotation ->
@@ -44,6 +44,10 @@ class JUnitTest {
fun `ensure all non android tests have 'ensure' or 'should' in function name`() =
allNonAndroidTests().assertTrue { it.name.containsEnsureOrShould() }
+ // We should exclude baselineprofile since it requires JUnit4
+ private fun projectScopeExceptBaseline() =
+ (Konsist.scopeFromProject() - Konsist.scopeFromDirectory("test/baselineprofile"))
+
private fun String.containsEnsureOrShould(): Boolean {
return contains("ensure") || contains("should") || contains("then")
}
diff --git a/android/test/baselineprofile/build.gradle.kts b/android/test/baselineprofile/build.gradle.kts
new file mode 100644
index 0000000000..6477b5adca
--- /dev/null
+++ b/android/test/baselineprofile/build.gradle.kts
@@ -0,0 +1,96 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.test)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.baselineprofile)
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.test.baselineprofile"
+ compileSdk = libs.versions.compile.sdk.get().toInt()
+ buildToolsVersion = libs.versions.build.tools.get()
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlin {
+ compilerOptions {
+ jvmTarget = JvmTarget.fromTarget(libs.versions.jvm.target.get())
+ allWarningsAsErrors = true
+ }
+ }
+
+ lint {
+ lintConfig = file("${rootProject.projectDir}/config/lint.xml")
+ abortOnError = true
+ warningsAsErrors = true
+ }
+
+ defaultConfig {
+ minSdk = 28
+ targetSdk = libs.versions.target.sdk.get().toInt()
+ targetProjectPath = ":app"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
+ }
+
+ targetProjectPath = ":app"
+
+ flavorDimensions += FlavorDimensions.BILLING
+ flavorDimensions += FlavorDimensions.INFRASTRUCTURE
+
+ productFlavors {
+ create(Flavors.OSS) { dimension = FlavorDimensions.BILLING }
+ create(Flavors.PLAY) { dimension = FlavorDimensions.BILLING }
+ create(Flavors.PROD) {
+ dimension = FlavorDimensions.INFRASTRUCTURE
+ buildConfigField(
+ type = "String",
+ name = "INFRASTRUCTURE_BASE_DOMAIN",
+ value = "\"mullvad.net\"",
+ )
+ }
+ create(Flavors.STAGEMOLE) {
+ dimension = FlavorDimensions.INFRASTRUCTURE
+ buildConfigField(
+ type = "String",
+ name = "INFRASTRUCTURE_BASE_DOMAIN",
+ value = "\"stagemole.eu\"",
+ )
+ }
+ }
+ buildFeatures { buildConfig = true }
+}
+
+// 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 { useConnectedDevices = true }
+
+// Force okio version to 3.9.1 to fix 2.10.0 appearing in the verification metadata file.
+// This is to avoid a osv-scanner complaining a about a vulnerability in okio 2.10.0.
+// Gradle already upgrades okio 2.10.0 to 3.9.1, but it still ends up in the metadata file.
+// If we update androidx.benchmark:benchmark-macro-junit4 we might be able to remove this.
+configurations.all { resolutionStrategy { force("com.squareup.okio:okio:3.9.1") } }
+
+dependencies {
+ implementation(projects.lib.ui.tag)
+ implementation(libs.androidx.junit)
+ implementation(libs.androidx.espresso)
+ implementation(libs.androidx.test.uiautomator)
+ implementation(libs.androidx.benchmark.macro.junit4)
+}
+
+androidComponents {
+ onVariants { v ->
+ val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
+ v.instrumentationRunnerArguments.put(
+ "targetAppId",
+ v.testedApks.map { artifactsLoader.load(it)?.applicationId },
+ )
+ }
+}
diff --git a/android/test/baselineprofile/src/main/AndroidManifest.xml b/android/test/baselineprofile/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..cc947c5679
--- /dev/null
+++ b/android/test/baselineprofile/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest />
diff --git a/android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/BaselineProfileGenerator.kt b/android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/BaselineProfileGenerator.kt
new file mode 100644
index 0000000000..3b292ad949
--- /dev/null
+++ b/android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/BaselineProfileGenerator.kt
@@ -0,0 +1,50 @@
+package net.mullvad.mullvadvpn.test.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 androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This generates a baseline profile for the Mullvad VPN app. Run this from gradle with: ./gradlew
+ * generatePlayProdReleaseBaselineProfile
+ *
+ * This should be done from time to time to keep the profile up to date with the app.
+ *
+ * NOTE: API 33+ or rooted API 28+ is required.
+ */
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class BaselineProfileGenerator {
+
+ @get:Rule val rule = BaselineProfileRule()
+
+ @Test
+ fun generate() {
+ rule.collect(
+ packageName =
+ InstrumentationRegistry.getArguments().getString("targetAppId")
+ ?: error("targetAppId not passed as instrumentation runner arg"),
+
+ // See:
+ // https://d.android.com/topic/performance/baselineprofiles/dex-layout-optimizations
+ includeInStartupProfile = true,
+ ) {
+ pressHome()
+ startActivityAndWait()
+ device.acceptPrivacy()
+ }
+ }
+
+ // This should use PrivacyPage from common but it is currently not possible to access the files
+ // in that module from here. A fix for this is tracked in: DROID-2165
+ private fun UiDevice.acceptPrivacy() {
+ val agreeSelector = By.text("Agree and continue")
+ findObject(agreeSelector)?.click()
+ }
+}
diff --git a/android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/StartupBenchmarks.kt b/android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/StartupBenchmarks.kt
new file mode 100644
index 0000000000..27f1189592
--- /dev/null
+++ b/android/test/baselineprofile/src/main/kotlin/net/mullvad/mullvadvpn/test/baselineprofile/StartupBenchmarks.kt
@@ -0,0 +1,76 @@
+package net.mullvad.mullvadvpn.test.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).
+ */
+@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")
+ ?: error("targetAppId not passed as instrumentation runner arg"),
+ metrics = listOf(StartupTimingMetric()),
+ compilationMode = compilationMode,
+ startupMode = StartupMode.COLD,
+ iterations = 10,
+ setupBlock = { pressHome() },
+ measureBlock = {
+ startActivityAndWait()
+
+ // 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
+ },
+ )
+ }
+}