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

❌
❌