Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ and they are covered automatically. To run them with a device or emulator attach
Add `-Pandroid.testInstrumentationRunnerArguments.requireMapLoaded=true` to also
wait for map tiles to render, which needs a real key.

The three library modules ship Baseline Profiles in
`src/main/generated/baselineProfiles/baseline-prof.txt`. They are generated by
the `:baselineprofile` module, which runs the `maps-app` demos on a device and
keeps only each library's own classes. Regenerate them after changing a hot path
(map setup, markers, clustering, shapes, overlays, ScaleBar), with an emulator
or device attached and a real API key:

```bash
./gradlew :maps-compose:generateBaselineProfile \
:maps-compose-utils:generateBaselineProfile \
:maps-compose-widgets:generateBaselineProfile
```

Running `maps-app` requires a Maps API key: put `MAPS_API_KEY=...` in
`secrets.properties` at the repo root (see `local.defaults.properties` for the
template). Never hardcode or commit API keys.
Expand Down
60 changes: 60 additions & 0 deletions baselineprofile/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import org.jetbrains.kotlin.gradle.dsl.JvmTarget

// Generates the Baseline Profiles shipped with maps-compose, maps-compose-utils and
// maps-compose-widgets by running the maps-app demos on a device. See BaselineProfileGenerator.
plugins {
alias(libs.plugins.android.test)
id("androidx.baselineprofile")
}

android {
namespace = "com.google.maps.android.compose.baselineprofile"
compileSdk = libs.versions.androidCompileSdk.get().toInt()

defaultConfig {
// Macrobenchmark and the Baseline Profile rule need API 28.
minSdk = 28
targetSdk = libs.versions.androidTargetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

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

targetProjectPath = ":maps-app"
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

baselineProfile {
// Run on the attached device or emulator. It needs a real MAPS_API_KEY so the maps load.
useConnectedDevices = true
}

dependencies {
implementation(libs.androidx.test.junit.ktx)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}
2 changes: 2 additions & 0 deletions baselineprofile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.maps.android.compose.baselineprofile

import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* Generates the Baseline Profiles of the library modules by running the maps-app demos that
* exercise them: the map itself, markers and Compose markers, clustering, shapes, overlays and
* the ScaleBar widget.
*
* Run it with an emulator or device attached and a real MAPS_API_KEY in secrets.properties:
* ```
* ./gradlew :maps-compose:generateBaselineProfile \
* :maps-compose-utils:generateBaselineProfile \
* :maps-compose-widgets:generateBaselineProfile
* ```
* Each module keeps only its own classes (see the `baselineProfile` filter in its build file) and
* the result is written to its `src/main/generated/baselineProfiles/baseline-prof.txt`, which is
* packaged in the AAR.
*/
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()

@Test
fun generate() {
rule.collect(packageName = TARGET_PACKAGE) {
pressHome()
startActivityAndWait()
DEMOS.forEach { demo -> exploreDemo(demo) }
}
}
}

/** Demos that cover the public API of maps-compose, maps-compose-utils and maps-compose-widgets. */
private val DEMOS = listOf(
"BasicMapActivity",
"MapInColumnActivity",
"markerexamples.AdvancedMarkersActivity",
"markerexamples.MarkerClusteringActivity",
"markerexamples.markerscollection.MarkersCollectionActivity",
"markerexamples.draggablemarkerscollectionwithpolygon.DraggableMarkersCollectionWithPolygonActivity",
"TileOverlayActivity",
"GroundOverlayActivity",
"ScaleBarActivity",
)

/**
* Opens [demo], waits for the map to load and pans and zooms it, so camera updates, recomposition
* of the map content and re-clustering are part of the profile.
*/
internal fun MacrobenchmarkScope.exploreDemo(demo: String) {
// startActivityAndWait relies on frame stats that the map's SurfaceView does not always
// report, so the demos are started with `am start -W`, which waits for the launch instead.
device.executeShellCommand("am start -W -n $packageName/.$demo")
// The map loads asynchronously after the first frame, and there is no view to wait for.
Thread.sleep(MAP_LOAD_DELAY_MS)

val centerX = device.displayWidth / 2
val centerY = device.displayHeight / 2
val distance = device.displayWidth / 4
device.swipe(centerX + distance, centerY, centerX - distance, centerY, SWIPE_STEPS)
device.waitForIdle()
device.swipe(centerX, centerY + distance, centerX, centerY - distance, SWIPE_STEPS)
device.waitForIdle()
// Double tap zooms in.
device.click(centerX, centerY)
device.click(centerX, centerY)
Thread.sleep(CAMERA_ANIMATION_DELAY_MS)

device.pressBack()
device.waitForIdle()
}

internal const val TARGET_PACKAGE = "com.google.maps.android.compose"

private const val MAP_LOAD_DELAY_MS = 3_000L
private const val CAMERA_ANIMATION_DELAY_MS = 1_000L
private const val SWIPE_STEPS = 20
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.maps.android.compose.baselineprofile

import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* Measures the effect of the Baseline Profiles: opening the maps-app markers and clustering demos
* without any ahead-of-time compilation, and with the Baseline Profiles of the app and its
* libraries applied.
*
* Run it on a physical device for meaningful numbers:
* ```
* ./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest
* ```
*/
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val rule = MacrobenchmarkRule()

@Test
fun startupCompilationNone() = startup(CompilationMode.None())

@Test
fun startupCompilationBaselineProfiles() = startup(CompilationMode.Partial(BaselineProfileMode.Require))

@Test
fun demosCompilationNone() = demos(CompilationMode.None())

@Test
fun demosCompilationBaselineProfiles() = demos(CompilationMode.Partial(BaselineProfileMode.Require))

private fun startup(compilationMode: CompilationMode) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = ITERATIONS,
setupBlock = { pressHome() },
) {
startActivityAndWait()
}

