From dde10427018ff62c3b7336fa63ef863c2d08e88c Mon Sep 17 00:00:00 2001 From: soraefir Date: Wed, 9 Sep 2026 00:27:13 +0200 Subject: [PATCH] Batching --- .../persistence/CowspentSQLiteOpenHelper.kt | 40 +++++++++++++++++-- .../persistence/CowspentServerSyncHelper.kt | 15 +++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt index 0d1f635..6ca4b8f 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt @@ -448,6 +448,26 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : // --- Bills logic --- + /** + * Runs [block] as one database transaction. + * + * Every write here commits on its own otherwise, which for a sync means a committed + * transaction per bill and another per ower - the dominant cost of a large project's first + * sync. Callers batch in chunks rather than wrapping everything: a sync interrupted halfway + * then keeps the chunks it already committed instead of rolling the lot back. + */ + fun inTransaction(block: () -> T): T { + val db = writableDatabase + db.beginTransaction() + try { + val result = block() + db.setTransactionSuccessful() + return result + } finally { + db.endTransaction() + } + } + fun addBill(b: DBBill): Long { val db = writableDatabase val values = ContentValues() @@ -627,11 +647,25 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : val cursor = db.query(table_bills, columnsBills, selection, selectionArgs, null, null, orderBy) val bills: MutableList = ArrayList() while (cursor.moveToNext()) { - val bill = getBillFromCursor(cursor) - bill.billOwers = getBillowersOfBill(bill.id) - bills.add(bill) + bills.add(getBillFromCursor(cursor)) } cursor.close() + if (bills.isEmpty()) return bills + + // Every ower of the matched bills in one query, keyed back to its bill. + // + // Fetching them per bill meant a project with thousands of bills issued thousands of + // queries on every list refresh - and that refresh runs on resume, on switching project, + // and after each sync. The bill ids are re-selected as a subquery rather than bound as + // arguments, which would blow past SQLite's limit on bound variables for a large project. + val owersByBill = getBillOwersCustom( + "$key_billId IN (SELECT $key_id FROM $table_bills WHERE $selection)", + selectionArgs, + null + ).groupBy { it.billId } + for (bill in bills) { + bill.billOwers = owersByBill[bill.id].orEmpty() + } return bills } diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt index 9170dc8..2e5d77d 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt @@ -896,6 +896,18 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen private fun applyRemoteBills( remoteBills: List, localBillsByRemoteId: Map + ) { + // Committed in chunks: one transaction per chunk turns a per-row commit into one write + // for the whole batch, while still leaving finished chunks on disk if the sync is cut + // short - a project part-way through is far better than one that rolled back. + remoteBills.chunked(BILL_APPLY_CHUNK).forEach { chunk -> + dbHelper.inTransaction { applyBillChunk(chunk, localBillsByRemoteId) } + } + } + + private fun applyBillChunk( + remoteBills: List, + localBillsByRemoteId: Map ) { for (remoteBill in remoteBills) { val localBill = localBillsByRemoteId[remoteBill.remoteId] @@ -1792,6 +1804,9 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen */ private const val UNCHANGED_RUN_TO_SETTLE = 25 + /** Bills written per transaction while applying a pull. */ + private const val BILL_APPLY_CHUNK = 500 + private var instance: CowspentServerSyncHelper? = null private val projectIdsToSync: MutableList = ArrayList()