Add another performance check for cold start time until the cached room list is displayed
This commit is contained in:
parent
e1bd189ba0
commit
daf7bea39e
7 changed files with 91 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
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.LoginEntryPoint
|
||||||
import io.element.android.features.login.api.LoginParams
|
import io.element.android.features.login.api.LoginParams
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -43,6 +44,7 @@ class NotLoggedInFlowNode(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val loginEntryPoint: LoginEntryPoint,
|
private val loginEntryPoint: LoginEntryPoint,
|
||||||
private val imageLoaderHolder: ImageLoaderHolder,
|
private val imageLoaderHolder: ImageLoaderHolder,
|
||||||
|
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
|
||||||
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root,
|
||||||
|
|
@ -65,6 +67,7 @@ class NotLoggedInFlowNode(
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
analyticsColdStartWatcher.whenLoggingIn()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onResume = {
|
onResume = {
|
||||||
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
|
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.annotations.ContributesNode
|
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.di.MatrixSessionCache
|
||||||
import io.element.android.appnav.intent.IntentResolver
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
import io.element.android.appnav.intent.ResolvedIntent
|
import io.element.android.appnav.intent.ResolvedIntent
|
||||||
|
|
@ -89,6 +90,7 @@ class RootFlowNode(
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val announcementService: AnnouncementService,
|
private val announcementService: AnnouncementService,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
|
||||||
) : BaseFlowNode<RootFlowNode.NavTarget>(
|
) : BaseFlowNode<RootFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.SplashScreen,
|
initialElement = NavTarget.SplashScreen,
|
||||||
|
|
@ -98,6 +100,7 @@ class RootFlowNode(
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
analyticsColdStartWatcher.start()
|
||||||
matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
|
matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
observeNavState()
|
observeNavState()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ setupDependencyInjection()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
|
implementation(projects.appnav)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
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.Announcement
|
||||||
import io.element.android.features.announcement.api.AnnouncementService
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||||
|
|
@ -86,6 +87,7 @@ class RoomListPresenter(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
private val announcementService: AnnouncementService,
|
private val announcementService: AnnouncementService,
|
||||||
|
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
private val encryptionService = client.encryptionService
|
private val encryptionService = client.encryptionService
|
||||||
|
|
||||||
|
|
@ -236,6 +238,8 @@ class RoomListPresenter(
|
||||||
)
|
)
|
||||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||||
else -> {
|
else -> {
|
||||||
|
coldStartWatcher.onRoomListVisible()
|
||||||
|
|
||||||
RoomListContentState.Rooms(
|
RoomListContentState.Rooms(
|
||||||
securityBannerState = securityBannerState,
|
securityBannerState = securityBannerState,
|
||||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
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.Announcement
|
||||||
import io.element.android.features.announcement.api.AnnouncementService
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
import io.element.android.features.home.impl.FakeDateTimeObserver
|
import io.element.android.features.home.impl.FakeDateTimeObserver
|
||||||
|
|
@ -673,5 +674,14 @@ class RoomListPresenterTest {
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
announcementService = announcementService,
|
announcementService = announcementService,
|
||||||
|
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class FakeAnalyticsColdStartWatcher : AnalyticsColdStartWatcher {
|
||||||
|
override fun start() {}
|
||||||
|
|
||||||
|
override fun whenLoggingIn() {}
|
||||||
|
|
||||||
|
override fun onRoomListVisible() {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ sealed class AnalyticsLongRunningTransaction(
|
||||||
val name: String,
|
val name: String,
|
||||||
val operation: 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 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 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)
|
data object NotificationTapOpensTimeline : AnalyticsLongRunningTransaction("A notification was tapped and it opened a timeline", null)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue