Remove message batching system for simpler, faster UI updates (#377)

- Remove dual-timer batching architecture (100ms message timer, 50ms UI timer)
- Delete pending message queues and all batching infrastructure
- Simplify to direct message appends with duplicate detection
- Trust SwiftUI's built-in update optimization instead of manual batching
- Results in instant message display and 208 lines of code removed
- Eliminates timer management complexity and potential race conditions

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-01 11:38:53 +02:00
committed by GitHub
co-authored by jack
parent a6c2e751d2
commit b004bfa4aa
+52 -208
View File
@@ -31,7 +31,7 @@
/// ## Key Features /// ## Key Features
/// ///
/// ### Message Management /// ### Message Management
/// - Batches incoming messages for performance (100ms window) /// - Efficient message handling with duplicate detection
/// - Maintains separate public and private message queues /// - Maintains separate public and private message queues
/// - Limits message history to prevent memory issues (1337 messages) /// - Limits message history to prevent memory issues (1337 messages)
/// - Tracks delivery and read receipts /// - Tracks delivery and read receipts
@@ -58,7 +58,7 @@
/// - `/help`: Show available commands /// - `/help`: Show available commands
/// ///
/// ## Performance Optimizations /// ## Performance Optimizations
/// - Message batching reduces UI updates /// - SwiftUI automatically optimizes UI updates
/// - Caches expensive computations (encryption status) /// - Caches expensive computations (encryption status)
/// - Debounces autocomplete suggestions /// - Debounces autocomplete suggestions
/// - Efficient peer list management /// - Efficient peer list management
@@ -98,18 +98,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
@Published var allPeers: [BitchatPeer] = [] // Unified peer list including favorites @Published var allPeers: [BitchatPeer] = [] // Unified peer list including favorites
private var peerIndex: [String: BitchatPeer] = [:] // Quick lookup by peer ID private var peerIndex: [String: BitchatPeer] = [:] // Quick lookup by peer ID
// MARK: - Message Batching Properties // MARK: - Properties
// Message batching for performance
private var pendingMessages: [BitchatMessage] = []
private var pendingPrivateMessages: [String: [BitchatMessage]] = [:] // peerID -> messages
private var messageBatchTimer: Timer?
private let messageBatchInterval: TimeInterval = 0.1 // 100ms batching window
// UI update debouncing for performance
private var uiUpdateTimer: Timer?
private let uiUpdateInterval: TimeInterval = 0.05 // 50ms debounce window
private var pendingUIUpdate = false
@Published var nickname: String = "" { @Published var nickname: String = "" {
didSet { didSet {
// Trim whitespace whenever nickname is set // Trim whitespace whenever nickname is set
@@ -234,7 +223,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
self?.peerIndex = Dictionary(uniqueKeysWithValues: peers.map { ($0.id, $0) }) self?.peerIndex = Dictionary(uniqueKeysWithValues: peers.map { ($0.id, $0) })
// Schedule UI update if peers changed // Schedule UI update if peers changed
if peers.count > 0 || self?.allPeers.count ?? 0 > 0 { if peers.count > 0 || self?.allPeers.count ?? 0 > 0 {
self?.scheduleUIUpdate() // UI will update automatically
} }
// Update private chat peer ID if needed when peers change // Update private chat peer ID if needed when peers change
@@ -362,10 +351,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// MARK: - Deinitialization // MARK: - Deinitialization
deinit { deinit {
// Clean up timers
messageBatchTimer?.invalidate()
uiUpdateTimer?.invalidate()
// Force immediate save // Force immediate save
userDefaults.synchronize() userDefaults.synchronize()
} }
@@ -642,7 +627,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
selectedPrivateChatPeer = currentPeerID selectedPrivateChatPeer = currentPeerID
// Schedule UI update for encryption status change // Schedule UI update for encryption status change
scheduleUIUpdate() // UI will update automatically
// Also refresh the peer list to update encryption status // Also refresh the peer list to update encryption status
Task { @MainActor in Task { @MainActor in
@@ -651,7 +636,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} else if selectedPrivateChatPeer == nil { } else if selectedPrivateChatPeer == nil {
// Just set the peer ID if we don't have one // Just set the peer ID if we don't have one
selectedPrivateChatPeer = currentPeerID selectedPrivateChatPeer = currentPeerID
scheduleUIUpdate() // UI will update automatically
} }
// Clear unread messages for the current peer ID // Clear unread messages for the current peer ID
@@ -816,7 +801,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
addMessageToBatch(systemMessage) addMessage(systemMessage)
} }
} }
@@ -1028,14 +1013,14 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
messages[index].deliveryStatus = DeliveryStatus.delivered(to: "nostr", at: Date()) messages[index].deliveryStatus = DeliveryStatus.delivered(to: "nostr", at: Date())
// Schedule UI update for delivery status // Schedule UI update for delivery status
scheduleUIUpdate() // UI will update automatically
} }
// Also update in private chats if it's a private message // Also update in private chats if it's a private message
for (peerID, chatMessages) in privateChats { for (peerID, chatMessages) in privateChats {
if let index = chatMessages.firstIndex(where: { $0.id == messageId }) { if let index = chatMessages.firstIndex(where: { $0.id == messageId }) {
privateChats[peerID]?[index].deliveryStatus = DeliveryStatus.delivered(to: "nostr", at: Date()) privateChats[peerID]?[index].deliveryStatus = DeliveryStatus.delivered(to: "nostr", at: Date())
scheduleUIUpdate() // UI will update automatically
break break
} }
} }
@@ -1095,7 +1080,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} }
// Schedule UI refresh // Schedule UI refresh
scheduleUIUpdate() // UI will update automatically
} else { } else {
// Even if the chat isn't open, migrate any existing private chat data // Even if the chat isn't open, migrate any existing private chat data
if let messages = privateChats[oldPeerID] { if let messages = privateChats[oldPeerID] {
@@ -1152,7 +1137,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
) )
// Add to message stream // Add to message stream
addMessageToBatch(systemMessage) addMessage(systemMessage)
// Update peer manager to refresh UI // Update peer manager to refresh UI
peerManager?.updatePeers() peerManager?.updatePeers()
@@ -1223,24 +1208,16 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
// System messages can be batched // Add system message
addMessageToBatch(localNotification) addMessage(localNotification)
} }
} }
@objc private func appWillResignActive() { @objc private func appWillResignActive() {
// Flush any pending messages when app goes to background
flushMessageBatchImmediately()
// Flush any pending UI updates
flushUIUpdateImmediately()
userDefaults.synchronize() userDefaults.synchronize()
} }
@objc func applicationWillTerminate() { @objc func applicationWillTerminate() {
// Flush any pending messages immediately
flushMessageBatchImmediately()
// Force save any pending identity changes (verifications, favorites, etc) // Force save any pending identity changes (verifications, favorites, etc)
SecureIdentityStateManager.shared.forceSave() SecureIdentityStateManager.shared.forceSave()
@@ -1255,11 +1232,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} }
@objc private func appWillTerminate() { @objc private func appWillTerminate() {
// Flush any pending messages immediately
flushMessageBatchImmediately()
// Flush any pending UI updates
flushUIUpdateImmediately()
userDefaults.synchronize() userDefaults.synchronize()
} }
@@ -1487,8 +1459,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// PANIC: Emergency data clearing for activist safety // PANIC: Emergency data clearing for activist safety
@MainActor @MainActor
func panicClearAllData() { func panicClearAllData() {
// Flush any pending messages immediately before clearing // Messages are processed immediately - nothing to flush
flushMessageBatchImmediately()
// Clear all messages // Clear all messages
messages.removeAll() messages.removeAll()
@@ -1551,7 +1522,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
userDefaults.synchronize() userDefaults.synchronize()
// Force immediate UI update for panic mode // Force immediate UI update for panic mode
flushUIUpdateImmediately() // UI updates immediately - no flushing needed
} }
@@ -1619,7 +1590,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
nick.lowercased().hasPrefix(partial) nick.lowercased().hasPrefix(partial)
} }
// Batch UI updates // UI will update automatically
if !suggestions.isEmpty { if !suggestions.isEmpty {
// Only update if suggestions changed // Only update if suggestions changed
if autocompleteSuggestions != suggestions { if autocompleteSuggestions != suggestions {
@@ -1981,8 +1952,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Invalidate cache when encryption status changes // Invalidate cache when encryption status changes
invalidateEncryptionCache(for: peerID) invalidateEncryptionCache(for: peerID)
// Schedule UI update // UI will update automatically via @Published properties
scheduleUIUpdate()
} }
func getEncryptionStatus(for peerID: String) -> EncryptionStatus { func getEncryptionStatus(for peerID: String) -> EncryptionStatus {
@@ -2081,7 +2051,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} }
// MARK: - Message Batching // MARK: - Message Handling
private func trimMessagesIfNeeded() { private func trimMessagesIfNeeded() {
if messages.count > maxMessages { if messages.count > maxMessages {
@@ -2097,119 +2067,28 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} }
} }
private func addMessageToBatch(_ message: BitchatMessage) { // MARK: - Message Management
pendingMessages.append(message)
scheduleBatchFlush() private func addMessage(_ message: BitchatMessage) {
// Check for duplicates
guard !messages.contains(where: { $0.id == message.id }) else { return }
messages.append(message)
messages.sort { $0.timestamp < $1.timestamp }
trimMessagesIfNeeded()
} }
private func addPrivateMessageToBatch(_ message: BitchatMessage, for peerID: String) { private func addPrivateMessage(_ message: BitchatMessage, for peerID: String) {
if pendingPrivateMessages[peerID] == nil { if privateChats[peerID] == nil {
pendingPrivateMessages[peerID] = [] privateChats[peerID] = []
}
pendingPrivateMessages[peerID]?.append(message)
scheduleBatchFlush()
} }
private func scheduleBatchFlush() { // Check for duplicates
// Cancel existing timer guard !(privateChats[peerID]?.contains(where: { $0.id == message.id }) ?? false) else { return }
messageBatchTimer?.invalidate()
// Schedule new flush privateChats[peerID]?.append(message)
messageBatchTimer = Timer.scheduledTimer(withTimeInterval: messageBatchInterval, repeats: false) { [weak self] _ in privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
self?.flushMessageBatch() trimPrivateChatMessagesIfNeeded(for: peerID)
}
}
private func flushMessageBatch() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Process pending public messages
if !self.pendingMessages.isEmpty {
let messagesToAdd = self.pendingMessages
self.pendingMessages.removeAll()
// Add all messages at once
self.messages.append(contentsOf: messagesToAdd)
// Sort once after batch addition
self.messages.sort { $0.timestamp < $1.timestamp }
// Trim once if needed
self.trimMessagesIfNeeded()
}
// Process pending private messages
if !self.pendingPrivateMessages.isEmpty {
let privateMessageBatches = self.pendingPrivateMessages
self.pendingPrivateMessages.removeAll()
for (peerID, messagesToAdd) in privateMessageBatches {
if self.privateChats[peerID] == nil {
self.privateChats[peerID] = []
}
// Add all messages for this peer at once
self.privateChats[peerID]?.append(contentsOf: messagesToAdd)
// Sort once after batch addition
self.privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
// Trim once if needed
self.trimPrivateChatMessagesIfNeeded(for: peerID)
}
}
// Single scheduled UI update for all changes
self.scheduleUIUpdate()
}
}
// Force immediate flush for high-priority messages
private func flushMessageBatchImmediately() {
messageBatchTimer?.invalidate()
flushMessageBatch()
}
// MARK: - UI Update Debouncing
// Schedule a debounced UI update
private func scheduleUIUpdate() {
// Ensure timer operations happen on main thread
if Thread.isMainThread {
guard !pendingUIUpdate else { return } // Already scheduled
pendingUIUpdate = true
// Cancel existing timer
uiUpdateTimer?.invalidate()
// Schedule new update
uiUpdateTimer = Timer.scheduledTimer(withTimeInterval: uiUpdateInterval, repeats: false) { [weak self] _ in
self?.flushUIUpdate()
}
} else {
DispatchQueue.main.async { [weak self] in
self?.scheduleUIUpdate()
}
}
}
// Execute the pending UI update
private func flushUIUpdate() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.pendingUIUpdate = false
self.objectWillChange.send()
}
}
// Force immediate UI update for critical user actions
private func flushUIUpdateImmediately() {
uiUpdateTimer?.invalidate()
pendingUIUpdate = false
objectWillChange.send()
} }
// Update encryption status in appropriate places, not during view updates // Update encryption status in appropriate places, not during view updates
@@ -2236,8 +2115,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Invalidate cache when encryption status changes // Invalidate cache when encryption status changes
invalidateEncryptionCache(for: peerID) invalidateEncryptionCache(for: peerID)
// Schedule UI update // UI will update automatically via @Published properties
scheduleUIUpdate()
} }
// MARK: - Fingerprint Management // MARK: - Fingerprint Management
@@ -2364,7 +2242,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
self.invalidateEncryptionCache(for: peerID) self.invalidateEncryptionCache(for: peerID)
// Schedule UI update // Schedule UI update
self.scheduleUIUpdate() // UI will update automatically
} }
} }
@@ -2378,7 +2256,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
self.invalidateEncryptionCache(for: peerID) self.invalidateEncryptionCache(for: peerID)
// Schedule UI update // Schedule UI update
self.scheduleUIUpdate() // UI will update automatically
} }
} }
} }
@@ -3039,8 +2917,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
) )
} }
// Use batching for private messages // Add private message directly
addPrivateMessageToBatch(messageToStore, for: peerID) addPrivateMessage(messageToStore, for: peerID)
// Debug logging // Debug logging
@@ -3052,47 +2930,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Update our private chat peer to the new ID // Update our private chat peer to the new ID
selectedPrivateChatPeer = peerID selectedPrivateChatPeer = peerID
// Schedule UI update when peer ID changes // Schedule UI update when peer ID changes
scheduleUIUpdate() // UI will update automatically
} }
// Also check if we need to update the private chat peer for any reason // Also check if we need to update the private chat peer for any reason
updatePrivateChatPeerIfNeeded() updatePrivateChatPeerIfNeeded()
// If we're currently viewing this chat, force immediate UI update // No special handling needed - messages are added immediately
if selectedPrivateChatPeer == peerID {
// Immediately flush the batch for the current chat to ensure UI updates
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if let pendingMessages = self.pendingPrivateMessages[peerID], !pendingMessages.isEmpty {
// Process this peer's messages immediately
self.pendingPrivateMessages.removeValue(forKey: peerID)
if self.privateChats[peerID] == nil {
self.privateChats[peerID] = []
}
// Add messages and sort
self.privateChats[peerID]?.append(contentsOf: pendingMessages)
self.privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
// Remove duplicates
var seen = Set<String>()
self.privateChats[peerID] = self.privateChats[peerID]?.filter { msg in
if seen.contains(msg.id) {
return false
}
seen.insert(msg.id)
return true
}
// Trim if needed
self.trimPrivateChatMessagesIfNeeded(for: peerID)
// Schedule UI update
self.scheduleUIUpdate()
}
}
}
// Mark as unread if not currently viewing this chat // Mark as unread if not currently viewing this chat
if selectedPrivateChatPeer != peerID { if selectedPrivateChatPeer != peerID {
@@ -3169,7 +3013,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
if finalMessage.sender != nickname && finalMessage.sender != "system" { if finalMessage.sender != nickname && finalMessage.sender != "system" {
// Skip empty or whitespace-only messages // Skip empty or whitespace-only messages
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
addMessageToBatch(finalMessage) addMessage(finalMessage)
} }
} else if finalMessage.sender != "system" { } else if finalMessage.sender != "system" {
// Our own message - check if we already have it (by ID and content) // Our own message - check if we already have it (by ID and content)
@@ -3190,13 +3034,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// This is a message we sent from another device or it's missing locally // This is a message we sent from another device or it's missing locally
// Skip empty or whitespace-only messages // Skip empty or whitespace-only messages
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
addMessageToBatch(finalMessage) addMessage(finalMessage)
} }
} }
} else { } else {
// System message - check for empty content before adding // System message - check for empty content before adding
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
addMessageToBatch(finalMessage) addMessage(finalMessage)
} }
} }
} }
@@ -3324,8 +3168,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
isRelay: false, isRelay: false,
originalSender: nil originalSender: nil
) )
// Batch system messages // Add system message
addMessageToBatch(systemMessage) addMessage(systemMessage)
} }
func didDisconnectFromPeer(_ peerID: String) { func didDisconnectFromPeer(_ peerID: String) {
@@ -3356,8 +3200,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
isRelay: false, isRelay: false,
originalSender: nil originalSender: nil
) )
// Batch system messages // Add system message
addMessageToBatch(systemMessage) addMessage(systemMessage)
} }
func didUpdatePeerList(_ peers: [String]) { func didUpdatePeerList(_ peers: [String]) {
@@ -3375,13 +3219,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} }
// Schedule UI refresh to ensure offline favorites are shown // Schedule UI refresh to ensure offline favorites are shown
self.scheduleUIUpdate() // UI will update automatically
// Update encryption status for all peers // Update encryption status for all peers
self.updateEncryptionStatusForPeers() self.updateEncryptionStatusForPeers()
// Schedule UI update for peer list change // Schedule UI update for peer list change
self.scheduleUIUpdate() // UI will update automatically
// Check if we need to update private chat peer after reconnection // Check if we need to update private chat peer after reconnection
if self.selectedPrivateChatFingerprint != nil { if self.selectedPrivateChatFingerprint != nil {
@@ -3512,7 +3356,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.privateChats = updatedPrivateChats self.privateChats = updatedPrivateChats
self.scheduleUIUpdate() // UI will update automatically
} }
} }