From 5a66f03400568753c8e02234a509ce742ed61a19 Mon Sep 17 00:00:00 2001 From: "evgeniy.chernomortsev" Date: Tue, 9 Dec 2025 14:50:34 +0400 Subject: [PATCH 1/2] fix(build): exclude allowBluetoothHFP on iOS Simulator allowBluetoothHFP is not available on iOS Simulator, causing build failures. Use conditional compilation to exclude this option when building for simulator while keeping it for device builds. --- bitchat/Features/voice/VoiceRecorder.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bitchat/Features/voice/VoiceRecorder.swift b/bitchat/Features/voice/VoiceRecorder.swift index 577799c9a..ad02d95db 100644 --- a/bitchat/Features/voice/VoiceRecorder.swift +++ b/bitchat/Features/voice/VoiceRecorder.swift @@ -58,11 +58,20 @@ final class VoiceRecorder: NSObject, AVAudioRecorderDelegate { guard session.recordPermission == .granted else { throw RecorderError.microphoneAccessDenied } + #if targetEnvironment(simulator) + // allowBluetoothHFP is not available on iOS Simulator + try session.setCategory( + .playAndRecord, + mode: .default, + options: [.defaultToSpeaker, .allowBluetoothA2DP] + ) + #else try session.setCategory( .playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP] ) + #endif try session.setActive(true, options: .notifyOthersOnDeactivation) #endif #if os(macOS) From b15d92ebb5794a506eccd9d306991fb7502f6417 Mon Sep 17 00:00:00 2001 From: "evgeniy.chernomortsev" Date: Tue, 9 Dec 2025 18:48:14 +0400 Subject: [PATCH 2/2] perf: optimize MessageDeduplicator performance (#472) - Make Entry struct Equatable for better testability - Remove down to 75% of maxCount instead of fixed 100 items for better amortization of cleanup cost - Reuse Date instance to reduce allocations in isDuplicate() - Add documentation comments for public methods - Improve memory capacity management in cleanup() --- bitchat/Utils/MessageDeduplicator.swift | 49 ++++++++++++++----------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/bitchat/Utils/MessageDeduplicator.swift b/bitchat/Utils/MessageDeduplicator.swift index 01534307f..00ea0eb5b 100644 --- a/bitchat/Utils/MessageDeduplicator.swift +++ b/bitchat/Utils/MessageDeduplicator.swift @@ -5,7 +5,7 @@ import Foundation /// Thread-safe deduplicator with LRU eviction and time-based expiry. /// Used for both message ID deduplication (network layer) and content key deduplication (UI layer). final class MessageDeduplicator { - private struct Entry { + private struct Entry: Equatable { let id: String let timestamp: Date } @@ -31,18 +31,20 @@ final class MessageDeduplicator { self.maxCount = maxCount } - /// Check if message is duplicate and add if not + /// Check if message is duplicate and add if not. + /// - Parameter id: The message identifier to check. + /// - Returns: `true` if the message was already seen, `false` otherwise. func isDuplicate(_ id: String) -> Bool { lock.lock() defer { lock.unlock() } - cleanupOldEntries() + let now = Date() + cleanupOldEntries(before: now.addingTimeInterval(-maxAge)) if lookup[id] != nil { return true } - let now = Date() entries.append(Entry(id: id, timestamp: now)) lookup[id] = now trimIfNeeded() @@ -89,18 +91,22 @@ final class MessageDeduplicator { } private func trimIfNeeded() { - // Soft-cap and advance head by a chunk to avoid O(n) shifting - if (entries.count - head) > maxCount { - let removeCount = min(100, entries.count - head) - for i in head..<(head + removeCount) { - lookup.removeValue(forKey: entries[i].id) - } - head += removeCount - // Periodically compact to reclaim memory - if head > entries.count / 2 { - entries.removeFirst(head) - head = 0 - } + let activeCount = entries.count - head + guard activeCount > maxCount else { return } + + // Remove down to 75% of maxCount for better amortization + let targetCount = (maxCount * 3) / 4 + let removeCount = activeCount - targetCount + + for i in head..<(head + removeCount) { + lookup.removeValue(forKey: entries[i].id) + } + head += removeCount + + // Compact when head exceeds half the array to reclaim memory + if head > entries.count / 2 { + entries.removeFirst(head) + head = 0 } } @@ -114,24 +120,25 @@ final class MessageDeduplicator { lookup.removeAll() } - /// Periodic cleanup + /// Periodic cleanup of expired entries and memory optimization. func cleanup() { lock.lock() defer { lock.unlock() } - cleanupOldEntries() + cleanupOldEntries(before: Date().addingTimeInterval(-maxAge)) - if entries.capacity > maxCount * 2 { + // Shrink capacity if significantly oversized + if entries.capacity > maxCount * 2 && entries.count < maxCount { entries.reserveCapacity(maxCount) } } - private func cleanupOldEntries() { - let cutoff = Date().addingTimeInterval(-maxAge) + private func cleanupOldEntries(before cutoff: Date) { while head < entries.count, entries[head].timestamp < cutoff { lookup.removeValue(forKey: entries[head].id) head += 1 } + // Compact when head exceeds half the array if head > 0 && head > entries.count / 2 { entries.removeFirst(head) head = 0