Fix ANRs when calculating DB sizes for Sentry (#7017)

* Fix ANRs when calculating DB sizes for Sentry

Instead of using the SDK's `Client.getStoreSizes()` method, manually check the sizes using the file system.

For this, the `SessionPaths` need to be synchronously accessible from a `MatrixClient`. I removed `SessionPathsProvider` too since it's not needed anymore.
This commit is contained in:
Jorge Martin Espinosa
2026-06-16 16:49:09 +02:00
committed by GitHub
parent 8e9c0bedf2
commit ac93044f49
19 changed files with 88 additions and 101 deletions
@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -53,6 +54,7 @@ import java.util.Optional
interface MatrixClient {
val sessionId: SessionId
val deviceId: DeviceId
val sessionPaths: SessionPaths
val userProfile: StateFlow<MatrixUser>
val roomListService: RoomListService
val spaceService: SpaceService
@@ -10,5 +10,5 @@ package io.element.android.libraries.matrix.api.analytics
import io.element.android.libraries.matrix.api.core.SessionId
fun interface GetDatabaseSizesUseCase {
suspend operator fun invoke(sessionId: SessionId): Result<SdkStoreSizes>
operator fun invoke(sessionId: SessionId): Result<SdkStoreSizes>
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2026 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.libraries.matrix.api.paths
import java.io.File
data class SessionPaths(
val fileDirectory: File,
val cacheDirectory: File,
) {
fun deleteRecursively() {
fileDirectory.deleteRecursively()
cacheDirectory.deleteRecursively()
}
}
@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -81,7 +82,6 @@ import io.element.android.libraries.matrix.impl.spaces.RustSpaceService
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.sync.map
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
@@ -137,6 +137,7 @@ import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
class RustMatrixClient(
override val sessionPaths: SessionPaths,
private val innerClient: Client,
private val sessionStore: SessionStore,
private val sessionDelegate: RustClientSessionDelegate,
@@ -187,8 +188,6 @@ class RustMatrixClient(
sessionDispatcher = sessionDispatcher,
)
private val sessionPathsProvider = SessionPathsProvider(sessionStore)
private val roomSyncSubscriber: RoomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers)
override val roomListService: RoomListService = RustRoomListService(
@@ -824,17 +823,16 @@ class RustMatrixClient(
private suspend fun getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {
val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L
val cacheSize = sessionDirectory.cacheDirectory.getSizeOfFiles()
val cacheSize = sessionPaths.cacheDirectory.getSizeOfFiles()
if (includeCryptoDb) {
cacheSize + sessionDirectory.fileDirectory.getSizeOfFiles()
cacheSize + sessionPaths.fileDirectory.getSizeOfFiles()
} else {
cacheSize + listOf(
"matrix-sdk-state.sqlite3",
"matrix-sdk-state.sqlite3-shm",
"matrix-sdk-state.sqlite3-wal",
).map { fileName ->
File(sessionDirectory.fileDirectory, fileName)
File(sessionPaths.fileDirectory, fileName)
}.sumOf { file ->
file.length()
}
@@ -843,7 +841,7 @@ class RustMatrixClient(
private suspend fun deleteSessionDirectory() = withContext(sessionDispatcher) {
// Delete all the files for this session
sessionPathsProvider.provides(sessionId)?.deleteRecursively()
sessionPaths.deleteRecursively()
}
private fun scheduleDatabaseVacuum() {
@@ -17,9 +17,9 @@ import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
@@ -102,10 +102,10 @@ class RustMatrixClientFactory(
client.restoreSession(sessionData.toSession())
create(client)
create(client, sessionData)
}
suspend fun create(client: Client): RustMatrixClient {
suspend fun create(client: Client, sessionData: SessionData): RustMatrixClient {
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
// Must be called before creating the sync service, timelines etc.
@@ -121,6 +121,7 @@ class RustMatrixClientFactory(
.finish()
return RustMatrixClient(
sessionPaths = sessionData.getSessionPaths(),
innerClient = client,
sessionStore = sessionStore,
appCoroutineScope = appCoroutineScope,
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright (c) 2026 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.
@@ -9,19 +9,53 @@ package io.element.android.libraries.matrix.impl.analytics
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.data.bytes
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
import io.element.android.libraries.matrix.api.core.SessionId
import java.io.File
import kotlin.math.max
/**
* An implementation of [GetDatabaseSizesUseCase] that manually calculates the database sizes by checking the file system.
* This is not as accurate as the SDK's internal method, but is used as a fallback for Sentry's non-suspendable hook where
* calling the SDK's suspendable method blocks the main thread and causes ANRs.
*/
@ContributesBinding(AppScope::class)
class DefaultGetDatabaseSizesUseCase(
private val clientProvider: Lazy<MatrixClientProvider>,
) : GetDatabaseSizesUseCase {
override suspend fun invoke(sessionId: SessionId): Result<SdkStoreSizes> {
override fun invoke(sessionId: SessionId): Result<SdkStoreSizes> {
val client = clientProvider.value.getOrNull(sessionId)
?: return Result.failure(IllegalArgumentException("No MatrixClient for session $sessionId"))
return client.getDatabaseSizes()
val fileDir = client.sessionPaths.fileDirectory
val cacheDir = client.sessionPaths.cacheDirectory
val cryptoSize = getDatabaseSize(fileDir, "matrix-sdk-crypto")
val stateSize = getDatabaseSize(fileDir, "matrix-sdk-state")
val eventCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-event-cache")
val mediaCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-media")
return Result.success(SdkStoreSizes(
stateStore = stateSize.bytes,
cryptoStore = cryptoSize.bytes,
eventCacheStore = eventCacheSize.bytes,
mediaStore = mediaCacheSize.bytes,
))
}
private fun getDatabaseSize(baseDirectory: File, databaseFileName: String): Long {
// Check the sizes of both the main database file and the -wal file, and return the max of the two:
// The -wal file can contain additional data not yet merged into the main file, becoming larger than it.
val databaseFile = File(baseDirectory, "$databaseFileName.sqlite3")
val walFile = File(baseDirectory, "$databaseFileName.sqlite3-wal")
val databaseSize = if (databaseFile.exists()) databaseFile.length() else 0L
val walSize = if (walFile.exists()) walFile.length() else 0L
return max(databaseSize, walSize)
}
}
@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDat
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
@@ -38,7 +39,6 @@ import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.keys.SecretGenerator
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.matrix.impl.toSession
import io.element.android.libraries.sessionstorage.api.LoginType
@@ -160,7 +160,7 @@ class RustMatrixAuthenticationService(
passphrase = pendingKey.formattedAsString(),
sessionPaths = currentSessionPaths,
)
val matrixClient = rustMatrixClientFactory.create(client)
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.addSession(sessionData)
@@ -235,7 +235,7 @@ class RustMatrixAuthenticationService(
// We restore the client using the just retrieved session data
client.restoreSession(sessionData.toSession())
val matrixClient = rustMatrixClientFactory.create(client)
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
// We wait for the verification state to be known
matrixClient.waitForKnownVerificationState()
@@ -325,7 +325,7 @@ class RustMatrixAuthenticationService(
passphrase = pendingKey.formattedAsString(),
sessionPaths = currentSessionPaths,
)
val matrixClient = rustMatrixClientFactory.create(client)
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
matrixClient.waitForKnownVerificationState()
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
@@ -390,7 +390,7 @@ class RustMatrixAuthenticationService(
passphrase = pendingKey.formattedAsString(),
sessionPaths = emptySessionPaths,
)
val matrixClient = rustMatrixClientFactory.create(client)
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.addSession(sessionData)
@@ -9,7 +9,7 @@
package io.element.android.libraries.matrix.impl.mapper
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import org.matrix.rustcomponents.sdk.Session
@@ -8,19 +8,10 @@
package io.element.android.libraries.matrix.impl.paths
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.sessionstorage.api.SessionData
import java.io.File
data class SessionPaths(
val fileDirectory: File,
val cacheDirectory: File,
) {
fun deleteRecursively() {
fileDirectory.deleteRecursively()
cacheDirectory.deleteRecursively()
}
}
internal fun SessionData.getSessionPaths(): SessionPaths {
return SessionPaths(
fileDirectory = File(sessionPath),
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.paths
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.matrix.api.paths.SessionPaths
import java.io.File
import java.util.UUID
@@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.storage
import io.element.android.libraries.androidutils.crypto.ClientSecret
import io.element.android.libraries.core.data.ByteUnit
import io.element.android.libraries.core.data.megaBytes
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.api.paths.SessionPaths
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder
@@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.storage
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.api.paths.SessionPaths
interface SqliteStoreBuilderProvider {
fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder
@@ -1,23 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector 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.libraries.matrix.impl.util
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.sessionstorage.api.SessionStore
class SessionPathsProvider(
private val sessionStore: SessionStore,
) {
suspend fun provides(sessionId: SessionId): SessionPaths? {
val sessionData = sessionStore.getSession(sessionId.value) ?: return null
return sessionData.getSessionPaths()
}
}
@@ -13,6 +13,7 @@ package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.data.bytes
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
@@ -141,6 +142,7 @@ class RustMatrixClientTest {
),
) = RustMatrixClient(
innerClient = client,
sessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")),
sessionStore = sessionStore,
appCoroutineScope = backgroundScope,
sessionDelegate = aRustClientSessionDelegate(
@@ -10,8 +10,8 @@ package io.element.android.libraries.matrix.impl.mapper
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
@@ -7,7 +7,7 @@
package io.element.android.libraries.matrix.impl.storage
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.api.paths.SessionPaths
class FakeSqliteStoreBuilderProvider : SqliteStoreBuilderProvider {
override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder {
@@ -1,41 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector 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.libraries.matrix.impl.util
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SessionPathsProviderTest {
@Test
fun `if session is not found, provides returns null`() = runTest {
val sut = SessionPathsProvider(InMemorySessionStore())
val result = sut.provides(A_SESSION_ID)
assertThat(result).isNull()
}
@Test
fun `if session is found, provides returns the data`() = runTest {
val store = InMemorySessionStore(
initialList = listOf(
aSessionData(
sessionPath = "/a/path/to/a/session",
cachePath = "/a/path/to/a/cache",
)
)
)
val sut = SessionPathsProvider(store)
val result = sut.provides(A_SESSION_ID)!!
assertThat(result.fileDirectory.absolutePath).isEqualTo("/a/path/to/a/session")
assertThat(result.cacheDirectory.absolutePath).isEqualTo("/a/path/to/a/cache")
}
}
@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -66,10 +67,12 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import java.io.File
import java.util.Optional
class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID,
override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")),
override val deviceId: DeviceId = A_DEVICE_ID,
override val sessionCoroutineScope: CoroutineScope = TestScope(),
private val userDisplayName: String? = A_USER_NAME,
@@ -33,7 +33,6 @@ import io.sentry.Sentry
import io.sentry.SentryOptions
import io.sentry.android.core.SentryAndroid
import io.sentry.protocol.SentryTransaction
import kotlinx.coroutines.runBlocking
import timber.log.Timber
@ContributesIntoSet(AppScope::class)
@@ -132,7 +131,7 @@ class SentryAnalyticsProvider(
if (sessionId != null) {
// This runs in a separate thread, so although using `runBlocking` is not great, at least it shouldn't freeze the app
// Also, the method is fairly quick, so the blocking shouldn't take longer than a few ms
val databaseSizes = runBlocking { getDatabaseSizesUseCase(sessionId) }.getOrNull()
val databaseSizes = getDatabaseSizesUseCase(sessionId).getOrNull()
databaseSizes?.stateStore?.let { transaction.setExtra(AnalyticsUserData.STATE_STORE_SIZE, it.into(ByteUnit.MB)) }
databaseSizes?.eventCacheStore?.let { transaction.setExtra(AnalyticsUserData.EVENT_CACHE_SIZE, it.into(ByteUnit.MB)) }