Add another performance check for cold start time until the cached room list is displayed

This commit is contained in:
Jorge Martín 2025-11-20 10:08:53 +01:00 committed by Jorge Martin Espinosa
parent e1bd189ba0
commit daf7bea39e
7 changed files with 91 additions and 0 deletions

View file

@ -24,6 +24,7 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.analytics.AnalyticsColdStartWatcher
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.architecture.BackstackView
@ -43,6 +44,7 @@ class NotLoggedInFlowNode(
@Assisted plugins: List<Plugin>,
private val loginEntryPoint: LoginEntryPoint,
private val imageLoaderHolder: ImageLoaderHolder,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
@ -65,6 +67,7 @@ class NotLoggedInFlowNode(
override fun onBuilt() {
super.onBuilt()
analyticsColdStartWatcher.whenLoggingIn()
lifecycle.subscribe(
onResume = {
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())

View file

@ -29,6 +29,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.analytics.AnalyticsColdStartWatcher
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent
@ -89,6 +90,7 @@ class RootFlowNode(
private val featureFlagService: FeatureFlagService,
private val announcementService: AnnouncementService,
private val analyticsService: AnalyticsService,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
) : BaseFlowNode<RootFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.SplashScreen,
@ -98,6 +100,7 @@ class RootFlowNode(
plugins = plugins
) {
override fun onBuilt() {
analyticsColdStartWatcher.start()
matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
super.onBuilt()
observeNavState()

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.analytics
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
/**
* Adds a performance check transaction measuring the time between a cold start (or, after we read the user consent after a cold start)
* until the cached room list is displayed. This check only takes place in a cold app start after the user is authenticated.
*/
interface AnalyticsColdStartWatcher {
fun start()
fun whenLoggingIn()
fun onRoomListVisible()
}
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultAnalyticsColdStartWatcher(
private val analyticsService: AnalyticsService,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : AnalyticsColdStartWatcher {
private val isColdStart = AtomicBoolean(true)
override fun start() {
analyticsService.userConsentFlow
.onEach { hasConsent ->
if (hasConsent) {
if (isColdStart.get()) {
Timber.d("Starting cold start check")
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStartUntilCachedRoomList)
} else {
error("The app is no longer in a cold start state")
}
}
}
.catch { Timber.w(it.message) }
.launchIn(appCoroutineScope)
}
override fun whenLoggingIn() {
if (isColdStart.getAndSet(false)) {
Timber.d("Canceled cold start check: user is logging in")
}
}
override fun onRoomListVisible() {
if (isColdStart.getAndSet(false)) {
Timber.d("Room list is visible, finishing cold start check")
analyticsService.removeLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStartUntilCachedRoomList)?.finish()
}
}
}

View file

@ -28,6 +28,7 @@ setupDependencyInjection()
dependencies {
implementation(projects.appconfig)
implementation(projects.appnav)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)

View file

@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.appnav.analytics.AnalyticsColdStartWatcher
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource
@ -86,6 +87,7 @@ class RoomListPresenter(
private val appPreferencesStore: AppPreferencesStore,
private val seenInvitesStore: SeenInvitesStore,
private val announcementService: AnnouncementService,
private val coldStartWatcher: AnalyticsColdStartWatcher,
) : Presenter<RoomListState> {
private val encryptionService = client.encryptionService
@ -236,6 +238,8 @@ class RoomListPresenter(
)
showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> {
coldStartWatcher.onRoomListVisible()
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
showNewNotificationSoundBanner = showNewNotificationSoundBanner,

View file

@ -13,6 +13,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.appnav.analytics.AnalyticsColdStartWatcher
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.FakeDateTimeObserver
@ -673,5 +674,14 @@ class RoomListPresenterTest {
appPreferencesStore = appPreferencesStore,
seenInvitesStore = seenInvitesStore,
announcementService = announcementService,
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
)
}
private class FakeAnalyticsColdStartWatcher : AnalyticsColdStartWatcher {
override fun start() {}
override fun whenLoggingIn() {}
override fun onRoomListVisible() {}
}

View file

@ -11,6 +11,7 @@ sealed class AnalyticsLongRunningTransaction(
val name: String,
val operation: String?,
) {
data object ColdStartUntilCachedRoomList : AnalyticsLongRunningTransaction("Cold start until cached room list is displayed", null)
data object FirstRoomsDisplayed : AnalyticsLongRunningTransaction("First rooms displayed after login or restoration", null)
data object ResumeAppUntilNewRoomsReceived : AnalyticsLongRunningTransaction("App was resumed and new room list items arrived", null)
data object NotificationTapOpensTimeline : AnalyticsLongRunningTransaction("A notification was tapped and it opened a timeline", null)