Fix a few FFI leaks (#405)

Fix a few FFI leaks

These are instances where we obtain an FFIObject and don't call
Close on it to release the underlying reference on the Rust side.

The worst instance here was leaking an object per room member
every time we refreshed the member list
This commit is contained in:
Chris Smith 2023-05-12 11:50:39 +01:00 committed by GitHub
parent ed16ea5e48
commit ebac9ef4b4
11 changed files with 67 additions and 59 deletions

View file

@ -0,0 +1,5 @@
appId: ${APP_ID}
---
- extendedWaitUntil:
visible: ${ROOM_NAME}
timeout: 10_000

View file

@ -1,5 +1,6 @@
appId: ${APP_ID} appId: ${APP_ID}
--- ---
- runFlow: ../assertions/assertRoomListSynced.yaml
- tapOn: "search" - tapOn: "search"
- inputText: ${ROOM_NAME.substring(0, 3)} - inputText: ${ROOM_NAME.substring(0, 3)}
- takeScreenshot: build/maestro/400-SearchRoom - takeScreenshot: build/maestro/400-SearchRoom

View file

@ -65,11 +65,12 @@ class CreateRoomRootPresenter @Inject constructor(
fun startDm(matrixUser: MatrixUser) { fun startDm(matrixUser: MatrixUser) {
startDmAction.value = Async.Uninitialized startDmAction.value = Async.Uninitialized
val existingDM = matrixClient.findDM(matrixUser.userId) matrixClient.findDM(matrixUser.userId).use { existingDM ->
if (existingDM == null) { if (existingDM == null) {
localCoroutineScope.createDM(matrixUser, startDmAction) localCoroutineScope.createDM(matrixUser, startDmAction)
} else { } else {
startDmAction.value = Async.Success(existingDM.roomId) startDmAction.value = Async.Success(existingDM.roomId)
}
} }
} }

View file

@ -131,14 +131,18 @@ class InviteListPresenter @Inject constructor(
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch { private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch {
suspend { suspend {
client.getRoom(roomId)?.acceptInvitation()?.getOrThrow() client.getRoom(roomId)?.use {
it.acceptInvitation().getOrThrow()
}
roomId roomId
}.execute(acceptedAction) }.execute(acceptedAction)
} }
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
suspend { suspend {
client.getRoom(roomId)?.rejectInvitation()?.getOrThrow() ?: Unit client.getRoom(roomId)?.use {
it.rejectInvitation().getOrThrow()
} ?: Unit
}.execute(declinedAction) }.execute(declinedAction)
} }

View file

@ -24,17 +24,18 @@ import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
object RoomMemberMapper { object RoomMemberMapper {
fun map(roomMember: RustRoomMember): RoomMember = fun map(roomMember: RustRoomMember): RoomMember = roomMember.use {
RoomMember( RoomMember(
UserId(roomMember.userId()), UserId(it.userId()),
roomMember.displayName(), it.displayName(),
roomMember.avatarUrl(), it.avatarUrl(),
mapMembership(roomMember.membership()), mapMembership(it.membership()),
roomMember.isNameAmbiguous(), it.isNameAmbiguous(),
roomMember.powerLevel(), it.powerLevel(),
roomMember.normalizedPowerLevel(), it.normalizedPowerLevel(),
roomMember.isIgnored(), it.isIgnored(),
) )
}
fun mapMembership(membershipState: RustMembershipState): RoomMembershipState = fun mapMembership(membershipState: RustMembershipState): RoomMembershipState =
when (membershipState) { when (membershipState) {

View file

@ -161,9 +161,10 @@ class RustMatrixRoom(
override suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId() val transactionId = genTransactionId()
val content = messageEventContentFromMarkdown(message) messageEventContentFromMarkdown(message).use { content ->
runCatching { runCatching {
innerRoom.send(content, transactionId) innerRoom.send(content, transactionId)
}
} }
} }

View file

@ -27,12 +27,12 @@ class MatrixTimelineItemMapper(
) { ) {
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use { fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
val asEvent = timelineItem.asEvent() val asEvent = it.asEvent()
if (asEvent != null) { if (asEvent != null) {
val eventTimelineItem = eventTimelineItemMapper.map(asEvent) val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
return MatrixTimelineItem.Event(eventTimelineItem) return MatrixTimelineItem.Event(eventTimelineItem)
} }
val asVirtual = timelineItem.asVirtual() val asVirtual = it.asVirtual()
if (asVirtual != null) { if (asVirtual != null) {
val virtualTimelineItem = virtualTimelineItemMapper.map(asVirtual) val virtualTimelineItem = virtualTimelineItemMapper.map(asVirtual)
return MatrixTimelineItem.Virtual(virtualTimelineItem) return MatrixTimelineItem.Virtual(virtualTimelineItem)

View file

@ -39,7 +39,7 @@ import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat
class EventMessageMapper { class EventMessageMapper {
fun map(message: Message): MessageContent = message.use { fun map(message: Message): MessageContent = message.use {
val type = message.msgtype().use { type -> val type = it.msgtype().use { type ->
when (type) { when (type) {
is MessageType.Audio -> { is MessageType.Audio -> {
AudioMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) AudioMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map())
@ -68,9 +68,9 @@ class EventMessageMapper {
} }
} }
MessageContent( MessageContent(
body = message.body(), body = it.body(),
inReplyTo = message.inReplyTo()?.eventId?.let(::EventId), inReplyTo = it.inReplyTo()?.eventId?.let(::EventId),
isEdited = message.isEdited(), isEdited = it.isEdited(),
type = type type = type
) )
} }

View file

@ -31,18 +31,18 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use { fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
EventTimelineItem( EventTimelineItem(
uniqueIdentifier = eventTimelineItem.uniqueIdentifier(), uniqueIdentifier = it.uniqueIdentifier(),
eventId = eventTimelineItem.eventId()?.let { EventId(it) }, eventId = it.eventId()?.let { EventId(it) },
isEditable = eventTimelineItem.isEditable(), isEditable = it.isEditable(),
isLocal = eventTimelineItem.isLocal(), isLocal = it.isLocal(),
isOwn = eventTimelineItem.isOwn(), isOwn = it.isOwn(),
isRemote = eventTimelineItem.isRemote(), isRemote = it.isRemote(),
localSendState = eventTimelineItem.localSendState()?.map(), localSendState = it.localSendState()?.map(),
reactions = eventTimelineItem.reactions().map(), reactions = it.reactions().map(),
sender = UserId(eventTimelineItem.sender()), sender = UserId(it.sender()),
senderProfile = eventTimelineItem.senderProfile().map(), senderProfile = it.senderProfile().map(),
timestamp = eventTimelineItem.timestamp().toLong(), timestamp = it.timestamp().toLong(),
content = contentMapper.map(eventTimelineItem.content()) content = contentMapper.map(it.content())
) )
} }
} }

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.timeline.item.event package io.element.android.libraries.matrix.impl.timeline.item.event
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
@ -39,7 +39,7 @@ import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) { class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) {
fun map(content: TimelineItemContent): EventContent = content.use { fun map(content: TimelineItemContent): EventContent = content.use {
when (val kind = content.kind()) { when (val kind = it.kind()) {
is TimelineItemContentKind.FailedToParseMessageLike -> { is TimelineItemContentKind.FailedToParseMessageLike -> {
FailedToParseMessageLikeContent( FailedToParseMessageLikeContent(
eventType = kind.eventType, eventType = kind.eventType,
@ -54,7 +54,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
) )
} }
TimelineItemContentKind.Message -> { TimelineItemContentKind.Message -> {
val message = content.asMessage() val message = it.asMessage()
if (message == null) { if (message == null) {
UnknownContent UnknownContent
} else { } else {

View file

@ -33,17 +33,16 @@ import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import java.util.Locale import java.util.Locale
import java.util.concurrent.Executors
class RoomListScreen( class RoomListScreen(
context: Context, context: Context,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers = Singleton.coroutineDispatchers,
) { ) {
private val clock = Clock.System private val clock = Clock.System
private val locale = Locale.getDefault() private val locale = Locale.getDefault()
@ -58,28 +57,24 @@ class RoomListScreen(
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
networkMonitor = NetworkMonitorImpl(context), networkMonitor = NetworkMonitorImpl(context),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
inviteStateDataSource = DefaultInviteStateDataSource( inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers)
matrixClient,
DefaultSeenInvitesStore(context),
CoroutineDispatchers(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
)
) )
@Composable @Composable
fun Content(modifier: Modifier = Modifier) { fun Content(modifier: Modifier = Modifier) {
fun onRoomClicked(roomId: RoomId) { fun onRoomClicked(roomId: RoomId) {
val room = matrixClient.getRoom(roomId)!!
val timeline = room.timeline()
Singleton.appScope.launch { Singleton.appScope.launch {
timeline.apply { withContext(coroutineDispatchers.io) {
initialize() matrixClient.getRoom(roomId)!!.use { room ->
paginateBackwards(20, 50) val timeline = room.timeline()
dispose()
timeline.apply {
// TODO This doesn't work reliably as initialize is asynchronous, and the timeline can't be used until it's finished
initialize()
paginateBackwards(20, 50)
dispose()
}
}
} }
} }
} }