There are very visible perf issues during app usage and scrolling that clearly indicates that the app is doing too much on the main thread. After digging for instances where Dispatchers.Main is used, it's an easy fix to switch to Dispatchers.IO, which visibility improve perf.
A few thoughts about perf considerations 1. There is no need to force Dispatchers.Main for data that is consumed as state by compose, since flows consumed as state will always flow on main, so we can use a background thread to guarantee best performance. 2. Using Dispatchers.IO is appropriate for disk/network operations to have a device-constrained thread pool that will avoid draining IO-related device resources. Using Dispatchers.Default is more appropriate for computational tasks (bitmap manipulation, delays, etc..) 3. There are a few instances of methods creating coroutine scopes in their body just to launch something that will delay. This is creating a lot of loose scopes, and you can avoid this by just moving scope creation to a class-level field and reusing it, or better yet, make your method suspending so that scope is controlled by the caller.
This commit is contained in:
@@ -13,6 +13,9 @@ import nostr.postr.events.Event
|
|||||||
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
|
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
|
||||||
*/
|
*/
|
||||||
object RelayPool: Relay.Listener {
|
object RelayPool: Relay.Listener {
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
private var relays = listOf<Relay>()
|
private var relays = listOf<Relay>()
|
||||||
private var listeners = setOf<Listener>()
|
private var listeners = setOf<Listener>()
|
||||||
|
|
||||||
@@ -116,7 +119,6 @@ object RelayPool: Relay.Listener {
|
|||||||
val live: RelayPoolLiveData = RelayPoolLiveData(this)
|
val live: RelayPoolLiveData = RelayPoolLiveData(this)
|
||||||
|
|
||||||
private fun refreshObservers() {
|
private fun refreshObservers() {
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
live.refresh()
|
live.refresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
|
class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
|
||||||
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
||||||
@@ -29,11 +30,8 @@ class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
|
|||||||
|
|
||||||
private var lastNotes: List<Note>? = null
|
private var lastNotes: List<Note>? = null
|
||||||
|
|
||||||
fun refresh() {
|
suspend fun refresh() = withContext(Dispatchers.IO) {
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
refreshSuspended()
|
||||||
scope.launch {
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshSuspended() {
|
private fun refreshSuspended() {
|
||||||
@@ -83,19 +81,16 @@ class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
|
|||||||
return (reactionCards + boostCards + textNoteCards).sortedBy { it.createdAt() }.reversed()
|
return (reactionCards + boostCards + textNoteCards).sortedBy { it.createdAt() }.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFeed(notes: List<Card>) {
|
private fun updateFeed(notes: List<Card>) {
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
val currentState = feedContent.value
|
||||||
scope.launch {
|
|
||||||
val currentState = feedContent.value
|
|
||||||
|
|
||||||
if (notes.isEmpty()) {
|
if (notes.isEmpty()) {
|
||||||
_feedContent.update { CardFeedState.Empty }
|
_feedContent.update { CardFeedState.Empty }
|
||||||
} else if (currentState is CardFeedState.Loaded) {
|
} else if (currentState is CardFeedState.Loaded) {
|
||||||
// updates the current list
|
// updates the current list
|
||||||
currentState.feed.value = notes
|
currentState.feed.value = notes
|
||||||
} else {
|
} else {
|
||||||
_feedContent.update { CardFeedState.Loaded(mutableStateOf(notes)) }
|
_feedContent.update { CardFeedState.Loaded(mutableStateOf(notes)) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class NostrChannelFeedViewModel: FeedViewModel(NostrChannelDataSource)
|
class NostrChannelFeedViewModel: FeedViewModel(NostrChannelDataSource)
|
||||||
class NostrChatRoomFeedViewModel: FeedViewModel(NostrChatRoomDataSource)
|
class NostrChatRoomFeedViewModel: FeedViewModel(NostrChatRoomDataSource)
|
||||||
@@ -86,25 +87,21 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val notes = newListFromDataSource()
|
val notes = newListFromDataSource()
|
||||||
|
|
||||||
val oldNotesState = feedContent.value
|
val oldNotesState = feedContent.value
|
||||||
if (oldNotesState is FeedState.Loaded) {
|
if (oldNotesState is FeedState.Loaded) {
|
||||||
if (notes != oldNotesState.feed) {
|
if (notes != oldNotesState.feed) {
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
updateFeed(notes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
updateFeed(notes)
|
updateFeed(notes)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
updateFeed(notes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFeed(notes: List<Note>) {
|
private fun updateFeed(notes: List<Note>) {
|
||||||
val currentState = feedContent.value
|
val currentState = feedContent.value
|
||||||
|
|
||||||
if (notes.isEmpty()) {
|
if (notes.isEmpty()) {
|
||||||
@@ -117,19 +114,18 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlerWaiting = false
|
private var handlerWaiting = AtomicBoolean()
|
||||||
fun invalidateData() {
|
@Synchronized
|
||||||
synchronized(handlerWaiting) {
|
private fun invalidateData() {
|
||||||
if (handlerWaiting) return
|
if (handlerWaiting.get()) return
|
||||||
|
|
||||||
handlerWaiting = true
|
handlerWaiting.set(true)
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(100)
|
delay(100)
|
||||||
refresh()
|
refresh()
|
||||||
handlerWaiting = false
|
handlerWaiting.set(false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (LocalCacheState) -> Unit = {
|
private val cacheListener: (LocalCacheState) -> Unit = {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class NostrUserProfileFollowsUserFeedViewModel(): UserFeedViewModel(
|
class NostrUserProfileFollowsUserFeedViewModel(): UserFeedViewModel(
|
||||||
NostrUserProfileFollowsDataSource
|
NostrUserProfileFollowsDataSource
|
||||||
@@ -39,13 +41,11 @@ open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel()
|
|||||||
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
|
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
|
||||||
val feedContent = _feedContent.asStateFlow()
|
val feedContent = _feedContent.asStateFlow()
|
||||||
|
|
||||||
fun refresh() {
|
suspend fun refresh() = withContext(Dispatchers.IO) {
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
refreshSuspended()
|
||||||
scope.launch {
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun refreshSuspended() {
|
private fun refreshSuspended() {
|
||||||
val notes = dataSource.loadTop()
|
val notes = dataSource.loadTop()
|
||||||
|
|
||||||
@@ -59,34 +59,30 @@ open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFeed(notes: List<User>) {
|
private fun updateFeed(notes: List<User>) {
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
val currentState = feedContent.value
|
||||||
scope.launch {
|
if (notes.isEmpty()) {
|
||||||
val currentState = feedContent.value
|
_feedContent.update { UserFeedState.Empty }
|
||||||
|
} else if (currentState is UserFeedState.Loaded) {
|
||||||
if (notes.isEmpty()) {
|
// updates the current list
|
||||||
_feedContent.update { UserFeedState.Empty }
|
currentState.feed.value = notes
|
||||||
} else if (currentState is UserFeedState.Loaded) {
|
} else {
|
||||||
// updates the current list
|
_feedContent.update { UserFeedState.Loaded(mutableStateOf(notes)) }
|
||||||
currentState.feed.value = notes
|
|
||||||
} else {
|
|
||||||
_feedContent.update { UserFeedState.Loaded(mutableStateOf(notes)) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlerWaiting = false
|
var handlerWaiting = AtomicBoolean()
|
||||||
fun invalidateData() {
|
|
||||||
synchronized(handlerWaiting) {
|
|
||||||
if (handlerWaiting) return
|
|
||||||
|
|
||||||
handlerWaiting = true
|
@Synchronized
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
private fun invalidateData() {
|
||||||
scope.launch {
|
if (handlerWaiting.get()) return
|
||||||
delay(100)
|
|
||||||
refresh()
|
handlerWaiting.set(true)
|
||||||
handlerWaiting = false
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
}
|
scope.launch {
|
||||||
|
delay(100)
|
||||||
|
refresh()
|
||||||
|
handlerWaiting.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user