Normal view

There are new articles available, click to refresh the page.
Before yesterdayMain stream

Watch your background: automatic Time Machine backups

By: hoakley
14 February 2025 at 15:30

Automatic Time Machine backups are the classic example of background scheduling and dispatch by Duet Activity Scheduler and Centralised Task Scheduling, the DAS-CTS system, and the stimulus for my personal interest, when they went awry in macOS Sierra. Before looking at how these are handled, I’ll start by showing how DAS sets up its budgets and its lists of scheduled activities. Following that I’ll describe how scheduling and dispatching automatic Time Machine backups have changed in macOS Sequoia.

Preparations

Shortly after user login, DAS starts gathering its budgets enabling it to score activities. These include thermal policies, shared memory and energy, for example
Allocating 188 budget on start for com.apple.dasd.systemEnergy
Each is given in notional units that can’t be correlated with anything external to DAS.

Following those, DAS loads saved activities by group. It’s not clear where those are stored, though, and presumably they were in the lists of activities for that user when they last logged out. A background system task helper is created, with a scheduler, listener and publisher, before DAS solicits activities for resubmission. Among the first of those is an activity run as root to clean up software update logs:
DAS Submitted: 0:com.apple.softwareupdated.logs-cleanup:27B9B4 at priority 5 with interval 604800 (Wed Feb 12 20:28:32 2025 - Thu Feb 13 20:28:32 2025)
DAS <private>: Optimal Score 0.5687 at <private> (Valid Until: <private>)

There are also some backup activities, including that to initiate automatic backups:
DAS Submitted: 0:com.apple.backupd-auto:9015F3 at priority 30 with interval 1800 (Wed Feb 12 08:05:28 2025 - Wed Feb 12 08:25:28 2025)
DAS <private>: Optimal Score 0.8535 at <private> (Valid Until: <private>)
DAS 0:com.apple.backupd-auto:9015F3:[
{name: Boot Time Policy, policyWeight: 0.010, response: {33, 0.00, [{[Minimum seconds after boot]: Required:300.00, Observed:22.19},]}}
], Decision: MNP}

As with many other activities, that can’t be considered for running until at least 5 minutes after boot.

Dispatch

Eventually, DAS decides to run backupd-auto to initiate the automatic backup:
DAS Rescoring all 535 activities [<private>]
DAS 0:com.apple.backupd-auto:3F876A:[ ], Decision: CP Score: 0.994671}
DAS '0:com.apple.backupd-auto:3F876A' CurrentScore: 0.994671, ThresholdScore: 0.095313 DecisionToRun:1
DAS REQUESTING START: 0:com.apple.backupd-auto:3F876A

That’s the cue for CTS to initiate the activity and run it. DAS then records
DAS STARTING <_DASActivity: "0:com.apple.backupd-auto:3F876A", Utility, 60s, [27/01/2025, 11:08:52 - 27/01/2025, 11:28:52], Started at 27/01/2025, 11:28:30, Group: com.apple.dasd.default, PID: 568>!
DAS 0:com.apple.backupd-auto:3F876A:[ ], Decision: CP Score: 0.994671}

backupd-auto only runs for a fraction of a second before it completes, and its next run is rescheduled in 30 minutes, not an hour:
CTS Completed: com.apple.backupd-auto (0x61e077ac0)
CTS Rescheduling: com.apple.backupd-auto (0x61e077ac0)
DAS SUBMITTING: 0:com.apple.backupd-auto:59E600
CTS _xpc_activity_set_state_from_cts: com.apple.backupd-auto (0x7ae068320), set activity state to 1
CTS _xpc_activity_end_running: com.apple.backupd-auto (0x7ae068320) seqno: 0.
DAS COMPLETED <_DASActivity: "0:com.apple.backupd-auto:3F876A", Utility, 60s, [27/01/2025, 11:08:52 - 27/01/2025, 11:28:52], Started at 27/01/2025, 11:28:30, Group: com.apple.dasd.default, PID: 568>
DAS NO LONGER RUNNING 0:com.apple.backupd-auto:3F876A ...Tasks running in group [com.apple.dasd.default] are 2!
DAS Submitted: 0:com.apple.backupd-auto:59E600 at priority 30 with interval 1800 (Mon Jan 27 11:43:29 2025 - Mon Jan 27 11:58:53 2025)

