Merge pull request #920 from malkovitc/feat/472-optimize-message-deduplicator

perf: optimize MessageDeduplicator performance
This commit is contained in:
jack
2026-01-04 12:41:48 -10:00
committed by GitHub
+28 -21
View File
@@ -5,7 +5,7 @@ import Foundation
/// Thread-safe deduplicator with LRU eviction and time-based expiry. /// Thread-safe deduplicator with LRU eviction and time-based expiry.
/// Used for both message ID deduplication (network layer) and content key deduplication (UI layer). /// Used for both message ID deduplication (network layer) and content key deduplication (UI layer).
final class MessageDeduplicator { final class MessageDeduplicator {
private struct Entry { private struct Entry: Equatable {
let id: String let id: String
let timestamp: Date let timestamp: Date
} }
@@ -31,18 +31,20 @@ final class MessageDeduplicator {
self.maxCount = maxCount 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 { func isDuplicate(_ id: String) -> Bool {
lock.lock() lock.lock()
defer { lock.unlock() } defer { lock.unlock() }
cleanupOldEntries() let now = Date()
cleanupOldEntries(before: now.addingTimeInterval(-maxAge))
if lookup[id] != nil { if lookup[id] != nil {
return true return true
} }
let now = Date()
entries.append(Entry(id: id, timestamp: now)) entries.append(Entry(id: id, timestamp: now))
lookup[id] = now lookup[id] = now
trimIfNeeded() trimIfNeeded()
@@ -89,18 +91,22 @@ final class MessageDeduplicator {
} }
private func trimIfNeeded() { private func trimIfNeeded() {
// Soft-cap and advance head by a chunk to avoid O(n) shifting let activeCount = entries.count - head
if (entries.count - head) > maxCount { guard activeCount > maxCount else { return }
let removeCount = min(100, entries.count - head)
for i in head..<(head + removeCount) { // Remove down to 75% of maxCount for better amortization
lookup.removeValue(forKey: entries[i].id) let targetCount = (maxCount * 3) / 4
} let removeCount = activeCount - targetCount
head += removeCount
// Periodically compact to reclaim memory for i in head..<(head + removeCount) {
if head > entries.count / 2 { lookup.removeValue(forKey: entries[i].id)
entries.removeFirst(head) }
head = 0 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() lookup.removeAll()
} }
/// Periodic cleanup /// Periodic cleanup of expired entries and memory optimization.
func cleanup() { func cleanup() {
lock.lock() lock.lock()
defer { lock.unlock() } 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) entries.reserveCapacity(maxCount)
} }
} }
private func cleanupOldEntries() { private func cleanupOldEntries(before cutoff: Date) {
let cutoff = Date().addingTimeInterval(-maxAge)
while head < entries.count, entries[head].timestamp < cutoff { while head < entries.count, entries[head].timestamp < cutoff {
lookup.removeValue(forKey: entries[head].id) lookup.removeValue(forKey: entries[head].id)
head += 1 head += 1
} }
// Compact when head exceeds half the array
if head > 0 && head > entries.count / 2 { if head > 0 && head > entries.count / 2 {
entries.removeFirst(head) entries.removeFirst(head)
head = 0 head = 0