Why macOS has to change, and how iOS is showing the way

One of the biggest, and longest-standing, problems in macOS is poor performance. It is shocking that today’s Xeon and Core i9 processors and GPUs can be brought to their knees just as readily as the Power Mac G4 systems we used almost twenty years ago when Apple released the Public Beta of Mac OS X.

Of course, when everything is running sweetly, performance isn’t a problem. Neither is getting wet a problem when it’s not raining. But when the heavens open and it rains cats and dogs, you need to be prepared. In most respects, macOS 10.15 is no better prepared than 10.0 was. We’ve all experienced it: the spinning beachball, opening a glacial Activity Monitor, and something called xyzhelperagent is burning CPU like it’s the most important process on the planet. And abcappd has popped up from nowhere to swallow up 20 GB of memory. Meanwhile you don’t know whether it’s safe to force them to quit, or even why they’re there in the first place.

The fundamental problem here is that nothing in macOS really manages processes, their lifecycles, or the resources they use. The simplest example is memory: a process can easily allocate itself far more memory than your Mac has, forcing macOS memory management to page much of that into swap storage on disk. If the process then accesses locations scattered across that memory, it will tie the system up paging data in and out from disk.

macOS does provide a mechanism for tackling situations when it comes under memory pressure: memorystatus and the enableAutomaticTermination option, which were introduced in 10.7, according to Jonathan Levin. These allow a process to be terminated automatically when macOS needs to free up more memory, and that process is idle. Unfortunately, in macOS, processes have to opt into this system, and as a result it appears to be little used.

The situation in iOS is completely different. iPhones and iPads have relatively little memory – even an iPhone XS Max only has 4 GB, and there are no options for more – and can’t use swap space because of the more limited life and capacity of their storage. So iOS/iPadOS is frequently under memory pressure, and has a system known as Jetsam which is used to kill processes to recover their memory. This isn’t optional either, but part of the rules.

The same applies to most other resources, such as CPU time, the GPU in your Mac’s graphics card and eGPUs, even access to database and other files. Modern Macs may have much more capable hardware than in the past, but any overambitious process can sieze what it likes and block more important processes from using those resources.

So macOS is gradually changing. Browse your log in Catalina, and you’ll now see the appearance of Jetsam, which doesn’t appear to have existed in Mojave. Most obvious, though, is a brand new subsystem RunningBoard, which I outlined here a couple of weeks ago. RunningBoard can manage process lifecycle and resource usage, although at this stage in macOS it mainly tracks and doesn’t intervene.

The exceptions to this are with the new Catalyst apps introduced in Catalina. These are the first of a new breed of app which can be run on iOS/iPadOS and macOS, and are primarily seen by Apple as a means of making it easier for developers to bring apps from the iPad to the Mac. There’s a great deal of politics involved here, which I’m not going to go near as it’s not relevant in this context, but these are fundamentally apps which have been developed for iPadOS, so they already live with Jetsam and RunningBoard there. Consequently, they’re an excellent opportunity for management by RunningBoard on macOS too.

Open one of these Catalyst apps in Catalina, and you’ll see some very unusual entries in the unified log. When I started LookUp, for example, after the usual exchanges with LaunchServices, RunningBoard announced that LookUp would be managed:
00.599370 RunningBoard [application<maccatalyst.in.muditbhargava.LookUp(501)>:969] This process will be managed.

RunningBoard then targeted the running but temporarily suspended LookUp app with its first ‘assertion’, and set its management variables:
00.599905 Set jetsam priority to 0 [0] flag[1]
00.599906 Resuming task.
00.599913 Set darwin role to: UserInteractiveNonFocal
00.599915 Set GPU priority to "deny"
00.599921 Disabled CPU monitoring
00.599923 Reset CPU monitoring limits to defaults
00.599924 Resumed CPU monitoring
00.599936 Calculated state for application<maccatalyst.in.muditbhargava.LookUp(501)>: running-active (role: UserInteractiveNonFocal)

The first of those entries reports this Catalyst app being given a Jetsam priority, which determines when it might be killed to free up memory, if required. When memory comes under pressure, Jetsam looks first at idle processes with the lowest priority and kills those it needs to. If that still doesn’t free sufficient memory, it turns to the next priority, and so on. Full details of Jetsam are given in Jonathan Levin’s book *OS Internals, volume 1, pages 273-280.

The Darwin Role sets the class of process, whether the user is able to interact with it, and whether it’s at the front or in the background. This app doesn’t explicitly access the GPU, so isn’t set to contend for that as a system resource. It’s also given the default settings for access to the CPU. Once those are done, RunningBoard marks that app active. It’s next brought to the front, and its Darwin Role changes to RoleUserInteractiveFocal to reflect that with another assertion.

Catalyst apps are quite complex beasts when they’re running. Regular Mac apps load the executable code itself, and maybe some ancillaries such as helpers to perform specific tasks. To support access to macOS, Catalyst apps use a service or daemon UIKitSystem (with the ID com.apple.uikitsystemapp). This in turn targets the Catalyst app with an assertion reflecting some of its system settings, which include features managed by RunningBoard. These include whether to prevent idle sleep, and control access to the GPU and CPU. Further assertions are made, and by the time that I came to quit this brief run of LookUp, RunningBoard’s list for the app contained nine assertions.

Quitting LookUp then emptied the list of assertions, setting its Darwin Role to NonUserInteractive, and finally recording its death with the distinctive log entry
33.132695 RunningBoard [application<maccatalyst.in.muditbhargava.LookUp(501)>:969] Death sentinel fired!
In a future article, I will look in more detail at how Catalyst apps are run in macOS.

Apple is doing this with Catalyst apps not because they need such careful management. Remember, these are cross-platform, and even the top of the range iPad Pro still only has 6 GB of memory, without any swap space, and most recent iPads only have 2 or 4 GB. However, iPad apps already have to live with lifecycle and resource management, so are an ideal testbed as Apple develops RunningBoard in macOS.

The next step we’re likely to see is opening of RunningBoard features to non-Catalyst apps, probably on a voluntary basis to begin with. But Apple’s goal must be to make this management system mandatory, at least for third-party processes. Look out for more about how RunningBoard is set to transform the macOS experience at WWDC in June 2020. That’s only just over six months away.

In the meantime, if you want to learn more about memory management in iOS/iPadOS, there’s an excellent introduction from a session about iOS 9, back in 2015.