All aboard for Catalyst: how macOS runs iPadOS apps

Of all the engineering projects in Catalina, the largest and most ambitious must be Catalyst, in which Apple has made it far easier to ship a macOS version of an existing iPadOS product. It’s also politically charged: many longstanding macOS developers have drawn attention to its limitations, and expressed concerns that Catalyst apps will compare poorly with those developed for macOS alone. This article avoids entering those controversies, but concentrates instead on how Catalyst apps run on macOS, as reflected in Catalina’s log.

Because Catalyst and its macOS support are so new, in these early versions of 10.15 they log verbosely. This may give the impression that they are slow and inefficient, and inordinately complex. But if older subsystems like LaunchServices were to make as verbose log entries, they would probably appear quite similar. This does provide an excellent opportunity to see Apple’s latest development work in action.

Catalyst apps depend on two new processes, UIKitSystem and runningboardd. The first of those is launched on demand, so if you don’t run any Catalyst apps it won’t appear listed in Activity Monitor. runningboardd is the service at the heart of Catalina’s new RunningBoard subsystem, and is launched during system startup so that it can track most user processes, although for the moment it only manages those which are Catalyst apps.

In UIKitSystem, there are at least two more -Board subsystems: FrontBoard and BaseBoard. These don’t appear to exist as separate processes as far as Activity Monitor is concerned, but are part of the Frameworks which make up UIKit support in Catalyst services. From their names, FrontBoard and BaseBoard appear related to the standard iOS/iPadOS app which manages the Home screen, SpringBoard, and iOS seems to have had both FrontBoard and BaseBoard for some time, perhaps iOS 9.0 or even earlier.

One of the fundamental differences between apps developed exclusively for macOS and those developed for iPadOS is their central class framework. In macOS, that’s AppKit, which provides basic app support, documents, and the human interface, including accessibility support. Its equivalent in iPadOS/iOS is UIKit. The two systems have some very different concepts and capabilities. For example, AppKit’s interface is centred on windows, views and menus, whilst UIKit has interface scenes.

UIKitSystem, which first appeared in Mojave, is a background app which appears to interface with the Catalyst app’s use of iPadOS services and bridges from them to macOS. Catalyst apps also have direct access to AppKit to enable their macOS versions to work and interact more like dedicated macOS apps, but UIKit is intended to provide a common core for them both.

RunningBoard is a resource and lifecycle manager which is new to Catalina. In addition to managing resources such as memory, it controls CPU and GPU usage. At this stage, RunningBoard only tracks macOS apps and doesn’t try to manage them. However, as such management is already mandatory in iOS/iPadOS, Catalyst apps are the first which are managed by RunningBoard. I have written about it here.

To examine how these different subsystems support Catalyst apps, I launched two, LookUp and Twitter, and followed what happened in the unified log. Over a period of less than twenty seconds, more than 10,000 log entries were written, almost all concerning those two apps. I have produced a short summary of extracts which are given at the end, and here describe what I think they reveal about Catalyst apps and their support in macOS 10.15.1.

As usual when opening an app through the macOS GUI, it is LaunchServices which starts handling the action. This reaches RunningBoard very quickly, less than 0.005 second later. Shortly after that, an AppleEvent is posted for the Catalyst app. Meanwhile, RunningBoard resolves the process number of the Catalyst app to establish its identity and details.

Once it knows the process it’s dealing with, RunningBoard decides to manage it, rather than just tracking it. It then acquires its first assertion, which gives the app RoleUserInteractiveNonFocal status, as it is a GUI rather than background app but isn’t currently at the front. Next, a new component engages: UIKitSystem, which is launched on demand when a Catalyst app runs, and quietly quits a little while after the last Catalyst app has closed.

After these come two significant log entries from the kernel:
0.591972 kernel memorystatus: set assertion priority(0) target LookUp:1220
0.59197 kernel memorystatus: Ignore assertion driven idle priority. Process not previously controlled LookUp:1220

These mark the fact that the Catalyst app has opted into the existing macOS scheme for dealing with memory pressure, memorystatus. This means that Catalyst apps can be terminated automatically when macOS needs to free up memory and their priority is low enough, typically when they’re idle in the background.

