Skip to content

Commit

Permalink
feat: add query profiling and cached paginated list [WPB-14826]
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk committed Dec 10, 2024
1 parent 8d9b34a commit cdeccfd
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 3 deletions.
8 changes: 8 additions & 0 deletions app/src/main/kotlin/com/wire/android/WireApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import co.touchlab.kermit.platformLogWriter
import com.wire.android.analytics.ObserveCurrentSessionAnalyticsUseCase
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.debug.DatabaseProfilingManager
import com.wire.android.di.ApplicationScope
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl
Expand Down Expand Up @@ -89,6 +90,9 @@ class WireApplication : BaseApp() {
@Inject
lateinit var currentScreenManager: CurrentScreenManager

@Inject
lateinit var databaseProfilingManager: DatabaseProfilingManager

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(wireWorkerFactory.get())
Expand Down Expand Up @@ -183,6 +187,10 @@ class WireApplication : BaseApp() {
logDeviceInformation()
// 5. Verify if we can initialize Anonymous Analytics
initializeAnonymousAnalytics()
// 6. Observe and update profiling when needed
globalAppScope.launch {
databaseProfilingManager.observeAndUpdateProfiling()
}
}

private fun initializeAnonymousAnalytics() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.debug

import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.KaliumCoreLogic
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.functional.mapToRightOr
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.scan
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class DatabaseProfilingManager @Inject constructor(
@KaliumCoreLogic private val coreLogic: CoreLogic,
private val globalDataStore: GlobalDataStore,
) {

suspend fun observeAndUpdateProfiling() {
globalDataStore.isLoggingEnabled()
.flatMapLatest { isLoggingEnabled ->
coreLogic.getGlobalScope().sessionRepository.allValidSessionsFlow()
.mapToRightOr(emptyList())
.map { it.map { it.userId } }
.scan(emptyList<UserId>()) { previousList, currentList -> currentList - previousList.toSet() }
.map { userIds -> isLoggingEnabled to userIds }
}
.filter { (_, userIds) -> userIds.isNotEmpty() }
.distinctUntilChanged()
.collect { (isLoggingEnabled, userIds) ->
userIds.forEach { userId ->
coreLogic.getSessionScope(userId).debug.changeProfiling(isLoggingEnabled)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.insertSeparators
import androidx.paging.map
import com.wire.android.BuildConfig
Expand Down Expand Up @@ -210,10 +211,11 @@ class ConversationListViewModelImpl @AssistedInject constructor(
}
}
.flowOn(dispatcher.io())
.cachedIn(viewModelScope)

private var notPaginatedConversationListState by mutableStateOf(ConversationListState.NotPaginated())
override val conversationListState: ConversationListState
get() = if (usePagination) {
override val conversationListState: ConversationListState =
if (usePagination) {
ConversationListState.Paginated(
conversations = conversationsPaginatedFlow,
domain = currentAccount.domain
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.debug

import com.wire.android.config.CoroutineTestExtension
import com.wire.android.datastore.GlobalDataStore
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.auth.AccountInfo
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.functional.Either
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CoroutineTestExtension::class)
class DatabaseProfilingManagerTest {

@Test
fun `given valid session and logging enabled, when observing, then profiling should be enabled`() =
runTest {
// given
val account = AccountInfo.Valid(UserId("user", "domain"))
val (arrangement, databaseProfilingManager) = Arrangement()
.withAllValidSessions(flowOf(Either.Right(listOf(account))))
.withIsLoggingEnabled(flowOf(true))
.arrange()

// when
val job = launch {
databaseProfilingManager.observeAndUpdateProfiling()
}
advanceUntilIdle()
// then
assertEquals(true, arrangement.profilingValues[account.userId])
job.cancel()
}

@Test
fun `given valid session and logging disabled, when observing, then profiling is disabled`() =
runTest {
// given
val account = AccountInfo.Valid(UserId("user", "domain"))
val (arrangement, databaseProfilingManager) = Arrangement()
.withAllValidSessions(flowOf(Either.Right(listOf(account))))
.withIsLoggingEnabled(flowOf(false))
.arrange()
// when
val job = launch {
databaseProfilingManager.observeAndUpdateProfiling()
}
advanceUntilIdle()
// then
assertEquals(false, arrangement.profilingValues[account.userId])
job.cancel()
}

@Test
fun `given valid session, when observing and logging changes from disabled to enabled, then profiling is enabled`() =
runTest {
// given
val account = AccountInfo.Valid(UserId("user", "domain"))
val (arrangement, databaseProfilingManager) = Arrangement()
.withAllValidSessions(flowOf(Either.Right(listOf(account))))
.withIsLoggingEnabled(flowOf(false))
.arrange()
// when
val job = launch {
databaseProfilingManager.observeAndUpdateProfiling()
}
arrangement.withIsLoggingEnabled(flowOf(true))
advanceUntilIdle()
// then
assertEquals(true, arrangement.profilingValues[account.userId])
job.cancel()
}

@Test
fun `given two valid sessions, when observing and logging changes from disabled to enabled, then profiling is enabled for both`() =
runTest {
// given
val account1 = AccountInfo.Valid(UserId("user1", "domain"))
val account2 = AccountInfo.Valid(UserId("user2", "domain"))
val (arrangement, databaseProfilingManager) = Arrangement()
.withAllValidSessions(flowOf(Either.Right(listOf(account1, account2))))
.withIsLoggingEnabled(flowOf(false))
.arrange()
// when
val job = launch {
databaseProfilingManager.observeAndUpdateProfiling()
}
arrangement.withIsLoggingEnabled(flowOf(true))
advanceUntilIdle()
// then
assertEquals(true, arrangement.profilingValues[account1.userId])
assertEquals(true, arrangement.profilingValues[account2.userId])
job.cancel()
}

@Test
fun `given valid session and logging enabled, when observing and new session appears, then profiling is enabled for both`() =
runTest {
// given
val account1 = AccountInfo.Valid(UserId("user1", "domain"))
val account2 = AccountInfo.Valid(UserId("user2", "domain"))
val validSessionsFlow = MutableStateFlow(Either.Right(listOf(account1)))
val (arrangement, databaseProfilingManager) = Arrangement()
.withAllValidSessions(validSessionsFlow)
.withIsLoggingEnabled(flowOf(true))
.arrange()
// when
val job = launch {
databaseProfilingManager.observeAndUpdateProfiling()
}
validSessionsFlow.value = Either.Right(listOf(account1, account2))
advanceUntilIdle()
// then
assertEquals(true, arrangement.profilingValues[account1.userId])
assertEquals(true, arrangement.profilingValues[account2.userId])
job.cancel()
}

private class Arrangement {

@MockK
lateinit var coreLogic: CoreLogic

@MockK
private lateinit var globalDataStore: GlobalDataStore

var profilingValues: PersistentMap<UserId, Boolean> = persistentMapOf()
private set

init {
MockKAnnotations.init(this, relaxed = true, relaxUnitFun = true)
coEvery { coreLogic.getSessionScope(any()).debug.changeProfiling(any()) } answers {
profilingValues = profilingValues.put(firstArg(), secondArg())
}
coEvery { coreLogic.getSessionScope(any()) } answers {
val userId = firstArg<UserId>()
mockk() {
coEvery { debug.changeProfiling(any()) } answers {
val profilingValue = firstArg<Boolean>()
profilingValues = profilingValues.put(userId, profilingValue)
}
}
}
}

fun withIsLoggingEnabled(isLoggingEnabledFlow: Flow<Boolean>) = apply {
coEvery { globalDataStore.isLoggingEnabled() } returns isLoggingEnabledFlow
}

fun withAllValidSessions(allValidSessionsFlow: Flow<Either<StorageFailure, List<AccountInfo>>>) = apply {
coEvery { coreLogic.getGlobalScope().sessionRepository.allValidSessionsFlow() } returns allValidSessionsFlow
}

fun arrange() = this to DatabaseProfilingManager(coreLogic, globalDataStore)
}
}

0 comments on commit cdeccfd

Please sign in to comment.