Were you able to resolve your issue?
I have encountered a similar problem and it seems that the Observation registrar (or rather whatever what SwiftUI is doing with it) is the culprict here. To my observation (and minimal example), the problem arises as soon when you try to protect your @Observable
storage using mutual exclusion.
Referring to the stack trace below, the value getter will be waiting for the lock of the setter to be released while the setter is somewhere deadlocked within the ObservationRegistrar withMutation call. The deadlock happens presumambly somewhere as part of the observation tracking. Checking against the open-source implementation of the Observation framework, I couldn't find a reason why it would deadlock there. Therefore the assumption, that SwiftUI is deadlocking itself somehow. Most logical explanation would be, that SwiftUI tries to acquire a in the observation tracking onChange handler that is currently hold when executing the View body. So a classic Lock Order Inversion.
So the questions:
Do you always need to make sure that custom locking mechanisms are within the withMutation(keyPath:_:)
call? Does SwiftUI account for that? Does it guarantee that it doesn't call the property getter before the mutation is completed (the public ObservationTracking interface only exposes a willSet change handler, didSet is exclusive to SwiftUI)? Lastly, was this fixed in SwiftData?
struct TestDeadLockView: View {
@Observable
final class ObservableTest: Sendable {
private nonisolated(unsafe) var _value = 0 // observed property
let lock = NSLock()
var value: Int {
get {
lock.withLock {
_value // implicitily calls access(keyPath:)
}
}
set {
lock.withLock {
_value = newValue // implicitily calls withMutation(keyPath:_:)
}
}
}
}
@State var test = ObservableTest()
var body: some View {
List {
HStack {
Text("Content")
Spacer()
Text("\(test.value)")
}
Button("Run") {
Task.detached { [test] in
for amount in 1..<100 {
usleep(50)
test.value += 1
}
}
}
}
}
}
Main Thread:
Thread 1 Queue : com.apple.main-thread (serial)
#0 0x00000001d877ecb4 in __psynch_mutexwait ()
#1 0x00000001ec51efa8 in _pthread_mutex_firstfit_lock_wait ()
#2 0x00000001ec51e9b8 in _pthread_mutex_firstfit_lock_slow ()
#3 0x0000000106568d98 in NSLocking.withLock<NSLock>(_:) ()
#4 0x0000000106568b18 in TestDeadLockView.ObservableTest.value.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:52
#5 0x0000000106569e0c in closure #1 in closure #1 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:69
#6 0x0000000106a55920 in ListRow.init(_:content:) at /Users/andi/Library/Developer/Xcode/DerivedData/UITests-fekuzmpvqcfiushfnpeypinhaado/SourcePackages/checkouts/SpeziViews/Sources/SpeziViews/Views/List/ListRow.swift:103
#7 0x0000000106a55b28 in ListRow.init<>(_:content:) at /Users/andi/Library/Developer/Xcode/DerivedData/UITests-fekuzmpvqcfiushfnpeypinhaado/SourcePackages/checkouts/SpeziViews/Sources/SpeziViews/Views/List/ListRow.swift:93
#8 0x0000000106569a28 in closure #1 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:68
#9 0x00000001942688cc in ___lldb_unnamed_symbol143772 ()
#10 0x00000001065697d4 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:67
#11 0x000000010656a7c8 in protocol witness for View.body.getter in conformance TestDeadLockView ()
#12 0x0000000193696528 in ___lldb_unnamed_symbol62086 ()
#13 0x00000001936e1738 in ___lldb_unnamed_symbol63606 ()
#14 0x00000001936df3bc in ___lldb_unnamed_symbol63534 ()
#15 0x0000000193691b30 in ___lldb_unnamed_symbol61998 ()
#16 0x00000001b82e9010 in AG::Graph::UpdateStack::update ()
#17 0x00000001b82e8bfc in AG::Graph::update_attribute ()
#18 0x00000001b82e87d8 in AG::Subgraph::update ()
#19 0x00000001936fb068 in ___lldb_unnamed_symbol64017 ()
#20 0x00000001936f9500 in ___lldb_unnamed_symbol63998 ()
#21 0x00000001936f7d7c in ___lldb_unnamed_symbol63982 ()
#22 0x00000001936f7b0c in ___lldb_unnamed_symbol63980 ()
#23 0x000000019360cc6c in ___lldb_unnamed_symbol59064 ()
#24 0x00000001918d6a4c in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#25 0x0000000190d353b4 in CA::Layer::layout_if_needed ()
#26 0x0000000190d34f38 in CA::Layer::layout_and_display_if_needed ()
#27 0x0000000190d900e0 in CA::Context::commit_transaction ()
#28 0x0000000190d05028 in CA::Transaction::commit ()
#29 0x0000000190d4ed7c in CA::Transaction::flush_as_runloop_observer ()
#30 0x000000019197fff4 in _UIApplicationFlushCATransaction ()
#31 0x000000019197d76c in _UIUpdateSequenceRun ()
#32 0x000000019197d3b0 in schedulerStepScheduledMainSection ()
#33 0x000000019197e254 in runloopSourceCallback ()
#34 0x000000018f69b834 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#35 0x000000018f69b7c8 in __CFRunLoopDoSource0 ()
#36 0x000000018f699298 in __CFRunLoopDoSources0 ()
#37 0x000000018f698484 in __CFRunLoopRun ()
#38 0x000000018f697cd8 in CFRunLoopRunSpecific ()
#39 0x00000001d45481a8 in GSEventRunModal ()
#40 0x0000000191cd090c in -[UIApplication _run] ()
#41 0x0000000191d849d0 in UIApplicationMain ()
#42 0x0000000193888148 in ___lldb_unnamed_symbol74307 ()
#43 0x0000000193834714 in ___lldb_unnamed_symbol71237 ()
#44 0x00000001938404d0 in ___lldb_unnamed_symbol71675 ()
#45 0x000000010656cbb8 in static UITestsApp.$main() ()
#46 0x000000010656d068 in main at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:85
#47 0x00000001b2d49e4c in start ()
The concurent thread:
Thread 12 Queue : com.apple.root.default-qos.cooperative (concurrent)
#0 0x00000001d877ecb4 in __psynch_mutexwait ()
#1 0x00000001ec51efa8 in _pthread_mutex_firstfit_lock_wait ()
#2 0x00000001ec51e9b8 in _pthread_mutex_firstfit_lock_slow ()
#3 0x000000019353c3d4 in _MovableLockLock ()
#4 0x00000001938625b4 in ___lldb_unnamed_symbol72967 ()
#5 0x000000023e01875c in merged partial apply forwarder for closure #4 @Sendable () -> () in closure #1 (Observation.ObservationTracking.Entry) -> Observation.ObservationTracking.Id in static Observation.ObservationTracking._installTracking(_: Observation.ObservationTracking, willSet: Swift.Optional<@Sendable (Observation.ObservationTracking) -> ()>, didSet: Swift.Optional<@Sendable (Observation.ObservationTracking) -> ()>) -> () ()
#6 0x000000023e018874 in partial apply forwarder for reabstraction thunk helper from @escaping @callee_guaranteed @Sendable () -> () to @escaping @callee_guaranteed @Sendable () -> (@out ()) ()
#7 0x000000023e017044 in function signature specialization <Arg[0] = Dead> of Observation.ObservationRegistrar.Context.willSet<τ_0_0, τ_0_1 where τ_0_0: Observation.Observable>(_: τ_0_0, keyPath: Swift.KeyPath<τ_0_0, τ_0_1>) -> () ()
#8 0x000000023e01757c in function signature specialization <Arg[0] = Dead> of Observation.ObservationRegistrar.willSet<τ_0_0, τ_0_1 where τ_0_0: Observation.Observable>(_: τ_0_0, keyPath: Swift.KeyPath<τ_0_0, τ_0_1>) -> () ()
#9 0x000000023e011e88 in Observation.ObservationRegistrar.withMutation<τ_0_0, τ_0_1, τ_0_2 where τ_0_0: Observation.Observable>(of: τ_0_0, keyPath: Swift.KeyPath<τ_0_0, τ_0_1>, _: () throws -> τ_0_2) throws -> τ_0_2 ()
#10 0x000000010656886c in TestDeadLockView.ObservableTest.withMutation<Int, ()>(keyPath:_:) at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/@__swiftmacro_7TestApp0A12DeadLockViewV010ObservableA00F0fMm_.swift:13
#11 0x000000010656858c in TestDeadLockView.ObservableTest._value.setter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/@__swiftmacro_7TestApp0A12DeadLockViewV010ObservableA0C6_value33_B4AA65106754BF17E8E6D1CDD7B2EB6BLL18ObservationTrackedfMa_.swift:11
#12 0x0000000106568e54 in closure #1 in TestDeadLockView.ObservableTest.value.setter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:58
#13 0x0000000106568e84 in partial apply for closure #1 in TestDeadLockView.ObservableTest.value.setter ()
#14 0x0000000106568da8 in NSLocking.withLock<NSLock>(_:) ()
#15 0x0000000106568ca0 in TestDeadLockView.ObservableTest.value.setter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:57
#16 0x000000010656a3b0 in closure #1 in closure #2 in closure #1 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:76
#17 0x000000010656fc64 in partial apply for closure #1 in closure #2 in closure #1 in TestDeadLockView.body.getter ()
#18 0x00000001065451c0 in thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> (@out A) ()
#19 0x000000010656fe78 in partial apply for thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> (@out A) ()