The two activities are distinguished by their appended hex IDs: the original and now-completed backupd-auto was 3F876A, and the new one is 59E600. This ensures that the next backup will be run in about 30 minutes, without losing the time taken to perform the backup. Instead, the backup is performed by the backupd process run by backupd-auto.

While Time Machine is estimating the time required to perform the backup, DAS dispatches an analytics activity, and another named com.apple.backupd-auto.dryspell.

Re-scheduling

Once the backup has been completed, Time Machine will normally re-schedule the next backup for an hour later. When it does, it announces in the log
Re-scheduled next backup in 60 minutes (plus or minus 30 minutes)
before submitting another activity with a new ID for backupd-auto. Immediately after that, it cancels the previously scheduled backupd-auto activity for 59E600, so the interval to the next automatic backup is about an hour following completion of the previous one.

New in Sequoia is this twice re-scheduling of the next automatic backup, first for 30 minutes later, then for the full hour. Previously, the first re-scheduling was for 60 minutes and there was no change made when the backup had completed. Presumably this is considered to be more reliable than in Sonoma and earlier.

Summary

This is summarised in the following chart.

Previous articles

Background activities with DAS-CTS
Scheduling XProtect Remediator scans
In-app background tasks

Watch your background: In-app background tasks

By: hoakley
10 February 2025 at 15:30

So far in this series about running background activities I have concentrated on those run using LaunchAgents and LaunchDaemons property lists. This article shows how an app can create its own background activities that will then be run periodically while it’s running. Apple recommends these for actions such as automatic saves, backups, periodic content fetching and other tasks that aren’t visible to the user. Although these can be implemented using separate XPC activities, they are more complicated and can often be avoided using NSBackgroundActivityScheduler, which uses XPC to schedule and dispatch arbitrary tasks.

Code

Scheduled background activities are simple to code using a completion handler. First create the activity with an appropriate identifier
let activity = NSBackgroundActivityScheduler(identifier: "co.eclecticlight.MyApp.tasks")

Then configure its properties. For example, for an activity to be repeated every theSeconds with a tolerance of theTolerance seconds at a Background Quality of Service (QoS)
activity.repeats = true
activity.interval = TimeInterval(theSeconds)
activity.tolerance = TimeInterval(theTolerance)
activity.qualityOfService = QualityOfService(rawValue: 9)

Submit that activity for scheduling and dispatch by DAS-CTS by enclosing its code within a block terminated with a completion handler
activity.schedule() { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
… do the task
completion(NSBackgroundActivityScheduler.Result.finished) }
Task code should be treated as running in a separate thread. If you need to return results to the main thread, for instance, do that back on the main thread, for example using
OperationQueue.main.addOperation {
… write result out etc.
}

To remove the activity from DAS-CTS, simply invalidate it with
activity.invalidate()

What happens

In the log excerpts below, I lump together entries from com.apple.xpc and com.apple.xpc.activity as CTS for the sake of simplicity. The identifier given is from my own testbed co.eclecticlight.DispatchRider.tasks. For this example, the activity runs the blowhole command tool to write a single entry in the log, a convenient marker. The activity is set to repeat at an interval of 10 minutes, with a tolerance of 1 minute, and with a Background QoS.

You’ll see references to XPC activity states. Referring to Apple’s source code, those are defined as

  • 0 XPC_ACTIVITY_STATE_CHECK_IN, check-in has been completed;
  • 1 XPC_ACTIVITY_STATE_WAIT, waiting to be dispatched and run;
  • 2 XPC_ACTIVITY_STATE_RUN, now eligible to be run;
  • 3 XPC_ACTIVITY_STATE_DEFER, to be placed back in its wait state with unchanged times;
  • 4 XPC_ACTIVITY_STATE_CONTINUE, will continue its operation beyond the return of its handler block, and used to extend an activity to include asynchronous operations;
  • 5 XPC_ACTIVITY_STATE_DONE, the activity has completed.
Registration

