From 22d5c9d9f4995598e1385f8369836b86125e6f81 Mon Sep 17 00:00:00 2001 From: Luca Auer Date: Sun, 15 Feb 2026 16:47:31 +0100 Subject: [PATCH] Fix thread starvation caused by not yielding or using an inappropriate thread pool (#2955) --- CHANGELOG.md | 1 + .../settings/screen/SettingsDataScreen.kt | 5 +- .../more/settings/screen/data/StorageInfo.kt | 44 ++++++++++++---- .../tachiyomi/data/cache/ChapterCache.kt | 14 +++-- .../data/download/DownloadManager.kt | 4 +- .../tachiyomi/data/download/DownloadStore.kt | 6 +-- .../tachiyomi/data/download/Downloader.kt | 6 +-- .../data/notification/NotificationReceiver.kt | 38 +++++++++----- .../tachiyomi/extension/ExtensionManager.kt | 19 ++++--- .../extension/util/ExtensionLoader.kt | 10 ++-- .../kanade/tachiyomi/ui/main/MainActivity.kt | 9 ++-- .../tachiyomi/ui/reader/ReaderViewModel.kt | 31 +++++++---- .../ui/updates/UpdatesScreenModel.kt | 2 +- .../java/mihon/core/migration/Migrator.kt | 4 +- .../java/tachiyomi/data/TransactionContext.kt | 51 ++++++++++++++----- .../moko-resources/base/strings.xml | 1 + 16 files changed, 162 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 675b47200..e1f7fc270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266)) - Optimize MAL search queries ([@MajorTanya](https://github.com/MajorTanya)) ([#2832](https://github.com/mihonapp/mihon/pull/2832)) - Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874)) +- Rework internals for better performance ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2955](https://github.com/mihonapp/mihon/pull/2955)) ### Changed - Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 9b65ed4c1..d1d3e5ff4 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -285,7 +285,10 @@ object SettingsDataScreen : SearchableSettings { val chapterCache = remember { Injekt.get() } var cacheReadableSizeSema by remember { mutableIntStateOf(0) } - val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize } + var cacheReadableSize by remember { mutableStateOf(context.stringResource(MR.strings.calculating)) } + LaunchedEffect(cacheReadableSizeSema) { + cacheReadableSize = chapterCache.getReadableSize() + } return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_storage_usage), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt index 5fed6c6ef..a8578d97d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt @@ -9,12 +9,18 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.util.storage.DiskUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -45,10 +51,24 @@ private fun StorageInfo( ) { val context = LocalContext.current - val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) } - val availableText = remember(available) { Formatter.formatFileSize(context, available) } - val total = remember(file) { DiskUtil.getTotalStorageSpace(file) } - val totalText = remember(total) { Formatter.formatFileSize(context, total) } + var available by remember(file) { mutableStateOf(-1L) } + var total by remember(file) { mutableStateOf(-1L) } + + LaunchedEffect(file) { + available = withContext(Dispatchers.IO) { DiskUtil.getAvailableStorageSpace(file) } + total = withContext(Dispatchers.IO) { DiskUtil.getTotalStorageSpace(file) } + } + + val availableText = if (available == -1L) { + stringResource(MR.strings.calculating) + } else { + Formatter.formatFileSize(context, available) + } + val totalText = if (total == -1L) { + stringResource(MR.strings.calculating) + } else { + Formatter.formatFileSize(context, total) + } Column( verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), @@ -58,13 +78,15 @@ private fun StorageInfo( style = MaterialTheme.typography.header, ) - LinearProgressIndicator( - modifier = Modifier - .clip(MaterialTheme.shapes.small) - .fillMaxWidth() - .height(12.dp), - progress = { (1 - (available / total.toFloat())) }, - ) + if (total > 0) { + LinearProgressIndicator( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .height(12.dp), + progress = { (1 - (available / total.toFloat())) }, + ) + } Text( text = stringResource(MR.strings.available_disk_space_info, availableText, totalText), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt index 1d47f5b7d..2c3b8e336 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -6,6 +6,8 @@ import com.jakewharton.disklrucache.DiskLruCache import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.saveTo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import logcat.LogPriority import okhttp3.Response @@ -42,17 +44,13 @@ class ChapterCache( */ private val cacheDir: File = diskCache.directory - /** - * Returns real size of directory. - */ - private val realSize: Long - get() = DiskUtil.getDirectorySize(cacheDir) - /** * Returns real size of directory in human readable format. */ - val readableSize: String - get() = Formatter.formatFileSize(context, realSize) + suspend fun getReadableSize(): String = withContext(Dispatchers.IO) { + val size = DiskUtil.getDirectorySize(cacheDir) + Formatter.formatFileSize(context, size) + } /** * Get page list from cache. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 2d7377488..7007030a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -104,10 +104,10 @@ class DownloadManager( return queueState.value.find { it.chapter.id == chapterId } } - fun startDownloadNow(chapterId: Long) { + suspend fun startDownloadNow(chapterId: Long) { val existingDownload = getQueuedDownloadOrNull(chapterId) // If not in queue try to start a new download - val toAdd = existingDownload ?: runBlocking { Download.fromChapterId(chapterId) } ?: return + val toAdd = existingDownload ?: Download.fromChapterId(chapterId) ?: return queueState.value.toMutableList().apply { existingDownload?.let { remove(it) } add(0, toAdd) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt index 3d95909bf..a804fcf73 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt @@ -90,7 +90,7 @@ class DownloadStore( /** * Returns the list of downloads to restore. It should be called in a background thread. */ - fun restore(): List { + suspend fun restore(): List { val objs = preferences.all .mapNotNull { it.value as? String } .mapNotNull { deserialize(it) } @@ -101,10 +101,10 @@ class DownloadStore( val cachedManga = mutableMapOf() for ((mangaId, chapterId) in objs) { val manga = cachedManga.getOrPut(mangaId) { - runBlocking { getManga.await(mangaId) } + getManga.await(mangaId) } ?: continue val source = sourceManager.get(manga.source) as? HttpSource ?: continue - val chapter = runBlocking { getChapter.await(chapterId) } ?: continue + val chapter = getChapter.await(chapterId) ?: continue downloads.add(Download(source, manga, chapter)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index ee7f1d273..c72dc0e7c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -111,9 +111,9 @@ class Downloader( var isPaused: Boolean = false init { - launchNow { - val chapters = async { store.restore() } - addAllToQueue(chapters.await()) + scope.launch { + val chapters = store.restore() + addAllToQueue(chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 18bafc211..02bf46938 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.runBlocking import tachiyomi.core.common.Constants import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.Chapter @@ -78,11 +79,18 @@ class NotificationReceiver : BroadcastReceiver() { ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context) // Open reader activity ACTION_OPEN_CHAPTER -> { - openChapter( - context, - intent.getLongExtra(EXTRA_MANGA_ID, -1), - intent.getLongExtra(EXTRA_CHAPTER_ID, -1), - ) + val pendingResult = goAsync() + launchIO { + try { + openChapter( + context, + intent.getLongExtra(EXTRA_MANGA_ID, -1), + intent.getLongExtra(EXTRA_CHAPTER_ID, -1), + ) + } finally { + pendingResult.finish() + } + } } // Mark updated manga chapters as read ACTION_MARK_AS_READ -> { @@ -147,16 +155,18 @@ class NotificationReceiver : BroadcastReceiver() { * @param mangaId id of manga * @param chapterId id of chapter */ - private fun openChapter(context: Context, mangaId: Long, chapterId: Long) { - val manga = runBlocking { getManga.await(mangaId) } - val chapter = runBlocking { getChapter.await(chapterId) } - if (manga != null && chapter != null) { - val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + private suspend fun openChapter(context: Context, mangaId: Long, chapterId: Long) { + val manga = getManga.await(mangaId) + val chapter = getChapter.await(chapterId) + withUIContext { + if (manga != null && chapter != null) { + val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(intent) + } else { + context.toast(MR.strings.chapter_error) } - context.startActivity(intent) - } else { - context.toast(MR.strings.chapter_error) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 2a11e8245..98163ca1f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import logcat.LogPriority import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat @@ -118,17 +119,19 @@ class ExtensionManager( * Loads and registers the installed extensions. */ private fun initExtensions() { - val extensions = ExtensionLoader.loadExtensions(context) + scope.launch { + val extensions = ExtensionLoader.loadExtensions(context) - installedExtensionMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } + installedExtensionMapFlow.value = extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } - untrustedExtensionMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } + untrustedExtensionMapFlow.value = extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } - _isInitialized.value = true + _isInitialized.value = true + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 739ce15cc..a55dd22a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import logcat.LogPriority import tachiyomi.core.common.util.system.logcat @@ -114,7 +115,7 @@ internal object ExtensionLoader { * * @param context The application context. */ - fun loadExtensions(context: Context): List { + suspend fun loadExtensions(context: Context): List { val pkgManager = context.packageManager val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -160,11 +161,10 @@ internal object ExtensionLoader { if (extPkgs.isEmpty()) return emptyList() // Load each extension concurrently and wait for completion - return runBlocking { - val deferred = extPkgs.map { + return coroutineScope { + extPkgs.map { async { loadExtension(context, it) } - } - deferred.awaitAll() + }.awaitAll() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index d46e400b0..5ccab88ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -131,8 +131,6 @@ class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) - val didMigration = Migrator.awaitAndRelease() - // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 if (!isTaskRoot) { finish() @@ -140,6 +138,11 @@ class MainActivity : BaseActivity() { } setComposeContent { + var didMigration by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + didMigration = Migrator.awaitAndRelease() + } + val context = LocalContext.current var incognito by remember { mutableStateOf(getIncognitoState.await(null)) } @@ -242,7 +245,7 @@ class MainActivity : BaseActivity() { ShowOnboarding() } - var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) } + var showChangelog by remember { mutableStateOf(didMigration == true && !BuildConfig.DEBUG) } if (showChangelog) { AlertDialog( onDismissRequest = { showChangelog = false }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 8f30d0cfa..1bc897279 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -145,18 +145,25 @@ class ReaderViewModel @JvmOverloads constructor( private var chapterToDownload: Download? = null - private val unfilteredChapterList by lazy { - val manga = manga!! - runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) } + private var unfilteredChapterListCache: List? = null + private suspend fun getUnfilteredChapterList(): List { + if (unfilteredChapterListCache == null) { + val manga = manga!! + unfilteredChapterListCache = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) + } + return unfilteredChapterListCache!! } /** * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first * time in a background thread to avoid blocking the UI. */ - private val chapterList by lazy { + private var chapterListCache: List? = null + private suspend fun getChapterList(): List { + chapterListCache?.let { return it } + val manga = manga!! - val chapters = runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) } + val chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) val selectedChapter = chapters.find { it.id == chapterId } ?: error("Requested chapter of id $chapterId not found in chapter list") @@ -205,7 +212,7 @@ class ReaderViewModel @JvmOverloads constructor( else -> chapters } - chaptersForReader + val result = chaptersForReader .sortedWith(getChapterSort(manga, sortDescending = false)) .run { if (readerPreferences.skipDupe().get()) { @@ -223,6 +230,8 @@ class ReaderViewModel @JvmOverloads constructor( } .map { it.toDbChapter() } .map(::ReaderChapter) + chapterListCache = result + return result } private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) } @@ -288,7 +297,7 @@ class ReaderViewModel @JvmOverloads constructor( val source = sourceManager.getOrStub(manga.source) loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) - loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) + loadChapter(loader!!, getChapterList().first { chapterId == it.chapter.id }) Result.success(true) } else { // Unlikely but okay @@ -313,6 +322,7 @@ class ReaderViewModel @JvmOverloads constructor( ): ViewerChapters { loader.loadChapter(chapter) + val chapterList = getChapterList() val chapterPos = chapterList.indexOf(chapter) val newChapters = ViewerChapters( chapter, @@ -511,11 +521,12 @@ class ReaderViewModel @JvmOverloads constructor( * If both conditions are satisfied enqueues chapter for delete * @param currentChapter current chapter, which is going to be marked as read. */ - private fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { + private suspend fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { val removeAfterReadSlots = downloadPreferences.removeAfterReadSlots().get() if (removeAfterReadSlots == -1) return // Determine which chapter should be deleted and enqueue + val chapterList = getChapterList() val currentChapterPosition = chapterList.indexOf(currentChapter) val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots) @@ -566,7 +577,7 @@ class ReaderViewModel @JvmOverloads constructor( .contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING) if (!markDuplicateAsRead) return - val duplicateUnreadChapters = unfilteredChapterList + val duplicateUnreadChapters = getUnfilteredChapterList() .mapNotNull { chapter -> if ( !chapter.read && @@ -679,7 +690,7 @@ class ReaderViewModel @JvmOverloads constructor( */ fun setMangaReadingMode(readingMode: ReadingMode) { val manga = manga ?: return - runBlocking(Dispatchers.IO) { + viewModelScope.launchIO { setMangaViewerFlags.awaitSetReadingMode(manga.id, readingMode.flagValue.toLong()) val currChapters = state.value.viewerChapters if (currChapters != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 7abba5623..1ef57fe74 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -239,7 +239,7 @@ class UpdatesScreenModel( } } - private fun startDownloadingNow(chapterId: Long) { + private suspend fun startDownloadingNow(chapterId: Long) { downloadManager.startDownloadNow(chapterId) } diff --git a/app/src/main/java/mihon/core/migration/Migrator.kt b/app/src/main/java/mihon/core/migration/Migrator.kt index c01a3873e..2168d58a1 100644 --- a/app/src/main/java/mihon/core/migration/Migrator.kt +++ b/app/src/main/java/mihon/core/migration/Migrator.kt @@ -35,7 +35,7 @@ object Migrator { result = null } - fun awaitAndRelease(): Boolean = runBlocking { - await().also { release() } + suspend fun awaitAndRelease(): Boolean { + return await().also { release() } } } diff --git a/data/src/main/java/tachiyomi/data/TransactionContext.kt b/data/src/main/java/tachiyomi/data/TransactionContext.kt index f804a0b8e..77154696b 100644 --- a/data/src/main/java/tachiyomi/data/TransactionContext.kt +++ b/data/src/main/java/tachiyomi/data/TransactionContext.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.asContextElement import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.util.concurrent.RejectedExecutionException import kotlin.concurrent.atomics.AtomicInt @@ -17,6 +19,10 @@ import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.coroutineContext import kotlin.coroutines.resume +// Global mutex to serialize transaction entry and prevent thread pool exhaustion. +// If you have multiple distinct database files/handlers, this should be a property of AndroidDatabaseHandler. +private val transactionMutex = Mutex() + /** * Returns the transaction dispatcher if we are on a transaction, or the database dispatchers. */ @@ -39,20 +45,41 @@ internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): Corouti * The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor. */ internal suspend fun AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T { - // Use inherited transaction context if available, this allows nested suspending transactions. - val transactionContext = - coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext() - return withContext(transactionContext) { - val transactionElement = coroutineContext[TransactionElement]!! - transactionElement.acquire() - try { - db.transactionWithResult { - runBlocking(transactionContext) { - block() + val transactionElement = coroutineContext[TransactionElement] + + // If we are already in a transaction, we don't need to lock the Mutex. + // We just reuse the existing thread/context. + if (transactionElement != null) { + return withContext(transactionElement.transactionDispatcher) { + transactionElement.acquire() + try { + db.transactionWithResult { + runBlocking(transactionElement.transactionDispatcher) { + block() + } } + } finally { + transactionElement.release() + } + } + } + + // transaction: Acquire Mutex BEFORE acquiring a thread. + // This ensures we only block a real thread when we have exclusive access. + return transactionMutex.withLock { + val transactionContext = createTransactionContext() + withContext(transactionContext) { + val element = coroutineContext[TransactionElement]!! + element.acquire() + try { + db.transactionWithResult { + runBlocking(transactionContext) { + block() + } + } + } finally { + element.release() } - } finally { - transactionElement.release() } } } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 5dd174aa8..8bc7ca486 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -171,6 +171,7 @@ Loading… + Calculating… InternalError: Check crash logs for further information