From 60601c1e0cf4f667f67e9f2708f75f75d87c585f Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Fri, 9 Oct 2026 07:21:41 -0700 Subject: [PATCH] refactor(diagnostics): Guard diagnostic counters with a lock instead of a queue Every dropped event and every delivered batch goes through DiagnosticCache, and a DispatchQueue.sync for a counter increment costs more than the increment. Adds UnfairLock.withLock, which inlines to the same code as lock()/unlock(). Co-authored-by: Cursor --- .../ServiceObjects/Cache/DiagnosticCache.swift | 16 ++++++++-------- LaunchDarkly/LaunchDarkly/UnfairLock.swift | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift index 44b570928..d97ff5bfd 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift @@ -16,10 +16,10 @@ protocol DiagnosticCaching { /// `com.launchdarkly.DiagnosticCache.diagnosticData`, forming keys of the form /// `com.launchdarkly.DiagnosticCache.diagnosticData.`. final class DiagnosticCache: DiagnosticCaching { - private static let cacheQueueLabel = "com.launchdarkly.DiagnosticCache.cacheQueue" - private let sdkKey: String - private var cacheQueue = DispatchQueue(label: cacheQueueLabel) + /// A lock rather than a queue because a full event store counts every refused evaluation here, on the thread that + /// evaluated, and a `DispatchQueue.sync` costs enough at that rate to be visible. + private let lock = UnfairLock() private var instanceId: String private var dataSinceDate: Int64 @@ -37,13 +37,13 @@ final class DiagnosticCache: DiagnosticCaching { } func getDiagnosticId() -> DiagnosticId { - cacheQueue.sync { + lock.withLock { DiagnosticId(diagnosticId: instanceId, sdkKey: sdkKey) } } func getCurrentStatsAndReset() -> DiagnosticStats { - cacheQueue.sync { + lock.withLock { let now = Date().millisSince1970 let stats = DiagnosticStats(id: DiagnosticId(diagnosticId: instanceId, sdkKey: sdkKey), creationDate: now, @@ -60,19 +60,19 @@ final class DiagnosticCache: DiagnosticCaching { } func incrementDroppedEventCount() { - cacheQueue.sync { + lock.withLock { droppedEvents += 1 } } func recordEventsInLastBatch(eventsInLastBatch: Int) { - cacheQueue.sync { + lock.withLock { self.eventsInLastBatch = eventsInLastBatch } } func addStreamInit(streamInit: DiagnosticStreamInit) { - cacheQueue.sync { + lock.withLock { streamInits.append(streamInit) } } diff --git a/LaunchDarkly/LaunchDarkly/UnfairLock.swift b/LaunchDarkly/LaunchDarkly/UnfairLock.swift index e35bfa325..99095606e 100644 --- a/LaunchDarkly/LaunchDarkly/UnfairLock.swift +++ b/LaunchDarkly/LaunchDarkly/UnfairLock.swift @@ -23,8 +23,24 @@ final class UnfairLock { func unlock() { os_unfair_lock_unlock(unfairLock) } + + /// Runs `body` holding the lock. Inlined, so it costs the same as calling `lock()` and `unlock()` around it. + @inline(__always) + func withLock(_ body: () throws -> T) rethrows -> T { + lock() + defer { unlock() } + return try body() + } } #else /// `os_unfair_lock` is Darwin only, so elsewhere this is Foundation's mutex. typealias UnfairLock = NSLock + +extension NSLock { + func withLock(_ body: () throws -> T) rethrows -> T { + lock() + defer { unlock() } + return try body() + } +} #endif