Victor Poupet
Victor Poupet

Reputation: 51

How to prevent Timer slowing down in background

I am writing a Mac OS app in Swift and want to repeat a task every 0.5s (more or less, high precision is not required). This app should run in the background while I use other applications.

I'm currently using a Timer:

Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true)

It starts fine with updates roughly every 0.5s but after some time in the background, the Timer slows down considerably to intervals of roughly 1s or 2s (it's very close to these values to it seems that the timer skips ticks or has a slowdown of a factor 2 or 4).

I suspect it's because the app is given a low priority after a few seconds in the background. Is there a way to avoid this? It can be either in the app settings in XCode by asking to stay active all the time, or possibly from the system when the app is run (or even but doing things differently without Timer but I'd rather keep it simple if possible).

Here is a minimal working example: the app only has a ViewController with this code

import Cocoa


class ViewController: NSViewController {
    var lastUpdate = Date().timeIntervalSince1970

    override func viewDidLoad() {
        super.viewDidLoad()

        let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) {
            timer in
            let now = Date().timeIntervalSince1970
            print(now - self.lastUpdate)
            self.lastUpdate = now
        }
        RunLoop.current.add(timer, forMode: .common)
    }
}

Output at start is

0.5277011394500732
0.5008649826049805
0.5000109672546387
0.49898695945739746
0.5005381107330322
0.5005340576171875
0.5000457763671875
...

But after a few seconds in the background it becomes

0.49993896484375
0.49997520446777344
0.5000619888305664
1.5194149017333984
1.0009620189666748
0.9984869956970215
2.0002501010894775
2.001321792602539
1.9989290237426758
...

If I bring the app back to the foreground, the timer goes back to 0.5s increments.

Note: I'm running Mac OSX 10.15.5 (Catalina) on an iMac

Upvotes: 4

Views: 1225

Answers (2)

jvarela
jvarela

Reputation: 3847

As I stated in my comment below, if you want granularities lower than 1.0 s, you should not use Timer objects, but rather GCD. I wrote a class MilliTimer you can use where you have improved granularity down to a few milliseconds. Please try this in a Playground and then in your app. In this example, I set the granularity of the timer based on GCD to 50 milliseconds. To adjust the delay pass the delay you want in milliseconds in the respective parameter of the initializer. In your case, you might be interested in 500 ms = 0.5 s.

import Cocoa

public class MilliTimer
{
    static let µseconds = 1000000.0
    static var lastUpdate = DispatchTime.now()

    var delay = 0
    var doStop = false
    var runs = 0
    let maxRuns = 50

    private class func timer(_ milliseconds:Int, closure:@escaping ()->())
    {
        let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(milliseconds)
        DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
    }

    init(delay:Int) {
        self.delay = delay
    }

    func delta() -> Double {
        let now = DispatchTime.now()
        let nowInMilliseconds = Double(now.uptimeNanoseconds) / MilliTimer.µseconds
        let lastUpdateInMilliseconds = Double(MilliTimer.lastUpdate.uptimeNanoseconds) / MilliTimer.µseconds
        let delta = nowInMilliseconds - lastUpdateInMilliseconds
        MilliTimer.lastUpdate = now
        return delta
    }

    func scheduleTimer()
    {
        MilliTimer.timer(delay) {
            print(self.delta())
            if self.doStop == false {
                self.scheduleTimer()
                self.runs += 1

                if self.runs > self.maxRuns {
                    self.stop()
                }
            }
        }
    }

    func stop() {
        doStop = true
    }
}

MilliTimer(delay: 50).scheduleTimer()
CFRunLoopRun()

Upvotes: 1

Parag Bafna
Parag Bafna

Reputation: 22930

This is because of the App Nap. You can disable App Nap but it is not recommended.

var activity: NSObjectProtocol?
activity = ProcessInfo().beginActivity(options: .userInitiatedAllowingIdleSystemSleep, reason: "Timer delay")

The default tolerance value of timer is zero but The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of tolerance property.

Upvotes: 4

Related Questions