In implementing Application Metrics for Swift we had a number of tasks that we needed to run at regular intervals, in our case every 2 seconds. One of these was adding a job to the global Dispatch Queue. The first implementation looked like this:

public init() throws {
  DispatchQueue.global(qos: .background).async {
    self.snoozeLatencyEmit(Date().timeIntervalSince1970 * 1000)
  }
}
 
private func snoozeLatencyEmit(_ startTime: Double) {
  if (latencyEnabled) {
    let timeNow = Date().timeIntervalSince1970 * 1000
    let latencyTime = timeNow - startTime
    emitData(LatencyData(timeOfSample: Int(startTime), duration:latencyTime))
    sleep(sleepInterval)
    DispatchQueue.global(qos: .background).async {
      self.snoozeLatencyEmit(Date().timeIntervalSince1970 * 1000)
    }
  }
}

This seemed to work and was checked in by the developer. A day or two later another team member noticed that when running SwiftMetricsDash, loading the main dashboard web page could take anything from 0-60 seconds. Initially we suspected some other new web sockets code, but when that was ruled out we found that it was this snoozeLatencyEmit function that caused the problem. In fact we were calling sleep on the same queue that Kitura uses to serve web pages for multiple 2 second intervals, effectively blocking the page from being served! Oops!

So we rewrote it. The second implementation looked like this:

let jobsQueue = DispatchQueue(label: "Swift Metrics Jobs Queue")
 
public init() throws{
  DispatchQueue.global(qos: .background).async {
    testLatency()
  }
}

private func testLatency() {
  if(latencyEnabled) {
    // Run every two seconds
    jobsQueue.asyncAfter(deadline: .now() + .seconds(2), execute: {
      let preDispatchTime = Date().timeIntervalSince1970 * 1000;
      DispatchQueue.global().async {
        let timeNow = Date().timeIntervalSince1970 * 1000
        let latencyTime = timeNow - preDispatchTime
        self.emitData(LatencyData(timeOfSample: Int(preDispatchTime), duration:latencyTime))
        self.testLatency()
      }
    })
  }

This seemed to work, with the added bonus of allowing web pages to load! However CPU usage suddenly went up to 20% or higher even when the Swift application being monitored was doing nothing. We tested this outside of the SwiftMetrics framework too and still saw high CPU although we couldn’t find much information about it. We thought perhaps asyncAfter is inherently badly performing if you’re running it constantly?

Update: We were running with Swift 3.0.1 on Linux at the time that we encountered this issue. We later tried this out on a Mac and also tried the most recent snapshot of 3.1 (publicly available from swift.org) on Linux and it seems to work fine in both of those scenarios, so it looks like it was actually a Swift bug and is no longer an issue in the upcoming 3.1 release. We’re going to stick to the other way though, in case our users are running Swift 3.0.1 on Linux!

So we went back to using sleep, but took care to only sleep our own queues this time.

let jobsQueue = DispatchQueue(label: "Swift Metrics Jobs Queue")

public init() throws {
  testLatency()
}

private func testLatency() {
  if(latencyEnabled) {
    // Run every two seconds
   jobsQueue.async {
      sleep(2)
      let preDispatchTime = Date().timeIntervalSince1970 * 1000;
      DispatchQueue.global().async {
        let timeNow = Date().timeIntervalSince1970 * 1000
        let latencyTime = timeNow - preDispatchTime
        self.emitData(LatencyData(timeOfSample: Int(preDispatchTime), duration:latencyTime))
        self.testLatency()
      }
    }
  }
}

Note: Apparently Timers are also a good way to schedule repeating tasks in Swift, but we wanted to work with queues for various reasons so this article doesn’t cover that case.

5 comments on"How NOT to schedule repeating background tasks in Swift"

  1. SonnyWerghis April 05, 2017

    So, what does this mean – that if you use a global queue, you run the risk that your code could affect the performance of another queue? This seems like a design flaw in GCD. I would expect GCD to pick a “global” queue that is not being used.

  2. Sian January April 06, 2017

    I would have expected that too, but I couldn’t find any information on how many global queues there actually are or how GCD decides which queue to use (other than QOS). Maybe there is only one ‘background’ queue?

  3. I don’t know how I stumbled upon this, but it was a very interesting read. Thanks for posting!

  4. Shmuel Kallner May 01, 2017

    Instead of sleeping in your asynchronous block, why not use DispatchSourceTimer, with code similar to this:

    timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
    timer.scheduleRepeating(deadline: DispatchTime.now(), interval: 2.0,
    leeway: DispatchTimeInterval.milliseconds(1))
    timer.setEventHandler(handler: self.someHandler)
    timer.resume()

Join The Discussion

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