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 <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-01 01:05:59 +02:00
committed by GitHub
co-authored by jack
parent 8f32edaa64
commit a6c2e751d2
3 changed files with 140 additions and 24 deletions
+118 -1
View File
@@ -765,6 +765,17 @@ class BluetoothMeshService: NSObject {
private var messageBloomFilter = OptimizedBloomFilter(expectedItems: 2000, falsePositiveRate: 0.01) private var messageBloomFilter = OptimizedBloomFilter(expectedItems: 2000, falsePositiveRate: 0.01)
private var bloomFilterResetTimer: Timer? 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 // MARK: - Network Size Estimation
// Network size estimation // Network size estimation
@@ -1493,7 +1504,10 @@ class BluetoothMeshService: NSObject {
// Setup app state notifications // Setup app state notifications
setupAppStateNotifications() 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 bloomFilterResetTimer = Timer.scheduledTimer(withTimeInterval: 300.0, repeats: true) { [weak self] _ in
self?.messageQueue.async(flags: .barrier) { self?.messageQueue.async(flags: .barrier) {
guard let self = self else { return } guard let self = self else { return }
@@ -1591,6 +1605,7 @@ class BluetoothMeshService: NSObject {
self.handshakeCoordinator.logHandshakeStates() self.handshakeCoordinator.logHandshakeStates()
#endif #endif
} }
END OF OLD TIMER SETUP */
// Schedule first peer ID rotation // Schedule first peer ID rotation
scheduleNextRotation() scheduleNextRotation()
@@ -1652,6 +1667,13 @@ class BluetoothMeshService: NSObject {
deinit { deinit {
cleanup() cleanup()
// Invalidate consolidated timers
highFrequencyTimer?.invalidate()
mediumFrequencyTimer?.invalidate()
lowFrequencyTimer?.invalidate()
// Invalidate other timers
scanDutyCycleTimer?.invalidate() scanDutyCycleTimer?.invalidate()
batteryMonitorTimer?.invalidate() batteryMonitorTimer?.invalidate()
coverTrafficTimer?.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 // MARK: - Message Sending
func sendBroadcastAnnounce() { func sendBroadcastAnnounce() {
+11 -12
View File
@@ -642,9 +642,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
selectedPrivateChatPeer = currentPeerID selectedPrivateChatPeer = currentPeerID
// Schedule UI update for encryption status change // Schedule UI update for encryption status change
DispatchQueue.main.async { [weak self] in scheduleUIUpdate()
self?.scheduleUIUpdate()
}
// Also refresh the peer list to update encryption status // Also refresh the peer list to update encryption status
Task { @MainActor in Task { @MainActor in
@@ -653,9 +651,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
DispatchQueue.main.async { [weak self] in scheduleUIUpdate()
self?.scheduleUIUpdate()
}
} }
// Clear unread messages for the current peer ID // Clear unread messages for the current peer ID
@@ -1986,9 +1982,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
invalidateEncryptionCache(for: peerID) invalidateEncryptionCache(for: peerID)
// Schedule UI update // Schedule UI update
DispatchQueue.main.async { [weak self] in scheduleUIUpdate()
self?.scheduleUIUpdate()
}
} }
func getEncryptionStatus(for peerID: String) -> EncryptionStatus { func getEncryptionStatus(for peerID: String) -> EncryptionStatus {
@@ -2181,6 +2175,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Schedule a debounced UI update // Schedule a debounced UI update
private func scheduleUIUpdate() { private func scheduleUIUpdate() {
// Ensure timer operations happen on main thread
if Thread.isMainThread {
guard !pendingUIUpdate else { return } // Already scheduled guard !pendingUIUpdate else { return } // Already scheduled
pendingUIUpdate = true pendingUIUpdate = true
@@ -2192,6 +2188,11 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
uiUpdateTimer = Timer.scheduledTimer(withTimeInterval: uiUpdateInterval, repeats: false) { [weak self] _ in uiUpdateTimer = Timer.scheduledTimer(withTimeInterval: uiUpdateInterval, repeats: false) { [weak self] _ in
self?.flushUIUpdate() self?.flushUIUpdate()
} }
} else {
DispatchQueue.main.async { [weak self] in
self?.scheduleUIUpdate()
}
}
} }
// Execute the pending UI update // Execute the pending UI update
@@ -2236,9 +2237,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
invalidateEncryptionCache(for: peerID) invalidateEncryptionCache(for: peerID)
// Schedule UI update // Schedule UI update
DispatchQueue.main.async { [weak self] in scheduleUIUpdate()
self?.scheduleUIUpdate()
}
} }
// MARK: - Fingerprint Management // MARK: - Fingerprint Management
+1 -1
View File
@@ -239,7 +239,7 @@ struct ContentView: View {
private func messagesView(privatePeer: String?) -> some View { private func messagesView(privatePeer: String?) -> some View {
ScrollViewReader { proxy in ScrollViewReader { proxy in
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 0) { LazyVStack(alignment: .leading, spacing: 0) {
// Extract messages based on context (private or public chat) // Extract messages based on context (private or public chat)
let messages: [BitchatMessage] = { let messages: [BitchatMessage] = {
if let privatePeer = privatePeer { if let privatePeer = privatePeer {