Improves caching of encrypted DMs:

- Migrates caching of decrypted value outside of the Event class
- Removes encrypted parts of NIP-17 from the cache
- Removes old NIP-04 messages from the cache
- Avoids deleting new NIP-17 plain text chats from memory
This commit is contained in:
Vitor Pamplona
2024-08-22 17:01:37 -04:00
parent 71b111ce8b
commit 448ea796ef
8 changed files with 263 additions and 183 deletions

View File

@@ -2290,14 +2290,16 @@ class Account(
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
mine.forEach { giftWrap ->
giftWrap.cachedGift(signer) { gift ->
giftWrap.unwrap(signer) { gift ->
if (gift is SealedGossipEvent) {
gift.cachedGossip(signer) { gossip -> LocalCache.justConsume(gossip, null) }
} else {
LocalCache.justConsume(gift, null)
gift.unseal(signer) { gossip ->
LocalCache.justConsume(gossip, null)
}
}
LocalCache.justConsume(gift, null)
}
LocalCache.consume(giftWrap, null)
}
@@ -2842,7 +2844,7 @@ class Account(
) {
if (!isWriteable()) return
return event.cachedGift(signer, onReady)
return event.unwrap(signer, onReady)
}
fun unseal(
@@ -2851,7 +2853,7 @@ class Account(
) {
if (!isWriteable()) return
return event.cachedGossip(signer, onReady)
return event.unseal(signer, onReady)
}
fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event)

View File

@@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@Stable
@@ -75,7 +77,7 @@ class Chatroom {
fun senderIntersects(keySet: Set<HexKey>): Boolean = authors.any { it.pubkeyHex in keySet }
fun pruneMessagesToTheLatestOnly(): Set<Note> {
val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
val sorted = roomMessages.sortedWith(DefaultFeedOrder)
val toKeep =
if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
@@ -84,7 +86,7 @@ class Chatroom {
} else {
// Old messages, keep the last one.
sorted.take(1).toSet()
} + sorted.filter { it.liveSet?.isInUse() ?: false }
} + sorted.filter { it.liveSet?.isInUse() ?: false } + sorted.filter { it.event !is PrivateDmEvent }
val toRemove = roomMessages.minus(toKeep)
roomMessages = toKeep

View File

@@ -299,10 +299,18 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
override fun consume(
event: Event,
relay: Relay,
) {
if (LocalCache.justVerify(event)) {
consumeAlreadyVerified(event, relay)
}
}
fun consumeAlreadyVerified(
event: Event,
relay: Relay,
) {
checkNotInMainThread()
if (LocalCache.justVerify(event)) {
when (event) {
is PrivateOutboxRelayListEvent -> {
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
@@ -339,14 +347,22 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
val noteEvent = note?.event as? GiftWrapEvent
if (noteEvent != null) {
if (relay.brief !in note.relays) {
noteEvent.cachedGift(account.signer) {
LocalCache.justConsume(noteEvent, relay)
noteEvent.innerEventId?.let {
(LocalCache.getNoteIfExists(it)?.event as? Event)?.let {
this.consumeAlreadyVerified(it, relay)
}
} ?: run {
event.unwrap(account.signer) {
this.consume(it, relay)
noteEvent.innerEventId = it.id
}
}
}
} else {
// new event
event.cachedGift(account.signer) {
event.unwrap(account.signer) {
LocalCache.justConsume(event, relay)
this.consume(it, relay)
}
@@ -359,15 +375,22 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
val noteEvent = note?.event as? SealedGossipEvent
if (noteEvent != null) {
if (relay.brief !in note.relays) {
// adds the relay to seal and inner chat
noteEvent.cachedGossip(account.signer) {
LocalCache.consume(noteEvent, relay)
LocalCache.justConsume(noteEvent, relay)
noteEvent.innerEventId?.let {
(LocalCache.getNoteIfExists(it)?.event as? Event)?.let {
LocalCache.justConsume(it, relay)
}
} ?: run {
event.unseal(account.signer) {
LocalCache.justConsume(it, relay)
noteEvent.innerEventId = it.id
}
}
}
} else {
// new event
event.cachedGossip(account.signer) {
event.unseal(account.signer) {
LocalCache.justConsume(event, relay)
LocalCache.justConsume(it, relay)
}
@@ -393,7 +416,6 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
}
}
}
}
override fun markAsSeenOnRelay(
eventId: String,
@@ -409,16 +431,30 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
}
private fun markInnerAsSeenOnRelay(
noteEvent: EventInterface,
newNoteEvent: EventInterface,
relay: Relay,
) {
LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay)
markInnerAsSeenOnRelay(newNoteEvent.id(), relay)
}
private fun markInnerAsSeenOnRelay(
eventId: HexKey,
relay: Relay,
) {
val note = LocalCache.getNoteIfExists(eventId)
if (note != null) {
note.addRelay(relay)
val noteEvent = note.event
if (noteEvent is GiftWrapEvent) {
noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, relay) }
noteEvent.innerEventId?.let {
markInnerAsSeenOnRelay(it, relay)
}
} else if (noteEvent is SealedGossipEvent) {
noteEvent.cachedGossip(account.signer) { rumor ->
markInnerAsSeenOnRelay(rumor, relay)
noteEvent.innerEventId?.let {
markInnerAsSeenOnRelay(it, relay)
}
}
}
}

View File

@@ -75,7 +75,8 @@ class EventNotificationConsumer(
pushWrappedEvent: GiftWrapEvent,
account: Account,
) {
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
// no need to cache
pushWrappedEvent.unwrap(account.signer) { notificationEvent ->
val consumed = LocalCache.hasConsumed(notificationEvent)
val verified = LocalCache.justVerify(notificationEvent)
Log.d("EventNotificationConsumer", "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.userProfile().toBestDisplayName()} consumed= $consumed && verified= $verified")
@@ -111,15 +112,19 @@ class EventNotificationConsumer(
when (event) {
is GiftWrapEvent -> {
event.cachedGift(account.signer) { unwrapAndConsume(it, account, onReady) }
event.unwrap(account.signer) {
unwrapAndConsume(it, account, onReady)
LocalCache.justConsume(event, null)
}
}
is SealedGossipEvent -> {
event.cachedGossip(account.signer) {
event.unseal(account.signer) {
if (!LocalCache.hasConsumed(it)) {
// this is not verifiable
LocalCache.justConsume(it, null)
onReady(it)
}
LocalCache.justConsume(event, null)
}
}
else -> {

View File

@@ -1364,7 +1364,18 @@ class AccountViewModel(
) {
when (event) {
is GiftWrapEvent -> {
event.cachedGift(account.signer) {
event.innerEventId?.let {
val existingNote = LocalCache.getNoteIfExists(it)
if (existingNote != null) {
unwrapIfNeeded(existingNote.event, onReady)
} else {
event.unwrap(account.signer) {
LocalCache.verifyAndConsume(it, null)
unwrapIfNeeded(it, onReady)
}
}
} ?: run {
event.unwrap(account.signer) {
val existingNote = LocalCache.getNoteIfExists(it.id)
if (existingNote != null) {
unwrapIfNeeded(existingNote.event, onReady)
@@ -1374,8 +1385,21 @@ class AccountViewModel(
}
}
}
}
is SealedGossipEvent -> {
event.cachedGossip(account.signer) {
event.innerEventId?.let {
val existingNote = LocalCache.getNoteIfExists(it)
if (existingNote != null) {
unwrapIfNeeded(existingNote.event, onReady)
} else {
event.unseal(account.signer) {
// this is not verifiable
LocalCache.justConsume(it, null)
unwrapIfNeeded(it, onReady)
}
}
} ?: run {
event.unseal(account.signer) {
val existingNote = LocalCache.getNoteIfExists(it.id)
if (existingNote != null) {
unwrapIfNeeded(existingNote.event, onReady)
@@ -1386,6 +1410,7 @@ class AccountViewModel(
}
}
}
}
else -> {
event?.id()?.let {
LocalCache.getNoteIfExists(it)?.let {

View File

@@ -43,10 +43,15 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -104,7 +109,7 @@ fun LoadRedirectScreen(
val event = note.event
if (event != null) {
withContext(Dispatchers.IO) { redirect(event, note, accountViewModel, nav) }
withContext(Dispatchers.IO) { redirect(event, accountViewModel, nav) }
}
}
@@ -118,35 +123,60 @@ fun LoadRedirectScreen(
}
fun redirect(
event: EventInterface,
note: Note,
eventId: HexKey,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val channelHex = note.channelHex()
LocalCache.getNoteIfExists(eventId)?.event?.let {
redirect(it, accountViewModel, nav)
}
}
fun redirect(
event: EventInterface,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val channelHex =
if (
event is ChannelMessageEvent ||
event is ChannelMetadataEvent ||
event is ChannelCreateEvent ||
event is LiveActivitiesChatMessageEvent ||
event is LiveActivitiesEvent
) {
(event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelCreateEvent)?.id
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
} else {
null
}
if (event is GiftWrapEvent) {
accountViewModel.unwrap(event) { redirect(it, note, accountViewModel, nav) }
} else if (event is SealedGossipEvent) {
accountViewModel.unseal(event) { redirect(it, note, accountViewModel, nav) }
} else {
if (event == null) {
// stay here, loading
} else if (event is ChannelCreateEvent) {
nav("Channel/${note.idHex}")
} else if (event is ChatroomKeyable) {
note.author?.let {
val withKey =
(event as ChatroomKeyable).chatroomKey(accountViewModel.userProfile().pubkeyHex)
accountViewModel.userProfile().createChatroom(withKey)
nav("Room/${withKey.hashCode()}")
event.innerEventId?.let {
redirect(it, accountViewModel, nav)
} ?: run {
accountViewModel.unwrap(event) { redirect(it, accountViewModel, nav) }
}
} else if (event is SealedGossipEvent) {
event.innerEventId?.let {
redirect(it, accountViewModel, nav)
} ?: run {
accountViewModel.unseal(event) { redirect(it, accountViewModel, nav) }
}
} else {
if (event is ChannelCreateEvent) {
nav("Channel/${event.id()}")
} else if (event is ChatroomKeyable) {
val withKey = event.chatroomKey(accountViewModel.userProfile().pubkeyHex)
accountViewModel.userProfile().createChatroom(withKey)
nav("Room/${withKey.hashCode()}")
} else if (channelHex != null) {
nav("Channel/$channelHex")
} else {
nav("Note/${note.idHex}")
nav("Note/${event.id()}")
}
}
}

View File

@@ -20,12 +20,14 @@
*/
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
@Immutable
@@ -37,11 +39,11 @@ class GiftWrapEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
@Transient var innerEventId: HexKey? = null
override fun countMemory(): Long =
super.countMemory() +
32 + (cachedInnerEvent.values.sumOf { pointerSizeInBytes + (it?.countMemory() ?: 0) }) // rough calculation
pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0)
fun copyNoContent(): GiftWrapEvent {
val copy =
@@ -54,48 +56,38 @@ class GiftWrapEvent(
sig,
)
copy.cachedInnerEvent = cachedInnerEvent
copy.innerEventId = innerEventId
return copy
}
override fun isContentEncoded() = true
fun preCachedGift(signer: NostrSigner): Event? = cachedInnerEvent[signer.pubKey]
fun addToCache(
pubKey: HexKey,
gift: Event,
) {
cachedInnerEvent = cachedInnerEvent + Pair(pubKey, gift)
}
@Deprecated(
message = "Heavy caching was removed from this class due to high memory use. Cache it separatedly",
replaceWith = ReplaceWith("unwrap"),
)
fun cachedGift(
signer: NostrSigner,
onReady: (Event) -> Unit,
) {
cachedInnerEvent[signer.pubKey]?.let {
onReady(it)
return
}
unwrap(signer) { gift ->
if (gift is WrappedEvent) {
gift.host = HostStub(this.id, this.pubKey, this.kind)
}
addToCache(signer.pubKey, gift)
) = unwrap(signer, onReady)
onReady(gift)
}
}
private fun unwrap(
fun unwrap(
signer: NostrSigner,
onReady: (Event) -> Unit,
) {
try {
plainContent(signer) { onReady(fromJson(it)) }
plainContent(signer) { giftStr ->
val gift = fromJson(giftStr)
if (gift is WrappedEvent) {
gift.host = HostStub(this.id, this.pubKey, this.kind)
}
innerEventId = gift.id
onReady(gift)
}
} catch (e: Exception) {
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
Log.w("GiftWrapEvent", "Couldn't Decrypt the content", e)
}
}

View File

@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
@Immutable
@@ -38,11 +39,11 @@ class SealedGossipEvent(
content: String,
sig: HexKey,
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
@Transient var innerEventId: HexKey? = null
override fun countMemory(): Long =
super.countMemory() +
pointerSizeInBytes + cachedInnerEvent.values.sumOf { pointerSizeInBytes + (it?.countMemory() ?: 0) }
pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0)
fun copyNoContent(): SealedGossipEvent {
val copy =
@@ -55,51 +56,38 @@ class SealedGossipEvent(
sig,
)
copy.cachedInnerEvent = cachedInnerEvent
copy.host = host
copy.innerEventId = innerEventId
return copy
}
override fun isContentEncoded() = true
fun preCachedGossip(signer: NostrSigner): Event? = cachedInnerEvent[signer.pubKey]
fun addToCache(
pubKey: HexKey,
gift: Event,
) {
cachedInnerEvent = cachedInnerEvent + Pair(pubKey, gift)
}
@Deprecated(
message = "Heavy caching was removed from this class due to high memory use. Cache it separatedly",
replaceWith = ReplaceWith("unseal"),
)
fun cachedGossip(
signer: NostrSigner,
onReady: (Event) -> Unit,
) {
cachedInnerEvent[signer.pubKey]?.let {
onReady(it)
return
}
) = unseal(signer, onReady)
unseal(signer) { gossip ->
val event = gossip.mergeWith(this)
if (event is WrappedEvent) {
event.host = host ?: HostStub(this.id, this.pubKey, this.kind)
}
addToCache(signer.pubKey, event)
onReady(event)
}
}
private fun unseal(
fun unseal(
signer: NostrSigner,
onReady: (Gossip) -> Unit,
onReady: (Event) -> Unit,
) {
try {
plainContent(signer) {
try {
onReady(Gossip.fromJson(it))
val gossip = Gossip.fromJson(it)
val event = gossip.mergeWith(this)
if (event is WrappedEvent) {
event.host = host ?: HostStub(this.id, this.pubKey, this.kind)
}
innerEventId = event.id
onReady(event)
} catch (e: Exception) {
Log.w("GossipEvent", "Fail to decrypt or parse Gossip", e)
}