Skip to content

Commit 9afa5ee

Browse files
authored
Add benchmark for FHIR Engine sync api (#2830)
* Benchmark sync download api * Add FHIR_SERVER_BASE_URL to properties * Benchmark sync upload api * Separate benchmarks for the different upload strategies that is, BundleRequest and IndividualRequest upload strategies * Add Sync api benchmarks to the macrobenchmark module * Update docs on how to setup for sync benchmarking
1 parent bd5687e commit 9afa5ee

File tree

17 files changed

+828
-40
lines changed

17 files changed

+828
-40
lines changed

benchmark-populate-server.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
POPULATION="${1}"
2+
SERVER_URL="${2}"
3+
SCRIPT_DIR=$(dirname "$0")
4+
SYNTHEA_DIR="$SCRIPT_DIR"/synthea
5+
6+
if [ ! -d "$SYNTHEA_DIR" ]; then
7+
# build Synthea according to https://github.com/synthetichealth/synthea
8+
git clone https://github.com/synthetichealth/synthea.git "$SYNTHEA_DIR"
9+
fi
10+
11+
rm -rf "$SYNTHEA_DIR"/output
12+
13+
cd "$SYNTHEA_DIR" || exit
14+
# generate valid R4 resources in output/fhir
15+
./run_synthea -m pregnancy -p "$POPULATION" -s 12345 --exporter.fhir.included_resources Patient --exporter.years_of_history 1 --exporter.fhir.use_us_core_ig false --exporter.fhir.transaction_bundle true --exporter.practitioner.fhir.export true --exporter.hospital.fhir.export true
16+
17+
cd - || exit
18+
# Upload hospital information
19+
for filename in "$SYNTHEA_DIR"/output/fhir/hospital*.json; do
20+
echo "Uploading $filename"
21+
curl "$SERVER_URL"/fhir --data-binary "@$filename" -H "Content-Type: application/fhir+json" > /dev/null
22+
done
23+
24+
# Upload practitioner information
25+
for filename in "$SYNTHEA_DIR"/output/fhir/practitioner*.json; do
26+
echo "Uploading $filename"
27+
curl "$SERVER_URL"/fhir --data-binary "@$filename" -H "Content-Type: application/fhir+json" > /dev/null
28+
done
29+
30+
# Upload fhir patients data
31+
for filename in "$SYNTHEA_DIR"/output/fhir/*.json; do
32+
echo "Uploading $filename"
33+
curl "$SERVER_URL"/fhir --data-binary "@$filename" -H "Content-Type: application/fhir+json" > /dev/null
34+
done

benchmark-start-server.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
CONTAINER_ID=$(docker ps --quiet --all --no-trunc --filter "name=sync-hapi-fhir-server")
2+
if [ -n "$CONTAINER_ID" ]; then
3+
docker container stop "$CONTAINER_ID" > /dev/null
4+
docker container rm "$CONTAINER_ID" > /dev/null
5+
fi
6+
7+
docker run -e JAVA_TOOL_OPTIONS="-Xmx1g" -p 8080:8080 --detach --name sync-hapi-fhir-server hapiproject/hapi:latest
8+
sleep 3
9+
10+
# Checks for whether the container started successfully
11+
if [ -z "$(docker ps --quiet --no-trunc --filter "name=sync-hapi-fhir-server")" ]; then
12+
echo "Failed to start Hapi server"
13+
exit 1
14+
fi
15+

benchmark-stop-server.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Stop container instance
2+
CONTAINER_ID=$(docker ps --quiet --no-trunc --filter "name=sync-hapi-fhir-server")
3+
docker container stop "$CONTAINER_ID"

buildSrc/src/main/kotlin/FirebaseTestLabConfig.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ fun Project.configureFirebaseTestLabForMacroBenchmark() {
6767
},
6868
)
6969
instrumentationApk.set(project.provider { "$buildDir/outputs/apk/benchmark/*.apk" })
70+
testTargets.set(
71+
listOf("notClass com.google.android.fhir.engine.macrobenchmark.FhirEngineSyncApiBenchmark"),
72+
)
7073
}
7174
}
7275

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
import java.io.File
18+
import java.io.FileInputStream
19+
import java.io.InputStreamReader
20+
import java.util.Properties
21+
import org.gradle.api.Project
22+
23+
/**
24+
* Retrieve the project local properties if they are available. If there is no local properties file
25+
* then an empty set of properties is returned.
26+
*/
27+
fun Project.gradleLocalProperties(file: String = "local.properties"): Properties {
28+
val properties = Properties()
29+
val localProperties = File(rootDir, file)
30+
if (localProperties.isFile) {
31+
InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
32+
properties.load(reader)
33+
}
34+
} else {
35+
println("Gradle local properties file not found at $localProperties")
36+
}
37+
return properties
38+
}

