Skip to content

Commit

Permalink
Fix Small Preview CrossFade not work
Browse files Browse the repository at this point in the history
Co-Authored-By: renovate[bot] <99822064+revonateb0t@users.noreply.github.com>
  • Loading branch information
xb2016 and revonateB0T committed Oct 28, 2024
1 parent d184165 commit bf38759
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 86 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ android {
freeCompilerArgs = listOf(
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-Xwhen-guards",

"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=kotlin.contracts.ExperimentalContracts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ object LimitConcurrencyInterceptor : Interceptor {
return if (url != null) {
when {
// Ex thumb server may not have h2 multiplexing support
URL_PREFIX_THUMB_EX in url -> semaphores.withPermit(URL_PREFIX_THUMB_EX) {
URL_PREFIX_THUMB_EX in url -> semaphores.withLock(URL_PREFIX_THUMB_EX) {
withContext(NonCancellable) { chain.proceed(chain.request) }
}
// H@H server may not have h2 multiplexing support
URL_SIGNATURE_THUMB_NORMAL in url -> semaphores.withPermit(url.substringBefore(URL_SIGNATURE_THUMB_NORMAL)) {
URL_SIGNATURE_THUMB_NORMAL in url -> semaphores.withLock(url.substringBefore(URL_SIGNATURE_THUMB_NORMAL)) {
withContext(NonCancellable) { chain.proceed(chain.request) }
}
// H2 multiplexing enabled
Expand Down
64 changes: 64 additions & 0 deletions app/src/main/java/com/hippo/ehviewer/coil/LockPool.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2024 Moedog
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.coil

import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

interface LockPool<Lock, K> {
fun acquire(key: K): Lock
fun release(key: K, lock: Lock)
suspend fun Lock.lock()
fun Lock.tryLock(): Boolean
fun Lock.unlock()
}

suspend inline fun <Lock, K, R> LockPool<Lock, K>.withLock(key: K, action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val lock = acquire(key)
return try {
lock.lock()
return try {
action()
} finally {
lock.unlock()
}
} finally {
release(key, lock)
}
}

suspend inline fun <Lock, K, R> LockPool<Lock, K>.withLockNeedSuspend(key: K, action: () -> R): Pair<R, Boolean> {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val lock = acquire(key)
return try {
val mustSuspend = !lock.tryLock()
if (mustSuspend) lock.lock()
return try {
action() to mustSuspend
} finally {
lock.unlock()
}
} finally {
release(key, lock)
}
}
83 changes: 14 additions & 69 deletions app/src/main/java/com/hippo/ehviewer/coil/MergeInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,81 +17,26 @@
*/
package com.hippo.ehviewer.coil

import coil.decode.DataSource
import coil.intercept.Interceptor
import coil.request.ImageRequest
import coil.request.ImageResult
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import coil.request.SuccessResult
import com.hippo.ehviewer.client.isNormalPreviewKey

object MergeInterceptor : Interceptor {
private val pendingContinuationMap: HashMap<String, MutableList<Continuation<Unit>>> = hashMapOf()
private val pendingContinuationMapLock = Mutex()
private val notifyScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val EMPTY_LIST = mutableListOf<Continuation<Unit>>()
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val originRequest = chain.request
val originListener = originRequest.listener
val key = originRequest.data as String
val newRequest = ImageRequest.Builder(originRequest).listener(
{ originListener?.onStart(it) },
{ originListener?.onCancel(it) },
{ request, result ->
originListener?.onError(request, result)
// Wake all pending continuations since this request is to be failed
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
},
{ request, result ->
originListener?.onSuccess(request, result)
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
},
).build()
private val mutex = NamedMutex<String>()

pendingContinuationMapLock.lock()
val existPendingContinuations = pendingContinuationMap[key]
if (existPendingContinuations == null) {
pendingContinuationMap[key] = EMPTY_LIST
pendingContinuationMapLock.unlock()
} else {
if (existPendingContinuations === EMPTY_LIST) pendingContinuationMap[key] = mutableListOf()
pendingContinuationMap[key]!!.apply {
suspendCancellableCoroutine { continuation ->
add(continuation)
pendingContinuationMapLock.unlock()
continuation.invokeOnCancellation { remove(continuation) }
}
}
}

try {
return withContext(originRequest.interceptorDispatcher) { chain.proceed(newRequest) }
} catch (e: CancellationException) {
notifyScope.launch {
pendingContinuationMapLock.withLock {
// Wake up a pending continuation to continue executing task
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) }
// If no successor, delete this entry from hashmap
successor ?: pendingContinuationMap.remove(key)
}
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val req = chain.request
val key = req.memoryCacheKey?.key?.takeIf { it.isNormalPreviewKey }
return if (key != null) {
val (result, suspended) = mutex.withLockNeedSuspend(key) { chain.proceed(req) }
when (result) {
is SuccessResult if (suspended) -> result.copy(dataSource = DataSource.MEMORY)
else -> result
}
throw e
} else {
chain.proceed(req)
}
}
}
52 changes: 52 additions & 0 deletions app/src/main/java/com/hippo/ehviewer/coil/NamedMutex.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 Moedog
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.coil

import androidx.collection.MutableScatterMap
import androidx.collection.mutableScatterMapOf
import io.ktor.utils.io.pool.DefaultPool
import kotlinx.coroutines.sync.Mutex

class MutexTracker(mutex: Mutex = Mutex(), private var count: Int = 0) : Mutex by mutex {
operator fun inc() = apply { count++ }
operator fun dec() = apply { count-- }
val isFree
get() = count == 0
}

object MutexPool : DefaultPool<MutexTracker>(capacity = 32) {
override fun produceInstance() = MutexTracker()
override fun validateInstance(mutex: MutexTracker) {
check(!mutex.isLocked)
check(mutex.isFree)
}
}

class NamedMutex<K>(val active: MutableScatterMap<K, MutexTracker> = mutableScatterMapOf()) : LockPool<MutexTracker, K> {
override fun acquire(key: K) = synchronized(active) { active.getOrPut(key) { MutexPool.borrow() }.inc() }
override fun release(key: K, lock: MutexTracker) = synchronized(active) {
lock.dec()
if (lock.isFree) {
active.remove(key)
MutexPool.recycle(lock)
}
}
override suspend fun MutexTracker.lock() = lock()
override fun MutexTracker.tryLock() = tryLock()
override fun MutexTracker.unlock() = unlock()
}
25 changes: 10 additions & 15 deletions app/src/main/java/com/hippo/ehviewer/coil/NamedSemaphore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package com.hippo.ehviewer.coil
import androidx.collection.mutableScatterMapOf
import io.ktor.utils.io.pool.DefaultPool
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit

class SemaphoreTracker(semaphore: Semaphore, private var count: Int = 0) : Semaphore by semaphore {
operator fun inc() = apply { count++ }
Expand All @@ -37,22 +36,18 @@ class SemaphorePool(val permits: Int) : DefaultPool<SemaphoreTracker>(capacity =
}
}

class NamedSemaphore<K>(val permits: Int) {
class NamedSemaphore<K>(val permits: Int) : LockPool<SemaphoreTracker, K> {
val pool = SemaphorePool(permits = permits)
val active = mutableScatterMapOf<K, SemaphoreTracker>()
}

suspend inline fun <K, R> NamedSemaphore<K>.withPermit(key: K, action: () -> R): R {
val semaphore = synchronized(active) { active.getOrPut(key) { pool.borrow() }.inc() }
return try {
semaphore.withPermit(action)
} finally {
synchronized(active) {
semaphore.dec()
if (semaphore.isFree) {
active.remove(key)
pool.recycle(semaphore)
}
override fun acquire(key: K) = synchronized(active) { active.getOrPut(key) { pool.borrow() }.inc() }
override fun release(key: K, lock: SemaphoreTracker) = synchronized(active) {
lock.dec()
if (lock.isFree) {
active.remove(key)
pool.recycle(lock)
}
}
override suspend fun SemaphoreTracker.lock() = acquire()
override fun SemaphoreTracker.tryLock() = tryAcquire()
override fun SemaphoreTracker.unlock() = release()
}

0 comments on commit bf38759

Please sign in to comment.