private fun demos(compilationMode: CompilationMode) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = listOf(FrameTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = ITERATIONS,
setupBlock = { pressHome() },
) {
exploreDemo("markerexamples.AdvancedMarkersActivity")
exploreDemo("markerexamples.MarkerClusteringActivity")
}
}

private const val ITERATIONS = 5
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ buildscript {
}
dependencies {
classpath(libs.android.gradle.plugin)
classpath(libs.androidx.baselineprofile.gradle.plugin)
classpath(libs.maps.secrets.plugin)
classpath(libs.kotlin.gradle.plugin)
classpath(libs.dokka.plugin)
Expand All @@ -34,6 +35,7 @@ plugins {
alias(libs.plugins.dependency.analysis)
alias(libs.plugins.versions)
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.spotless)
alias(libs.plugins.binary.compatibility.validator)
Expand Down
7 changes: 7 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mapsutils = "7.0.0"

# Testing
androidCore = "1.7.0"
androidxBenchmark = "1.5.0"
androidxtest = "1.7.0"
espresso = "3.7.0"
junit = "4.13.2"
Expand All @@ -45,10 +46,12 @@ kover = "0.9.11"
robolectric = "4.17"
screenshot = "0.0.1-alpha14"
truth = "1.4.5"
uiautomator = "2.4.0"

[libraries]
# Base
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
androidx-baselineprofile-gradle-plugin = { module = "androidx.benchmark:benchmark-baseline-profile-gradle-plugin", version.ref = "androidxBenchmark" }
dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
gradle-maven-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "gradleMavenPublishPlugin" }
binary-compatibility-validator = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version.ref = "binaryCompatibilityValidator" }
Expand Down Expand Up @@ -86,6 +89,8 @@ androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", vers
androidx-test-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitktx" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidCore" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxtest" }
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" }
androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidxBenchmark" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
Expand All @@ -99,6 +104,8 @@ truth = { module = "com.google.truth:truth", version.ref = "truth" }
[plugins]
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
android-application = { id = "com.android.application", version.ref = "agp" }
android-test = { id = "com.android.test", version.ref = "agp" }
androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxBenchmark" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
dependency-analysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
Expand Down
19 changes: 19 additions & 0 deletions maps-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ plugins {
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
alias(libs.plugins.compose.compiler)
alias(libs.plugins.screenshot)
// Builds the release variants that :baselineprofile runs to generate the libraries' profiles.
id("androidx.baselineprofile")
jacoco
}

Expand All @@ -36,6 +38,23 @@ android {
getByName("release") {
enableAndroidTestCoverage = true
}
// Used by :baselineprofile to generate the libraries' Baseline Profiles and to benchmark them.
// The androidx.baselineprofile plugin does not add these on this AGP version, so they are
// declared here, matching what it would create: release, not minified, not instrumented
// for coverage, profileable and signed with the debug key.
listOf("nonMinifiedRelease", "benchmarkRelease").forEach { name ->
create(name) {
initWith(getByName("release"))
matchingFallbacks += "release"
isMinifyEnabled = false
isShrinkResources = false
isDebuggable = false
isProfileable = true
enableAndroidTestCoverage = false
enableUnitTestCoverage = false
signingConfig = signingConfigs.getByName("debug")
}
}
}

namespace = "com.google.maps.android.compose"
Expand Down
10 changes: 10 additions & 0 deletions maps-compose-utils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.compose.compiler)
id("androidx.baselineprofile")
id("android.maps.compose.PublishingConventionPlugin")
id("org.jetbrains.dokka")
}
Expand Down Expand Up @@ -74,7 +75,16 @@ composeCompiler {
}
}

baselineProfile {
// The profile is generated from maps-app. Keep only this library's classes.
filter {
include("com.google.maps.android.compose.clustering.**")
include("com.google.maps.android.compose.wms.**")
}
}

dependencies {
baselineProfile(project(":baselineprofile"))
api(project(":maps-compose"))

implementation(libs.androidx.core)
Expand Down
Loading
Loading