When that code is run, the log first records the code block being added as an XPC activity:
CTS xpc_activity_register: co.eclecticlight.DispatchRider.tasks, criteria: dictionary
CTS [0x6000017c3570] activating connection: mach=true listener=true peer=false name=com.apple.xpc.activity
CTS _xpc_activity_register: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), 0
CTS xpc_activity_set_criteria: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), dict
CTS xpc_activity_set_criteria, lower half: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), dict
CTS _xpc_activity_set_criteria: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), dict
CTS _xpc_activity_set_criteria: xpc_set_event co.eclecticlight.DispatchRider.tasks, 1
CTS Subscribed to event co.eclecticlight.DispatchRider.tasks using token 1640

With its check-in completed, the activity is put in the wait state, awaiting dispatch:
CTS xpc_activity_set_criteria: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), setting state now to 1
CTS _xpc_activity_set_state: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), 1
CTS Creating on XPC add event: co.eclecticlight.DispatchRider.tasks
CTS Created: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
CTS Submitting: 501:co.eclecticlight.DispatchRider.tasks:922FF9 (CTS Activity 0x9cca6d900)

DAS now registers the activity, sets its priority at 5, gives it a time window to be run, and a score it must reach to be dispatched by DAS:
DAS SUBMITTING: 501:co.eclecticlight.DispatchRider.tasks:922FF9
CTS Registered: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
DAS Submitted: 501:co.eclecticlight.DispatchRider.tasks:922FF9 at priority 5 with interval 600 (Mon Jan 27 14:58:01 2025 - Mon Jan 27 15:08:01 2025)
DAS <private>: Optimal Score 0.5687 at <private> (Valid Until: <private>)

Note that the time window allocated here is in the past, to ensure that it already meets time criteria to undergo its first run. The priority assigned to this activity of 5 is different from its QoS of 9.

Dispatch

As with its other activities, dispatch of in-app background tasks starts when DAS rescores all its activities and the task’s score exceeds its threshold:
DAS default dasd dasd Rescoring all 534 activities [<private>]
DAS scoring dasd dasd 501:co.eclecticlight.DispatchRider.tasks:922FF9:[ ], Decision: CP Score: 0.995792}
DAS '501:co.eclecticlight.DispatchRider.tasks:922FF9' CurrentScore: 0.995792, ThresholdScore: 0.531465 DecisionToRun:1
DAS With <private> ...Tasks pre-running in group [com.apple.dasd.default] are 1!
DAS REQUESTING START: 501:co.eclecticlight.DispatchRider.tasks:922FF9
CTS DAS told us to run co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
DAS Setting timer (isWaking=1, activityRequiresWaking=0) between <private> and <private> for <private>

With that, CTS makes the activity eligible to be run, and it’s dispatched:
CTS evaluating activities
CTS co.eclecticlight.DispatchRider.tasks state change 1 -> 2
CTS Initiating: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
CTS [0x9cca6ea80] activating connection: mach=false listener=true peer=false name=(anonymous)
CTS [0x9cca6ea80] Channel could not return listener port.
CTS [0x9cca6df40] activating connection: name=com.apple.xpc.activity publishToken=1640
CTS [0x149e0a3b0] activating connection: mach=false listener=false peer=true name=com.apple.xpc.activity.peer[605].0x149e0a3b0
CTS _xpc_activity_dispatch: beginning dispatch, activity name co.eclecticlight.DispatchRider.tasks, seqno 1
CTS _xpc_activity_dispatch: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60): found an activity with matching seqno 1
CTS _xpc_activity_begin_running: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60) seqno: 1.
CTS _xpc_activity_dispatch: lower half, activity name co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), seqno from top half was 1
CTS [0x14b106880] activating connection: mach=false listener=false peer=false name=(anonymous)
CTS _xpc_activity_dispatch: created connection 0x14b106880 for activity name co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), seqno 1
CTS _xpc_activity_set_state: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), 2
CTS _xpc_activity_set_state: send new state to CTS: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), 2
CTS [0x9cca6e940] activating connection: mach=false listener=false peer=true name=com.apple.xpc.anonymous.0x9cca6ea80.peer[2992].0x9cca6e940
CTS Running (PID 2992): co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
DAS STARTING: <private&gt
CTS _xpc_activity_set_state_from_cts: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), set activity state to 2

