Timeline: try to optimise a bit the loading

This commit is contained in:
ganfra 2021-12-03 12:14:35 +01:00
parent 76eddef840
commit 014da84ba6
4 changed files with 79 additions and 45 deletions

View file

@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -26,13 +25,11 @@ import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.internal.closeQuietly
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
@ -47,16 +44,16 @@ import java.util.concurrent.atomic.AtomicReference
internal class DefaultTimeline internal constructor(private val roomId: String,
private val initialEventId: String?,
private val settings: TimelineSettings,
private val realmConfiguration: RealmConfiguration,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler,
settings: TimelineSettings,
paginationTask: PaginationTask,
getEventTask: GetContextOfEventTask,
fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
timelineEventMapper: TimelineEventMapper,
timelineInput: TimelineInput,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
threadsAwarenessHandler: ThreadsAwarenessHandler,
eventDecryptor: TimelineEventDecryptor) : Timeline {
companion object {
@ -76,6 +73,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
private val sequencer = SemaphoreCoroutineSequencer()
private val strategyDependencies = LoadTimelineStrategy.Dependencies(
timelineScope = timelineScope,
eventDecryptor = eventDecryptor,
timelineSettings = settings,
paginationTask = paginationTask,
@ -139,6 +137,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
override fun restartWithEventId(eventId: String?) {
timelineScope.launch {
openAround(eventId)
postSnapshot()
}
}
@ -165,7 +164,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
}.get()
}
private suspend fun loadMore(count: Long, direction: Timeline.Direction) = withContext(timelineDispatcher) {
private suspend fun loadMore(count: Long, direction: Timeline.Direction) {
val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId)"
Timber.v("$baseLogMessage started")
if (!isStarted.get()) {
@ -174,21 +173,21 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
val currentState = getPaginationState(direction)
if (!currentState.hasMoreToLoad) {
Timber.v("$baseLogMessage : nothing more to load")
return@withContext
return
}
if (currentState.loading) {
Timber.v("$baseLogMessage : already loading")
return@withContext
return
}
updateState(direction) {
it.copy(loading = true)
}
val loadMoreResult = strategy.loadMore(count, direction)
Timber.v("$baseLogMessage: result $loadMoreResult")
val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END
updateState(direction) {
it.copy(loading = false, hasMoreToLoad = hasMoreToLoad)
}
postSnapshot()
}
private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) {
@ -210,12 +209,12 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
it.copy(loading = false, hasMoreToLoad = true)
}
strategy.onStart()
postSnapshot()
}
private fun postSnapshot() {
timelineScope.launch {
val snapshot = strategy.buildSnapshot()
Timber.v("Post snapshot of ${snapshot.size} items")
withContext(Dispatchers.Main) {
listeners.forEach {
tryOrNull { it.onTimelineUpdated(snapshot) }
@ -242,7 +241,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
stateReference.set(newValue)
withContext(Dispatchers.Main) {
listeners.forEach {
tryOrNull { it.onStateUpdated() }
tryOrNull { it.onStateUpdated(direction, newValue) }
}
}
}

View file

@ -20,6 +20,7 @@ import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -62,6 +63,7 @@ internal class LoadTimelineStrategy(
data class Dependencies(
val timelineSettings: TimelineSettings,
val timelineScope: CoroutineScope,
val realm: AtomicReference<Realm>,
val eventDecryptor: TimelineEventDecryptor,
val paginationTask: PaginationTask,
@ -214,7 +216,8 @@ internal class LoadTimelineStrategy(
uiEchoManager = uiEchoManager,
threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
initialEventId = mode.originEventId(),
onBuiltEvents = dependencies.onEventsUpdated
onBuiltEvents = dependencies.onEventsUpdated,
timelineScope = dependencies.timelineScope
)
}
}

View file

@ -22,7 +22,8 @@ import io.realm.RealmObjectChangeListener
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -50,6 +51,7 @@ private const val PAGINATION_COUNT = 50
* It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any.
*/
internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
private val timelineScope: CoroutineScope,
private val timelineSettings: TimelineSettings,
private val roomId: String,
private val timelineId: String,
@ -65,18 +67,30 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
private val chunkObjectListener = RealmObjectChangeListener<ChunkEntity> { _, changeSet ->
if(changeSet?.isDeleted.orFalse()){
if (changeSet == null) return@RealmObjectChangeListener
if (changeSet.isDeleted.orFalse()) {
return@RealmObjectChangeListener
}
Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet?.changedFields?.joinToString(",")}")
if(changeSet?.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD).orFalse()){
Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet.changedFields?.joinToString(",")}")
if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD)) {
isLastForward.set(chunkEntity.isLastForward)
}
if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) {
nextChunk = createTimelineChunk(chunkEntity.nextChunk)
timelineScope.launch {
nextChunk?.loadMore(PAGINATION_COUNT.toLong(), Timeline.Direction.FORWARDS)
}
}
if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) {
prevChunk = createTimelineChunk(chunkEntity.prevChunk)
timelineScope.launch {
prevChunk?.loadMore(PAGINATION_COUNT.toLong(), Timeline.Direction.BACKWARDS)
}
}
}
private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet ->
val frozenResults = results.freeze()
Timber.v("on timeline event changed: $changeSet")
handleDatabaseChangeSet(frozenResults, changeSet)
}
@ -116,18 +130,22 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
suspend fun loadMore(count: Long, direction: Timeline.Direction): LoadMoreResult {
val loadFromDbCount = loadFromDb(count, direction)
Timber.v("Has loaded $loadFromDbCount items from db")
val offsetCount = count - loadFromDbCount
// We have built the right amount of data
if (offsetCount == 0L) {
onBuiltEvents()
return LoadMoreResult.SUCCESS
return if (offsetCount == 0L) {
LoadMoreResult.SUCCESS
} else {
delegateLoadMore(offsetCount, direction)
}
}
private suspend fun delegateLoadMore(offsetCount: Long, direction: Timeline.Direction): LoadMoreResult {
return if (direction == Timeline.Direction.FORWARDS) {
val nextChunkEntity = chunkEntity.nextChunk
if (nextChunkEntity == null) {
// Fetch next chunk from server if not in the db
val token = chunkEntity.nextToken
fetchFromServer(token, direction)
fetchFromServer(chunkEntity.nextToken, direction)
} else {
// otherwise we delegate to the next chunk
if (nextChunk == null) {
@ -139,8 +157,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
val prevChunkEntity = chunkEntity.prevChunk
if (prevChunkEntity == null) {
// Fetch prev chunk from server if not in the db
val token = chunkEntity.prevToken
fetchFromServer(token, direction)
fetchFromServer(chunkEntity.prevToken, direction)
} else {
// otherwise we delegate to the prev chunk
if (prevChunk == null) {
@ -227,6 +244,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
timelineEventEntities.removeChangeListener(timelineEventCollectionListener)
}
/**
* This method tries to read events from the current chunk.
*/
private suspend fun loadFromDb(count: Long, direction: Timeline.Direction): Long {
val displayIndex = getNextDisplayIndex(direction) ?: return 0
val baseQuery = timelineEventEntities.where()
@ -247,6 +267,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
builtEvents.add(timelineEvent)
}
}
onBuiltEvents()
return timelineEvents.size.toLong()
}
@ -264,7 +285,6 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
}
private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent {
val timelineEvent = buildTimelineEvent(this)
val transactionId = timelineEvent.root.unsignedData?.transactionId
@ -284,23 +304,11 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
(uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
}
private fun createTimelineChunk(chunkEntity: ChunkEntity): TimelineChunk {
return TimelineChunk(
chunkEntity = chunkEntity,
timelineSettings = timelineSettings,
timelineId = timelineId,
eventDecryptor = eventDecryptor,
roomId = roomId,
paginationTask = paginationTask,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
timelineEventMapper = timelineEventMapper,
uiEchoManager = uiEchoManager,
threadsAwarenessHandler = threadsAwarenessHandler,
initialEventId = null,
onBuiltEvents = onBuiltEvents
)
}
/**
* Will try to fetch a new chunk on the home server.
* It will take care to update the database by inserting new events and linking new chunk
* with this one.
*/
private suspend fun fetchFromServer(token: String?, direction: Timeline.Direction): LoadMoreResult {
val paginationResult = try {
if (token == null) {
@ -309,6 +317,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), PAGINATION_COUNT)
fetchTokenAndPaginateTask.execute(taskParams)
} else {
Timber.v("Fetch more events on server")
val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT)
paginationTask.execute(taskParams)
}
@ -337,6 +346,10 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
return offset
}
/**
* This method is responsible for managing insertions and updates of events on this chunk.
*
*/
private fun handleDatabaseChangeSet(frozenResults: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
val insertions = changeSet.insertionRanges
for (range in insertions) {
@ -385,6 +398,25 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
builtEvents.last().displayIndex - 1
}
}
private fun createTimelineChunk(chunkEntity: ChunkEntity?): TimelineChunk? {
if (chunkEntity == null) return null
return TimelineChunk(
chunkEntity = chunkEntity,
timelineScope = timelineScope,
timelineSettings = timelineSettings,
timelineId = timelineId,
eventDecryptor = eventDecryptor,
roomId = roomId,
paginationTask = paginationTask,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
timelineEventMapper = timelineEventMapper,
uiEchoManager = uiEchoManager,
threadsAwarenessHandler = threadsAwarenessHandler,
initialEventId = null,
onBuiltEvents = this.onBuiltEvents
)
}
}
private fun RealmQuery<TimelineEventEntity>.offsets(

View file

@ -310,9 +310,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) {
if(!state.hasMoreToLoad) {
if (!state.hasMoreToLoad) {
backgroundHandler.post {
requestModelBuild()
requestDelayedModelBuild(0)
}
}
}
@ -324,7 +324,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
currentSnapshot = newSnapshot
val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(listUpdateCallback)
requestModelBuild()
requestDelayedModelBuild(0)
inSubmitList = false
}
}