Fix thread starvation caused by not yielding or using an inappropriate thread pool (#2955)

This commit is contained in:
Luca Auer
2026-02-15 16:47:31 +01:00
committed by GitHub
parent 5be5a4e819
commit 22d5c9d9f4
16 changed files with 162 additions and 83 deletions
@@ -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.
@@ -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)
@@ -90,7 +90,7 @@ class DownloadStore(
/**
* Returns the list of downloads to restore. It should be called in a background thread.
*/
fun restore(): List<Download> {
suspend fun restore(): List<Download> {
val objs = preferences.all
.mapNotNull { it.value as? String }
.mapNotNull { deserialize(it) }
@@ -101,10 +101,10 @@ class DownloadStore(
val cachedManga = mutableMapOf<Long, Manga?>()
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))
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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<LoadResult.Success>()
.associate { it.extension.pkgName to it.extension }
installedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Success>()
.associate { it.extension.pkgName to it.extension }
untrustedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension }
untrustedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension }
_isInitialized.value = true
_isInitialized.value = true
}
}
/**
@@ -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<LoadResult> {
suspend fun loadExtensions(context: Context): List<LoadResult> {
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()
}
}
@@ -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<Boolean?>(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 },
@@ -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<tachiyomi.domain.chapter.model.Chapter>? = null
private suspend fun getUnfilteredChapterList(): List<tachiyomi.domain.chapter.model.Chapter> {
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<ReaderChapter>? = null
private suspend fun getChapterList(): List<ReaderChapter> {
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) {
@@ -239,7 +239,7 @@ class UpdatesScreenModel(
}
}
private fun startDownloadingNow(chapterId: Long) {
private suspend fun startDownloadingNow(chapterId: Long) {
downloadManager.startDownloadNow(chapterId)
}