How to measure DispatchQueue back pressure

350 Views Asked by At

My program has many parallel processes each on their own queue. I'd like to be able to visualize/measure the back pressure of the queues.

One approach is to count every block that enters and exits, but I'm sure GCD has this information already. Is there a better approach to measuring back pressure?

2

There are 2 best solutions below

1
On

If you want to visualize what is going on, you can use OSLog, post .begin and .end events and watch it in Instruments’ “Points of Interest”. (See WWDC 2019 Getting Started with Instruments.)

So, import os.log:

import os.log

Then create a log:

private let pointsOfInterestLog = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: .pointsOfInterest)

Enqueue 100 tasks:

for i in 0..<100 {
    enqueueTask(i) {
        print("done \(i)")
    }
}

Where the routine posts a .begin event to the points of interest log before dispatching the task, and posts a .end event when the task actually starts, e.g.

func enqueueTask(_ index: Int, completion: @escaping () -> Void) {
    let id = OSSignpostID(log: pointsOfInterestLog)
    os_signpost(.begin, log: pointsOfInterestLog, name: "backlog", signpostID: id, "queued %d", index)

    queue.async {
        os_signpost(.end, log: pointsOfInterestLog, name: "backlog", signpostID: id, "started %d", index)
        ...
        completion()
    }
}

Then profile the app (with command+i or “Product” » “Profile”) and choose, for example, “Time Profiler” (which includes the “Points of Interest” tool). Start a recording an you will see a visual representation of your backlog:

enter image description here

(I expanded the “Points of Interest” to be big enough to show all 100 backlogged tasks.)

This is one way to visualize your backlog. I must confess that I generally use “Points of Interest” not to show the backlog, but rather to show when these tasks are actually running (i.e. .begin when the dispatched task actually starts running and .end when the dispatched task finishes). Or I use the “thread” view in Instruments to see how my worker threads are being used. Or I use the “CPU” view in Instruments to see how my CPUs are being used.

Regardless, as you can see, Instruments can be used to visualize whatever time ranges you want, in this case, the time between the adding of the task to the queue and when it starts running.

3
On

There is no API for querying the number of pending blocks in a GCD queue. That said, since you're asking about a queue that you "own", you can wrap it in a way that lets you keep track of that. For instance, you could make your own wrappers around dispatch_[a]sync that would increment a counter whenever you enqueued a block, and also wrapped the block to decrement the counter when the block completes. This really wouldn't be that hard to implement. (dispatch_queue_get_specific would likely be a good place to start...)

FWIW, a few years back, I would've suggested you use NSOperationQueue, but even NSOperationQueue has deprecated its operationCount property, so it's probably fair to say that Apple is unlikely to provide this functionality going forward, so implementing it yourself is probably your best option, if you really need this functionality.

This is probably not immediately helpful to you, but if you need to measure "back pressure" on GCD queues, you're probably not using them in the right way. If you're enqueuing work items that block during execution, that's bad, and will eventually lead to thread starvation. If you're enqueuing tons of work items that you might want to cancel, say if the user changed screens or something, then you should work on a pattern for cancellable work items (or use NSOperation). I'm struggling to think of a use case that would demand that you be able to measure "back pressure" that couldn't be solved with your own code.