This important change is immediately reflected in RunningBoard, which sets the Jetsam priority for the process to 0, which effectively expresses the same thing, as Jetsam is the new implementation for memorystatus. This puts it first in line to be quit if macOS comes under memory pressure.

At this stage, the list of processes known to and being tracked by RunningBoard includes two relevant daemons, and

Trust evaluation of the app doesn’t occur until around 0.015 seconds after the start of its launch sequence, and is instigated by AppstoreAgent. After that, the app requests and is granted its sandbox. A little later, TCC comes into play, and establishes the app’s Attribution Chain and entitlements. With those preparations completed, and 0.14 seconds elapsed, FrontBoard starts bootstrapping the app.

RunningBoard then acquires the app’s first ‘power assertion’, which feeds through to the kernel to change its “aggressiveness”, idle time, and to disable the idle sleep timer. At the same time, FrontBoard activates the app’s Watchdog. This monitors an initial ‘resource allowance’ of 1200 seconds, at a refresh interval of 150 seconds, which is quickly modified to a refresh interval of “-1” second, which probably means on demand rather than at regular intervals.

At this stage, FrontBoard declares the launch complete, the Watchdog having recorded a ‘resource consumption’ of only 0.01 second. Shortly afterwards, that is updated to 0.014 second.

The next steps with this Catalyst app are its CloudKit connections, which start 0.18 seconds after the start of launch. After a very long series of log entries, those are largely complete, and BaseBoard goes to work dealing with transactions to update scenes, change the app state to Working, and launch an additional process. Further transactions follow, which record it meeting Milestones set by BaseBoard.

Then, around 0.34 seconds after the start of launch, comes another important kernel entry:
0.924501 kernel memorystatus: set assertion priority(10) target LookUp:1220
This changes the app’s priority for memorystatus to 10, making it far less likely to be quit when macOS comes under memory pressure. This is reflected by RunningBoard setting its corresponding Jetsam priority to 10.

Finally in this record, FrontBoard’s Watchdog notes that the app’s resource allowance has now fallen to 1199.99 seconds, and sets its refresh interval back to 150 seconds.

This complex sequence shows how a typical, and relatively simple, Catalyst app works, and how it can work with UIKit on a system which expects apps to use AppKit instead. Of the new subsystems involved, only RunningBoard is likely to be of significance more generally to macOS software in the future. UIKitSystem, FrontBoard and BaseBoard seem only intended for use by Catalyst apps. This is a great deal of engineering work, indicating Apple’s confidence in the future of Catalyst, and in porting iPadOS apps to run on macOS.

Appendix: Log extracts

0.582097 LaunchServices LaunchApplication: appToLaunch={ "ApplicationType"="Foreground", "CFBundleExecutablePath"="/Applications/", "CFBundleExecutablePathDeviceID"=16777220, "CFBundleExecutablePathINode"=12082893, "CFBundleIdentifier"="", "CFBundleName"="LookUp", "CFBundlePackageType"="APPL", "LSBundlePath"="/Applications/", "LSBundlePathDeviceID"=16777220, "LSBundlePathINode"=12082886, "LSExecutableFormat"="LSExecutableMachOFormat", "LSLaunchDLabel"="" } modifiers: { "AddPSNArgument"=true, "LSAdditionalEnvironmentVars"={ }, "LSLaunchAsync"=true, "LSLaunchStoppedTemporarily"=true } args=[ NULL ]

0.586909 RunningBoardServices Acquiring assertion: <RBSAssertionDescriptor; foregroundApp:1220; ID: 0x0; target: 1220>
2019-11-26 21:30:00.587070+0000 Info 31964 265 connection runningboardd RunningBoard Received message from [daemon<>:133] (euid 0): acquireAssertionWithDescriptor:error:

0.587882 appleeventsd AE CONNECTION: peer=? peer-pid=340 got event {options={SupressInitialOAPPEvent=true, }, appName="LookUp", command=700, bundleID="", pid=1220, asn=696490U, supressOAPP=true, signature=1061109567, }

0.590375 RunningBoard Resolved pid 1220 to [application<>:1220]

0.591126 RunningBoard [application<>:1220] This process will be managed.
0.591131 RunningBoard Now tracking process: [application<>:1220]
0.591299 RunningBoard Acquiring assertion targeting application<> from originator [daemon<>:133] with description <RBSAssertionDescriptor; foregroundApp:1220; ID: 265-133-369; target: 1220> attributes = {
<RBSDomainAttribute: 0x7f83aed18580; domain:; name: RoleUserInteractiveNonFocal; sourceEnvironment: 0x0>;

0.591513 UIKitSystem RunningBoardServices observedProcessStatesDidChange

0.591972 kernel memorystatus: set assertion priority(0) target LookUp:1220
0.591974 kernel memorystatus: Ignore assertion driven idle priority. Process not previously controlled LookUp:1220
0.591976 RunningBoard [application<>:1220] Set jetsam priority to 0 [0] flag[1]
0.591977 RunningBoard [application<>:1220] Resuming task.
0.591983 RunningBoard [application<>:1220] Set darwin role to: UserInteractiveNonFocal
0.591985 RunningBoard [application<>:1220] Set GPU priority to "deny"
0.591990 RunningBoard [application<>:1220] Disabled CPU monitoring
0.591992 RunningBoard [application<>:1220] Reset CPU monitoring limits to defaults
0.591994 RunningBoard [application<>:1220] Resumed CPU monitoring

0.594661 RunningBoard daemon<> assertion details 1/1. ID:265-133-94 Explanation:"backgroundApp:454" Attributes:
"<RBSDomainAttribute: 0x7f83aef2c830; domain:; name: RoleNonUserInteractive; sourceEnvironment: 0x0>"
0.594690 RunningBoard daemon<> assertion details 1/3. ID:265-133-347 Explanation:"backgroundApp:1150" Attributes:
"<RBSDomainAttribute: 0x7f83aee01c40; domain:; name: RoleNonUserInteractive; sourceEnvironment: 0x0>"
0.594777 RunningBoard daemon<> assertion details 2/3. ID:265-1150-350 Explanation:"FBSystemShell" Attributes:
"<RBSRunningReasonAttribute: 0x7f83aee2d330; runningReason: 10005>",
"<RBSGPUAccessGrant: 0x7f83aee2ac10>",
"<RBSCPUAccessGrant: 0x7f83aee15470; role: UserInteractiveNonFocal>",
"<RBSJetsamPriorityGrant: 0x7f83aee12c50; priority: Foreground>",
"<RBSResistTerminationGrant: 0x7f83aee15920; terminationResistance: Interactive>",
"<RBSEndowmentGrant: 0x7f83aee03f80; namespace:>"
0.594805 RunningBoard daemon<> assertion details 3/3. ID:265-1150-352 Explanation:"injecting ""-"" to 1150<>" Attributes:
"<RBSHereditaryGrant: 0x7f83aed2ec30> {\n endowmentNamespace =;\n hasEncodedEndowment = YES;\n}"

0.599767 appstoreagent Security SecTrustEvaluateIfNecessary

0.670766 secinitd LookUp[1220]: AppSandbox request
0.671197 secinitd LookUp[1220]: root path for bundle "<private>" of main executable "<private>"
0.672625 secinitd Security SecTrustEvaluateIfNecessary
0.677903 secinitd LookUp[1220]: AppSandbox request successful

0.703104 tccd AttributionChain: REQ:{ID:, PID[1220], auid: 501, euid: 501, binary path: '/Applications/'}
0.703117 tccd Composed entitlement check for ({ID:, PID[1220], auid: 501, euid: 501, binary path: '/Applications/'}, from: kTCCServiceListenEvent to parent: kTCCServicePostEvent
0.703124 tccd Composed entitlement check for ({ID:, PID[1220], auid: 501, euid: 501, binary path: '/Applications/'}, from: kTCCServicePostEvent to parent: kTCCServiceAccessibility

0.727984 UIKitSystem FrontBoard Bootstrapping application<>

0.729341 RunningBoard Acquired process power assertion with ID 34779 for pid 1220
0.729341 RunningBoard Acquired first power assertion
0.729342 Watchdog UIKitSystem FrontBoardServices [application<>:1220] Activating <FBProcessWatchdog: 0x600001d46680; name: process-launch>
0.729362 UIKitSystem FrontBoard <FBApplicationProcess: 0x7fe728d7b4c0; application<>:1220> Starting watchdog: <FBProcessWatchdog: 0x600001d46680; name: process-launch>
0.729430 kernel PMRD: aggressiveness changed: system 60->0, display 60
0.729431 Watchdog UIKitSystem FrontBoardServices [application<>:1220] [scheduledTime] Now monitoring resource allowance of 1200.00s (at refreshInterval 150.00s)
0.729432 kernel PMRD: idle time -> 0 secs (ena 0)
0.729433 kernel PMRD: idle sleep timer disabled
0.729435 kernel PMRD: changePowerStateToPriv(4)
0.729437 Watchdog UIKitSystem FrontBoardServices [application<>:1220] [realTime] Now monitoring resource allowance of 1200.00s (at refreshInterval -1.00s)

0.743077 UIKitSystem FrontBoard [application<>:1220] Launch complete.
0.743096 Watchdog UIKitSystem FrontBoardServices [application<>:1220] Deactivating <FBProcessWatchdog: 0x600001d46680; name: process-launch>
0.743104 Watchdog UIKitSystem FrontBoardServices [application<>:1220] [scheduledTime] Stopped monitoring.
0.743137 Watchdog UIKitSystem FrontBoardServices [application<>:1220] [scheduledTime] Updated resource consumption: 0.010s (0.00%)
0.743147 Watchdog UIKitSystem FrontBoardServices [application<>:1220] [realTime] Stopped monitoring.
0.743161 Watchdog UIKitSystem FrontBoardServices [application<>:1220] [realTime] Updated resource consumption: 0.014s (0.00%)
0.743185 UIKitSystem FrontBoard <FBApplicationProcess: 0x7fe728d7b4c0; application<>:1220> Stopping watchdog: <FBProcessWatchdog: 0x600001d46680; name: process-launch>

0.766309 CloudKit Writing down current persona (null) in container options

0.921944 BaseBoard <FBApplicationUpdateScenesTransaction:0x6000037005a0> Adding child transaction: <FBApplicationProcessLaunchTransaction: 0x600003100ee0>
0.922043 BaseBoard <FBApplicationUpdateScenesTransaction:0x6000037005a0> Life assertion taken for reason: beginning
0.922052 BaseBoard <FBApplicationUpdateScenesTransaction:0x6000037005a0> State changed from 'Initial' to 'Working'
0.922063 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> Life assertion taken for reason: beginning
0.922069 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> State changed from 'Initial' to 'Working'
0.922083 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> Milestones added: processWillBeginLaunching
0.922090 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> Milestones added: processDidFinishLaunching

0.924391 BaseBoard <FBUpdateSceneTransaction:0x600003105260> Milestone satisfied: synchronizedCommit
0.924431 BaseBoard <FBApplicationUpdateScenesTransaction:0x6000037005a0> Milestone satisfied: synchronizedCommit
0.924445 BaseBoard <FBUpdateSceneTransaction:0x600003105260> Life assertion removed for reason: beginning
0.924455 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> Milestone satisfied: processWillBeginLaunching
0.924482 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> Milestone satisfied: processDidFinishLaunching
0.924488 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> State changed from 'Working' to 'Done Working'
0.924493 RunningBoard [application<>:1220] Applying updated state
0.924495 BaseBoard <FBApplicationUpdateScenesTransaction:0x6000037005a0> Child transaction finished work: <FBApplicationProcessLaunchTransaction: 0x600003100ee0>
0.924501 kernel memorystatus: set assertion priority(10) target LookUp:1220
0.924503 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> Life assertion removed for reason: beginning
0.924504 RunningBoard [application<>:1220] Set jetsam priority to 10 [0] flag[1]
0.924506 BaseBoard <FBApplicationProcessLaunchTransaction:0x600003100ee0> State changed from 'Done Working' to 'Completed'

0.924736 FrontBoardServices [application<>:1220] [realTime] Now monitoring resource allowance of 1199.99s (at refreshInterval -1.00s)
0.924771 FrontBoardServices [application<>:1220] [scheduledTime] Now monitoring resource allowance of 1199.99s (at refreshInterval 150.00s)