Skip to content

Configure performance benchmarks #2810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
7 changes: 2 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@ allprojects {
}

subprojects {
// We have some empty folders like the :contrib root folder, which Gradle recognizes as projects.
// Don't configure plugins for those folders.
if (project.buildFile.exists()) {
configureLicensee()
}
applyLicenseeConfig()

tasks.withType(Test::class.java).configureEach {
maxParallelForks = 1
if (project.providers.environmentVariable("GITHUB_ACTIONS").isPresent) {
Expand Down
20 changes: 18 additions & 2 deletions buildSrc/src/main/kotlin/LicenseeConfig.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 Google LLC
* Copyright 2023-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,7 +18,23 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure

fun Project.configureLicensee() {
fun Project.applyLicenseeConfig() {
// Skip project "demo:benchmark" since it's a "com.android.test" project which is not compatible
// with Licensee
if (project.path == ":engine:benchmarks:macrobenchmark") {
return
}

// We have some empty folders like the :contrib root folder, which Gradle recognizes as projects.
// Don't configure plugins for those folders.
if (!project.buildFile.exists()) {
return
}

configureLicensee()
}

private fun Project.configureLicensee() {
apply(plugin = "app.cash.licensee")
configure<app.cash.licensee.LicenseeExtension> {
allow("Apache-2.0")
Expand Down
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/Plugins.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ object Plugins {
const val navSafeArgs = "androidx.navigation.safeargs.kotlin"
const val ruler = "com.spotify.ruler"
const val spotless = "com.diffplug.spotless"
const val androidTest = "com.android.test"
}

// classpath plugins
Expand All @@ -44,7 +45,7 @@ object Plugins {

object Versions {
const val androidGradlePlugin = "8.9.2"
const val benchmarkPlugin = "1.1.0"
const val benchmarkPlugin = "1.3.4"
const val dokka = "1.9.20"
const val kspPlugin = "2.1.20-2.0.1"
const val kotlin = "2.1.20"
Expand Down
1 change: 1 addition & 0 deletions engine/benchmarks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/synthea
13 changes: 13 additions & 0 deletions engine/benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Benchmarking

# How to run the benchmark with Synthea for different population sizes
1. Change into the project's root directory
2. Run script to generate population data `sh engine/benchmark/generate_synthea.sh [<population-size>]`
```bash
# For population of 100 patients
sh engine/benchmark/generate_synthea.sh 100
```
3. Run benchmarks
```bash
./gradlew :engine:benchmark:connectedReleaseAndroidTest
```
1 change: 1 addition & 0 deletions engine/benchmarks/macrobenchmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
41 changes: 41 additions & 0 deletions engine/benchmarks/macrobenchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
plugins {
id(Plugins.BuildPlugins.androidTest)
id(Plugins.BuildPlugins.kotlinAndroid)
}

android {
namespace = "com.google.android.fhir.engine.macrobenchmark"
compileSdk = Sdk.COMPILE_SDK

defaultConfig {
minSdk = Sdk.MIN_SDK
targetSdk = Sdk.TARGET_SDK
testInstrumentationRunner = Dependencies.androidJunitRunner
}

buildTypes {
// This benchmark buildType is used for benchmarking, and should function like your
// release build (for example, with minification on). It"s signed with a debug key
// for easy local/CI testing.
create("benchmark") {
isDebuggable = true
signingConfig = getByName("debug").signingConfig
matchingFallbacks += listOf("release")
}
}

targetProjectPath = ":engine:benchmarks:app"
@Suppress("UnstableApiUsage")
experimentalProperties["android.experimental.self-instrumenting"] = true

kotlin { jvmToolchain(11) }
}

dependencies {
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}

androidComponents { beforeVariants(selector().all()) { it.enable = it.buildType == "benchmark" } }
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,77 @@
/*
* Copyright 2025 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.android.fhir.engine.macrobenchmark

import android.content.Context
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* This is an example startup benchmark.
*
* It navigates to the device's home screen, and launches the default activity.
*
* Before running this benchmark:
* 1) switch your app's active build variant in the Studio (affects Studio runs only)
* 2) add `<profileable android:shell="true" />` to your app's manifest, within the `<application>`
* tag
*
* Run this benchmark from Studio to see startup measurements, and captured system traces for
* investigating your app's performance.
*/
@OptIn(ExperimentalMetricApi::class)
@RunWith(AndroidJUnit4::class)
class ExampleBenchmark {

@get:Rule val benchmarkRule = MacrobenchmarkRule()

private val applicationContext = ApplicationProvider.getApplicationContext<Context>()

@Test
fun tracingCreate() {
benchmarkRule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = listOf(TraceSectionMetric("Create API")),
iterations = DEFAULT_ITERATIONS,
startupMode = null,
setupBlock = { startActivityAndWait() },
) {
clickOnId("create")
}
}

private fun MacrobenchmarkScope.clickOnId(resourceId: String) {
val selector = By.res(TARGET_PACKAGE, resourceId)
if (!device.wait(Until.hasObject(selector), 2_500)) {
fail("Did not find object with id $resourceId")
}

device.findObject(selector).click()
// Chill to ensure we capture the end of the click span in the trace.
Thread.sleep(100)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2025 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.android.fhir.engine.macrobenchmark

const val TARGET_PACKAGE = "com.google.android.fhir.engine.benchmarks.app"
const val DEFAULT_ITERATIONS = 10
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ Alternatively, from the command line, run the connectedCheck to run all of the t
./gradlew :engine:benchmark:connectedReleaseAndroidTest
```

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
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ plugins {
}

android {
namespace = "com.google.android.fhir.benchmark"
namespace = "com.google.android.fhir.engine.microbenchmark"
compileSdk = Sdk.COMPILE_SDK
defaultConfig {
minSdk = Sdk.MIN_SDK
testInstrumentationRunner = Dependencies.androidBenchmarkRunner
// Enable measuring on an emulator, or devices with low battery
// testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] =
// "EMULATOR,LOW-BATTERY"
}

testBuildType = "release"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2025 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.android.fhir.microbenchmark

import android.content.Context
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import com.google.android.fhir.FhirEngineConfiguration
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.search.count
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.Resource
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@LargeTest
@RunWith(AndroidJUnit4::class)
class DemoFhirEngineBenchmark {

@get:Rule val benchmarkRule = BenchmarkRule()
private val applicationContext = ApplicationProvider.getApplicationContext<Context>()
private val assetManager = applicationContext.assets
private val fhirContext = FhirContext.forCached(FhirVersionEnum.R4)

@Test
fun create() {
val bulkFiles =
assetManager.list(BULK_DATA_DIR)?.filter { it.endsWith(".ndjson") } ?: emptyList()
val resources =
bulkFiles
.asSequence()
.map { assetManager.open("$BULK_DATA_DIR/$it") }
.flatMap { inputStream -> inputStream.bufferedReader().readLines() }
.map { fhirContext.newJsonParser().parseResource(it) as Resource }
.toList()

val fhirEngine = FhirEngineProvider.getInstance(applicationContext)

benchmarkRule.measureRepeated { runBlocking { fhirEngine.create(*resources.toTypedArray()) } }
assertThat(runBlocking { fhirEngine.count<Patient> {} }).isGreaterThan(1L)
}

companion object {
private const val BULK_DATA_DIR = "bulk_data"

@JvmStatic
@BeforeClass
fun oneTimeSetup() {
FhirEngineProvider.init(FhirEngineConfiguration(testMode = true))
}

@JvmStatic
@AfterClass
fun oneTimeTearDown() {
FhirEngineProvider.cleanup()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 Google LLC
* Copyright 2022-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.google.android.fhir.benchmark
package com.google.android.fhir.microbenchmark

import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 Google LLC
* Copyright 2023-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,13 +14,14 @@
* limitations under the License.
*/

package com.google.android.fhir.benchmark
package com.google.android.fhir.microbenchmark

import android.content.Context
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.work.Data
import androidx.work.ListenableWorker
Expand Down Expand Up @@ -73,6 +74,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@LargeTest
@RunWith(AndroidJUnit4::class)
class FhirSyncWorkerBenchmark {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2023-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.google.android.fhir.benchmark
package com.google.android.fhir.microbenchmark

import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
Expand Down
Loading
Loading