7
let expecation = expectationWithDescription("do tasks")
for i in 0...40 {

    let afterTiming = 0.3 * Double(i)
    let startTime = CFAbsoluteTimeGetCurrent()

    let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(afterTiming * Double(NSEC_PER_SEC)))

    dispatch_after(delayTime, dispatch_get_main_queue()) {
        let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
        print("\(afterTiming) - \(timeElapsed) : \(i)")
    }
}

waitForExpectationWithTimeout(14)

after 30 executes its almost a second off, and console start acting weird with showing two and two print lines simultaneously

9.0 - 9.88806998729706 : 30
9.3 - 9.88832598924637 : 31

Is there any way for an XCTest to get closer to actually doing the requests "on correct time"? Like getting the request that should be done after 9 seconds not being done after 9.88 seconds..

bogen
  • 9,954
  • 9
  • 50
  • 89
  • 1
    I removed the `XCTest` tag as this doesn't have anything specific to do with XCTests. It's only coincidental that you need this for a test. Although... I'd love to see what you're actually trying to test like this (and then add the tag back). – nhgrif Apr 18 '16 at 12:35
  • @nhgrif it is important that XCTest is mentione because it actually calls waitForExpectationsWithTimeout() - I'm not sure if that will work with for example blocking the main thread in tests – bogen Apr 18 '16 at 12:37
  • 1
    If you feel mentioning `XCTest` is important, then you *really* need to provide the test that you're trying to run for context. As it stands now, this is a bit of an [XY Problem](http://meta.stackexchange.com/q/66377/244435). – nhgrif Apr 18 '16 at 12:38
  • @nhgrif Yeah, probably true its not that big deal really. – bogen Apr 18 '16 at 19:44
  • It seems you are waiting for 10 seconds, but you need to wait at least 12 seconds. – gnasher729 Apr 19 '16 at 08:43
  • The closure is never executed sooner than at the given time. However, there is no way to guarantee an exact time. I believe 0.5 secs is the maximum error. When there is a delay, it's absolutely possible to execute several enqueued closures at once. – Sulthan Apr 28 '16 at 22:51
  • The best results I was able to get were based on: 1) Creating a separate serial queue as a target for `dispatch_after` , 2) doing an async dispatch back to main to print, and 3) pre-loading an array with all the delay times. My final line was "12.0 - 12.1404519677162 : 40" but I still got that strange burst effect on the console. – Phillip Mills Apr 29 '16 at 00:10
  • In `dispatch_after`, `dispatch_source_set_timer` is called with a leeway of 0xffffffffffffffff. Why don't you use NSTimer or a GCD timer? – Willeke Apr 29 '16 at 22:30
  • You really need to explain why, it's highly unlikely that this is the best approach... – Wain May 01 '16 at 16:59

2 Answers2

2

Not sure if you're set on using dispatch, but NSTimer is behaving much more accurately in my testing. Here's some sample code to run an action numOfTimes times

var iterationNumber = 0
var startTime = CFAbsoluteTimeGetCurrent()
let numOfTimes = 30

// Start a repeating timer that calls 'action' every 0.3 seconds 
var timer = NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: #selector(ViewController.action), userInfo: nil, repeats: true)

func action() {
    // Print relevant information
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print("\(Double(iterationNumber+1) * 0.3) - \(timeElapsed) : \(CFAbsoluteTimeGetCurrent())")

    // Increment iteration number and check if we've hit the limit
    iterationNumber += 1
    if iterationNumber > numOfTimes {
        timer.invalidate()
    }
}

Some of the output of the program:

7.8 - 7.80285203456879 : 25
8.1 - 8.10285001993179 : 26
8.4 - 8.40283703804016 : 27
8.7 - 8.70284104347229 : 28
9.0 - 9.00275802612305 : 29
9.3 - 9.3028250336647 : 30

It stays within a few milliseconds of the expected time (I'm assuming that's the time it takes to call CFAbsoluteTimeGetCurrent) and doesn't drift or clump them together.

One disadvantage to this approach is that it doesn't schedule each action all up front like it does in your code, although I'm not sure if that matters to you.

Bernem
  • 298
  • 3
  • 9
  • 1
    The documented precision for `NSTimer` is 50-100 ms, see http://stackoverflow.com/questions/9737877/how-to-get-a-accurate-timer-in-ios so this could be good enough. – Sulthan May 01 '16 at 19:47
  • Good to know. Thanks! – Bernem May 01 '16 at 20:15
  • Also, `NSTimer` provides a tolerance value that allows you to specify the amount of strictness the system has to execute the task on time. While the default value is 0, even if actual precision is a bit off, `NSTimer` automatically schedules its next fire date regardless of when the previous fire actually took place in order to prevent drag. – Matthew Seaman May 02 '16 at 04:10
1

Using CADisplayLink, a high precision timer

(not a nice code, just hacked together fast)

var displayLink: CADisplayLink?
var startTime: CFAbsoluteTime = 0
var nextTime: CFAbsoluteTime = 0
var index: Int = 0

func testIncrement() {
    self.startTime = CFAbsoluteTimeGetCurrent()
    self.nextTime = self.startTime

    displayLink = CADisplayLink(target: self, selector: #selector(execute))
    displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)

    let expectation = expectationWithDescription("test")

    self.waitForExpectationsWithTimeout(20.0, handler: nil)
}

func execute() {
    let currentTime = CFAbsoluteTimeGetCurrent()

    if (currentTime - nextTime < 0) {
        return
    }

    let timeElapsed = currentTime - startTime

    print("\(timeElapsed) : \(index)")

    index += 1
    nextTime = startTime + 0.3 * CFAbsoluteTime(index)

    if (index > 30) {
        displayLink?.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
    }
}

Note the method is actually executed multiple times and internally you have to check whether enough time elapsed.

Sulthan
  • 128,090
  • 22
  • 218
  • 270