mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
perf(home): reduce redundant requests on home refresh
Drop a duplicate getLibraries call from loadLatestLibraryContent by reusing the libraries already loaded earlier in the same refresh. Add a 30s debounce to HomeRefreshCoordinator.onResumed so rapid resume/tab-switch events coalesce into a single refresh; pull-to-refresh bypasses the gate via onRefresh. Rate-limit SyncPlaybackPositionsHomeRefreshSideEffect to once every 6h with a 25-item cap and a rotating window cursor, so users with many downloads no longer fire one getItemInfo call per item on every resume.
This commit is contained in:
@@ -1,27 +1,56 @@
|
||||
package hu.bbara.purefin.core.feature.browse.home.refresh
|
||||
|
||||
import android.os.SystemClock
|
||||
import hu.bbara.purefin.core.data.HomeRepository
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.concurrent.atomics.AtomicLong
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
|
||||
// Singleton so the debounce window is shared across all ViewModel instances.
|
||||
// A new coordinator per ViewModel would reset the timer on configuration
|
||||
// changes and defeat the purpose of coalescing rapid resume/tab-switch
|
||||
// events.
|
||||
@Singleton
|
||||
class HomeRefreshCoordinator @Inject constructor(
|
||||
private val homeRepository: HomeRepository,
|
||||
private val sideEffects: Set<@JvmSuppressWildcards HomeRefreshSideEffect>,
|
||||
) {
|
||||
private val sideEffectsMutex = Mutex()
|
||||
|
||||
// AtomicLong matches the rate-limit pattern used by the playback-position
|
||||
// side effect and is safe to read/write from any coroutine dispatcher.
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private val lastRefreshAtMs = AtomicLong(0L)
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
suspend fun onResumed() {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (now - lastRefreshAtMs.load() < RESUME_DEBOUNCE_MS) {
|
||||
Timber.tag(TAG).d("Skipping onResumed refresh (debounced)")
|
||||
return
|
||||
}
|
||||
refreshHomeData()
|
||||
runSideEffects()
|
||||
// Updated unconditionally on completion. refreshHomeData() catches
|
||||
// non-cancellation exceptions internally, so a failed refresh still
|
||||
// counts as "we just touched the server" and the next onResumed
|
||||
// within RESUME_DEBOUNCE_MS will be skipped. Pull-to-refresh always
|
||||
// bypasses this gate via onRefresh.
|
||||
lastRefreshAtMs.store(SystemClock.elapsedRealtime())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
suspend fun onRefresh(setRefreshing: (Boolean) -> Unit) {
|
||||
refreshHomeData(setRefreshing)
|
||||
runSideEffects()
|
||||
lastRefreshAtMs.store(SystemClock.elapsedRealtime())
|
||||
}
|
||||
|
||||
private suspend fun refreshHomeData(
|
||||
@@ -54,4 +83,11 @@ class HomeRefreshCoordinator @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "HomeRefreshCoordinator"
|
||||
// Coalesce rapid resume/tab-switch events into a single refresh.
|
||||
// Pull-to-refresh bypasses this gate via onRefresh.
|
||||
const val RESUME_DEBOUNCE_MS = 30_000L
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user