From a6c2e751d273bed20f48798a8d83d64da071cf76 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:05:59 +0200 Subject: [PATCH] Performance optimizations: LazyVStack, UI batching, timer consolidation (#375) - Replace VStack with LazyVStack for message list to enable on-demand rendering - Batch UI updates in ChatViewModel to reduce main thread operations - Consolidate 12 timers into 3 (high/medium/low frequency) in BluetoothMeshService - Fix thread safety in scheduleUIUpdate with main thread check These changes significantly improve app responsiveness and reduce CPU usage. Co-authored-by: jack --- bitchat/Services/BluetoothMeshService.swift | 119 +++++++++++++++++++- bitchat/ViewModels/ChatViewModel.swift | 43 ++++--- bitchat/Views/ContentView.swift | 2 +- 3 files changed, 140 insertions(+), 24 deletions(-) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index d77b06fe..6e1db1a6 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -765,6 +765,17 @@ class BluetoothMeshService: NSObject { private var messageBloomFilter = OptimizedBloomFilter(expectedItems: 2000, falsePositiveRate: 0.01) private var bloomFilterResetTimer: Timer? + // MARK: - Consolidated Timers + + // Consolidated timers for better performance + private var highFrequencyTimer: Timer? // 2s - critical real-time tasks + private var mediumFrequencyTimer: Timer? // 20s - regular maintenance + private var lowFrequencyTimer: Timer? // 5min - cleanup and optimization + + // Track execution counters for tasks that don't run every timer tick + private var mediumTimerTickCount = 0 + private var lowTimerTickCount = 0 + // MARK: - Network Size Estimation // Network size estimation @@ -1493,7 +1504,10 @@ class BluetoothMeshService: NSObject { // Setup app state notifications setupAppStateNotifications() - // Start bloom filter reset timer (reset every 5 minutes) + // Setup consolidated timers for better performance + setupConsolidatedTimers() + + /* OLD TIMER SETUP - REPLACED BY CONSOLIDATED TIMERS bloomFilterResetTimer = Timer.scheduledTimer(withTimeInterval: 300.0, repeats: true) { [weak self] _ in self?.messageQueue.async(flags: .barrier) { guard let self = self else { return } @@ -1591,6 +1605,7 @@ class BluetoothMeshService: NSObject { self.handshakeCoordinator.logHandshakeStates() #endif } + END OF OLD TIMER SETUP */ // Schedule first peer ID rotation scheduleNextRotation() @@ -1652,6 +1667,13 @@ class BluetoothMeshService: NSObject { deinit { cleanup() + + // Invalidate consolidated timers + highFrequencyTimer?.invalidate() + mediumFrequencyTimer?.invalidate() + lowFrequencyTimer?.invalidate() + + // Invalidate other timers scanDutyCycleTimer?.invalidate() batteryMonitorTimer?.invalidate() coverTrafficTimer?.invalidate() @@ -1807,6 +1829,101 @@ class BluetoothMeshService: NSObject { } } + // MARK: - Consolidated Timer Management + + private func setupConsolidatedTimers() { + // High-frequency timer (2s) - critical real-time tasks + highFrequencyTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + + // Critical real-time tasks that need frequent checking + self.cleanupUnknownPeers() + self.checkAckTimeouts() + } + + // Medium-frequency timer (20s) - regular maintenance + mediumFrequencyTimer = Timer.scheduledTimer(withTimeInterval: 20.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.mediumTimerTickCount += 1 + + // Every 20s: Connection keep-alive + self.sendKeepAlivePings() + + // Every 20s: Peer availability check + self.checkPeerAvailability() + + // Every 40s (every 2 ticks): Stale peer cleanup & write queue cleanup + if self.mediumTimerTickCount % 2 == 0 { + self.cleanupStalePeers() + self.cleanExpiredWriteQueues() + } + } + + // Low-frequency timer (5min) - cleanup and optimization + lowFrequencyTimer = Timer.scheduledTimer(withTimeInterval: 300.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.lowTimerTickCount += 1 + + // Every 5min: Bloom filter reset and processed message cleanup + self.messageQueue.async(flags: .barrier) { [weak self] in + guard let self = self else { return } + + // Adapt Bloom filter size based on network size + let networkSize = self.estimatedNetworkSize + self.messageBloomFilter = OptimizedBloomFilter.adaptive(for: networkSize) + + // Clean up old processed messages (keep last 10 minutes of messages) + self.processedMessagesLock.lock() + let cutoffTime = Date().addingTimeInterval(-600) // 10 minutes ago + let originalCount = self.processedMessages.count + self.processedMessages = self.processedMessages.filter { _, state in + state.firstSeen > cutoffTime + } + + // Also enforce max size limit during cleanup + if self.processedMessages.count > self.maxProcessedMessages { + let targetSize = Int(Double(self.maxProcessedMessages) * 0.8) + let sortedByFirstSeen = self.processedMessages.sorted { $0.value.firstSeen < $1.value.firstSeen } + let toKeep = Array(sortedByFirstSeen.suffix(targetSize)) + self.processedMessages = Dictionary(uniqueKeysWithValues: toKeep) + } + + let removedCount = originalCount - self.processedMessages.count + self.processedMessagesLock.unlock() + + if removedCount > 0 { + SecureLogger.log("🧹 Cleaned up \(removedCount) old processed messages (kept \(self.processedMessages.count) from last 10 minutes)", + category: SecureLogger.session, level: .debug) + } + } + + // Every 5min: Memory cleanup + self.performMemoryCleanup() + + // Every 10min (every 2 ticks): Version cache and dedup cleanup + if self.lowTimerTickCount % 2 == 0 { + self.cleanupExpiredVersionCache() + self.cleanupExpiredDedupEntries() + + // Clean up stale handshakes + let stalePeerIDs = self.handshakeCoordinator.cleanupStaleHandshakes() + if !stalePeerIDs.isEmpty { + for peerID in stalePeerIDs { + // Also remove from noise service + self.cleanupPeerCryptoState(peerID) + SecureLogger.log("Cleaned up stale handshake for peer: \(peerID)", + category: SecureLogger.handshake, level: .info) + } + } + } + + // Every 60min (every 12 ticks): Identity cache cleanup + if self.lowTimerTickCount % 12 == 0 { + self.cleanExpiredIdentityCache() + } + } + } + // MARK: - Message Sending func sendBroadcastAnnounce() { diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index be60587c..24092945 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -642,9 +642,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { selectedPrivateChatPeer = currentPeerID // Schedule UI update for encryption status change - DispatchQueue.main.async { [weak self] in - self?.scheduleUIUpdate() - } + scheduleUIUpdate() // Also refresh the peer list to update encryption status Task { @MainActor in @@ -653,9 +651,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { } else if selectedPrivateChatPeer == nil { // Just set the peer ID if we don't have one selectedPrivateChatPeer = currentPeerID - DispatchQueue.main.async { [weak self] in - self?.scheduleUIUpdate() - } + scheduleUIUpdate() } // Clear unread messages for the current peer ID @@ -1986,9 +1982,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { invalidateEncryptionCache(for: peerID) // Schedule UI update - DispatchQueue.main.async { [weak self] in - self?.scheduleUIUpdate() - } + scheduleUIUpdate() } func getEncryptionStatus(for peerID: String) -> EncryptionStatus { @@ -2181,16 +2175,23 @@ class ChatViewModel: ObservableObject, BitchatDelegate { // Schedule a debounced UI update private func scheduleUIUpdate() { - 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() + // 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() + } } } @@ -2236,9 +2237,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { invalidateEncryptionCache(for: peerID) // Schedule UI update - DispatchQueue.main.async { [weak self] in - self?.scheduleUIUpdate() - } + scheduleUIUpdate() } // MARK: - Fingerprint Management diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index d9a4c67a..5943191e 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -239,7 +239,7 @@ struct ContentView: View { private func messagesView(privatePeer: String?) -> some View { ScrollViewReader { proxy in ScrollView { - VStack(alignment: .leading, spacing: 0) { + LazyVStack(alignment: .leading, spacing: 0) { // Extract messages based on context (private or public chat) let messages: [BitchatMessage] = { if let privatePeer = privatePeer {