perf(xmpp): fix slow MAM/MUC-MAM and move OMEMO decryption off the main
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

thread

- Replace O(n^2) origin-id/stanza-id lookups in AppModel with hash
  indexes
  and stop rebuilding the whole index on every unseen stanza-id, so
  archive
  application no longer grows quadratically with history size.
- Run OMEMO decryption on a serial background queue instead of the main
  actor, and decrypt archived stanzas in small batches with a shorter
  yield,
  keeping the UI responsive during large archive catch-ups.
- Show the "Синхронизация истории…" banner only for the one-page
  bootstrap
  window; incremental backlog catch-up continues silently in the
  background.
- Fix MUC-MAM: publish decoded group mutations at the end of room
  catch-up;
  they were previously accumulated and then silently dropped.
- Retry a failed catch-up pass automatically (bounded) and lengthen the
  query/apply timeouts so a single slow page no longer leaves history
  unloaded until the next app activation.
This commit is contained in:
wt committed 2026-08-17 15:32:05 +07:00
1 parent dee7e94e92
commit 9b56d2ebcf
8 files changed
+329 -167

No files matched your search

+52 -8
View File
@@ -78,6 +78,7 @@ final class AppModel: ObservableObject {
private var isApplyingArchiveBatch = false
private var messageIndexByStorageKey: [String: Int] = [:]
private var messageIndexByStanzaKey: [String: Int] = [:]
private var messageIndexByOriginKey: [String: Int] = [:]
private var firstMessageIndexByID: [String: Int] = [:]
private var selectedMessagesCacheConversationID: String?
private var selectedMessagesCache: [ChatMessage] = []
@@ -1949,9 +1950,7 @@ final class AppModel: ObservableObject {
return // уже есть это сообщение
}
if let originID = envelope.originID,
messages.contains(where: {
$0.conversationID == envelope.peerJID.lowercased() && $0.originID == originID
}) {
messageIndex(originID: originID, conversationID: envelope.peerJID) != nil {
mergeServerIdentity(from: envelope, matchingOriginID: originID)
return
}
@@ -2049,9 +2048,10 @@ final class AppModel: ObservableObject {
from envelope: XMPPService.MessageEnvelope,
matchingOriginID originID: String
) {
guard let index = messages.firstIndex(where: {
$0.conversationID == envelope.peerJID.lowercased() && $0.originID == originID
}) else { return }
guard let index = messageIndex(
originID: originID,
conversationID: envelope.peerJID
) else { return }
if messages[index].stanzaID == nil {
messages[index].stanzaID = envelope.stanzaID
}
@@ -2520,6 +2520,13 @@ final class AppModel: ObservableObject {
conversationID: merged.conversationID
)] = index
}
if let originID = merged.originID {
messageIndexByOriginKey[
Self.localDeletionKey(
messageID: originID,
conversationID: merged.conversationID
)] = index
}
updateConversationPreview(for: merged, incrementUnread: false)
return false
}
@@ -2540,6 +2547,13 @@ final class AppModel: ObservableObject {
conversationID: message.conversationID
)] = insertedIndex
}
if let originID = message.originID {
messageIndexByOriginKey[
Self.localDeletionKey(
messageID: originID,
conversationID: message.conversationID
)] = insertedIndex
}
let shouldIncrement =
unreadOverride
?? (message.direction == .incoming && selectedConversationID != message.conversationID)
@@ -2626,20 +2640,41 @@ final class AppModel: ObservableObject {
messageID: referenceID,
conversationID: normalizedConversationID
)
if let index = messageIndexByStanzaKey[key],
messages.indices.contains(index),
guard let index = messageIndexByStanzaKey[key] else { return nil }
if messages.indices.contains(index),
messages[index].conversationID == normalizedConversationID,
messages[index].stanzaID == referenceID
{
return index
}
// Only rebuild when the key exists but points at a stale entry. A
// genuine miss is expected during archive catch-up for every new
// stanza-id and must not trigger an O(n) rebuild per message.
rebuildMessageIndex()
return messageIndexByStanzaKey[key]
}
private func messageIndex(originID: String, conversationID: String) -> Int? {
let normalizedConversationID = conversationID.lowercased()
let key = Self.localDeletionKey(
messageID: originID,
conversationID: normalizedConversationID
)
guard let index = messageIndexByOriginKey[key] else { return nil }
if messages.indices.contains(index),
messages[index].conversationID == normalizedConversationID,
messages[index].originID == originID
{
return index
}
rebuildMessageIndex()
return messageIndexByOriginKey[key]
}
private func rebuildMessageIndex() {
messageIndexByStorageKey.removeAll(keepingCapacity: true)
messageIndexByStanzaKey.removeAll(keepingCapacity: true)
messageIndexByOriginKey.removeAll(keepingCapacity: true)
firstMessageIndexByID.removeAll(keepingCapacity: true)
for (index, message) in messages.enumerated() {
let key = Self.localDeletionKey(
@@ -2661,6 +2696,15 @@ final class AppModel: ObservableObject {
messageIndexByStanzaKey[stanzaKey] = index
}
}
if let originID = message.originID {
let originKey = Self.localDeletionKey(
messageID: originID,
conversationID: message.conversationID
)
if messageIndexByOriginKey[originKey] == nil {
messageIndexByOriginKey[originKey] = index
}
}
}
}