diff --git a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift index f28c2038582..224426e086a 100644 --- a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift +++ b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift @@ -52,9 +52,9 @@ final class HeartbeatStorage: Sendable, HeartbeatStorageProtocol { // MARK: - Instance Management /// Statically allocated cache of `HeartbeatStorage` instances keyed by string IDs. - private nonisolated(unsafe) static var cachedInstances: AtomicBox< + private static let cachedInstances: FIRAllocatedUnfairLock< [String: WeakContainer] - > = AtomicBox([:]) + > = FIRAllocatedUnfairLock(initialState: [:]) /// Gets an existing `HeartbeatStorage` instance with the given `id` if one exists. Otherwise, /// makes a new instance with the given `id`. diff --git a/FirebaseCore/Internal/Sources/Utilities/FIRAllocatedUnfairLock.swift b/FirebaseCore/Internal/Sources/Utilities/FIRAllocatedUnfairLock.swift new file mode 100644 index 00000000000..ae52faefce6 --- /dev/null +++ b/FirebaseCore/Internal/Sources/Utilities/FIRAllocatedUnfairLock.swift @@ -0,0 +1,66 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import os.lock + +/// A reference wrapper around `os_unfair_lock`. Replace this class with +/// `OSAllocatedUnfairLock` once we support only iOS 16+. For an explanation +/// on why this is necessary, see the docs: +/// https://developer.apple.com/documentation/os/osallocatedunfairlock +public final class FIRAllocatedUnfairLock: @unchecked Sendable { + private var lockPointer: UnsafeMutablePointer + private var state: State + + public init(initialState: sending State) { + lockPointer = UnsafeMutablePointer + .allocate(capacity: 1) + lockPointer.initialize(to: os_unfair_lock()) + state = initialState + } + + public convenience init() where State == Void { + self.init(initialState: ()) + } + + public func lock() { + os_unfair_lock_lock(lockPointer) + } + + public func unlock() { + os_unfair_lock_unlock(lockPointer) + } + + @discardableResult + public func withLock(_ body: (inout State) throws -> R) rethrows -> R { + let value: R + lock() + defer { unlock() } + value = try body(&state) + return value + } + + @discardableResult + public func withLock(_ body: () throws -> R) rethrows -> R { + let value: R + lock() + defer { unlock() } + value = try body() + return value + } + + deinit { + lockPointer.deallocate() + } +} diff --git a/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift b/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift index 8d5a03f942c..c48cea653f7 100644 --- a/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift +++ b/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift @@ -405,13 +405,13 @@ class HeartbeatStorageTests: XCTestCase { // type '[WeakContainer]' to a `@Sendable` closure // (`DispatchQueue.global().async { ... }`). final class WeakRefs: @unchecked Sendable { - private(set) var weakRefs: [WeakContainer] = [] // Lock is used to synchronize `weakRefs` during concurrent access. - private let weakRefsLock = NSLock() + private(set) var weakRefs = + FIRAllocatedUnfairLock<[WeakContainer]>(initialState: []) func append(_ weakRef: WeakContainer) { - weakRefsLock.withLock { - weakRefs.append(weakRef) + weakRefs.withLock { + $0.append(weakRef) } } } @@ -436,8 +436,10 @@ class HeartbeatStorageTests: XCTestCase { // Then // The `weakRefs` array's references should all be nil; otherwise, something is being // unexpectedly strongly retained. - for weakRef in weakRefs.weakRefs { - XCTAssertNil(weakRef.object, "Potential memory leak detected.") + weakRefs.weakRefs.withLock { refs in + for weakRef in refs { + XCTAssertNil(weakRef.object, "Potential memory leak detected.") + } } } }