mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 17:45:19 +00:00
Merge pull request #920 from malkovitc/feat/472-optimize-message-deduplicator
perf: optimize MessageDeduplicator performance
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user