Skip to content

Commit 66f44fc

Browse files
authored
[Core] Make instance manager conform to Swift 6 principles (#13933)
1 parent 4b25fc4 commit 66f44fc

File tree

2 files changed

+61
-8
lines changed

2 files changed

+61
-8
lines changed

FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,41 @@ final class HeartbeatStorage: HeartbeatStorageProtocol {
5151
// MARK: - Instance Management
5252

5353
/// Statically allocated cache of `HeartbeatStorage` instances keyed by string IDs.
54-
private static var cachedInstances: [String: WeakContainer<HeartbeatStorage>] = [:]
54+
#if compiler(>=6)
55+
// In Swift 6, this property is not concurrency-safe because it is
56+
// nonisolated global shared mutable state. Because this target's
57+
// deployment version does not support Swift concurrency, it is marked as
58+
// `nonisolated(unsafe)` to disable concurrency-safety checks. The
59+
// property's access is protected by an external synchronization mechanism
60+
// (see `instancesLock` property).
61+
private nonisolated(unsafe) static var cachedInstances: [
62+
String: WeakContainer<HeartbeatStorage>
63+
] = [:]
64+
#else
65+
// TODO(Xcode 16): Delete this block when minimum supported Xcode is
66+
// Xcode 16.
67+
private static var cachedInstances: [
68+
String: WeakContainer<HeartbeatStorage>
69+
] = [:]
70+
#endif // compiler(>=6)
71+
72+
/// Used to synchronize concurrent access to the `cachedInstances` property.
73+
private static let instancesLock = NSLock()
5574

5675
/// Gets an existing `HeartbeatStorage` instance with the given `id` if one exists. Otherwise,
5776
/// makes a new instance with the given `id`.
5877
///
5978
/// - Parameter id: A string identifier.
6079
/// - Returns: A `HeartbeatStorage` instance.
6180
static func getInstance(id: String) -> HeartbeatStorage {
62-
if let cachedInstance = cachedInstances[id]?.object {
63-
return cachedInstance
64-
} else {
65-
let newInstance = HeartbeatStorage.makeHeartbeatStorage(id: id)
66-
cachedInstances[id] = WeakContainer(object: newInstance)
67-
return newInstance
81+
instancesLock.withLock {
82+
if let cachedInstance = cachedInstances[id]?.object {
83+
return cachedInstance
84+
} else {
85+
let newInstance = HeartbeatStorage.makeHeartbeatStorage(id: id)
86+
cachedInstances[id] = WeakContainer(object: newInstance)
87+
return newInstance
88+
}
6889
}
6990
}
7091

@@ -88,7 +109,9 @@ final class HeartbeatStorage: HeartbeatStorageProtocol {
88109

89110
deinit {
90111
// Removes the instance if it was cached.
91-
Self.cachedInstances.removeValue(forKey: id)
112+
_ = Self.instancesLock.withLock {
113+
Self.cachedInstances.removeValue(forKey: id)
114+
}
92115
}
93116

94117
// MARK: - HeartbeatStorageProtocol

FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,36 @@ class HeartbeatStorageTests: XCTestCase {
399399
// Then
400400
wait(for: expectations, timeout: 1.0, enforceOrder: true)
401401
}
402+
403+
func testForMemoryLeakInInstanceManager() {
404+
// Given
405+
let id = "testID"
406+
var weakRefs: [WeakContainer<HeartbeatStorage>] = []
407+
// Lock is used to synchronize `weakRefs` during concurrent access.
408+
let weakRefsLock = NSLock()
409+
410+
// When
411+
// Simulate concurrent access. This will help expose race conditions that could cause a crash.
412+
let group = DispatchGroup()
413+
for _ in 0 ..< 100 {
414+
group.enter()
415+
DispatchQueue.global().async {
416+
let instance = HeartbeatStorage.getInstance(id: id)
417+
weakRefsLock.withLock {
418+
weakRefs.append(WeakContainer(object: instance))
419+
}
420+
group.leave()
421+
}
422+
}
423+
group.wait()
424+
425+
// Then
426+
// The `weakRefs` array's references should all be nil; otherwise, something is being
427+
// unexpectedly strongly retained.
428+
for weakRef in weakRefs {
429+
XCTAssertNil(weakRef.object, "Potential memory leak detected.")
430+
}
431+
}
402432
}
403433

404434
private class StorageFake: Storage {

0 commit comments

Comments
 (0)