Add extra logs for sending media (#5218)

* Add extra logs for sending media

We have some issues with stuck media uploads that could use some logs to understand where the process gets stuck.

* Fix mocks for test
This commit is contained in:
Jorge Martin Espinosa 2025-08-26 09:02:15 +02:00 committed by GitHub
parent f245fe2399
commit d9dde270b2
5 changed files with 36 additions and 5 deletions

View file

@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorPresenter import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorPresenter
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.firstInstanceOf import io.element.android.libraries.core.coroutine.firstInstanceOf
@ -261,6 +262,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mediaOptimizationConfig = mediaOptimizationConfig, mediaOptimizationConfig = mediaOptimizationConfig,
).fold( ).fold(
onSuccess = { mediaUploadInfo -> onSuccess = { mediaUploadInfo ->
Timber.d("Media ${mediaUploadInfo.file.path.orEmpty().hash()} finished processing, it's now ready to upload")
sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo) sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
}, },
onFailure = { onFailure = {

View file

@ -58,6 +58,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
@ -77,7 +78,9 @@ class AttachmentsPreviewPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val mockMediaUrl: Uri = mockk("localMediaUri") private val mockMediaUrl: Uri = mockk("localMediaUri") {
every { path } returns "/path/to/media"
}
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.timeline package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -338,6 +339,7 @@ class RustTimeline(
formattedCaption: String?, formattedCaption: String?,
inReplyToEventId: EventId?, inReplyToEventId: EventId?,
): Result<MediaUploadHandler> { ): Result<MediaUploadHandler> {
Timber.d("Sending image ${file.path.hash()}")
return sendAttachment(listOfNotNull(file, thumbnailFile)) { return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendImage( inner.sendImage(
params = UploadParameters( params = UploadParameters(
@ -363,6 +365,7 @@ class RustTimeline(
formattedCaption: String?, formattedCaption: String?,
inReplyToEventId: EventId?, inReplyToEventId: EventId?,
): Result<MediaUploadHandler> { ): Result<MediaUploadHandler> {
Timber.d("Sending video ${file.path.hash()}")
return sendAttachment(listOfNotNull(file, thumbnailFile)) { return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendVideo( inner.sendVideo(
params = UploadParameters( params = UploadParameters(
@ -387,6 +390,7 @@ class RustTimeline(
formattedCaption: String?, formattedCaption: String?,
inReplyToEventId: EventId?, inReplyToEventId: EventId?,
): Result<MediaUploadHandler> { ): Result<MediaUploadHandler> {
Timber.d("Sending audio ${file.path.hash()}")
return sendAttachment(listOf(file)) { return sendAttachment(listOf(file)) {
inner.sendAudio( inner.sendAudio(
params = UploadParameters( params = UploadParameters(
@ -410,6 +414,7 @@ class RustTimeline(
formattedCaption: String?, formattedCaption: String?,
inReplyToEventId: EventId?, inReplyToEventId: EventId?,
): Result<MediaUploadHandler> { ): Result<MediaUploadHandler> {
Timber.d("Sending file ${file.path.hash()}")
return sendAttachment(listOf(file)) { return sendAttachment(listOf(file)) {
inner.sendFile( inner.sendFile(
params = UploadParameters( params = UploadParameters(

View file

@ -11,6 +11,7 @@ import android.net.Uri
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.flatMapCatching import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -20,6 +21,8 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import timber.log.Timber
import java.io.File
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class MediaSender @AssistedInject constructor( class MediaSender @AssistedInject constructor(
@ -43,6 +46,7 @@ class MediaSender @AssistedInject constructor(
mimeType: String, mimeType: String,
mediaOptimizationConfig: MediaOptimizationConfig, mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> { ): Result<MediaUploadInfo> {
Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType")
return preProcessor return preProcessor
.process( .process(
uri = uri, uri = uri,
@ -58,7 +62,9 @@ class MediaSender @AssistedInject constructor(
formattedCaption: String?, formattedCaption: String?,
inReplyToEventId: EventId?, inReplyToEventId: EventId?,
): Result<Unit> { ): Result<Unit> {
val mediaLogId = mediaId(mediaUploadInfo.file)
return getTimeline().flatMap { return getTimeline().flatMap {
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
it.sendMedia( it.sendMedia(
uploadInfo = mediaUploadInfo, uploadInfo = mediaUploadInfo,
caption = caption, caption = caption,
@ -66,7 +72,7 @@ class MediaSender @AssistedInject constructor(
inReplyToEventId = inReplyToEventId, inReplyToEventId = inReplyToEventId,
) )
} }
.handleSendResult() .handleSendResult(mediaLogId)
} }
suspend fun sendMedia( suspend fun sendMedia(
@ -92,7 +98,7 @@ class MediaSender @AssistedInject constructor(
inReplyToEventId = inReplyToEventId, inReplyToEventId = inReplyToEventId,
) )
} }
.handleSendResult() .handleSendResult(mediaId(uri))
} }
suspend fun sendVoiceMessage( suspend fun sendVoiceMessage(
@ -122,17 +128,19 @@ class MediaSender @AssistedInject constructor(
inReplyToEventId = inReplyToEventId, inReplyToEventId = inReplyToEventId,
) )
} }
.handleSendResult() .handleSendResult(mediaId(uri))
} }
private fun Result<Unit>.handleSendResult() = this private fun Result<Unit>.handleSendResult(mediaId: String) = this
.onFailure { error -> .onFailure { error ->
val job = ongoingUploadJobs.remove(Job) val job = ongoingUploadJobs.remove(Job)
Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
if (error !is CancellationException) { if (error !is CancellationException) {
job?.cancel() job?.cancel()
} }
} }
.onSuccess { .onSuccess {
Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
ongoingUploadJobs.remove(Job) ongoingUploadJobs.remove(Job)
} }
@ -195,6 +203,7 @@ class MediaSender @AssistedInject constructor(
@Suppress("RunCatchingNotAllowed") @Suppress("RunCatchingNotAllowed")
return handler return handler
.mapCatching { uploadHandler -> .mapCatching { uploadHandler ->
Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}")
ongoingUploadJobs[Job] = uploadHandler ongoingUploadJobs[Job] = uploadHandler
uploadHandler.await() uploadHandler.await()
} }
@ -214,3 +223,6 @@ class MediaSender @AssistedInject constructor(
*/ */
fun cleanUp() = preProcessor.cleanUp() fun cleanUp() = preProcessor.cleanUp()
} }
private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash()
private fun mediaId(file: File): String = file.path.orEmpty().hash()

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.file.createTmpFile
import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileName
import io.element.android.libraries.androidutils.file.safeRenameTo import io.element.android.libraries.androidutils.file.safeRenameTo
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.androidutils.media.runAndRelease
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
@ -107,6 +108,8 @@ class AndroidMediaPreProcessor @Inject constructor(
}.mapFailure { MediaPreProcessor.Failure(it) } }.mapFailure { MediaPreProcessor.Failure(it) }
override fun cleanUp() { override fun cleanUp() {
Timber.d("Cleaning up temporary media files")
// Clear temporary files created in older versions of the app // Clear temporary files created in older versions of the app
cacheDir.listFiles()?.onEach { file -> cacheDir.listFiles()?.onEach { file ->
if (file.isFile) { if (file.isFile) {
@ -129,6 +132,7 @@ class AndroidMediaPreProcessor @Inject constructor(
} }
private suspend fun processFile(uri: Uri, mimeType: String): MediaUploadInfo { private suspend fun processFile(uri: Uri, mimeType: String): MediaUploadInfo {
Timber.d("Processing file ${uri.path.orEmpty().hash()}")
val file = copyToTmpFile(uri) val file = copyToTmpFile(uri)
val info = FileInfo( val info = FileInfo(
mimetype = mimeType, mimetype = mimeType,
@ -140,6 +144,7 @@ class AndroidMediaPreProcessor @Inject constructor(
} }
private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo { private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo {
Timber.d("Finished processing, post-processing ${uri.path.orEmpty().hash()}")
val name = context.getFileName(uri) ?: return this val name = context.getFileName(uri) ?: return this
val renamedFile = File(context.cacheDir, name).also { val renamedFile = File(context.cacheDir, name).also {
file.safeRenameTo(it) file.safeRenameTo(it)
@ -154,6 +159,7 @@ class AndroidMediaPreProcessor @Inject constructor(
} }
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
Timber.d("Processing image ${uri.path.orEmpty().hash()}")
suspend fun processImageWithCompression(): MediaUploadInfo { suspend fun processImageWithCompression(): MediaUploadInfo {
// Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail. // Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail.
val orientation = contentResolver.openInputStream(uri).use { input -> val orientation = contentResolver.openInputStream(uri).use { input ->
@ -217,6 +223,7 @@ class AndroidMediaPreProcessor @Inject constructor(
} }
private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo { private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo {
Timber.d("Processing video ${uri.path.orEmpty().hash()}")
val resultFile = runCatchingExceptions { val resultFile = runCatchingExceptions {
videoCompressor.compress(uri, videoCompressionPreset) videoCompressor.compress(uri, videoCompressionPreset)
.onEach { .onEach {
@ -244,12 +251,14 @@ class AndroidMediaPreProcessor @Inject constructor(
thumbnailFile = thumbnailInfo?.file thumbnailFile = thumbnailInfo?.file
) )
} else { } else {
Timber.d("Could not transcode video ${uri.path.orEmpty().hash()}, sending original file as plain file")
// If the video could not be compressed, just use the original one, but send it as a file // If the video could not be compressed, just use the original one, but send it as a file
return processFile(uri, MimeTypes.OctetStream) return processFile(uri, MimeTypes.OctetStream)
} }
} }
private suspend fun processAudio(uri: Uri, mimeType: String?): MediaUploadInfo { private suspend fun processAudio(uri: Uri, mimeType: String?): MediaUploadInfo {
Timber.d("Processing audio ${uri.path.orEmpty().hash()}")
val file = copyToTmpFile(uri) val file = copyToTmpFile(uri)
return MediaMetadataRetriever().runAndRelease { return MediaMetadataRetriever().runAndRelease {
setDataSource(context, Uri.fromFile(file)) setDataSource(context, Uri.fromFile(file))