Skip to content

Commit bf9fb51

Browse files
committed
Add macrobenchmark tests module for engine benchmarks
1 parent 9440c54 commit bf9fb51

22 files changed

+290
-20
lines changed

build.gradle.kts

+2-5
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@ allprojects {
2727
}
2828

2929
subprojects {
30-
// We have some empty folders like the :contrib root folder, which Gradle recognizes as projects.
31-
// Don't configure plugins for those folders.
32-
if (project.buildFile.exists()) {
33-
configureLicensee()
34-
}
30+
applyLicenseeConfig()
31+
3532
tasks.withType(Test::class.java).configureEach {
3633
maxParallelForks = 1
3734
if (project.providers.environmentVariable("GITHUB_ACTIONS").isPresent) {

buildSrc/src/main/kotlin/LicenseeConfig.kt

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,23 @@ import org.gradle.api.Project
1818
import org.gradle.kotlin.dsl.apply
1919
import org.gradle.kotlin.dsl.configure
2020

21-
fun Project.configureLicensee() {
21+
fun Project.applyLicenseeConfig() {
22+
// Skip project "demo:benchmark" since it's a "com.android.test" project which is not compatible
23+
// with Licensee
24+
if (project.path == ":engine:benchmarks:macrobenchmark") {
25+
return
26+
}
27+
28+
// We have some empty folders like the :contrib root folder, which Gradle recognizes as projects.
29+
// Don't configure plugins for those folders.
30+
if (!project.buildFile.exists()) {
31+
return
32+
}
33+
34+
configureLicensee()
35+
}
36+
37+
private fun Project.configureLicensee() {
2238
apply(plugin = "app.cash.licensee")
2339
configure<app.cash.licensee.LicenseeExtension> {
2440
allow("Apache-2.0")

buildSrc/src/main/kotlin/Plugins.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ object Plugins {
2929
const val navSafeArgs = "androidx.navigation.safeargs.kotlin"
3030
const val ruler = "com.spotify.ruler"
3131
const val spotless = "com.diffplug.spotless"
32+
const val androidTest = "com.android.test"
3233
}
3334

3435
// classpath plugins
@@ -44,7 +45,7 @@ object Plugins {
4445

4546
object Versions {
4647
const val androidGradlePlugin = "8.9.2"
47-
const val benchmarkPlugin = "1.1.0"
48+
const val benchmarkPlugin = "1.3.4"
4849
const val dokka = "1.9.20"
4950
const val kspPlugin = "2.1.20-2.0.1"
5051
const val kotlin = "2.1.20"

engine/benchmarks/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/synthea

engine/benchmarks/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Benchmarking
2+
3+
# How to run the benchmark with Synthea for different population sizes
4+
1. Change into the project's root directory
5+
2. Run script to generate population data `sh engine/benchmark/generate_synthea.sh [<population-size>]`
6+
```bash
7+
# For population of 100 patients
8+
sh engine/benchmark/generate_synthea.sh 100
9+
```
10+
3. Run benchmarks
11+
```bash
12+
./gradlew :engine:benchmark:connectedReleaseAndroidTest
13+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
plugins {
2+
id(Plugins.BuildPlugins.androidTest)
3+
id(Plugins.BuildPlugins.kotlinAndroid)
4+
}
5+
6+
android {
7+
namespace = "com.google.android.fhir.engine.macrobenchmark"
8+
compileSdk = Sdk.COMPILE_SDK
9+
10+
defaultConfig {
11+
minSdk = Sdk.MIN_SDK
12+
targetSdk = Sdk.TARGET_SDK
13+
testInstrumentationRunner = Dependencies.androidJunitRunner
14+
}
15+
16+
buildTypes {
17+
// This benchmark buildType is used for benchmarking, and should function like your
18+
// release build (for example, with minification on). It"s signed with a debug key
19+
// for easy local/CI testing.
20+
create("benchmark") {
21+
isDebuggable = true
22+
signingConfig = getByName("debug").signingConfig
23+
matchingFallbacks += listOf("release")
24+
}
25+
}
26+
27+
targetProjectPath = ":engine:benchmarks:app"
28+
@Suppress("UnstableApiUsage")
29+
experimentalProperties["android.experimental.self-instrumenting"] = true
30+
31+
kotlin { jvmToolchain(11) }
32+
}
33+
34+
dependencies {
35+
implementation(libs.androidx.test.ext.junit)
36+
implementation(libs.androidx.test.espresso.core)
37+
implementation(libs.androidx.uiautomator)
38+
implementation(libs.androidx.benchmark.macro.junit4)
39+
}
40+
41+
androidComponents { beforeVariants(selector().all()) { it.enable = it.buildType == "benchmark" } }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<manifest />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.engine.macrobenchmark
18+
19+
import android.content.Context
20+
import androidx.benchmark.macro.ExperimentalMetricApi
21+
import androidx.benchmark.macro.MacrobenchmarkScope
22+
import androidx.benchmark.macro.TraceSectionMetric
23+
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
24+
import androidx.test.core.app.ApplicationProvider
25+
import androidx.test.ext.junit.runners.AndroidJUnit4
26+
import androidx.test.uiautomator.By
27+
import androidx.test.uiautomator.Until
28+
import org.junit.Assert.fail
29+
import org.junit.Rule
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
33+
/**
34+
* This is an example startup benchmark.
35+
*
36+
* It navigates to the device's home screen, and launches the default activity.
37+
*
38+
* Before running this benchmark:
39+
* 1) switch your app's active build variant in the Studio (affects Studio runs only)
40+
* 2) add `<profileable android:shell="true" />` to your app's manifest, within the `<application>`
41+
* tag
42+
*
43+
* Run this benchmark from Studio to see startup measurements, and captured system traces for
44+
* investigating your app's performance.
45+
*/
46+
@OptIn(ExperimentalMetricApi::class)
47+
@RunWith(AndroidJUnit4::class)
48+
class ExampleBenchmark {
49+
50+
@get:Rule val benchmarkRule = MacrobenchmarkRule()
51+
52+
private val applicationContext = ApplicationProvider.getApplicationContext<Context>()
53+
54+
@Test
55+
fun tracingCreate() {
56+
benchmarkRule.measureRepeated(
57+
packageName = TARGET_PACKAGE,
58+
metrics = listOf(TraceSectionMetric("Create API")),
59+
iterations = DEFAULT_ITERATIONS,
60+
startupMode = null,
61+
setupBlock = { startActivityAndWait() },
62+
) {
63+
clickOnId("create")
64+
}
65+
}
66+
67+
private fun MacrobenchmarkScope.clickOnId(resourceId: String) {
68+
val selector = By.res(TARGET_PACKAGE, resourceId)
69+
if (!device.wait(Until.hasObject(selector), 2_500)) {
70+
fail("Did not find object with id $resourceId")
71+
}
72+
73+
device.findObject(selector).click()
74+
// Chill to ensure we capture the end of the click span in the trace.
75+
Thread.sleep(100)
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.engine.macrobenchmark
18+
19+
const val TARGET_PACKAGE = "com.google.android.fhir.engine.benchmarks.app"
20+
const val DEFAULT_ITERATIONS = 10

engine/benchmark/README.md renamed to engine/benchmarks/microbenchmark/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ Alternatively, from the command line, run the connectedCheck to run all of the t
3131
./gradlew :engine:benchmark:connectedReleaseAndroidTest
3232
```
3333

34-
In this case, results will be saved to the `outputs/androidTest-results/connected/<device>/test-result.pb`. To visualize on Android Studio, click Run / Import Tests From File and find the `.pb` file
34+
In this case, results will be saved to the `outputs/androidTest-results/connected/<device>/test-result.pb`. To visualize on Android Studio, click Run / Import Tests From File and find the `.pb` file

engine/benchmark/build.gradle.kts renamed to engine/benchmarks/microbenchmark/build.gradle.kts

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ plugins {
55
}
66

77
android {
8-
namespace = "com.google.android.fhir.benchmark"
8+
namespace = "com.google.android.fhir.engine.microbenchmark"
99
compileSdk = Sdk.COMPILE_SDK
1010
defaultConfig {
1111
minSdk = Sdk.MIN_SDK
1212
testInstrumentationRunner = Dependencies.androidBenchmarkRunner
13+
// Enable measuring on an emulator, or devices with low battery
14+
// testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] =
15+
// "EMULATOR,LOW-BATTERY"
1316
}
1417

1518
testBuildType = "release"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.microbenchmark
18+
19+
import android.content.Context
20+
import androidx.benchmark.junit4.BenchmarkRule
21+
import androidx.benchmark.junit4.measureRepeated
22+
import androidx.test.core.app.ApplicationProvider
23+
import androidx.test.ext.junit.runners.AndroidJUnit4
24+
import androidx.test.filters.LargeTest
25+
import ca.uhn.fhir.context.FhirContext
26+
import ca.uhn.fhir.context.FhirVersionEnum
27+
import com.google.android.fhir.FhirEngineConfiguration
28+
import com.google.android.fhir.FhirEngineProvider
29+
import com.google.android.fhir.search.count
30+
import com.google.common.truth.Truth.assertThat
31+
import kotlinx.coroutines.runBlocking
32+
import org.hl7.fhir.r4.model.Patient
33+
import org.hl7.fhir.r4.model.Resource
34+
import org.junit.AfterClass
35+
import org.junit.BeforeClass
36+
import org.junit.Rule
37+
import org.junit.Test
38+
import org.junit.runner.RunWith
39+
40+
@LargeTest
41+
@RunWith(AndroidJUnit4::class)
42+
class DemoFhirEngineBenchmark {
43+
44+
@get:Rule val benchmarkRule = BenchmarkRule()
45+
private val applicationContext = ApplicationProvider.getApplicationContext<Context>()
46+
private val assetManager = applicationContext.assets
47+
private val fhirContext = FhirContext.forCached(FhirVersionEnum.R4)
48+
49+
@Test
50+
fun create() {
51+
val bulkFiles =
52+
assetManager.list(BULK_DATA_DIR)?.filter { it.endsWith(".ndjson") } ?: emptyList()
53+
val resources =
54+
bulkFiles
55+
.asSequence()
56+
.map { assetManager.open("$BULK_DATA_DIR/$it") }
57+
.flatMap { inputStream -> inputStream.bufferedReader().readLines() }
58+
.map { fhirContext.newJsonParser().parseResource(it) as Resource }
59+
.toList()
60+
61+
val fhirEngine = FhirEngineProvider.getInstance(applicationContext)
62+
63+
benchmarkRule.measureRepeated { runBlocking { fhirEngine.create(*resources.toTypedArray()) } }
64+
assertThat(runBlocking { fhirEngine.count<Patient> {} }).isGreaterThan(1L)
65+
}
66+
67+
companion object {
68+
private const val BULK_DATA_DIR = "bulk_data"
69+
70+
@JvmStatic
71+
@BeforeClass
72+
fun oneTimeSetup() {
73+
FhirEngineProvider.init(FhirEngineConfiguration(testMode = true))
74+
}
75+
76+
@JvmStatic
77+
@AfterClass
78+
fun oneTimeTearDown() {
79+
FhirEngineProvider.cleanup()
80+
}
81+
}
82+
}

engine/benchmark/src/androidTest/java/com/google/android/fhir/benchmark/EngineDatabaseBenchmark.kt renamed to engine/benchmarks/microbenchmark/src/androidTest/java/com/google/android/fhir/microbenchmark/EngineDatabaseBenchmark.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2023 Google LLC
2+
* Copyright 2022-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.android.fhir.benchmark
17+
package com.google.android.fhir.microbenchmark
1818

1919
import androidx.benchmark.junit4.BenchmarkRule
2020
import androidx.benchmark.junit4.measureRepeated

engine/benchmark/src/androidTest/java/com/google/android/fhir/benchmark/FhirSyncWorkerBenchmark.kt renamed to engine/benchmarks/microbenchmark/src/androidTest/java/com/google/android/fhir/microbenchmark/FhirSyncWorkerBenchmark.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,13 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.android.fhir.benchmark
17+
package com.google.android.fhir.microbenchmark
1818

1919
import android.content.Context
2020
import androidx.benchmark.junit4.BenchmarkRule
2121
import androidx.benchmark.junit4.measureRepeated
2222
import androidx.test.core.app.ApplicationProvider
2323
import androidx.test.ext.junit.runners.AndroidJUnit4
24+
import androidx.test.filters.LargeTest
2425
import androidx.test.filters.SdkSuppress
2526
import androidx.work.Data
2627
import androidx.work.ListenableWorker
@@ -73,6 +74,7 @@ import org.junit.Rule
7374
import org.junit.Test
7475
import org.junit.runner.RunWith
7576

77+
@LargeTest
7678
@RunWith(AndroidJUnit4::class)
7779
class FhirSyncWorkerBenchmark {
7880

engine/benchmark/src/androidTest/java/com/google/android/fhir/benchmark/GzipUploadInterceptorBenchmark.kt renamed to engine/benchmarks/microbenchmark/src/androidTest/java/com/google/android/fhir/microbenchmark/GzipUploadInterceptorBenchmark.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.android.fhir.benchmark
17+
package com.google.android.fhir.microbenchmark
1818

1919
import androidx.benchmark.junit4.BenchmarkRule
2020
import androidx.benchmark.junit4.measureRepeated

0 commit comments

Comments
 (0)