mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 18:45:20 +00:00
Optimization 4: Batch message updates for better performance
- Added message batching system with 100ms window - Created pendingMessages array for public messages - Created pendingPrivateMessages dictionary for private messages - Added scheduleBatchFlush() to consolidate updates - Batch all incoming messages from didReceiveMessage - Batch system messages (connect/disconnect/screenshot) - Keep immediate updates for user's own sent messages - Single UI update per batch instead of per message - Flush pending messages on app background/terminate - Clear timer on deinit This reduces UI updates when multiple messages arrive quickly
This commit is contained in:
@@ -19,6 +19,12 @@ class ChatViewModel: ObservableObject {
|
|||||||
@Published var messages: [BitchatMessage] = []
|
@Published var messages: [BitchatMessage] = []
|
||||||
private let maxMessages = 1337 // Maximum messages before oldest are removed
|
private let maxMessages = 1337 // Maximum messages before oldest are removed
|
||||||
@Published var connectedPeers: [String] = []
|
@Published var connectedPeers: [String] = []
|
||||||
|
|
||||||
|
// 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
|
||||||
@Published var nickname: String = ""
|
@Published var nickname: String = ""
|
||||||
@Published var isConnected = false
|
@Published var isConnected = false
|
||||||
@Published var privateChats: [String: [BitchatMessage]] = [:] // peerID -> messages
|
@Published var privateChats: [String: [BitchatMessage]] = [:] // peerID -> messages
|
||||||
@@ -148,6 +154,9 @@ class ChatViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
// Clean up timer
|
||||||
|
messageBatchTimer?.invalidate()
|
||||||
|
|
||||||
// Force immediate save
|
// Force immediate save
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
@@ -378,10 +387,13 @@ class ChatViewModel: ObservableObject {
|
|||||||
mentions: mentions.isEmpty ? nil : mentions,
|
mentions: mentions.isEmpty ? nil : mentions,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add to main messages
|
// Add to main messages immediately for user feedback
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
trimMessagesIfNeeded()
|
trimMessagesIfNeeded()
|
||||||
|
|
||||||
|
// Force immediate UI update for user's own messages
|
||||||
|
objectWillChange.send()
|
||||||
|
|
||||||
// Send via mesh with mentions
|
// Send via mesh with mentions
|
||||||
meshService.sendMessage(content, mentions: mentions)
|
meshService.sendMessage(content, mentions: mentions)
|
||||||
}
|
}
|
||||||
@@ -433,7 +445,7 @@ class ChatViewModel: ObservableObject {
|
|||||||
let isFavorite = isFavorite(peerID: peerID)
|
let isFavorite = isFavorite(peerID: peerID)
|
||||||
DeliveryTracker.shared.trackMessage(message, recipientID: peerID, recipientNickname: recipientNickname, isFavorite: isFavorite)
|
DeliveryTracker.shared.trackMessage(message, recipientID: peerID, recipientNickname: recipientNickname, isFavorite: isFavorite)
|
||||||
|
|
||||||
// Trigger UI update
|
// Immediate UI update for user's own messages
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
|
|
||||||
// Send via mesh with the same message ID
|
// Send via mesh with the same message ID
|
||||||
@@ -583,16 +595,21 @@ class ChatViewModel: ObservableObject {
|
|||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
isRelay: false
|
isRelay: false
|
||||||
)
|
)
|
||||||
messages.append(localNotification)
|
// System messages can be batched
|
||||||
trimMessagesIfNeeded()
|
addMessageToBatch(localNotification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillResignActive() {
|
@objc private func appWillResignActive() {
|
||||||
|
// Flush any pending messages when app goes to background
|
||||||
|
flushMessageBatchImmediately()
|
||||||
|
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func applicationWillTerminate() {
|
@objc func applicationWillTerminate() {
|
||||||
|
// Flush any pending messages immediately
|
||||||
|
flushMessageBatchImmediately()
|
||||||
|
|
||||||
// Verify identity key is still there
|
// Verify identity key is still there
|
||||||
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
||||||
@@ -604,6 +621,9 @@ class ChatViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillTerminate() {
|
@objc private func appWillTerminate() {
|
||||||
|
// Flush any pending messages immediately
|
||||||
|
flushMessageBatchImmediately()
|
||||||
|
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,6 +727,9 @@ class ChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
// PANIC: Emergency data clearing for activist safety
|
// PANIC: Emergency data clearing for activist safety
|
||||||
func panicClearAllData() {
|
func panicClearAllData() {
|
||||||
|
// Flush any pending messages immediately before clearing
|
||||||
|
flushMessageBatchImmediately()
|
||||||
|
|
||||||
// Clear all messages
|
// Clear all messages
|
||||||
messages.removeAll()
|
messages.removeAll()
|
||||||
privateChats.removeAll()
|
privateChats.removeAll()
|
||||||
@@ -1280,6 +1303,82 @@ class ChatViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Message Batching
|
||||||
|
|
||||||
|
private func addMessageToBatch(_ message: BitchatMessage) {
|
||||||
|
pendingMessages.append(message)
|
||||||
|
scheduleBatchFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addPrivateMessageToBatch(_ message: BitchatMessage, for peerID: String) {
|
||||||
|
if pendingPrivateMessages[peerID] == nil {
|
||||||
|
pendingPrivateMessages[peerID] = []
|
||||||
|
}
|
||||||
|
pendingPrivateMessages[peerID]?.append(message)
|
||||||
|
scheduleBatchFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleBatchFlush() {
|
||||||
|
// Cancel existing timer
|
||||||
|
messageBatchTimer?.invalidate()
|
||||||
|
|
||||||
|
// Schedule new flush
|
||||||
|
messageBatchTimer = Timer.scheduledTimer(withTimeInterval: messageBatchInterval, repeats: false) { [weak self] _ in
|
||||||
|
self?.flushMessageBatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 UI update for all changes
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force immediate flush for high-priority messages
|
||||||
|
private func flushMessageBatchImmediately() {
|
||||||
|
messageBatchTimer?.invalidate()
|
||||||
|
flushMessageBatch()
|
||||||
|
}
|
||||||
|
|
||||||
// Update encryption status in appropriate places, not during view updates
|
// Update encryption status in appropriate places, not during view updates
|
||||||
private func updateEncryptionStatus(for peerID: String) {
|
private func updateEncryptionStatus(for peerID: String) {
|
||||||
let noiseService = meshService.getNoiseService()
|
let noiseService = meshService.getNoiseService()
|
||||||
@@ -1897,16 +1996,11 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
privateChats[peerID]?.append(messageToStore)
|
// Use batching for private messages
|
||||||
// Sort messages by timestamp to ensure proper ordering
|
addPrivateMessageToBatch(messageToStore, for: peerID)
|
||||||
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
|
|
||||||
trimPrivateChatMessagesIfNeeded(for: peerID)
|
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
|
|
||||||
// Trigger UI update for private chats
|
|
||||||
objectWillChange.send()
|
|
||||||
|
|
||||||
// Check if we're in a private chat with this peer's fingerprint
|
// Check if we're in a private chat with this peer's fingerprint
|
||||||
// This handles reconnections with new peer IDs
|
// This handles reconnections with new peer IDs
|
||||||
if let chatFingerprint = selectedPrivateChatFingerprint,
|
if let chatFingerprint = selectedPrivateChatFingerprint,
|
||||||
@@ -1974,10 +2068,7 @@ extension ChatViewModel: 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 {
|
||||||
messages.append(finalMessage)
|
addMessageToBatch(finalMessage)
|
||||||
// Sort messages by timestamp to ensure proper ordering
|
|
||||||
messages.sort { $0.timestamp < $1.timestamp }
|
|
||||||
trimMessagesIfNeeded()
|
|
||||||
}
|
}
|
||||||
} 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)
|
||||||
@@ -1998,17 +2089,13 @@ extension ChatViewModel: 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 {
|
||||||
messages.append(finalMessage)
|
addMessageToBatch(finalMessage)
|
||||||
messages.sort { $0.timestamp < $1.timestamp }
|
|
||||||
trimMessagesIfNeeded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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 {
|
||||||
messages.append(finalMessage)
|
addMessageToBatch(finalMessage)
|
||||||
messages.sort { $0.timestamp < $1.timestamp }
|
|
||||||
trimMessagesIfNeeded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2134,11 +2221,8 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil
|
originalSender: nil
|
||||||
)
|
)
|
||||||
messages.append(systemMessage)
|
// Batch system messages
|
||||||
trimMessagesIfNeeded()
|
addMessageToBatch(systemMessage)
|
||||||
|
|
||||||
// Force UI update
|
|
||||||
objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func didDisconnectFromPeer(_ peerID: String) {
|
func didDisconnectFromPeer(_ peerID: String) {
|
||||||
@@ -2169,11 +2253,8 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil
|
originalSender: nil
|
||||||
)
|
)
|
||||||
messages.append(systemMessage)
|
// Batch system messages
|
||||||
trimMessagesIfNeeded()
|
addMessageToBatch(systemMessage)
|
||||||
|
|
||||||
// Force UI update
|
|
||||||
objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func didUpdatePeerList(_ peers: [String]) {
|
func didUpdatePeerList(_ peers: [String]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user