Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions baselineProfile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
58 changes: 58 additions & 0 deletions baselineProfile/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.baselineprofile)
}

android {
namespace = "org.mifos.baselineprofile"
compileSdk = 36

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = "11"
}

defaultConfig {
minSdk = 28
targetSdk = 36

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

targetProjectPath = ":cmp-android"

flavorDimensions += listOf("contentType")
productFlavors {
create("demo") { dimension = "contentType" }
create("prod") { dimension = "contentType" }
}

}

// 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
}

dependencies {
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.espresso.core)
implementation(libs.androidx.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 }
)
}
Comment on lines +50 to +57
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle potential null applicationId gracefully.

The expression artifactsLoader.load(it)?.applicationId can yield null if the artifacts fail to load or the applicationId is unavailable. Passing a null value to instrumentationRunnerArguments would cause the benchmark to fail silently or with an unclear error at runtime.

🛡️ Proposed fix to add null handling
 androidComponents {
     onVariants { v ->
         val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
         v.instrumentationRunnerArguments.put(
             "targetAppId",
-            v.testedApks.map { artifactsLoader.load(it)?.applicationId }
+            v.testedApks.map { apks ->
+                artifactsLoader.load(apks)?.applicationId
+                    ?: error("Failed to load applicationId from tested APKs for variant ${v.name}")
+            }
         )
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
androidComponents {
onVariants { v ->
val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
v.instrumentationRunnerArguments.put(
"targetAppId",
v.testedApks.map { artifactsLoader.load(it)?.applicationId }
)
}
androidComponents {
onVariants { v ->
val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
v.instrumentationRunnerArguments.put(
"targetAppId",
v.testedApks.map { apks ->
artifactsLoader.load(apks)?.applicationId
?: error("Failed to load applicationId from tested APKs for variant ${v.name}")
}
)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@baselineProfile/build.gradle.kts` around lines 50 - 57, The current
onVariants block may put nullable applicationIds into
instrumentationRunnerArguments via v.testedApks.map {
artifactsLoader.load(it)?.applicationId }; change this to handle nulls by using
v.testedApks.mapNotNull { artifactsLoader.load(it)?.applicationId } and only
call v.instrumentationRunnerArguments.put("targetAppId", ...) when the resulting
collection is not empty (e.g., joinToString(",") for multiple ids) or provide a
safe default (empty string) to avoid inserting nulls; update references in the
androidComponents/onVariants block (v, artifactsLoader, getBuiltArtifactsLoader,
testedApks, load, applicationId, instrumentationRunnerArguments) accordingly.

}
1 change: 1 addition & 0 deletions baselineProfile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.mifos.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 :cmp-android: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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.mifos.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")
?: 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
}
)
}
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ plugins {
alias(libs.plugins.ktrofit) apply false

alias(libs.plugins.room) apply false
alias(libs.plugins.baselineprofile) apply false
}

object DynamicVersion {
Expand Down
2 changes: 2 additions & 0 deletions cmp-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ plugins {
alias(libs.plugins.roborazzi)
alias(libs.plugins.aboutLibraries)
alias(libs.plugins.ksp)
alias(libs.plugins.android.application)
}

val packageNameSpace: String = libs.versions.androidPackageNamespace.get()
Expand Down Expand Up @@ -132,6 +133,7 @@ dependencies {
implementation(libs.filekit.compose)
implementation(libs.filekit.dialog.compose)
implementation(libs.filekit.coil)
"baselineProfile"(project(":baselineprofile"))

runtimeOnly(libs.androidx.compose.runtime)
debugImplementation(libs.androidx.compose.ui.tooling)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ kotlin.code.style=official
# https://developer.android.com/build/releases/gradle-plugin#default-changes
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false
android.testOptions.unitTests.isIncludeAndroidResources = true
android.testOptions.unitTests.isIncludeAndroidResources=true
org.jetbrains.compose.experimental.jscanvas.enabled=true

RblClientIdProp=a
Expand Down
9 changes: 9 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ androidPackageNamespace = "cmp.android.app"
room = "2.8.4"
sqliteBundled = "2.6.2"

#Benchmarking
benchmarkMacroJunit4 = "1.4.1"
uiautomator = "2.3.0"
espressoCore = "3.7.0"

[libraries]
android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
Expand Down Expand Up @@ -329,6 +334,10 @@ aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref
aboutlibraries-compose-core = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutLibraries" }
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" }

androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }

[bundles]
androidx-compose-ui-test = [
"androidx-compose-ui-test",
Expand Down
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@ check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
Java Home: [${System.getProperty("java.home")}]
https://developer.android.com/build/jdks#jdk-config-in-studio
""".trimIndent()
}
}
include(":baselineprofile")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Module include path casing likely breaks project resolution on case-sensitive environments.

Line 92 includes :baselineprofile, but the checked-in module directory is baselineProfile (capital P). Without explicit projectDir mapping, Gradle may fail to locate the module on Linux CI.

🐛 Proposed fix
 include(":baselineprofile")
+project(":baselineprofile").projectDir = file("baselineProfile")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
include(":baselineprofile")
include(":baselineprofile")
project(":baselineprofile").projectDir = file("baselineProfile")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@settings.gradle.kts` at line 92, The included module name casing is wrong:
change the include(":baselineprofile") entry to match the actual directory name
(use ":baselineProfile") or alternatively add an explicit projectDir mapping for
project(":baselineprofile") to file("baselineProfile"); update the
settings.gradle.kts entry so the module identifier and the filesystem directory
casing match (or are explicitly mapped) to avoid resolution failures on
case-sensitive CI.

Loading