Microsecond level precision when dispatching Tasks, possible?

I have a need to synchronize X number of iOS devices over the MultipeerConnectivity framework with a sub-millisecond level of precision. What I'm running into right now is I can get things close, within 1ms or so typically. I really need it to be exact, to the microsecond..

All devices serve as advertiser and browser, so any device can invoke an action that the remainder should act on at the same time. I am doing a stripped down implementation of Precision Time Protocol in order to figure out the time delta individually from device to device. To handle the slack a command is to occur exactly 1000ms after receipt.

Sequence of events:

  • Device sends command to connected devices (invoking device starts its clock)
  • Connected devices receive command and determine the delay from the invoking device
  • This delta is converted into nanoseconds
  • Task.sleep with nanoseconds using a continuous clock
  • When task resumes, action is invoked

What kind of guarantee does the caller have that the task will resume at the appointed time and not some bit later?

try await Task.sleep(until: .now + .nanoseconds(nanoseconds), clock: .continuous)

I want to ensure I'm using the most precise way of delaying execution.

Thanks!

1 Like

iOS is not a realtime system, and many things in Swift (anything that directly or indirectly triggers locks or allocations) are not realtime safe either. The closest to realtime you get is with audio which perhaps is not appropriate in your case unless you want to synchronise with audio instead of radios. To get a feeling how precise you can be I'd do an experiment of sending a ten or a hundred of packets at a steady rate of one packet per ms, ideally without using sleep at all (just polling), and record those packets arrival on the receiving side. The variance in latency of arrived packets is your precision, give or take. Network framework is a different implementation compared to MultipeerConnectivity so I'd give it a try as well. I'd also check if it's possible to read the current GPS clock time.

I wonder how many devices you need to synchronise and what's the end goal?

2 Likes

I have implemented synchronized audio playback across iOS devices using the multipeer connectivity framework. This was pre-Swift so was using Objective-C / GCD, and audio sync only requires about 10ms accuracy for the human ear, but I think this information might still be helpful. There were frankly a lot of techniques that combined together to make this work, but since you’re already down to 1ms difference, this was the last mile change you might need.

The main thing to know here is that no, the delivery of timer events on iOS is not guaranteed to be exact due to something called timer coalescing or timer “slop”. In order to save power / effort, iOS may slightly shift the delivery of a timeout such that it can deliver multiple events to your process at once rather than in rapid succession. Again, not sure if this still applies to Swift tasks but the solution is fairly easy and worth a try.

Rather than attempting to dispatch the event to the exact right instant, set the timeout to 5-10ms early and when it fires, enter a while loop comparing the current instant to the desired fire instant until you reach it. Yes, this will block the current thread you’re on so be mindful of that, but it was the only way I could get it to work. Typing on a phone so I don’t have a code snippet for you but hope this was helpful!

6 Likes

I think it is possible to disable timer coalescing, for example with the strict timer flag.

That’s a good point, I think I tried using this at the time and it helped but was not consistent enough to rely on entirely. As the docs say it is a “best effort” to respect the leeway.

Also worth nothing that this is a flag for GCD timers, and not Swift concurrency. I see Task.sleep takes an optional tolerance arg which probably behaves similarly to GCD timer leeway, though not sure if passing 0 or nil here communicates no tolerance.

Either way, trying to wake up a Task may sometimes need to wake up an OS thread (if it cannot acquire one that is already active), this can take up to a few milliseconds based on my not-so-recent tests. For better consistency, always aim to wake up slightly earlier than you need to so you can acquire a thread and loop until the exact right instant.

I need to use a dispatch timer source to do that, don't I?

Let's assume you can get the synchronised time down to 1µs precision or better:

while time() < deadline { noop() } // busy waiting loop, bad but most precise
// ***
doSomethingAtThisPreciseTime()

so that just before the *** point your timing is exact to under µs precision. What would stop OS to suspend the thread at *** point for an arbitrary amount of time which could be well beyond 1µs, or even 1ms) to run another thread on that CPU core? (unless you somehow reserve a specific CPU core to be used just by your thread and pin that thread to that CPU core).

2 Likes

Yes, that may be one of those "use the right tool for the job" kind of thing, but I'll leave such judgment to you ;)

Typically I have between 4-6 devices at any given time to sync. I might have missed the constraint that none of the devices will have a network connection other than the MultipeerConnectivity set up one.

Yep, FTM Network framework supports not just normal networking but also MultipeerConnectivity style p2p networking, that's why I recommend it.

A valid question. Again speaking in the context of GCD I think selecting a quality of service such as user interactive should mitigate the possibility of this happening to service lower QoS queues and their thread pools. Presumably the main thread has the highest QoS priority and ideally is never taken off the CPU mid-execution to service a background thread or backgrounded app process. I don’t know for sure that it works this way, but anything else would be pretty terrible for the UX and would sort of defeat the purpose of having a main thread vs background threads. I am fairly certain I ran the logic I described on the main thread and was willing to drop a few UI frames looping on it to get the synchronization accuracy I wanted.

Apologies I can’t speak intelligently on this topic from the perspective of Swift concurrency, though for something mission critical I would focus more on getting something that works rather than using the latest and greatest APIs. GCD is an incredibly mature library with lots of example code around the web. Just my two cents!

2 Likes