perf(xmpp): fix slow MAM/MUC-MAM and move OMEMO decryption off the main
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:
1 parent
dee7e94e92
commit
9b56d2ebcf
8 files changed
+329
-167
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,14 @@ import Foundation
|
||||
enum ArchiveMessageBatchPolicy {
|
||||
static let pageSize = 24
|
||||
static let bootstrapMessageLimit = 40
|
||||
static let decodeSliceSize = 1
|
||||
/// Decrypt this many archived stanzas per main-actor slice before yielding.
|
||||
/// One-by-one decryption turns a large MAM catch-up into hours of serialized
|
||||
/// work; a small batch keeps frames mostly intact while cutting the number
|
||||
/// of run-loop hand-offs by the same factor.
|
||||
static let decodeSliceSize = 8
|
||||
static let maximumBufferedStanzas = 48
|
||||
/// Leave roughly half of a 60 Hz frame between archived decryptions. This
|
||||
/// keeps UIScrollView touch handling responsive even when OMEMO work is
|
||||
/// unusually expensive on older devices.
|
||||
static let interSliceDelayNanoseconds: UInt64 = 8_000_000
|
||||
/// A short yield between archived decryption slices keeps touch handling
|
||||
/// responsive without the 8 ms per single message that previously dominated
|
||||
/// catch-up time.
|
||||
static let interSliceDelayNanoseconds: UInt64 = 1_000_000
|
||||
}
|
||||
@@ -5,8 +5,15 @@ import Foundation
|
||||
enum ArchiveSyncRecoveryPolicy {
|
||||
static let incrementalOverlap: TimeInterval = 60
|
||||
static let interPageDelayNanoseconds: UInt64 = 100_000_000
|
||||
static let queryTimeoutNanoseconds: UInt64 = 12_000_000_000
|
||||
static let pageApplyTimeoutNanoseconds: UInt64 = 8_000_000_000
|
||||
static let queryTimeoutNanoseconds: UInt64 = 20_000_000_000
|
||||
static let pageApplyTimeoutNanoseconds: UInt64 = 20_000_000_000
|
||||
static let pageRetryLimit = 1
|
||||
static let resumeAfterCaptureDelayNanoseconds: UInt64 = 3_000_000_000
|
||||
/// Delay before automatically resuming a failed catch-up pass while the
|
||||
/// app stays connected and foregrounded. Prevents a single slow page from
|
||||
/// silently leaving history unloaded until the next app activation.
|
||||
static let retryAfterFailureDelayNanoseconds: UInt64 = 20_000_000_000
|
||||
/// Consecutive automatic catch-up retries before falling back to
|
||||
/// resume-on-activation, so a persistently broken server cannot loop.
|
||||
static let maximumAutomaticRetries = 3
|
||||
}
|
||||
Reference in new issue
Block a user