No sooner has it started than CTS is waiting for it to complete and return to a checked-in state:
CTS __XPC_ACTIVITY_CALLING_HANDLER__: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), current state 2, pending state 0
CTS _xpc_activity_set_state: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), 4
DAS STARTING <_DASActivity: "501:co.eclecticlight.DispatchRider.tasks:922FF9", Maintenance, 60s, [27/01/2025, 14:58:01 - 27/01/2025, 15:08:01], Started at 27/01/2025, 14:59:53, Group: com.apple.dasd.default, PID: 2992>!
DAS Activity <private> has preventDeviceSleep 0. PluggedIn state: 1
DAS With <private> ...Tasks running in group [com.apple.dasd.default] are 2!
DAS 501:co.eclecticlight.DispatchRider.tasks:922FF9:[ ], Decision: CP Score: 0.995792}

The activity runs, in this case writing an entry in the log:
BWH Blowhole snorted!

Its state is then set to indicate it has completed:
CTS _xpc_activity_set_state: co.eclecticlight.DispatchRider.tasks (0x6000019c8e60), 5

Deregistration

This takes place when the activity is invalidated and removes it from scheduling and dispatch.

That perhaps unexpectedly results in its state being changed to make it ready to run:
CTS [0x9cca6e940] invalidated because the client process (pid 2992) either cancelled the connection or exited
CTS Client connection closed: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
CTS co.eclecticlight.DispatchRider.tasks state change 2 -> 3

But instead of that, it’s deferred and submitted for cancellation:
CTS Deferring: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
CTS Canceling: 501:co.eclecticlight.DispatchRider.tasks:922FF9 (CTS Activity 0x9cca6d900)
CTS Submitting: 501:co.eclecticlight.DispatchRider.tasks:0D2ADD (CTS Activity 0x9cca6d900)
DAS SUBMITTING: 501:co.eclecticlight.DispatchRider.tasks:0D2ADD
DAS CANCELED: 501:co.eclecticlight.DispatchRider.tasks:922FF9 at priority 5
DAS NO LONGER RUNNING 501:co.eclecticlight.DispatchRider.tasks:922FF9 ...Tasks running in group [com.apple.dasd.default] are 1!
CTS Unregistered on XPC remove event: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)

Although it has now completed execution, it still has an entry in the process table, as a process in the terminated state. This allows the parent process to read its exit status via the wait system call. For this, the activity is made a zombie:
CTS Creating zombie: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
CTS Set timer for zombie: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
DAS Submitted: 501:co.eclecticlight.DispatchRider.tasks:0D2ADD at priority 5 with interval 600 (Mon Jan 27 14:58:01 2025 - Mon Jan 27 15:08:01 2025)
DAS 501:co.eclecticlight.DispatchRider.tasks:0D2ADD:[ ], Decision: CP Score: 0.995792}
DAS '501:co.eclecticlight.DispatchRider.tasks:0D2ADD' CurrentScore: 0.995792, ThresholdScore: 0.547394 DecisionToRun:1
DAS Running <private> immediately on submission
DAS With <private> ...Tasks pre-running in group [com.apple.dasd.default] are 1!
DAS REQUESTING START: 501:co.eclecticlight.DispatchRider.tasks:0D2ADD
CTS DAS told us to run co.eclecticlight.DispatchRider.tasks (0x9cca6d900)

The zombie’s entry is removed from the process table, an action known as reaping, and its state set to -1:
CTS evaluating activities
CTS Reaping zombie: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
CTS co.eclecticlight.DispatchRider.tasks state change 3 -> -1
CTS Canceling: 501:co.eclecticlight.DispatchRider.tasks:0D2ADD (CTS Activity 0x9cca6d900)
CTS [0x9cca6ea80] invalidated because the current process cancelled the connection by calling xpc_connection_cancel()
CTS REAPED zombie: co.eclecticlight.DispatchRider.tasks (0x9cca6d900)
DAS CANCELED: 501:co.eclecticlight.DispatchRider.tasks:0D2ADD at priority 5

Dispatch intervals

In this example, the activity was set to repeat at an interval of 10 minutes, with a tolerance of 1 minute. According to Apple’s documentation, that should result in the activity being dispatched and run within a window of 9-11 minute intervals from the previous run. To assess how well DAS-CTS performed in this case, intervals between log entries by blowhole were calculated. They ranged between 5m08s and 16m34s, with an average of 9m29s (9 minutes 29 seconds), over 21 runs with 20 intervals. A total of 11 (55%) took place within the expected window, and their distribution is shown in the histogram below.

Most of those took place soon after user login, during a period of sustained and intensive background activity as Spotlight indexing took place, a backup was performed, and XProtect Remediator scans were undertaken.

Summary

  • Running background activities as arbitrary blocks of code within an app is simple using NSBackgroundActivityScheduler, and doesn’t require the use of XPC.
  • XPC activity states range from 0 to 5, as given above.
  • Background activities are then scheduled and dispatched by DAS-CTS.
  • Activity priorities are different from Quality of Service (QoS).
  • Dispatch is performed by DAS telling CTS to run an activity.
  • Deregistration first turns the activity into a zombie to return its result to the parent process, then reaps it from the process table.
  • Background activities aren’t run at constant intervals, but most intervals between runs should be within the window set by their interval and tolerance.
  • Running background activities using NSBackgroundActivityScheduler is an excellent way for them to be scheduled flexibly.

Previous articles

Background activities with DAS-CTS
Scheduling XProtect Remediator scans

Watch your background: Scheduling XProtect Remediator scans

By: hoakley
5 February 2025 at 15:30

Many of the background activities managed by Duet Activity Scheduler and Centralised Task Scheduling, the DAS-CTS system, are largely invisible to the user, as they’re services supporting other services. Two of the best-known activities are automatic Time Machine backups and XProtect Remediator scans for malware. I first came to learn about DAS-CTS eight years ago when investigating problems with backups in macOS Sierra, and have written extensively here about how they work. This article concentrates instead on XProtect Remediator scans, which are more recent and an important part of security defence in macOS.

XProtect Remediator (XPR) appears, and to a degree can work, like an app, located in /Library/Apple/System/Library/CoreServices/XProtect.app. Look inside its bundle and in the MacOS folder you’ll see its code alongside 24 scanning modules. These are run through LaunchAgents and LaunchDaemons property lists in its Resources folder. Once a day XPR runs a paired set of scans to detect and remove or remediate known malicious software. These are scheduled and dispatched by the DAS-CTS system, and understanding how they work helps diagnose and address problems that can arise with XPR scans.

Launch Events

XPR contains two key property lists setting up its scans, com.apple.XProtect.agent.scan.plist and com.apple.XProtect.daemon.scan.plist. These are essentially the same, the first to run scans as the current user, and the second as root. They define three types of Launch Events, which for the user are:

  • a fast scan, com.apple.XProtect.PluginService.agent.fast.scan, performed at intervals of 6 hours (21600 seconds), and run when on battery;
  • a standard scan, com.apple.XProtect.PluginService.agent.scan, performed at intervals of 24 hours (86400 seconds), but not run when on battery;
  • a slow scan, com.apple.XProtect.PluginService.agent.slow.scan, performed at intervals of 7 days (604800 seconds), but not run when on battery.

These are assigned a ProcessType of Background, so are scheduled and dispatched by the DAS-CTS system.

What the scans do

In the standard scan, each of the scanning modules is run in turn, once using the agent version running as the current user, normally 501, and once using the daemon version as root, user 0.

You’re unlikely to see any evidence of a fast scan at the moment, although they should be run every 6 hours. Fast scans only run specific scan modules designated by Apple as meriting such frequent checks. This was used during a period of high threat soon after XPR was first introduced, but as far as I’m aware hasn’t been used since. Currently, when a fast scan is run, each scanning module in turn is reported as not being required to run, and those single-line entries are the only records left in the log. That could of course change in response to a change in threat.

As a slow scan is only run once a week, they’re hard to locate in the log. So far I haven’t discovered any.

The results from standard scans are thus the only XPR activities you’re likely to see in the logs, and using XProCheck.

You can read relevant log entries using the DAS Scheduling log extract feature in Mints, or a generic log browser like Ulbow using the filter predicate
subsystem == "com.apple.duetactivityscheduler" OR subsystem CONTAINS "com.apple.xpc"

Registration

During startup, XPR’s property lists are read and its activities are registered by DAS-CTS. For example, this series of log entries reports the LaunchAgent for standard scans being registered following user login:

CTS Creating on XPC add event: com.apple.XProtect.PluginService.agent.scan
CTS Created: com.apple.XProtect.PluginService.agent.scan (0x9cc8652c0)
DAS Submit activity: <private> in group: <private> with capacity: 1
CTS Submitting: 501:com.apple.XProtect.PluginService.agent.scan:0B9197 (CTS Activity 0x9cc8652c0)
DAS SUBMITTING: 501:com.apple.XProtect.PluginService.agent.scan:0B9197
CTS Registered: com.apple.XProtect.PluginService.agent.scan (0x9cc8652c0)
DAS Submitted: 501:com.apple.XProtect.PluginService.agent.scan:0B9197 at priority 30 with interval 86400 (Sun Jan 26 23:23:56 2025 - Mon Jan 27 23:23:56 2025)

Its priority setting is different from the Quality of Service (QoS) set in its property lists, which is Utility, matching a QoS raw number of 17, not 30. That’s common, and demonstrates how QoS values set in the API aren’t the same as those used internally in macOS.

At this stage, DAS makes its first assessment as to whether to dispatch this activity. As this occurs within five minutes of starting up, DAS decides it Must Not Proceed, MNP. The Optimal Score for that activity is reported, and set as the minimum score it must achieve before DAS will dispatch it to be run.

DAS 501:com.apple.XProtect.PluginService.agent.scan:0B9197:[
{name: Boot Time Policy, policyWeight: 0.010, response: {33, 0.00, [{[Minimum seconds after boot]: Required:300.00, Observed:39.00},]}}
{name: Device Activity Policy, policyWeight: 20.000, response: {33, 0.00, [{deviceActivity == 1}]}}
], Decision: MNP}
DAS <private>: Optimal Score 0.6872 at <private> (Valid Until: <private>)

Dispatch

DAS rescores activities in its list at frequent intervals. When an XPR activity, here the LaunchDaemon version of XPR’s standard scan, exceeds the threshold, DAS makes the decision that the activity Can Proceed, CP.

DAS 0:com.apple.XProtect.PluginService.daemon.scan:847936:[ ], Decision: CP Score: 0.980125}
DAS '0:com.apple.XProtect.PluginService.daemon.scan:847936' CurrentScore: 0.980125, ThresholdScore: 0.587700 DecisionToRun:1
DAS With <private> ...Tasks pre-running in group [com.apple.dasd.default] are 1!
DAS 501:com.apple.XProtect.PluginService.agent.scan:0B9197:[ ], Decision: CP Score: 0.980125}

Some activities, presumably those identified as CPUIntensive or DiskIntensive in their property lists, are then checked against others for compatibility. This is marked by a sequence of those checks, such as

DuetAS '501:com.apple.XProtect.PluginService.agent.scan:0B9197' has compatibility score of -1.000000 with 0:com.apple.XProtect.PluginService.daemon.scan:847936 (Started at <Not yet started>). Bailing out.

In this case, it ensures that user and root scans aren’t run at the same time.

Once DAS is happy that starting this scan is desirable and compatible, it then requests CTS to start the activity.

DAS REQUESTING START: 0:com.apple.XProtect.PluginService.daemon.scan:847936
CTS DAS told us to run com.apple.XProtect.PluginService.daemon.scan (0x61e2fc140)
CTS evaluating activities
CTS com.apple.XProtect.PluginService.daemon.scan state change 1 -> 2
CTS Initiating: com.apple.XProtect.PluginService.daemon.scan (0x61e2fc140)
CTS _xpc_activity_dispatch: beginning dispatch, activity name com.apple.XProtect.PluginService.daemon.scan, seqno 0
CTS _xpc_activity_dispatch: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0): found an activity with matching seqno 0
CTS _xpc_activity_begin_running: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0) seqno: 0.
CTS _xpc_activity_dispatch: lower half, activity name com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), seqno from top half was 0
CTS _xpc_activity_dispatch: created connection 0x123004ac0 for activity name com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), seqno 0
CTS _xpc_activity_set_state: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), 2
CTS _xpc_activity_set_state: send new state to CTS: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), 2
CTS Running (PID 2351): com.apple.XProtect.PluginService.daemon.scan (0x61e2fc140)
DAS STARTING: <private>
CTS _xpc_activity_set_state_from_cts: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), set activity state to 2
CTS XPC_ACTIVITY_CALLING_HANDLER__: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), current state 2, pending state 0
CTS _xpc_activity_set_state: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), 4

DAS then records that the activity started:

DAS STARTING <_DASActivity: "0:com.apple.XProtect.PluginService.daemon.scan:847936", Utility, 60s, [26/01/2025, 23:23:54 - 27/01/2025, 23:23:54], Started at 27/01/2025, 12:05:36, Group: com.apple.dasd.default, Intensive: CPU Disk, PID: 2351>!
DAS Activity <private> has preventDeviceSleep 0. PluggedIn state: 1
DAS With <private> ...Tasks running in group [com.apple.dasd.default] are 3!
DAS Started <private>, total runtime from previous runs 0.0 mins
DAS 0:com.apple.XProtect.PluginService.daemon.scan:847936:[ ], Decision: CP Score: 0.980125}

Diagnosis

The only common problem encountered in running XPR is an apparent failure to run daily scans. Relevant causes are:

  • Insufficient log records, that don’t cover sufficient time to capture a set of scans.
  • Running a notebook on battery, as standard scans won’t be dispatched unless the Mac is running on mains power.
  • Running other CPU- or disk-intensive tasks, which may cause scans to be deferred until those are completed.
  • High thermal load, which will cause scans to be deferred until thermal conditions have improved. This is the origin of Duet in Duet Activity Scheduler, referring to the CoreDuet system responsible for monitoring the Mac’s operating environment and managing tasks appropriately.
  • Failure of DAS to score activities in its list, currently extremely improbable, but can only be diagnosed from its log entries.
  • Other failures in DAS-CTS, which again require log analysis.

One of the difficulties in diagnosis is that there’s no command tool that can inspect DAS activity lists, making log inspection essential in most cases.

Watch your background: background activities with DAS-CTS

By: hoakley
29 January 2025 at 15:30

Much of what macOS does happens in the background. You don’t have to refresh the Spotlight indexes when you edit and change documents, nor should you have to run backups manually, or flush caches. Those are all tasks that are intended to be performed by services within macOS, in the background, out of sight and out of mind. They are also tasks that can go wrong or cause problems, so it’s valuable to understand how they work. This article is the first of a small series looking at how background tasks are run, and what can go wrong with them.

In broad terms, tasks that run in the background fall into three types:

  • Background services, that are launched during startup, appear in the list of processes in Activity Monitor, and work on demand. Examples include cfprefsd to manage preferences and mds_stores to manage Spotlight’s indexes. Activity Monitor normally lists a total of around 500.
  • Background activities, that are typically run at flexible time intervals according their need. Examples include Time Machine backups and XProtect Remediator scans. There’s normally a total of over 500.
  • Background threads, that are created and controlled within an app, and are counted in the thread totals given in Activity Monitor. Most apps now use these to take advantage of the multiple CPU cores in Macs. There’s an average of around 3-5 threads per process.

Of those, I’m going to start by looking at background activities, as they’re least-known and barely documented, although some provide the most important services to the user.

Background activities

If you open Activity Monitor shortly after you’ve logged into your Mac and watch the processes taking up most % CPU and in the CPU History window, you’ll see a succession that become very active for a while, then disappear. Although some are background services running all the time, many stay for a while, do their work, then vanish: most are background activities, and in Apple silicon Macs usually run on the E cores alone.

Most background activities are run because they’re set up using property lists stored in the LaunchAgents or LaunchDaemons folders in one of the Library folders, /System/Library, /Library or ~/Library. Daemons are run as root (user 0), while Agents are run as a user (normally the current user, 501). One notable feature that you’ll see in their property lists is the key-value pair
<key>ProcessType</key>
<string>Background</string>

Apple explains this in man launchd.plist:
This optional key describes, at a high level, the intended purpose of the job. The system will apply resource limits based on what kind of job it is. If left unspecified, the system will apply light resource limits to the job, throttling its CPU usage and I/O bandwidth. This classification is preferable to using the HardResourceLimits, SoftResourceLimits and Nice keys. The following are valid values:
Background
Background jobs are generally processes that do work that was not directly requested by the user. The resource limits applied to Background jobs are intended to prevent them from disrupting the user experience.

I’ll return to those resource limits in a later article.

In-app background activity scheduling

