diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 4fbe208..3d010bb 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -13,6 +13,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index dcbe9d0..5ae5967 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -29,9 +29,13 @@ android {
versionCode = 1
versionName = "1.0"
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ // testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
+ testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "ACTIVITY-MISSING"
}
+ testBuildType = "benchmark"
+
buildTypes {
release {
isMinifyEnabled = false
@@ -40,6 +44,13 @@ android {
"proguard-rules.pro"
)
}
+ create("benchmark") {
+ initWith(buildTypes.getByName("release"))
+ signingConfig = signingConfigs.getByName("debug") // sign with debug key for easy deployment
+ installation {
+ enableBaselineProfile = false // ← disables profile embedding for this build type only
+ }
+ }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
@@ -61,6 +72,16 @@ android {
getByName("release") {
java.srcDirs("build/generated/ksp/release/kotlin")
}
+ getByName("benchmark") {
+ java.srcDirs("build/generated/ksp/benchmark/kotlin")
+ }
+ }
+}
+
+androidComponents {
+ onVariants(selector().withBuildType("benchmark")) { variant ->
+ variant.packaging.resources.excludes.add("baseline-prof.txt")
+ variant.packaging.resources.excludes.add("baseline.profm")
}
}
@@ -79,6 +100,9 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+ implementation(libs.androidx.benchmark.junit4)
+ androidTestImplementation(libs.androidx.runner)
+ androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..e166224
--- /dev/null
+++ b/app/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/app/src/androidTest/java/com/bitcointxoko/gudariwallet/benchmark/PaymentDaoBenchmark.kt b/app/src/androidTest/java/com/bitcointxoko/gudariwallet/benchmark/PaymentDaoBenchmark.kt
new file mode 100644
index 0000000..222f271
--- /dev/null
+++ b/app/src/androidTest/java/com/bitcointxoko/gudariwallet/benchmark/PaymentDaoBenchmark.kt
@@ -0,0 +1,164 @@
+package com.bitcointxoko.gudariwallet.benchmark
+
+import android.content.Context
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bitcointxoko.gudariwallet.data.db.AppDatabase
+import com.bitcointxoko.gudariwallet.data.db.PaymentDao
+import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PaymentDaoBenchmark {
+
+ @get:Rule
+ val benchmarkRule = BenchmarkRule()
+
+ private lateinit var db: AppDatabase
+ private lateinit var dao: PaymentDao
+
+ @Before
+ fun setup() {
+ val context = ApplicationProvider.getApplicationContext()
+ db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ dao = db.paymentDao()
+
+ val now = System.currentTimeMillis()
+ val fakePayments = (1..5_000).map { i ->
+ PaymentRecordEntity(
+ checkingId = "checking_$i",
+ paymentHash = "hash_$i",
+ amountMsat = if (i % 2 == 0) (i * 1000L) else -(i * 1000L),
+ feeMsat = 1000L,
+ memo = if (i % 3 == 0) "Coffee payment $i" else null,
+ time = (now - i * 60_000L).toString(),
+ createdAt = (now / 1000) - (i * 60L),
+ status = when (i % 4) {
+ 0 -> "complete"
+ 1 -> "paid"
+ 2 -> "pending"
+ else -> "failed"
+ },
+ bolt11 = "lnbc${i}n1...",
+ pending = i % 4 == 2,
+ preimage = "preimage_$i",
+ extra = null,
+ savedAt = now - (i * 60_000L),
+ pubkey = if (i % 5 == 0) "pubkey_$i" else null,
+ alias = if (i % 5 == 0) "alias_$i" else null
+ )
+ }
+ runBlocking { dao.insertAll(fakePayments) }
+ }
+
+ @After
+ fun teardown() {
+ db.close()
+ }
+
+ @Test
+ fun getAll_baseline() = benchmarkRule.measureRepeated {
+ runBlocking { dao.getAll() }
+ }
+
+ @Test
+ fun getPage_30rows() = benchmarkRule.measureRepeated {
+ runBlocking { dao.getPage(limit = 30, offset = 0) }
+ }
+
+ @Test
+ fun getPage_30rows_deepOffset() = benchmarkRule.measureRepeated {
+ runBlocking { dao.getPage(limit = 30, offset = 2_000) }
+ }
+
+ @Test
+ fun queryAll_noFilters() = benchmarkRule.measureRepeated {
+ runBlocking {
+ dao.queryAll(null, null, null, null, null, null)
+ }
+ }
+
+ @Test
+ fun queryAll_withSearchQuery() = benchmarkRule.measureRepeated {
+ runBlocking {
+ dao.queryAll("Coffee", null, null, null, null, null)
+ }
+ }
+
+ @Test
+ fun queryAll_statusFilter() = benchmarkRule.measureRepeated {
+ runBlocking {
+ dao.queryAll(null, "complete,paid", null, null, null, null)
+ }
+ }
+
+ @Test
+ fun queryAll_combinedFilters() = benchmarkRule.measureRepeated {
+ val now = System.currentTimeMillis() / 1000
+ runBlocking {
+ dao.queryAll(
+ searchQuery = "Coffee",
+ statusPattern = "complete,paid",
+ minAmountMsat = 10_000L,
+ maxAmountMsat = 5_000_000L,
+ minCreatedAt = now - 86_400 * 30,
+ maxCreatedAt = now
+ )
+ }
+ }
+
+ @Test
+ fun getByCheckingId_lookup() = benchmarkRule.measureRepeated {
+ runBlocking { dao.getByCheckingId("checking_2500") }
+ }
+
+ @Test
+ fun count_rows() = benchmarkRule.measureRepeated {
+ runBlocking { dao.count() }
+ }
+
+ @Test
+ fun concurrentReadWrite_walBenefit() = benchmarkRule.measureRepeated {
+ // Simulate a write happening concurrently with a read
+ // (WAL allows these to proceed in parallel; default journal blocks)
+ val writeJob = CoroutineScope(Dispatchers.IO).launch {
+ dao.insert(
+ PaymentRecordEntity(
+ checkingId = "concurrent_${System.nanoTime()}",
+ paymentHash = "hash_concurrent",
+ amountMsat = 1000L,
+ feeMsat = 10L,
+ memo = "concurrent write",
+ time = System.currentTimeMillis().toString(),
+ createdAt = System.currentTimeMillis() / 1000,
+ status = "complete",
+ bolt11 = "lnbc...",
+ pending = false,
+ preimage = "preimage",
+ extra = null,
+ savedAt = System.currentTimeMillis(),
+ pubkey = null,
+ alias = null
+ )
+ )
+ }
+ // Read while write is in-flight
+ runBlocking {
+ dao.getPage(limit = 30, offset = 0)
+ writeJob.join()
+ }
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 27857b6..8eee81b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,8 @@
+
+
diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt
index 44aac37..31e0f3b 100644
--- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt
+++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt
@@ -14,7 +14,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
LnurlCacheEntity::class,
NodeAliasCacheEntity::class,
],
- version = 5, // ← bumped
+ version = 6,
exportSchema = false
)
@TypeConverters(PaymentConverters::class)
@@ -70,13 +70,22 @@ abstract class AppDatabase : RoomDatabase() {
}
}
- val MIGRATION_4_5 = object : Migration(4, 5) {
+ private val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE payment_records ADD COLUMN pubkey TEXT")
db.execSQL("ALTER TABLE payment_records ADD COLUMN alias TEXT")
}
}
+ private val MIGRATION_5_6 = object : Migration(5, 6) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("CREATE INDEX IF NOT EXISTS `index_payment_record_status` ON `payment_records` (`status`)")
+ db.execSQL("CREATE INDEX IF NOT EXISTS `index_payment_record_created_at` ON `payment_records` (`createdAt`)")
+ db.execSQL("CREATE INDEX IF NOT EXISTS `index_payment_record_amount_msat` ON `payment_records` (`amountMsat`)")
+ db.execSQL("CREATE INDEX IF NOT EXISTS `index_payment_record_status_created_at` ON `payment_records` (`status`, `createdAt`)")
+ }
+ }
+
fun getInstance(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
@@ -84,7 +93,7 @@ abstract class AppDatabase : RoomDatabase() {
AppDatabase::class.java,
"gudari_wallet.db"
)
- .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) // ← added
+ .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
.build()
.also { INSTANCE = it }
}
diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt
index 6daff05..ac0f39e 100644
--- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt
+++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentRecordEntity.kt
@@ -2,10 +2,19 @@ package com.bitcointxoko.gudariwallet.data.db
import androidx.room.ColumnInfo
import androidx.room.Entity
+import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
-@Entity(tableName = "payment_records")
+@Entity(
+ tableName = "payment_records",
+ indices = [
+ Index(value = ["status"]), // WHERE status LIKE ?
+ Index(value = ["createdAt"]), // WHERE created_at BETWEEN ? AND ?
+ Index(value = ["amountMsat"]), // WHERE amount_msat BETWEEN ? AND ?
+ Index(value = ["status", "createdAt"]) // composite: common combined filter
+ ]
+)
@TypeConverters(PaymentConverters::class)
data class PaymentRecordEntity(
@PrimaryKey
diff --git a/build.gradle.kts b/build.gradle.kts
index 0b5c6ec..73bab94 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,4 +3,6 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.android.test) apply false
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 8e9a99a..fb54c5d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,3 +22,16 @@ android.disallowKotlinSourceSets=false
ksp.useKsp2=true
ksp.incremental=true
ksp.incremental.log=false
+
+# Enable configuration cache
+org.gradle.configuration-cache=true
+
+# Parallel project execution (useful if you add more modules later)
+org.gradle.parallel=true
+
+# Increase heap if you see OOM during KSP/compilation
+# org.gradle.jvmargs=-Xmx4g -XX:+UseG1GC
+
+# Enable build caching (caches task outputs across clean builds)
+org.gradle.caching=true
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3670f91..85bfb4a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -30,6 +30,9 @@ secp256k1-kmp = "0.23.0"
lyricist = "1.8.0"
material3 = "1.4.0"
work = "2.9.1"
+benchmark = "1.4.1"
+runner = "1.5.2"
+androidx-test-ext = "1.2.1"
[libraries]
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
@@ -75,6 +78,9 @@ lyricist = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist"
lyricist-processor = { module = "cafe.adriel.lyricist:lyricist-processor", version.ref = "lyricist" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
+androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark" }
+androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
+androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -82,3 +88,6 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "ko
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
+androidx-benchmark = { id = "androidx.benchmark", version.ref = "benchmark" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+android-test = { id = "com.android.test", version.ref = "agp" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 62b1393..12ef809 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -23,5 +23,4 @@ dependencyResolutionManagement {
}
rootProject.name = "Gudari Wallet"
-include(":app")
-
\ No newline at end of file
+include(":app")
\ No newline at end of file