With the integration of Apple's pushToTalk framework - we create the PTChannelManager using its async initializer from AppDidFinishLaunching - using an actor to ensure the PTChannelManager is only created once.
With this we have been seeing a lot of crashes for users in our analytics dashboards happening about ~2 seconds after app launch around a task-dealloc.
Here is a simplified version of our actor and Manager - where the manager just shows the init. The init of it is an async optional init because the creation of the PTChannelManager uses an async throws.
actor PushToTalkDeviceContainer {
private var internalPushToTalkManagerTask: Task<PushToTalkManager?, Never>?
func pushToTalkManager() async -> PushToTalkManager? {
#if !os(visionOS)
if let internalPushToTalkManagerTask {
return await internalPushToTalkManagerTask.value
}
let internalPushToTalkManagerTask = Task<PushToTalkManager?, Never> {
return await PushToTalkManagerImp()
}
self.internalPushToTalkManagerTask = internalPushToTalkManagerTask
return await internalPushToTalkManagerTask.value
#else
return nil
#endif
}
}
public class PushToTalkManagerImp: PushToTalkManager {
public let onPushToTalkDelegationEvent: AnyPublisher<PushToTalkDelegationEvent, Never>
public let onPushToTalkAudioSessionChange: AnyPublisher<PushToTalkManagerAudioSessionChange, Never>
public let onChannelRestoration: AnyPublisher<UUID, Never>
private let ptChannelManager: PTChannelManager
private let restorationDelegate: PushToTalkRestorationDelegate
private let delegate: PushToTalkDelegate
init?() async {
self.delegate = PushToTalkDelegate()
self.restorationDelegate = PushToTalkRestorationDelegate()
self.onPushToTalkDelegationEvent = delegate.pushToTalkDelegationSubject.eraseToAnyPublisher()
self.onPushToTalkAudioSessionChange = delegate.audioSessionSubject.eraseToAnyPublisher()
self.onChannelRestoration = restorationDelegate.restorationDelegateSubject.eraseToAnyPublisher()
do {
ptChannelManager = try await PTChannelManager.channelManager(delegate: delegate, restorationDelegate: restorationDelegate)
} catch {
return nil
}
}
}
The crash stack trace is as follows:
0 libsystem_kernel.dylib 0x00000001e903342c __pthread_kill + 8 (:-1)
1 libsystem_pthread.dylib 0x00000001fcdd2c0c pthread_kill + 268 (pthread.c:1721)
2 libsystem_c.dylib 0x00000001a7ed6c34 __abort + 136 (abort.c:159)
3 libsystem_c.dylib 0x00000001a7ed6bac abort + 192 (abort.c:126)
4 libswift_Concurrency.dylib 0x00000001ab2bf7c8 swift::swift_Concurrency_fatalErrorv(unsigned int, char const*, char*) + 32 (Error.cpp:25)
5 libswift_Concurrency.dylib 0x00000001ab2bf7e8 swift::swift_Concurrency_fatalError(unsigned int, char const*, ...) + 32 (Error.cpp:35)
6 libswift_Concurrency.dylib 0x00000001ab2c39a8 swift_task_dealloc + 128 (TaskAlloc.cpp:59)
7 MyApp 0x0000000104908e04 PushToTalkManagerImp.__allocating_init() + 40 (PushToTalkManager.swift:0)
8 MyApp 0x0000000104908e04 closure #1 in PushToTalkDeviceContainer.pushToTalkManager() + 60
9 MyApp 0x00000001041882e9 specialized thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out A) + 1 (<compiler-generated>:0)
10 MyApp 0x0000000103a652bd partial apply for specialized thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out A) + 1 (<compiler-generated>:0)
11 libswift_Concurrency.dylib 0x00000001ab2c2775 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1 (Task.cpp:463)
I have a few different answers here:
-In terms of the specific crash, I'm not entire sure what EXACTLY is failing. I wasn't able to trace the language log message back to it's origin and those log messages aren't always that helpful. I'd need to see a full crash log to take a closer look at this.
-Have you tested this code on other failure points, notably macOS?
You specifically excluded visionOS, but PTChannelManager has a few other failure points with macOS being the most likely to occur (the other's are configuration issues which would always fail and running in the simulator).
As a side note here, I'm not sure the actor architecture here is doing very much to help you. The PTT delegate methods are going to be called on an private dispatch queue and I'd expect your app to be structured such that "PushToTalkManager" was only being accessed in a controlled/limited way.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware