How macOS runs background activities: 4 Using XPC Activity

According to Apple’s Daemons and Services Programming Guide (last revised substantially in 2011-12), XPC services are managed by launchd. However, the more recent Energy Efficiency Guide for Mac Apps refers to activities created through XPC Activity as being managed by CTS.

This article explores how a developer can use XPC Activity as a way of running concurrent activities, and how these are handled in the dispatching systems in macOS.

Code

Using the example code provided in that more recent manual, I used two different task configurations, based on the same completion in Swift 3. The first sets up a repeating activity thus:
var criteria = xpc_dictionary_create(nil, nil, 0)
xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REPEATING, true)
xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_DELAY, XPC_ACTIVITY_INTERVAL_1_MIN)
xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_INTERVAL, XPC_ACTIVITY_INTERVAL_1_MIN)
xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY)
xpc_activity_register("co.eclecticlight.GCDlaunch.check", criteria) {activity in
let task = Process()
task.launchPath = "/usr/local/bin/blowhole"
task.arguments = []
let outPipe = Pipe()
task.standardOutput = outPipe
task.launch()
task.waitUntilExit()
os_log("Finished GCDlaunch.")
}

The second contains the same completion, but configures it as a one-off task, using
xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REPEATING, false)
xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_DELAY, XPC_ACTIVITY_INTERVAL_1_MIN)
xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_GRACE_PERIOD, 3 * XPC_ACTIVITY_INTERVAL_1_MIN)
xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY)

CTS and DAS

When the one-shot version is run, the following entries are made in Sierra’s unified log:

First, Centralized Task Scheduling (CTS) acknowledges the XPC Activity by working out the window in which to run it, and submitting that to Duet Activity Scheduler (DAS). In this case, the activity is intended to be run between 18:35:30 and 18:38:30. That is set by the dictionary entries shown above.
18:34:30.416633 Submitting DASActivity: <_DASActivity: "501:co.eclecticlight.GCDlaunch.check", Utility, 60s, [15/05/2017, 18:35:30 - 15/05/2017, 18:38:30]>
18:34:30.416752 Registered unmanaged XPC Activity: co.eclecticlight.GCDlaunch.check
18:34:30.416777 co.eclecticlight.GCDlaunch.check: <dictionary: 0x7ff60663a830> { count = 6, transaction: 1, voucher = 0x7ff606744ad0, contents =
"Repeating" => <bool: 0x7fffde7f1ac8>: false
"_SequenceNumber" => <uint64: 0x7ff60660c7b0>: 1
"GracePeriod" => <int64: 0x7ff606627b20>: 180
"name" => <string: 0x7ff60660f2e0> { length = 32, contents = "co.eclecticlight.GCDlaunch.check" }
"Delay" => <int64: 0x7ff606626ae0>: 60
"Priority" => <string: 0x7ff606609690> { length = 7, contents = "Utility" }
}

Periodically, DAS rescores all the activities it has in its queue. On this occasion, that XPC Activity task attains a score sufficiently close to 1.0 to enable it to proceed. Accordingly DAS instructs CTS to run the activity:
18:36:08.779496 DuetHeuristic-BM DuetActivitySchedulerDaemon Rescoring all 78 activities [<private>]
18:36:08.779871 DuetHeuristic-BM DuetActivitySchedulerDaemon <private>:[
<private> ] sumScores:27.010000, denominator:29.010000, FinalDecision: Can Proceed FinalScore: 0.931058}
18:36:08.781969 DuetHeuristic-BM DuetActivitySchedulerDaemon Running activities : <private>
18:36:08.782159 UserEventAgent DuetActivityScheduler STARTING: <private>

CTS now takes on the running of the activity, first reporting that it was DAS which “told us to run” it. It then changes the activity’s state, and activates it:
18:36:08.782170 DAS told us to run co.eclecticlight.GCDlaunch.check
18:36:08.869172 evaluating activities
18:36:08.869215 co.eclecticlight.GCDlaunch.check state change 1 -> 2
18:36:08.869218 Initiating XPC Activity: co.eclecticlight.GCDlaunch.check
18:36:08.869783 taking power assertion: co.eclecticlight.GCDlaunch.check: 39694

Once the activity has completed, CTS changes its state and shuts it down:
18:36:09.013050 co.eclecticlight.GCDlaunch.check state change 2 -> 5
18:36:09.013065 Completed XPC Activity: co.eclecticlight.GCDlaunch.check
18:36:09.013069 Completing DASActivity: 501:co.eclecticlight.GCDlaunch.check
18:36:09.013139 releasing power assertion: 39694
18:36:09.013472 XPC Activity client connection closed: co.eclecticlight.GCDlaunch.check
18:36:09.013490 Unregistered unmanaged XPC Activity: co.eclecticlight.GCDlaunch.check
18:36:09.013492 co.eclecticlight.GCDlaunch.check state change 5 -> -1

If the activity were to be a repeating one, then CTS would return to the top, to work out the next window in which the activity was to be run, and submit that to DAS for its activity queue, and so the sequence would cycle through each time the activity is to be run.

Similarity with NSBackgroundActivity

In the first article in this series, I drew out the call and log sequence for NSBackgroundActivityScheduler. As far as log entries go, XPC Activity appears identical:

gcd01

A PDF is here: gcd01a

There may well be differences in the mechanism which are not reflected in CTS and DAS. However, this evidence implies that using NSBackgroundActivityScheduler and XPC Activity are identical in terms of activity scheduling and dispatch, using CTS and DAS.

If this is correct, it confirms my initial draft diagram of the ways in which macOS manages tasks on processor cores:

GCD4

And in PDF from here: GCD4

It is perhaps worth noting that Apple’s latest documentation does not refer to NSBackgroundActivityScheduler and XPC Activity invoking different mechanisms within macOS, but refers to them as different APIs.