GCD – Accurate Enough for MIDI?

I’m not familiar with either GCD or high precision timers on the Mac (I don’t currently write code for iOS but I’m sure it’s similar). Since I’m working on MIDI code, this means I’m going to have to start learning about timers in some detail.

A question that comes up early on in my investigation is: is Grand Central Dispatch accurate enough, or do I need to go with real time scheduling?

With this in mind I wrote the following little swift class:

class GCDTask {

	init() {
		var timebase_info = mach_timebase_info_data_t()
		mach_timebase_info(&timebase_info);
		numer = UInt64(timebase_info.numer)
		denom = UInt64(timebase_info.denom)
	}
	
	func run() {
		lastNow = mach_absolute_time()
		dispatch_after(
			dispatch_time(DISPATCH_TIME_NOW, Int64(1 * NSEC_PER_SEC)),
			dispatch_get_main_queue()) {
				let curNow = mach_absolute_time()
				let delta = curNow - self.lastNow
				let nsDelta = delta * self.denom / self.numer 
				Swift.print("Elapsed time \(nsDelta) nanoseconds");
		}
	}
	
	var lastNow = mach_absolute_time()
	var numer = UInt64(0)
	var denom = UInt64(0)
}

I invoked it in my app delegate pretty simply:

	func applicationDidFinishLaunching(aNotification: NSNotification) {
		let task = GCDTask()
		task.run()
	}

The output of this is:

Elapsed time 1000202402 nanoseconds

I think that’s off by .02%. Hard to say if that’s significant.

There are few things to say here:

  • There are other GCD queues to try
  • Does the percentage of error get larger as the delay gets shorter?
  • I think I’ve got a reference count problem. Self should not be referred to directly in the closure.

I’ll worry about this sort of detail later.


Sometime later…

I changed the run() method to take a milliseconds argument like so:

	func run(ms: UInt64) {
		lastNow = mach_absolute_time()
		dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 
						Int64(ms * NSEC_PER_MSEC)),
			dispatch_get_main_queue()) {
				let curNow = mach_absolute_time()
				let delta = curNow - self.lastNow
				let nsDelta = delta * self.denom / self.numer
				Swift.print("Elapsed time \(nsDelta) nanoseconds");
		}
	}

And then I changed the app delegate so that it calls run 4 times. The calls are backwards from the order they execute. I wanted to make sure this would work:

	func applicationDidFinishLaunching(aNotification: NSNotification) {
		let task = GCDTask()
		task.run(2000)
		task.run(1000)
		task.run(1)
		task.run(0)
	}

The output is somewhat surprising:

Elapsed time 6262661 nanoseconds
Elapsed time 7722924 nanoseconds
Elapsed time 1000220551 nanoseconds
Elapsed time 2172275761 nanoseconds

A task that was supposed to run immediately ran 6ms later, if my math is right. A task that was supposed to run in 1 ms ran after 7. Also, the task that was supposed to run in 2 seconds ran with considerably less accuracy than the task that was supposed to run in one second.

I rearranged the tasks so that they are installed in order:

	func applicationDidFinishLaunching(aNotification: NSNotification) {
		let task = GCDTask()
		task.run(0)
		task.run(1)
		task.run(1000)
		task.run(2000)
	}

And this is the output:

Elapsed time 1217050 nanoseconds
Elapsed time 3380713 nanoseconds
Elapsed time 1000173028 nanoseconds
Elapsed time 2187949998 nanoseconds

This improves things, but I don’t think it improves things enough. GCD is probably not the way to go for MIDI event scheduling.


And … a bit later still…

So, about the issue with the reference count problem and the closure —

I decided to fix it. The issue is that the closure has a strong reference to self, and the object has a strong reference to the closure. One fixes this in trailing closures like so (I *think*):

	func run(ms: UInt64) {
		lastNow = mach_absolute_time()
		dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
										Int64(ms * NSEC_PER_MSEC)),
			dispatch_get_main_queue()) {
				[unowned self] in
					let curNow = mach_absolute_time()
					let delta = curNow - self.lastNow
					let nsDelta = delta * self.denom / self.numer
					Swift.print("Elapsed time \(nsDelta) nanoseconds");
		}
	}

The “[unowned self]” thing is a “Capture List”, and you can easily find info on it elsewhere. One thing to point out is that after I added “[unowned self]” above, the code crashed. The source of the crash was that my GCDTask object was a local variable of applicationDidFinishLaunching(). Once I turned it into a property, the crash no longer happened.

Leave a Reply

Your email address will not be published. Required fields are marked *