mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Timeline: try to optimise a bit the loading
This commit is contained in:
parent
76eddef840
commit
014da84ba6
4 changed files with 79 additions and 45 deletions
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue