mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 14:05:18 +00:00
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:
@@ -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() {
|
||||||
|
|||||||
@@ -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,16 +2175,23 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Schedule a debounced UI update
|
// Schedule a debounced UI update
|
||||||
private func scheduleUIUpdate() {
|
private func scheduleUIUpdate() {
|
||||||
guard !pendingUIUpdate else { return } // Already scheduled
|
// Ensure timer operations happen on main thread
|
||||||
|
if Thread.isMainThread {
|
||||||
pendingUIUpdate = true
|
guard !pendingUIUpdate else { return } // Already scheduled
|
||||||
|
|
||||||
// Cancel existing timer
|
pendingUIUpdate = true
|
||||||
uiUpdateTimer?.invalidate()
|
|
||||||
|
// Cancel existing timer
|
||||||
// Schedule new update
|
uiUpdateTimer?.invalidate()
|
||||||
uiUpdateTimer = Timer.scheduledTimer(withTimeInterval: uiUpdateInterval, repeats: false) { [weak self] _ in
|
|
||||||
self?.flushUIUpdate()
|
// 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)
|
invalidateEncryptionCache(for: peerID)
|
||||||
|
|
||||||
// Schedule UI update
|
// Schedule UI update
|
||||||
DispatchQueue.main.async { [weak self] in
|
scheduleUIUpdate()
|
||||||
self?.scheduleUIUpdate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fingerprint Management
|
// MARK: - Fingerprint Management
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user