22

Lately I've been using dispatch_after instead of performSelector:withObject:afterDelay when I want to trigger some code after a delay. The code is cleaner, it has access to the enclosing scope, I can put the code in-line instead of writing a throw-away method, etc, etc.

My code might look like this:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
  delay * NSEC_PER_SEC),
  dispatch_get_main_queue(),
  ^{
    //Delayed-execution code goes here.
  }
);

However, I recently discovered that the time-to-excution from this code seems to run pretty consistently about 10% slower than requested. If I ask for a delay of 10 seconds, my block gets executed about 11 seconds later. This is on an iOS device. The times seem to match quite closely on the simulator.

The code I'm using to test this is pretty simple:

NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
  delay * NSEC_PER_SEC),
  dispatch_get_main_queue(),
  ^{
    NSTimeInterval actualDelay = [NSDate timeIntervalSinceReferenceDate] - startTime;
    NSLog(@"Requested delay = %.3f. Atual delay = %.3f", delay, actualDelay);
    //Delayed-execution code goes here.
  }
);

I've tested on devices from an iOS 4S to an iPad Air and the extra delay is pretty consistent. I haven't yet tested on an older device like an iPhone 4 or an iPad 2, although I will do that soon.

I might expect 20-50 ms of "slop" in the delay, but a consistent 10% - 11% overshoot is odd.

I've added a "fudge factor" to my code that adjusts for the extra delay, but I find it surprising:

#define  delay_fudge 0.912557 //Value calculated based on averages from testing.


NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
  delay * delay_fudge *  NSEC_PER_SEC),
  dispatch_get_main_queue(),
  ^{
    NSTimeInterval actualDelay = [NSDate timeIntervalSinceReferenceDate] - startTime;
    NSLog(@"Requested delay = %.3f. Actual delay = %.3f", delay, actualDelay);
    //Delayed-execution code goes here.
  }
);

I should probably do more analysis and see if there is a fixed increase in delay plus a delay factor or a straight percent delay, or perhaps some non-linear scale to the error, but for now a simple multiplier seems to do pretty well.

Duncan C
  • 128,072
  • 22
  • 173
  • 272

1 Answers1

23

You may have heard about Timer Coalescing and App Nap - which helps to reduce power consumption.

What you are observing here is the effect of delaying system events up to a certain "leeway value" in order to be able to execute them all together at one point in time, "Timer Coalescing". That will increase the duration the CPU can dwell on its power reduced mode.

For dispatch lib, there is a flag which can be used to increase the accuracy of the "leeway value", which also affects eventually the accuracy of a timer (see below). I don't think its a good idea to make timers unnecessary accurate - for mobile devices for example.

My suspicion is, that dispatch_after will use a dispatch timer with a certain leeway value set, which is implementation defined.

You can implement quite accurate timers with dispatch lib, using dispatch_source_set_timer(), where you can also specify the "leeway value".

See also: dispatch/source.h

/*!
 * @typedef dispatch_source_timer_flags_t
 * Type of dispatch_source_timer flags
 *
 * @constant DISPATCH_TIMER_STRICT
 * Specifies that the system should make a best effort to strictly observe the
 * leeway value specified for the timer via dispatch_source_set_timer(), even
 * if that value is smaller than the default leeway value that would be applied
 * to the timer otherwise. A minimal amount of leeway will be applied to the
 * timer even if this flag is specified.
 *
 * CAUTION: Use of this flag may override power-saving techniques employed by
 * the system and cause higher power consumption, so it must be used with care
 * and only when absolutely necessary.
 */

#define DISPATCH_TIMER_STRICT 0x1

...

 * Any fire of the timer may be delayed by the system in order to improve power
 * consumption and system performance. The upper limit to the allowable delay
 * may be configured with the 'leeway' argument, the lower limit is under the
 * control of the system.
 *
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • That sounds like a good theory on why it's delaying. Looking at th docs I see how you would use dispatch_source_set_timer to change the setting of a "timer source", but then it leaves me high and dry as to what a dispatch_source_t timer source is, how I create one, or how I use one to create timers. Even trying to search in application frameworks isn't revealing anything. Can you point me in the right direction? I don't know where to start in figuring this out. I guess I need to slog through lots of GCD documentation. – Duncan C Jan 21 '14 at 23:22
  • @DuncanC I can share some code here [RXTimer](https://gist.github.com/couchdeveloper/8550768), which is an implementation of a timer using dispatch lib. – CouchDeveloper Jan 21 '14 at 23:38
  • Thanks @CouchDeveloper. That looks like exactly what I need. You just happen to have a cocoa wrapper for exactly what I need in your back pocket, huh? Cool. One question. I gather that the call to dispatch_source_cancel in the timer event handler kills the timer after one call so it isn't a repeating timer? Why not add a repeats: bool parameter to the init, like NSTimer has, and only call dispatch_source_cancel if repeats = NO? – Duncan C Jan 22 '14 at 01:58
  • @DuncanC It's a "one shot timer". One can implement a repeating timer easily, though. You can experiment with RXTimer, of course, and add that feature. Possibly, I'll take a look too, and update the Gist sample when I've some spare time. And yes, that was already in my "back pocket" ;) – CouchDeveloper Jan 22 '14 at 09:39
  • 8
    For what it's worth, a glance at the implementation of dispatch_after_f at http://www.opensource.apple.com/source/libdispatch/libdispatch-339.1.9/src/queue.c confirms a default leeway of 10% of the delay. – bdash Jan 28 '14 at 10:19
  • @bdash Thanks for looking it up and for the confirmation. The results indicate exactly this amount of leeway ;) – CouchDeveloper Jan 28 '14 at 10:25
  • @CouchDeveloper: I thought that App Nap is a OS X Mavericks feature. Is it done on iOS as well? – Martin R Mar 04 '14 at 08:41
  • @MartinR This particular feature "App Nap" as advertised is likely only on Mavericks. On iOS we have foreground and background processing. On iOS (and on the device), dispatch_after behaves exactly like on Mavericks. That is, the _leeway_ value and thus "timer coalescing" actually is in effect. Though, I could imagine, that the implementation of timer coalescing within the kernel might depend on the hardware. Note, timer coalescing has been implemented in Windows 7, too. – CouchDeveloper Mar 04 '14 at 09:42
  • I had to rewrite a lot of code because I was using dispatch_after which run perfectly in simulator but did not call some of my callbacks on device (iOS 8 iPhone4s). I scheduled it on the main thread,I have no idea if this changed anything. Warning for people! – Evgeni Petrov Nov 14 '14 at 12:42
  • @DuncanC - In swift you can use: `let timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, timerQueue)` – diegoreymendez Jan 27 '16 at 11:50
  • @bdash whoa great find! this was really bugging me. – jasongregori Apr 04 '18 at 17:41
  • I tested dispatch_after, dispatch_source_t and performSelectorAfterDelay and performSelectorAfterDelay was often slightly more accurate. dispatch_after as mentioned is nearly 10% inaccurate and therefore near useless in a large number of cases. – RunLoop May 08 '19 at 17:00
  • @RunLoop There's no guarantee anyway, _when_ a dispatched function will be actually executed. The queue may already have a number of other functions that have been enqueued that will be executed before, or there are no active threads available, since there are other things to process on the CPU. So, again, disptach queues are first and foremost "queues". Enqueueing a function does not mean it will be executed immediately. – CouchDeveloper May 10 '19 at 07:08