Skip to content

Commit

Permalink
Maintain Shadow state for observable state on MainActor
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Nov 4, 2024
1 parent b11535b commit be6d363
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 75 deletions.
4 changes: 3 additions & 1 deletion Sources/SpeziBluetooth/Bluetooth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ import Spezi
public final class Bluetooth: Module, EnvironmentAccessible, Sendable {
@Observable
class Storage {
var nearbyDevices: OrderedDictionary<UUID, any BluetoothDevice> = [:]
@MainActor var nearbyDevices: OrderedDictionary<UUID, any BluetoothDevice> = [:]

nonisolated init() {}
}

nonisolated static let logger = Logger(subsystem: "edu.stanford.spezi.bluetooth", category: "Bluetooth")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import OrderedCollections

@Observable
final class BluetoothManagerStorage: ValueObservable, Sendable {
private let _isScanning = ManagedAtomic<Bool>(false)
private let _state = ManagedAtomic<BluetoothState>(.unknown)
private let _isScanning = ManagedAtomicMainActorBuffered<Bool>(false)
private let _state = ManagedAtomicMainActorBuffered<BluetoothState>(.unknown)

@ObservationIgnored private nonisolated(unsafe) var _discoveredPeripherals: OrderedDictionary<UUID, BluetoothPeripheral> = [:]
private let _discoveredPeripherals: MainActorBuffered<OrderedDictionary<UUID, BluetoothPeripheral>> = .init([:])
private let rwLock = RWLock()

@SpeziBluetooth var retrievedPeripherals: OrderedDictionary<UUID, WeakReference<BluetoothPeripheral>> = [:] {
Expand Down Expand Up @@ -46,20 +46,21 @@ final class BluetoothManagerStorage: ValueObservable, Sendable {

@inlinable var readOnlyDiscoveredPeripherals: OrderedDictionary<UUID, BluetoothPeripheral> {
access(keyPath: \._discoveredPeripherals)
return rwLock.withReadLock {
_discoveredPeripherals
}
return _discoveredPeripherals.load(using: rwLock)
}

@SpeziBluetooth var state: BluetoothState {
get {
readOnlyState
}
set {
withMutation(keyPath: \._state) {
_state.store(newValue, ordering: .relaxed)
let didChange = _state.storeAndCompare(newValue) { @Sendable mutation in
self.withMutation(keyPath: \._state, mutation)
}

if didChange {
_$simpleRegistrar.triggerDidChange(for: \.state, on: self)
}
_$simpleRegistrar.triggerDidChange(for: \.state, on: self)

for continuation in subscribedContinuations.values {
continuation.yield(state)
Expand All @@ -72,10 +73,13 @@ final class BluetoothManagerStorage: ValueObservable, Sendable {
readOnlyIsScanning
}
set {
withMutation(keyPath: \._isScanning) {
_isScanning.store(newValue, ordering: .relaxed)
let didChange = _isScanning.storeAndCompare(newValue) { @Sendable mutation in
self.withMutation(keyPath: \._isScanning, mutation)
}

if didChange {
_$simpleRegistrar.triggerDidChange(for: \.isScanning, on: self) // didSet
}
_$simpleRegistrar.triggerDidChange(for: \.isScanning, on: self) // didSet
}
}

Expand All @@ -84,11 +88,10 @@ final class BluetoothManagerStorage: ValueObservable, Sendable {
readOnlyDiscoveredPeripherals
}
set {
withMutation(keyPath: \._discoveredPeripherals) {
rwLock.withWriteLock {
_discoveredPeripherals = newValue
}
_discoveredPeripherals.store(newValue, using: rwLock) { @Sendable mutation in
self.withMutation(keyPath: \._discoveredPeripherals, mutation)
}

_$simpleRegistrar.triggerDidChange(for: \.discoveredPeripherals, on: self) // didSet
}
}
Expand Down
85 changes: 85 additions & 0 deletions Sources/SpeziBluetooth/CoreBluetooth/Model/MainActorBuffered.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation
import SpeziFoundation


final class MainActorBuffered<Value: Sendable>: Sendable {
private nonisolated(unsafe) var unsafeValue: Value
@MainActor private(set) var mainActorValue: Value?

init(_ value: Value) {
self.unsafeValue = value
self.mainActorValue = value
}

func loadUnsafe() -> Value {
loadIfMainActor() ?? unsafeValue
}

func load(using lock: NSLock) -> Value {
loadIfMainActor() ?? lock.withLock {
unsafeValue
}
}

func load(using lock: RWLock) -> Value {
loadIfMainActor() ?? lock.withReadLock {
unsafeValue
}
}

private func loadIfMainActor() -> Value? {
if Thread.isMainThread {
MainActor.assumeIsolated {
mainActorValue
}
} else {
nil
}
}

private func _store(_ newValue: Value, mutation: sending @MainActor @escaping (@MainActor () -> Void) -> Void) {
Task { @MainActor in
let valueMutation = { @MainActor in
self.mainActorValue = newValue
}

mutation(valueMutation)
}
}

func store(_ newValue: Value, using lock: NSLock, mutation: sending @MainActor @escaping (@MainActor () -> Void) -> Void) {
lock.withLock {
unsafeValue = newValue
}
_store(newValue, mutation: mutation)
}

func store(_ newValue: Value, using lock: RWLock, mutation: sending @MainActor @escaping (@MainActor () -> Void) -> Void) {
lock.withWriteLock {
unsafeValue = newValue
}
_store(newValue, mutation: mutation)
}
}


extension MainActorBuffered where Value: Equatable {
func storeAndCompare(_ newValue: Value, using lock: RWLock, mutation: sending @MainActor @escaping (@MainActor () -> Void) -> Void) -> Bool {
let didChange = lock.withWriteLock {
let didChange = unsafeValue != newValue
unsafeValue = newValue
return didChange
}
_store(newValue, mutation: mutation)

return didChange
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Atomics
import Foundation


final class ManagedAtomicMainActorBuffered<Value: AtomicValue & Sendable>: Sendable where Value.AtomicRepresentation.Value == Value {
private let managedValue: ManagedAtomic<Value>
@MainActor private var mainActorValue: Value?

init(_ value: Value) {
self.managedValue = ManagedAtomic(value)
self.mainActorValue = value
}

@_semantics("atomics.requires_constant_orderings")
@inlinable
func load(ordering: AtomicLoadOrdering = .relaxed) -> Value {
if Thread.isMainThread {
MainActor.assumeIsolated {
mainActorValue
} ?? managedValue.load(ordering: ordering)
} else {
managedValue.load(ordering: ordering)
}
}

@_semantics("atomics.requires_constant_orderings")
private func mutateMainActorBuffer(
_ newValue: Value,
mutation: sending @MainActor @escaping (@MainActor () -> Void) -> Void
) {
Task { @MainActor in
let valueMutation = { @MainActor in
self.mainActorValue = newValue
}

mutation(valueMutation)
}
}

@_semantics("atomics.requires_constant_orderings")
@inlinable
func store(
_ newValue: Value,
ordering: AtomicStoreOrdering = .relaxed,
mutation: sending @MainActor @escaping (@MainActor () -> Void) -> Void
) {
managedValue.store(newValue, ordering: ordering)
mutateMainActorBuffer(newValue, mutation: mutation)
}
}


extension ManagedAtomicMainActorBuffered where Value: Equatable {
@_semantics("atomics.requires_constant_orderings")
@inlinable
func storeAndCompare(
_ newValue: Value,
ordering: AtomicUpdateOrdering = .relaxed,
mutation: sending @MainActor @escaping (@MainActor () -> Void) -> Void
) -> Bool {
let previousValue = managedValue.exchange(newValue, ordering: ordering)
mutateMainActorBuffer(newValue, mutation: mutation)

return previousValue != newValue
}
}
61 changes: 25 additions & 36 deletions Sources/SpeziBluetooth/CoreBluetooth/Model/PeripheralStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import Foundation
/// into a separate state container that is `@Observable`.
@Observable
final class PeripheralStorage: ValueObservable, Sendable {
private let _state: ManagedAtomic<PeripheralState>
private let _rssi: ManagedAtomic<Int>
private let _nearby: ManagedAtomic<Bool>
private let _state: ManagedAtomicMainActorBuffered<PeripheralState>
private let _rssi: ManagedAtomicMainActorBuffered<Int>
private let _nearby: ManagedAtomicMainActorBuffered<Bool>
private let _lastActivityTimeIntervalSince1970BitPattern: ManagedAtomic<UInt64> // workaround to store store Date atomically
// swiftlint:disable:previous identifier_name

@ObservationIgnored private nonisolated(unsafe) var _peripheralName: String?
@ObservationIgnored private nonisolated(unsafe) var _advertisementData: AdvertisementData
private let _peripheralName: MainActorBuffered<String?>
private let _advertisementData: MainActorBuffered<AdvertisementData>
// Its fine to have a single lock. Readers will be isolated anyways to the SpeziBluetooth global actor.
// The only side-effect is, that readers will wait for any write to complete, which is fine as peripheralName is rarely updated.
private let lock = RWLock()
Expand All @@ -45,8 +45,9 @@ final class PeripheralStorage: ValueObservable, Sendable {
@inlinable var name: String? {
access(keyPath: \._peripheralName)
access(keyPath: \._advertisementData)

return lock.withReadLock {
_peripheralName ?? _advertisementData.localName
_peripheralName.loadUnsafe() ?? _advertisementData.loadUnsafe().localName
}
}

Expand All @@ -67,9 +68,7 @@ final class PeripheralStorage: ValueObservable, Sendable {

@inlinable var readOnlyAdvertisementData: AdvertisementData {
access(keyPath: \._advertisementData)
return lock.withReadLock {
_advertisementData
}
return _advertisementData.load(using: lock)
}

var readOnlyLastActivity: Date {
Expand All @@ -80,16 +79,11 @@ final class PeripheralStorage: ValueObservable, Sendable {
@SpeziBluetooth var peripheralName: String? {
get {
access(keyPath: \._peripheralName)
return lock.withReadLock {
_peripheralName
}
return _peripheralName.load(using: lock)
}
set {
let didChange = newValue != _peripheralName
withMutation(keyPath: \._peripheralName) {
lock.withWriteLock {
_peripheralName = newValue
}
let didChange = _peripheralName.storeAndCompare(newValue, using: lock) { @Sendable mutation in
self.withMutation(keyPath: \._peripheralName, mutation)
}

if didChange {
Expand All @@ -103,9 +97,8 @@ final class PeripheralStorage: ValueObservable, Sendable {
readOnlyRssi
}
set {
let didChange = newValue != readOnlyRssi
withMutation(keyPath: \._rssi) {
_rssi.store(newValue, ordering: .relaxed)
let didChange = _rssi.storeAndCompare(newValue) { @Sendable mutation in
self.withMutation(keyPath: \._rssi, mutation)
}
if didChange {
_$simpleRegistrar.triggerDidChange(for: \.rssi, on: self)
Expand All @@ -118,11 +111,8 @@ final class PeripheralStorage: ValueObservable, Sendable {
readOnlyAdvertisementData
}
set {
let didChange = newValue != _advertisementData
withMutation(keyPath: \._advertisementData) {
lock.withWriteLock {
_advertisementData = newValue
}
let didChange = _advertisementData.storeAndCompare(newValue, using: lock) { @Sendable mutation in
self.withMutation(keyPath: \._advertisementData, mutation)
}

if didChange {
Expand All @@ -136,10 +126,10 @@ final class PeripheralStorage: ValueObservable, Sendable {
readOnlyState
}
set {
let didChange = newValue != readOnlyState
withMutation(keyPath: \._state) {
_state.store(newValue, ordering: .relaxed)
let didChange = _state.storeAndCompare(newValue) { @Sendable mutation in
self.withMutation(keyPath: \._state, mutation)
}

if didChange {
_$simpleRegistrar.triggerDidChange(for: \.state, on: self)
}
Expand All @@ -151,9 +141,8 @@ final class PeripheralStorage: ValueObservable, Sendable {
readOnlyNearby
}
set {
let didChange = newValue != readOnlyNearby
withMutation(keyPath: \._nearby) {
_nearby.store(newValue, ordering: .relaxed)
let didChange = _nearby.storeAndCompare(newValue) { @Sendable mutation in
self.withMutation(keyPath: \._nearby, mutation)
}

if didChange {
Expand All @@ -166,11 +155,11 @@ final class PeripheralStorage: ValueObservable, Sendable {
@ObservationIgnored let _$simpleRegistrar = ValueObservationRegistrar<PeripheralStorage>()

init(peripheralName: String?, rssi: Int, advertisementData: AdvertisementData, state: PeripheralState, lastActivity: Date = .now) {
self._peripheralName = peripheralName
self._advertisementData = advertisementData
self._rssi = ManagedAtomic(rssi)
self._state = ManagedAtomic(state)
self._nearby = ManagedAtomic(false)
self._peripheralName = MainActorBuffered(peripheralName)
self._advertisementData = MainActorBuffered(advertisementData)
self._rssi = ManagedAtomicMainActorBuffered(rssi)
self._state = ManagedAtomicMainActorBuffered(state)
self._nearby = ManagedAtomicMainActorBuffered(false)
self._lastActivity = lastActivity
self._lastActivityTimeIntervalSince1970BitPattern = ManagedAtomic(lastActivity.timeIntervalSince1970.bitPattern)
}
Expand Down
Loading

0 comments on commit be6d363

Please sign in to comment.