Batch database operations during backup restore for improved performance (#3267)
* perf(backup): batch database operations during restore Reduce overhead by chunking manga and repo restoration into transactions of 100 entries. Categories now restored in a single transaction. Optimized DatabaseHandler to reuse existing transaction contexts instead of creating redundant nested ones. Changed TransactionElement to internal to allow AndroidDatabaseHandler to check transaction state from the same package. * fix(backup): make restore progress and errors thread-safe Use AtomicInteger for progress and synchronizedList for errors to prevent race conditions during concurrent restoration coroutines. * perf(backup): update progress notification only once per chunk Reduce UI/IPC overhead by moving notification updates out of the inner loop, triggering them only after each 100-item transaction chunk. Reduced on my emulator from 46 seconds to 27 * fix: remove invalid awaitAsOne() call on insert() return type categoriesQueries.insert() returns Long (the inserted row ID), not an ExecutableQuery<T>, so awaitAsOne() is not applicable. * fix(feedback): Use `CopyOnWriteArrayList` and kotlin `AtomicInt`, add a changelog entry Note that using kotlin `AtomicInt` instead of java `AtomicInteger` requires opting into an experimental API. --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
@@ -20,6 +20,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Change the term "Obsolete" to "Orphaned" for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3383](https://github.com/mihonapp/mihon/pull/3383))
|
- Change the term "Obsolete" to "Orphaned" for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3383](https://github.com/mihonapp/mihon/pull/3383))
|
||||||
- Remove text limit of manga notes ([@AntsyLich](https://github.com/AntsyLich)) ([#3410](https://github.com/mihonapp/mihon/pull/3410))
|
- Remove text limit of manga notes ([@AntsyLich](https://github.com/AntsyLich)) ([#3410](https://github.com/mihonapp/mihon/pull/3410))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Batch database operations during backup restore for improved performance ([@Lolle2000la](https://github.com/Lolle2000la)) ([#3267](https://github.com/mihonapp/mihon/pull/3267))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184))
|
- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184))
|
||||||
- Continue reading button missing when unread filter is off ([@AntsyLich](https://github.com/AntsyLich)) ([#3382](https://github.com/mihonapp/mihon/pull/3382))
|
- Continue reading button missing when unread filter is off ([@AntsyLich](https://github.com/AntsyLich)) ([#3382](https://github.com/mihonapp/mihon/pull/3382))
|
||||||
|
|||||||
@@ -19,17 +19,26 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import kotlin.concurrent.atomics.AtomicInt
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.incrementAndFetch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
class BackupRestorer(
|
class BackupRestorer(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val notifier: BackupNotifier,
|
private val notifier: BackupNotifier,
|
||||||
private val isSync: Boolean,
|
private val isSync: Boolean,
|
||||||
|
|
||||||
|
private val database: Database = Injekt.get(),
|
||||||
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
||||||
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
||||||
private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(),
|
private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(),
|
||||||
@@ -37,8 +46,8 @@ class BackupRestorer(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private var restoreAmount = 0
|
private var restoreAmount = 0
|
||||||
private var restoreProgress = 0
|
private val restoreProgress = AtomicInt(0)
|
||||||
private val errors = mutableListOf<Pair<Date, String>>()
|
private val errors = CopyOnWriteArrayList<Pair<Date, String>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of source ID to source name from backup data
|
* Mapping of source ID to source name from backup data
|
||||||
@@ -111,10 +120,10 @@ class BackupRestorer(
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
categoriesRestorer(backupCategories)
|
categoriesRestorer(backupCategories)
|
||||||
|
|
||||||
restoreProgress += 1
|
val progress = restoreProgress.incrementAndFetch()
|
||||||
notifier.showRestoreProgress(
|
notifier.showRestoreProgress(
|
||||||
context.stringResource(MR.strings.categories),
|
context.stringResource(MR.strings.categories),
|
||||||
restoreProgress,
|
progress,
|
||||||
restoreAmount,
|
restoreAmount,
|
||||||
isSync,
|
isSync,
|
||||||
)
|
)
|
||||||
@@ -125,18 +134,23 @@ class BackupRestorer(
|
|||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) = launch {
|
) = launch {
|
||||||
mangaRestorer.sortByNew(backupMangas)
|
mangaRestorer.sortByNew(backupMangas)
|
||||||
.forEach {
|
.chunked(100)
|
||||||
ensureActive()
|
.forEach { chunk ->
|
||||||
|
database.transaction {
|
||||||
|
chunk.forEach {
|
||||||
|
ensureActive()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mangaRestorer.restore(it, backupCategories)
|
mangaRestorer.restore(it, backupCategories)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
||||||
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress.incrementAndFetch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
|
||||||
restoreProgress += 1
|
|
||||||
notifier.showRestoreProgress(it.title, restoreProgress, restoreAmount, isSync)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,10 +164,10 @@ class BackupRestorer(
|
|||||||
categories,
|
categories,
|
||||||
)
|
)
|
||||||
|
|
||||||
restoreProgress += 1
|
val progress = restoreProgress.incrementAndFetch()
|
||||||
notifier.showRestoreProgress(
|
notifier.showRestoreProgress(
|
||||||
context.stringResource(MR.strings.app_settings),
|
context.stringResource(MR.strings.app_settings),
|
||||||
restoreProgress,
|
progress,
|
||||||
restoreAmount,
|
restoreAmount,
|
||||||
isSync,
|
isSync,
|
||||||
)
|
)
|
||||||
@@ -163,10 +177,10 @@ class BackupRestorer(
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
preferenceRestorer.restoreSource(preferences)
|
preferenceRestorer.restoreSource(preferences)
|
||||||
|
|
||||||
restoreProgress += 1
|
val progress = restoreProgress.incrementAndFetch()
|
||||||
notifier.showRestoreProgress(
|
notifier.showRestoreProgress(
|
||||||
context.stringResource(MR.strings.source_settings),
|
context.stringResource(MR.strings.source_settings),
|
||||||
restoreProgress,
|
progress,
|
||||||
restoreAmount,
|
restoreAmount,
|
||||||
isSync,
|
isSync,
|
||||||
)
|
)
|
||||||
@@ -176,19 +190,24 @@ class BackupRestorer(
|
|||||||
backupExtensionStores: List<BackupExtensionStore>,
|
backupExtensionStores: List<BackupExtensionStore>,
|
||||||
) = launch {
|
) = launch {
|
||||||
backupExtensionStores
|
backupExtensionStores
|
||||||
.forEach {
|
.chunked(100)
|
||||||
ensureActive()
|
.forEach { chunk ->
|
||||||
|
database.transaction {
|
||||||
|
chunk.forEach {
|
||||||
|
ensureActive()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
extensionStoreRestorer(it)
|
extensionStoreRestorer(it)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}")
|
errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress.incrementAndFetch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
notifier.showRestoreProgress(
|
notifier.showRestoreProgress(
|
||||||
context.stringResource(MR.strings.extensionStores),
|
context.stringResource(MR.strings.extensionStores),
|
||||||
restoreProgress,
|
restoreProgress.load(),
|
||||||
restoreAmount,
|
restoreAmount,
|
||||||
isSync,
|
isSync,
|
||||||
)
|
)
|
||||||
@@ -208,7 +227,7 @@ class BackupRestorer(
|
|||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
// Empty
|
// Empty
|
||||||
}
|
}
|
||||||
return File("")
|
return File("")
|
||||||
|
|||||||
+12
-10
@@ -19,16 +19,18 @@ class CategoriesRestorer(
|
|||||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||||
var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
||||||
|
|
||||||
val categories = backupCategories
|
val categories = database.transactionWithResult {
|
||||||
.sortedBy { it.order }
|
backupCategories
|
||||||
.map {
|
.sortedBy { it.order }
|
||||||
val dbCategory = dbCategoriesByName[it.name]
|
.map {
|
||||||
if (dbCategory != null) return@map dbCategory
|
val dbCategory = dbCategoriesByName[it.name]
|
||||||
val order = nextOrder++
|
if (dbCategory != null) return@map dbCategory
|
||||||
database.categoriesQueries
|
val order = nextOrder++
|
||||||
.insert(it.name, order, it.flags)
|
database.categoriesQueries
|
||||||
.let { id -> it.toCategory(id).copy(order = order) }
|
.insert(it.name, order, it.flags)
|
||||||
}
|
.let { id -> it.toCategory(id).copy(order = order) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
libraryPreferences.categorizedDisplaySettings.set(
|
libraryPreferences.categorizedDisplaySettings.set(
|
||||||
(dbCategories + categories)
|
(dbCategories + categories)
|
||||||
|
|||||||
Reference in New Issue
Block a user