Run more db queries inside the IO pool, convert some RxJava references to Coroutines

This commit is contained in:
Jobobby04 2021-01-26 00:32:53 -05:00
parent 64eeab7c5e
commit 18f02a85ac
8 changed files with 74 additions and 45 deletions

View File

@ -49,7 +49,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
val mdex = mdex ?: throw Exception("Mangadex not enabled") val mdex = mdex ?: throw Exception("Mangadex not enabled")
val mangaMetadata = db.getFlatMetadataForManga(track.manga_id).executeAsBlocking() val mangaMetadata = db.getFlatMetadataForManga(track.manga_id).executeOnIO()
?.raise<MangaDexSearchMetadata>() ?.raise<MangaDexSearchMetadata>()
?: throw Exception("Invalid manga metadata") ?: throw Exception("Invalid manga metadata")
val followStatus = FollowStatus.fromInt(track.status) val followStatus = FollowStatus.fromInt(track.status)

View File

@ -20,6 +20,7 @@ import exh.search.QueryComponent
import exh.search.SearchEngine import exh.search.SearchEngine
import exh.search.Text import exh.search.Text
import exh.util.cancellable import exh.util.cancellable
import exh.util.executeOnIO
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -114,7 +115,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
// Prepare filter object // Prepare filter object
val parsedQuery = searchEngine.parseQuery(savedSearchText) val parsedQuery = searchEngine.parseQuery(savedSearchText)
val mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().executeAsBlocking() val mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().executeOnIO()
val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count) val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count)
if (mangaWithMetaIds.isNotEmpty()) { if (mangaWithMetaIds.isNotEmpty()) {
val mangaIdCol = mangaWithMetaIdsQuery.getColumnIndex(MangaTable.COL_ID) val mangaIdCol = mangaWithMetaIdsQuery.getColumnIndex(MangaTable.COL_ID)
@ -137,8 +138,8 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
// No meta? Filter using title // No meta? Filter using title
filterManga(parsedQuery, item.manga) filterManga(parsedQuery, item.manga)
} else { } else {
val tags = db.getSearchTagsForManga(mangaId).executeAsBlocking() val tags = db.getSearchTagsForManga(mangaId).executeOnIO()
val titles = db.getSearchTitlesForManga(mangaId).executeAsBlocking() val titles = db.getSearchTitlesForManga(mangaId).executeOnIO()
filterManga(parsedQuery, item.manga, false, tags, titles) filterManga(parsedQuery, item.manga, false, tags, titles)
} }
} else { } else {
@ -166,7 +167,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
private suspend fun filterManga(queries: List<QueryComponent>, manga: LibraryManga, checkGenre: Boolean = true, searchTags: List<SearchTag>? = null, searchTitles: List<SearchTitle>? = null): Boolean { private suspend fun filterManga(queries: List<QueryComponent>, manga: LibraryManga, checkGenre: Boolean = true, searchTags: List<SearchTag>? = null, searchTitles: List<SearchTitle>? = null): Boolean {
val mappedQueries = queries.groupBy { it.excluded } val mappedQueries = queries.groupBy { it.excluded }
val tracks = if (hasLoggedServices) db.getTracks(manga).executeAsBlocking().toList() else null val tracks = if (hasLoggedServices) db.getTracks(manga).executeOnIO() else null
val source = sourceManager.get(manga.source) val source = sourceManager.get(manga.source)
val genre = if (checkGenre) manga.getGenres().orEmpty() else emptyList() val genre = if (checkGenre) manga.getGenres().orEmpty() else emptyList()
val hasNormalQuery = mappedQueries[false]?.all { queryComponent -> val hasNormalQuery = mappedQueries[false]?.all { queryComponent ->

View File

@ -643,7 +643,7 @@ class SettingsEhController : SettingsController() {
val allMeta = db.getFavoriteMangaWithMetadata().executeAsBlocking().filter { val allMeta = db.getFavoriteMangaWithMetadata().executeAsBlocking().filter {
it.source == EH_SOURCE_ID || it.source == EXH_SOURCE_ID it.source == EH_SOURCE_ID || it.source == EXH_SOURCE_ID
}.mapNotNull { }.mapNotNull {
db.getFlatMetadataForManga(it.id!!).executeAsBlocking() db.getFlatMetadataForManga(it.id!!).executeOnIO()
?.raise<EHentaiSearchMetadata>() ?.raise<EHentaiSearchMetadata>()
}.toList() }.toList()

View File

@ -60,7 +60,7 @@ object DebugFunctions {
}.toList() }.toList()
allManga.forEach { manga -> allManga.forEach { manga ->
val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>() ?: return@forEach val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>() ?: return@forEach
// remove age flag // remove age flag
meta.aged = false meta.aged = false
db.insertFlatMetadataAsync(meta.flatten()).await() db.insertFlatMetadataAsync(meta.flatten()).await()
@ -95,7 +95,7 @@ object DebugFunctions {
)?.getMangaDetails(manga.toMangaInfo())?.let { networkManga -> )?.getMangaDetails(manga.toMangaInfo())?.let { networkManga ->
manga.copyFrom(networkManga.toSManga()) manga.copyFrom(networkManga.toSManga())
manga.initialized = true manga.initialized = true
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeOnIO()
} }
} }
} }
@ -113,7 +113,7 @@ object DebugFunctions {
}.toList() }.toList()
allManga.forEach { manga -> allManga.forEach { manga ->
val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>() ?: return@forEach val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>() ?: return@forEach
galleries += "Aged: ${meta.aged}\t Title: ${manga.title}" galleries += "Aged: ${meta.aged}\t Title: ${manga.title}"
} }
} }
@ -123,7 +123,7 @@ object DebugFunctions {
fun countAgedFlagInEXHManga(): Int { fun countAgedFlagInEXHManga(): Int {
var agedAmount = 0 var agedAmount = 0
runBlocking { runBlocking {
val metadataManga = db.getFavoriteMangaWithMetadata().executeAsBlocking() val metadataManga = db.getFavoriteMangaWithMetadata().executeOnIO()
val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga -> val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga ->
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) { if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
@ -132,7 +132,7 @@ object DebugFunctions {
}.toList() }.toList()
allManga.forEach { manga -> allManga.forEach { manga ->
val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>() ?: return@forEach val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>() ?: return@forEach
if (meta.aged) { if (meta.aged) {
// remove age flag // remove age flag
agedAmount++ agedAmount++
@ -203,7 +203,7 @@ object DebugFunctions {
EHentaiUpdateWorker.scheduleBackground(app) EHentaiUpdateWorker.scheduleBackground(app)
} }
fun listScheduledJobs() = app.jobScheduler.allPendingJobs.map { j -> fun listScheduledJobs() = app.jobScheduler.allPendingJobs.joinToString(",\n") { j ->
""" """
{ {
info: ${j.id}, info: ${j.id},
@ -212,7 +212,7 @@ object DebugFunctions {
intervalMillis: ${j.intervalMillis}, intervalMillis: ${j.intervalMillis},
} }
""".trimIndent() """.trimIndent()
}.joinToString(",\n") }
fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll() fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll()

View File

@ -7,6 +7,7 @@ import android.app.job.JobService
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import com.elvishew.xlog.Logger
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -45,20 +46,18 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.ArrayList import java.util.ArrayList
import kotlin.coroutines.CoroutineContext
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.days import kotlin.time.days
import kotlin.time.hours import kotlin.time.hours
class EHentaiUpdateWorker : JobService(), CoroutineScope { class EHentaiUpdateWorker : JobService() {
override val coroutineContext: CoroutineContext private val scope = CoroutineScope(Dispatchers.Default + Job())
get() = Dispatchers.Default + Job()
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val updateHelper: EHentaiUpdateHelper by injectLazy() private val updateHelper: EHentaiUpdateHelper by injectLazy()
private val logger = XLog.tag("EHUpdater") private val logger: Logger = XLog.tag("EHUpdater").build()
private val updateNotifier by lazy { LibraryUpdateNotifier(this) } private val updateNotifier by lazy { LibraryUpdateNotifier(this) }
@ -86,7 +85,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
* to end the job entirely. Regardless of the value returned, your job must stop executing. * to end the job entirely. Regardless of the value returned, your job must stop executing.
*/ */
override fun onStopJob(params: JobParameters?): Boolean { override fun onStopJob(params: JobParameters?): Boolean {
runBlocking { this@EHentaiUpdateWorker.coroutineContext[Job]?.cancelAndJoin() } runBlocking { scope.coroutineContext[Job]?.cancelAndJoin() }
return false return false
} }
@ -122,7 +121,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
* extras configured with [ This object serves to identify this specific running job instance when calling][JobInfo.Builder.setExtras] * extras configured with [ This object serves to identify this specific running job instance when calling][JobInfo.Builder.setExtras]
*/ */
override fun onStartJob(params: JobParameters): Boolean { override fun onStartJob(params: JobParameters): Boolean {
launch { scope.launch {
startUpdating() startUpdating()
logger.d("Update job completed!") logger.d("Update job completed!")
jobFinished(params, false) jobFinished(params, false)
@ -145,7 +144,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
return@mapNotNull null return@mapNotNull null
} }
val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking() val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()
?: return@mapNotNull null ?: return@mapNotNull null
val raisedMeta = meta.raise<EHentaiSearchMetadata>() val raisedMeta = meta.raise<EHentaiSearchMetadata>()
@ -274,7 +273,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
return new to db.getChapters(manga).executeOnIO() return new to db.getChapters(manga).executeOnIO()
} catch (t: Throwable) { } catch (t: Throwable) {
if (t is EHentai.GalleryNotFoundException) { if (t is EHentai.GalleryNotFoundException) {
val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>() val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>()
if (meta != null) { if (meta != null) {
// Age dead galleries // Age dead galleries
logger.d("Aged %s - notfound", manga.id) logger.d("Aged %s - notfound", manga.id)
@ -345,8 +344,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
} }
fun launchBackgroundTest(context: Context): String { fun launchBackgroundTest(context: Context): String {
val jobScheduler = context.jobScheduler return if (context.jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
return if (jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
logger.e("Failed to schedule background test job!") logger.e("Failed to schedule background test job!")
"Failed" "Failed"
} else { } else {

View File

@ -2,6 +2,8 @@ package exh.metadata.metadata.base
import com.pushtorefresh.storio.operations.PreparedOperation import com.pushtorefresh.storio.operations.PreparedOperation
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.metadata.sql.models.SearchMetadata import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTag import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle import exh.metadata.sql.models.SearchTitle
@ -9,6 +11,9 @@ import exh.util.executeOnIO
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
@ -32,23 +37,35 @@ data class FlatMetadata(
} }
} }
fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<FlatMetadata?> { interface PreparedSuspendOperation<T> : PreparedOperation<T> {
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions /**
val single = Single.fromCallable { * Creates a [Flow] that emits the result of of Operation
val meta = getSearchMetadataForManga(mangaId).executeAsBlocking() *
* Example:
* override fun asFlow(): Flow<T> = flow { emit(operation()) }
*
*/
fun asFlow(): Flow<T>
/**
* Executes operation asynchronously in the I/O thread pool.
*/
suspend fun executeOnIO(): T
}
fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedSuspendOperation<FlatMetadata?> =
preparedOperationFromSuspend {
val meta = getSearchMetadataForManga(mangaId).executeOnIO()
if (meta != null) { if (meta != null) {
val tags = getSearchTagsForManga(mangaId).executeAsBlocking() val tags = getSearchTagsForManga(mangaId).executeOnIO()
val titles = getSearchTitlesForManga(mangaId).executeAsBlocking() val titles = getSearchTitlesForManga(mangaId).executeOnIO()
FlatMetadata(meta, tags, titles) FlatMetadata(meta, tags, titles)
} else null } else null
} }
return preparedOperationFromSingle(single) private fun <T> preparedOperationFromSuspend(operation: suspend () -> T): PreparedSuspendOperation<T> {
} return object : PreparedSuspendOperation<T> {
private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperation<T> {
return object : PreparedOperation<T> {
/** /**
* Creates [rx.Observable] that emits result of Operation. * Creates [rx.Observable] that emits result of Operation.
* *
@ -57,7 +74,7 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
* *
* @return observable result of operation with only one [rx.Observer.onNext] call. * @return observable result of operation with only one [rx.Observer.onNext] call.
*/ */
override fun createObservable() = single.toObservable() override fun createObservable() = runAsObservable(operation)
/** /**
* Executes operation synchronously in current thread. * Executes operation synchronously in current thread.
@ -66,11 +83,11 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
* Notice: Blocking I/O operation should not be executed on the Main Thread, * Notice: Blocking I/O operation should not be executed on the Main Thread,
* it can cause ANR (Activity Not Responding dialog), block the UI and drop animations frames. * it can cause ANR (Activity Not Responding dialog), block the UI and drop animations frames.
* So please, execute blocking I/O operation only from background thread. * So please, execute blocking I/O operation only from background thread.
* See [WorkerThread]. * See [androidx.annotation.WorkerThread].
* *
* @return nullable result of operation. * @return nullable result of operation.
*/ */
override fun executeAsBlocking() = single.toBlocking().value() override fun executeAsBlocking() = runBlocking { operation() }
/** /**
* Creates [rx.Observable] that emits result of Operation. * Creates [rx.Observable] that emits result of Operation.
@ -80,7 +97,7 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
* *
* @return observable result of operation with only one [rx.Observer.onNext] call. * @return observable result of operation with only one [rx.Observer.onNext] call.
*/ */
override fun asRxObservable() = single.toObservable() override fun asRxObservable() = runAsObservable(operation)
/** /**
* Creates [rx.Single] that emits result of Operation lazily when somebody subscribes to it. * Creates [rx.Single] that emits result of Operation lazily when somebody subscribes to it.
@ -89,7 +106,21 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
* *
* @return single result of operation. * @return single result of operation.
*/ */
override fun asRxSingle() = single override fun asRxSingle(): Single<T> = runAsObservable(operation).toSingle()
/**
* Creates a [Flow] that emits the result of of Operation
*
* Example:
* override fun asFlow(): Flow<T> = flow { emit(operation()) }
*
*/
override fun asFlow(): Flow<T> = flow { emit(operation()) }
/**
* Executes operation asynchronously in the I/O thread pool.
*/
override suspend fun executeOnIO(): T = withIOContext { operation() }
} }
} }

View File

@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.util.lang.asFlow
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
@ -16,7 +15,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -50,6 +48,6 @@ class MetadataViewPresenter(
} }
private fun getMangaMetaObservable(): Flow<FlatMetadata?> { private fun getMangaMetaObservable(): Flow<FlatMetadata?> {
return db.getFlatMetadataForManga(manga.id!!).asRxObservable().asFlow() return db.getFlatMetadataForManga(manga.id!!).asFlow()
} }
} }

View File

@ -1,6 +1,7 @@
package exh.util package exh.util
import com.pushtorefresh.storio.operations.PreparedOperation import android.database.Cursor
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetCursor
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects
@ -18,4 +19,4 @@ suspend fun <T> PreparedPutObject<T>.executeOnIO(): PutResult = withContext(Disp
suspend fun <T> PreparedPutCollectionOfObjects<T>.executeOnIO(): PutResults<T> = withContext(Dispatchers.IO) { executeAsBlocking() } suspend fun <T> PreparedPutCollectionOfObjects<T>.executeOnIO(): PutResults<T> = withContext(Dispatchers.IO) { executeAsBlocking() }
suspend fun <T> PreparedOperation<T>.executeOnIO(): T? = withContext(Dispatchers.IO) { executeAsBlocking() } suspend fun PreparedGetCursor.executeOnIO(): Cursor = withContext(Dispatchers.IO) { executeAsBlocking() }