Skip to content

Commit b3a8fcd

Browse files
committed
[Core] Make instance manager conform to Swift 6 principles
1 parent 8328630 commit b3a8fcd

File tree

2 files changed

+59
-8
lines changed

2 files changed

+59
-8
lines changed

FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,30 @@ 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+
private nonisolated(unsafe) static var cachedInstances: [
55+
String: WeakContainer<HeartbeatStorage>
56+
] =
57+
[:]
58+
59+
/// Used to synchronize mutations
60+
private static let instancesLock = NSLock()
5561

5662
/// Gets an existing `HeartbeatStorage` instance with the given `id` if one exists. Otherwise,
5763
/// makes a new instance with the given `id`.
5864
///
5965
/// - Parameter id: A string identifier.
6066
/// - Returns: A `HeartbeatStorage` instance.
6167
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
68+
instancesLock.withLock {
69+
if let cachedInstance = cachedInstances[id]?.object {
70+
print("fred")
71+
return cachedInstance
72+
} else {
73+
print("bob")
74+
let newInstance = HeartbeatStorage.makeHeartbeatStorage(id: id)
75+
cachedInstances[id] = WeakContainer(object: newInstance)
76+
return newInstance
77+
}
6878
}
6979
}
7080

@@ -88,7 +98,9 @@ final class HeartbeatStorage: HeartbeatStorageProtocol {
8898

8999
deinit {
90100
// Removes the instance if it was cached.
91-
Self.cachedInstances.removeValue(forKey: id)
101+
_ = Self.instancesLock.withLock {
102+
Self.cachedInstances.removeValue(forKey: id)
103+
}
92104
}
93105

94106
// MARK: - HeartbeatStorageProtocol

FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,45 @@ class HeartbeatStorageTests: XCTestCase {
399399
// Then
400400
wait(for: expectations, timeout: 1.0, enforceOrder: true)
401401
}
402+
403+
func testForMemoryLeakInInstanceManagemer() {
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+
#if !os(macOS)
426+
// Simulate memory pressure. This will force the system to more aggressively reclaim memory.
427+
// This simulation makes the test more sensitive to potential leaks.
428+
NotificationCenter.default.post(
429+
name: UIApplication.didReceiveMemoryWarningNotification,
430+
object: UIApplication.shared
431+
)
432+
#endif // !os(macOS)
433+
434+
// Then
435+
// The `weakRefs` array's references should all be nil; otherwise, something is being
436+
// unexpectedly strongly retained.
437+
for weakRef in weakRefs {
438+
XCTAssertNil(weakRef.object, "Potential memory leak detected.")
439+
}
440+
}
402441
}
403442

404443
private class StorageFake: Storage {

0 commit comments

Comments
 (0)