docs/use/FEL/Benchmarking.md

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ In Android Studio, set your build variants to `release` and run your benchmark a
5757
![gutter test action](https://developer.android.com/static/topic/performance/images/benchmark_images/microbenchmark_run.png)
5858

5959
The results will be similar to this:
60+
6061
```
6162
1,297,374 ns 5345 allocs trace EngineDatabaseBenchmark.createAndGet
6263
1,114,474,793 ns 4922289 allocs trace FhirSyncWorkerBenchmark.oneTimeSync_50patients
@@ -104,19 +105,82 @@ The Microbenchmark results can be accessed through the following steps
104105

105106
## Macrobenchmark module
106107

107-
The _FHIR Engine Library_ macrobenchmark tests are located in the module `:engine:benchmarks:macrobenchmark`
108+
The _FHIR Engine Library_ macrobenchmark tests are located in the module `:engine:benchmarks:macrobenchmark`.
108109

109-
### Prerequisite
110+
### Set Up
110111

111-
Requires the _FHIR Engine Library_ Benchmark App configured with the relevant benchmark data described in the section for the _FHIR Engine Library_ Benchmark App
112+
1. Set up the _FHIR Engine Library_ Benchmark App with [the relevant data](#configuration)
112113

113-
### Running
114+
2. Start the local Hapi server
114115

115-
To run, use the command
116+
Execute
116117

117-
```shell
118-
./gradlew :engine:benchmarks:macrobenchmark:connectedCheck
119-
```
118+
```shell
119+
sh benchmark-start-server.sh
120+
```
121+
122+
The script uses [Docker](https://docs.docker.com/engine/install/)
123+
to start up a container with the [image](https://hub.docker.com/r/hapiproject/hapi) from Hapi.
124+
125+
It runs the docker image with the default configuration, mapping port 8080 from the container to port 8080 in the host.
126+
127+
Once running, you can access <http://localhost:8080/> in the browser to access the HAPI FHIR server's UI
128+
or use <http://localhost:8080/fhir/> as the base URL for your REST requests
129+
3. Populate the Hapi server with data generated from Synthea
130+
131+
Execute
132+
133+
```shell
134+
sh benchmark-populate-server.sh 100 0.0.0.0:8080
135+
```
136+
137+
This generates Synthea data for a population of 100 patients and uploads the data to the local Hapi
138+
server started at port 8080
139+
4. Check that the Android device and the host computer are connected to the same network
140+
5. Get the host machine's domain or LAN IP address
141+
142+
For mac
143+
144+
```shell
145+
hostname
146+
```
147+
148+
For linux
149+
150+
```shell
151+
hostname -I | awk '{print $1}'
152+
```
153+
154+
6. Update `local.properties` file, located in the root folder, with the server url
155+
156+
```properties
157+
FHIR_SERVER_BASE_URL=http://192.168.0.24:8080/fhir/
158+
```
159+
160+
replacing `192.168.0.24` with your domain/address
161+
7. Run Gradle sync
162+
8. [Run the Macrobenchmark](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview#run-benchmark) tests in the `engine:benchmarks:macrobenchmark` module
163+
164+
Execute
165+
166+
```shell
167+
./gradlew :engine:benchmarks:macrobenchmark:connectedCheck
168+
```
169+
170+
The JSON results are automatically copied from the device to the host. These are written on the host machine in the location
171+
`engine/benchmarks/macrobenchmark/build/outputs/connected_android_test_additional_output/benchmark/connected/device_id/`
172+
173+
9. After all the tests are complete, you can stop the benchmark server
174+
175+
```shell
176+
sh benchmark-populate-server.sh
177+
```
178+
179+
For reliable benchmarks:
180+
181+
* Use physical devices: Emulators won’t give you consistent, real-world data.
182+
* Kill all background services: Anything running outside your app adds noise.
183+
* Lock the device state: Keep the brightness, network and battery levels consistent.
120184

121185
### Continuous Integration (CI)
122186

engine/benchmarks/app/build.gradle.kts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ android {
1717
versionName = "1.0"
1818

1919
testInstrumentationRunner = Dependencies.androidJunitRunner
20+
21+
val baseUrlKey = "FHIR_SERVER_BASE_URL"
22+
val fhirServerBaseUrl =
23+
gradleLocalProperties().getProperty(baseUrlKey) ?: properties[baseUrlKey]?.toString() ?: ""
24+
buildConfigField(type = "String", name = baseUrlKey, "\"$fhirServerBaseUrl\"")
2025
}
2126

2227
buildTypes {
@@ -41,7 +46,10 @@ android {
4146
targetCompatibility = JavaVersion.VERSION_11
4247
}
4348
kotlinOptions { jvmTarget = "11" }
44-
buildFeatures { compose = true }
49+
buildFeatures {
50+
compose = true
51+
buildConfig = true
52+
}
4553

4654
packaging { resources.excludes.addAll(listOf("META-INF/ASL-2.0.txt", "META-INF/LGPL-3.0.txt")) }
4755

@@ -65,6 +73,8 @@ dependencies {
6573
implementation(libs.androidx.navigation.compose)
6674
implementation(libs.bundles.androidx.tracing)
6775
implementation(libs.kotlinx.serialization.json)
76+
implementation(libs.androidx.work.runtime)
77+
implementation(libs.androidx.datastore.preferences)
6878

6979
testImplementation(libs.junit)
7080
androidTestImplementation(libs.androidx.test.ext.junit)

engine/benchmarks/app/src/main/java/com/google/android/fhir/engine/benchmarks/app/MainActivity.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.android.fhir.engine.benchmarks.app
1818

19+
import android.app.Application
1920
import android.os.Bundle
2021
import androidx.activity.ComponentActivity
2122
import androidx.activity.compose.setContent
@@ -54,12 +55,18 @@ class MainActivity : ComponentActivity() {
5455
override fun onCreate(savedInstanceState: Bundle?) {
5556
super.onCreate(savedInstanceState)
5657
enableEdgeToEdge()
57-
setContent { AndroidfhirTheme { AndroidfhirApp(resourcesDataProvider, fhirEngine) } }
58+
setContent {
59+
AndroidfhirTheme { AndroidfhirApp(application, resourcesDataProvider, fhirEngine) }
60+
}
5861
}
5962
}
6063

6164
@Composable
62-
fun AndroidfhirApp(resourcesDataProvider: ResourcesDataProvider, fhirEngine: FhirEngine) {
65+
fun AndroidfhirApp(
66+
application: Application,
67+
resourcesDataProvider: ResourcesDataProvider,
68+
fhirEngine: FhirEngine,
69+
) {
6370
val navController = rememberNavController()
6471
NavHost(navController, startDestination = Screen.HomeScreen) {
6572
composable<Screen.HomeScreen> {
@@ -112,7 +119,7 @@ fun AndroidfhirApp(resourcesDataProvider: ResourcesDataProvider, fhirEngine: Fhi
112119
object : ViewModelProvider.Factory {
113120
override fun <T : ViewModel> create(modelClass: Class<T>): T {
114121
@Suppress("UNCHECKED_CAST")
115-
return SyncApiViewModel(resourcesDataProvider, fhirEngine) as T
122+
return SyncApiViewModel(application, resourcesDataProvider, fhirEngine) as T
116123
}
117124
},
118125
)

engine/benchmarks/app/src/main/java/com/google/android/fhir/engine/benchmarks/app/MainApplication.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ class MainApplication : Application() {
3737

3838
FhirEngineProvider.init(
3939
FhirEngineConfiguration(
40-
enableEncryptionIfSupported = true,
40+
enableEncryptionIfSupported = !BuildConfig.DEBUG,
4141
RECREATE_AT_OPEN,
4242
ServerConfiguration(
43-
"https://hapi.fhir.org/baseR4/",
43+
// "https://hapi.fhir.org/baseR4/"
44+
BuildConfig.FHIR_SERVER_BASE_URL,
4445
httpLogger =
4546
HttpLogger(
4647
HttpLogger.Configuration(

0 commit comments

Comments
 (0)