Most background activities should be set up using a LaunchAgent or LaunchDaemon property list, but code in an app or command tool can also create a background activity using the NSBackgroundActivityScheduler API. Apple suggests those might be appropriate for actions such as automatic saves, backups, periodic content fetching, and installation of updates. I’m not aware of any apps that use that, other than my own experimental utility DispatchRider, which I’m in the process of updating. However, if you know, I’d be very grateful to hear of them.

In Swift, this might be implemented in outline as:
// create the activity
let activity = NSBackgroundActivityScheduler(identifier: "com.orgname.appname.tasks")
// set it up
activity.repeats = true
activity.interval = TimeInterval( … )
activity.qualityOfService = QualityOfService( … )
activity.tolerance = TimeInterval( … )
// schedule it
activity.schedule() { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
// activity code
completion(NSBackgroundActivityScheduler.Result.finished) }

// stop the activity
activity.invalidate()

Flexible scheduling and dispatch

Normally, when a LaunchAgent or LaunchDaemon is set to run at specified time intervals, launchd will run it at exactly that set time, as long as the Mac is running and awake. When Time Machine was introduced, it backed up using that mechanism, every hour at exactly the same minutes and seconds, as regular as clockwork. If you happened to be in the middle of demanding tasks, then those backups competed with your use of the Mac.

Seven years later, Time Machine was one of the first clients of a new flexible scheduling and dispatch system operated jointly by Duet Activity Scheduler (DAS) and Centralised Task Scheduling (CTS), using lightweight inter-process communication (XPC), DAS-CTS for short.

Background activities are either registered through their property list, or using NSBackgroundActivityScheduler, with XPC Activity (com.apple.xpc.activity). Activities are submitted with their time interval, a tolerance interval around that, and Quality of Service (QoS) to determine how aggressively they will be scheduled. From those, DAS assigns them a priority (different from their QoS), a time window, and an optimal score, used to determine when that activity should be performed.

The activity is then added to the list maintained by DAS. At frequent intervals, DAS checks through its list of activities, rescores each, and decides whether to dispatch that activity via CTS. Various policies are used to determine whether an activity should proceed. One simple example applies to many activities, which aren’t to be run until at least five minutes have elapsed since the most recent boot. DAS simply compares the Minimum seconds after boot required against observed time elapsed.

In some circumstances, DAS checks through the list of running activities to see whether one of those is incompatible with the activity it’s intending to approve for dispatch. That ensures, for example, that two activities competing for the same resource can’t be run together. As Apple promises, “the system can intelligently decide when to perform the task based on the specified criteria,” which also take into account environmental factors such as thermal pressure.

DAS thus arrives at a decision as to whether to proceed and dispatch an activity, recorded as either MNP (must not proceed) or CP (can proceed). In the latter case, it then tells CTS to run the activity, which proceeds via XPC, although XPC isn’t required to be used by the activity. On completion, an activity intended to be repeated is updated in the list maintained by DAS, ready for it to be dispatched for its next run.

Diagnosis and treatment

Since teething problems in macOS Sierra were fixed in High Sierra, the DAS-CTS system has been remarkably reliable, and run successfully for long periods between reboots. That’s just as well, as it has no management utility, and can only be observed through its entries in the log. To see it at work, filter log entries with a predicate for the subsystems com.apple.duetactivityscheduler and com.apple.xpc.activity. This is available as one of the standard log viewers in Mints.

DAS-CTS can only be reset/restarted by restarting the Mac. As we learned during its problems in Sierra, trying anything else is a waste of time.

Summary

Background activities:

  • are one of three common methods used to run tasks in the background, and used for over 500 activities in macOS Sequoia;
  • are typically run at flexible time intervals according their need, as in Time Machine backups and XProtect Remediator scans;
  • are most commonly created from property lists in LaunchAgents or LaunchDaemons folders, where the ProcessType key is set to Background;
  • can also be created in code using the NSBackgroundActivityScheduler API;
  • are managed by Duet Activity Scheduler (DAS) and Centralised Task Scheduling (CTS), using XPC, in the DAS-CTS system;
  • are registered with DAS when created, and added to its list of scheduled activities that are rescored frequently;
  • when DAS determines an activity can proceed, it tells CTS to run the activity, and it’s updated in the list ready for dispatch next time;
  • problems can only be diagnosed using the log, and the only way to reset DAS-CTS is to restart.

❌
❌