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.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -26,13 +25,11 @@ import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import org.matrix.android.sdk.api.extensions.tryOrNull 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.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings 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.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler 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, internal class DefaultTimeline internal constructor(private val roomId: String,
private val initialEventId: String?, private val initialEventId: String?,
private val settings: TimelineSettings,
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler, private val readReceiptHandler: ReadReceiptHandler,
settings: TimelineSettings,
paginationTask: PaginationTask, paginationTask: PaginationTask,
getEventTask: GetContextOfEventTask, getEventTask: GetContextOfEventTask,
fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
timelineEventMapper: TimelineEventMapper, timelineEventMapper: TimelineEventMapper,
timelineInput: TimelineInput, timelineInput: TimelineInput,
private val threadsAwarenessHandler: ThreadsAwarenessHandler, threadsAwarenessHandler: ThreadsAwarenessHandler,
eventDecryptor: TimelineEventDecryptor) : Timeline { eventDecryptor: TimelineEventDecryptor) : Timeline {
companion object { companion object {
@ -76,6 +73,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
private val sequencer = SemaphoreCoroutineSequencer() private val sequencer = SemaphoreCoroutineSequencer()
private val strategyDependencies = LoadTimelineStrategy.Dependencies( private val strategyDependencies = LoadTimelineStrategy.Dependencies(
timelineScope = timelineScope,
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
timelineSettings = settings, timelineSettings = settings,
paginationTask = paginationTask, paginationTask = paginationTask,
@ -139,6 +137,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
override fun restartWithEventId(eventId: String?) { override fun restartWithEventId(eventId: String?) {
timelineScope.launch { timelineScope.launch {
openAround(eventId) openAround(eventId)
postSnapshot()
} }
} }
@ -165,7 +164,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
}.get() }.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)" val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId)"
Timber.v("$baseLogMessage started") Timber.v("$baseLogMessage started")
if (!isStarted.get()) { if (!isStarted.get()) {
@ -174,21 +173,21 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
val currentState = getPaginationState(direction) val currentState = getPaginationState(direction)
if (!currentState.hasMoreToLoad) { if (!currentState.hasMoreToLoad) {
Timber.v("$baseLogMessage : nothing more to load") Timber.v("$baseLogMessage : nothing more to load")
return@withContext return
} }
if (currentState.loading) { if (currentState.loading) {
Timber.v("$baseLogMessage : already loading") Timber.v("$baseLogMessage : already loading")
return@withContext return
} }
updateState(direction) { updateState(direction) {
it.copy(loading = true) it.copy(loading = true)
} }
val loadMoreResult = strategy.loadMore(count, direction) val loadMoreResult = strategy.loadMore(count, direction)
Timber.v("$baseLogMessage: result $loadMoreResult")
val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END
updateState(direction) { updateState(direction) {
it.copy(loading = false, hasMoreToLoad = hasMoreToLoad) it.copy(loading = false, hasMoreToLoad = hasMoreToLoad)
} }
postSnapshot()
} }
private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) { 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) it.copy(loading = false, hasMoreToLoad = true)
} }
strategy.onStart() strategy.onStart()
postSnapshot()
} }
private fun postSnapshot() { private fun postSnapshot() {
timelineScope.launch { timelineScope.launch {
val snapshot = strategy.buildSnapshot() val snapshot = strategy.buildSnapshot()
Timber.v("Post snapshot of ${snapshot.size} items")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
listeners.forEach { listeners.forEach {
tryOrNull { it.onTimelineUpdated(snapshot) } tryOrNull { it.onTimelineUpdated(snapshot) }
@ -242,7 +241,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String,
stateReference.set(newValue) stateReference.set(newValue)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
listeners.forEach { 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.OrderedRealmCollectionChangeListener
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.extensions.orFalse 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.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -62,6 +63,7 @@ internal class LoadTimelineStrategy(
data class Dependencies( data class Dependencies(
val timelineSettings: TimelineSettings, val timelineSettings: TimelineSettings,
val timelineScope: CoroutineScope,
val realm: AtomicReference<Realm>, val realm: AtomicReference<Realm>,
val eventDecryptor: TimelineEventDecryptor, val eventDecryptor: TimelineEventDecryptor,
val paginationTask: PaginationTask, val paginationTask: PaginationTask,
@ -214,7 +216,8 @@ internal class LoadTimelineStrategy(
uiEchoManager = uiEchoManager, uiEchoManager = uiEchoManager,
threadsAwarenessHandler = dependencies.threadsAwarenessHandler, threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
initialEventId = mode.originEventId(), 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.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort 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.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull 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.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. * 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, internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
private val timelineScope: CoroutineScope,
private val timelineSettings: TimelineSettings, private val timelineSettings: TimelineSettings,
private val roomId: String, private val roomId: String,
private val timelineId: 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 isLastForward = AtomicBoolean(chunkEntity.isLastForward)
private val chunkObjectListener = RealmObjectChangeListener<ChunkEntity> { _, changeSet -> private val chunkObjectListener = RealmObjectChangeListener<ChunkEntity> { _, changeSet ->
if(changeSet?.isDeleted.orFalse()){ if (changeSet == null) return@RealmObjectChangeListener
if (changeSet.isDeleted.orFalse()) {
return@RealmObjectChangeListener return@RealmObjectChangeListener
} }
Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet?.changedFields?.joinToString(",")}") Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet.changedFields?.joinToString(",")}")
if(changeSet?.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD).orFalse()){ if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD)) {
isLastForward.set(chunkEntity.isLastForward) 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 -> private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet ->
val frozenResults = results.freeze() val frozenResults = results.freeze()
Timber.v("on timeline event changed: $changeSet")
handleDatabaseChangeSet(frozenResults, 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 { suspend fun loadMore(count: Long, direction: Timeline.Direction): LoadMoreResult {
val loadFromDbCount = loadFromDb(count, direction) val loadFromDbCount = loadFromDb(count, direction)
Timber.v("Has loaded $loadFromDbCount items from db")
val offsetCount = count - loadFromDbCount val offsetCount = count - loadFromDbCount
// We have built the right amount of data // We have built the right amount of data
if (offsetCount == 0L) { return if (offsetCount == 0L) {
onBuiltEvents() LoadMoreResult.SUCCESS
return LoadMoreResult.SUCCESS } else {
delegateLoadMore(offsetCount, direction)
} }
}
private suspend fun delegateLoadMore(offsetCount: Long, direction: Timeline.Direction): LoadMoreResult {
return if (direction == Timeline.Direction.FORWARDS) { return if (direction == Timeline.Direction.FORWARDS) {
val nextChunkEntity = chunkEntity.nextChunk val nextChunkEntity = chunkEntity.nextChunk
if (nextChunkEntity == null) { if (nextChunkEntity == null) {
// Fetch next chunk from server if not in the db // Fetch next chunk from server if not in the db
val token = chunkEntity.nextToken fetchFromServer(chunkEntity.nextToken, direction)
fetchFromServer(token, direction)
} else { } else {
// otherwise we delegate to the next chunk // otherwise we delegate to the next chunk
if (nextChunk == null) { if (nextChunk == null) {
@ -139,8 +157,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
val prevChunkEntity = chunkEntity.prevChunk val prevChunkEntity = chunkEntity.prevChunk
if (prevChunkEntity == null) { if (prevChunkEntity == null) {
// Fetch prev chunk from server if not in the db // Fetch prev chunk from server if not in the db
val token = chunkEntity.prevToken fetchFromServer(chunkEntity.prevToken, direction)
fetchFromServer(token, direction)
} else { } else {
// otherwise we delegate to the prev chunk // otherwise we delegate to the prev chunk
if (prevChunk == null) { if (prevChunk == null) {
@ -227,6 +244,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
timelineEventEntities.removeChangeListener(timelineEventCollectionListener) timelineEventEntities.removeChangeListener(timelineEventCollectionListener)
} }
/**
* This method tries to read events from the current chunk.
*/
private suspend fun loadFromDb(count: Long, direction: Timeline.Direction): Long { private suspend fun loadFromDb(count: Long, direction: Timeline.Direction): Long {
val displayIndex = getNextDisplayIndex(direction) ?: return 0 val displayIndex = getNextDisplayIndex(direction) ?: return 0
val baseQuery = timelineEventEntities.where() val baseQuery = timelineEventEntities.where()
@ -247,6 +267,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
builtEvents.add(timelineEvent) builtEvents.add(timelineEvent)
} }
} }
onBuiltEvents()
return timelineEvents.size.toLong() return timelineEvents.size.toLong()
} }
@ -264,7 +285,6 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
} }
private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent { private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent {
val timelineEvent = buildTimelineEvent(this) val timelineEvent = buildTimelineEvent(this)
val transactionId = timelineEvent.root.unsignedData?.transactionId val transactionId = timelineEvent.root.unsignedData?.transactionId
@ -284,23 +304,11 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
(uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
} }
private fun createTimelineChunk(chunkEntity: ChunkEntity): TimelineChunk { /**
return TimelineChunk( * Will try to fetch a new chunk on the home server.
chunkEntity = chunkEntity, * It will take care to update the database by inserting new events and linking new chunk
timelineSettings = timelineSettings, * with this one.
timelineId = timelineId, */
eventDecryptor = eventDecryptor,
roomId = roomId,
paginationTask = paginationTask,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
timelineEventMapper = timelineEventMapper,
uiEchoManager = uiEchoManager,
threadsAwarenessHandler = threadsAwarenessHandler,
initialEventId = null,
onBuiltEvents = onBuiltEvents
)
}
private suspend fun fetchFromServer(token: String?, direction: Timeline.Direction): LoadMoreResult { private suspend fun fetchFromServer(token: String?, direction: Timeline.Direction): LoadMoreResult {
val paginationResult = try { val paginationResult = try {
if (token == null) { 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) val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), PAGINATION_COUNT)
fetchTokenAndPaginateTask.execute(taskParams) fetchTokenAndPaginateTask.execute(taskParams)
} else { } else {
Timber.v("Fetch more events on server")
val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT) val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT)
paginationTask.execute(taskParams) paginationTask.execute(taskParams)
} }
@ -337,6 +346,10 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
return offset return offset
} }
/**
* This method is responsible for managing insertions and updates of events on this chunk.
*
*/
private fun handleDatabaseChangeSet(frozenResults: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) { private fun handleDatabaseChangeSet(frozenResults: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
val insertions = changeSet.insertionRanges val insertions = changeSet.insertionRanges
for (range in insertions) { for (range in insertions) {
@ -385,6 +398,25 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity,
builtEvents.last().displayIndex - 1 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( 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) { override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) {
if(!state.hasMoreToLoad) { if (!state.hasMoreToLoad) {
backgroundHandler.post { backgroundHandler.post {
requestModelBuild() requestDelayedModelBuild(0)
} }
} }
} }
@ -324,7 +324,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
currentSnapshot = newSnapshot currentSnapshot = newSnapshot
val diffResult = DiffUtil.calculateDiff(diffCallback) val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(listUpdateCallback) diffResult.dispatchUpdatesTo(listUpdateCallback)
requestModelBuild() requestDelayedModelBuild(0)
inSubmitList = false inSubmitList = false
} }
} }