Activity Monitor Anatomy

3 months ago 6

Introduction Link to heading

Imagine you opened Activity Monitor to check how much memory your application consumes.

But wait, which number should you actually look at? Suppose there’s “Memory” showing 800 MB, and “Real Memory” showing 1 GB. What’s the difference?

Note

Activity Monitor doesn’t display those two readings by default. To show them, you need to right-click on a table header and select them.

This exact scenario happened to me more times than I’d like to admit. As someone who keeps Activity Monitor perpetually open, I found myself constantly puzzled by these two memory metrics. Coming from a Linux background where RSS (Resident Set Size) tells a straightforward story, macOS’s dual memory reporting felt like reading tea leaves.

When building desktop applications, users inevitably notice high memory usage and start asking tough questions. “Why does your app use so much memory?” becomes a lot harder to answer when you’re not entirely sure what the numbers mean yourself.

In this post, I’ll describe my findings – how you can programmatically read Memory and Real Memory metrics and what exactly they mean.

Peering Behind the Curtain Link to heading

I assumed Activity Monitor was this monolithic application doing all the heavy lifting of process monitoring. But when I started digging into its internals, it turns out Activity Monitor is just the tip of the iceberg. The real work happens in a background process called sysmond – the system monitor daemon. Activity Monitor simply uses a system library called libsysmond.dylib to communicate with this daemon through XPC (Inter-Process Communication). sysmond handles all the complex process introspection, while multiple clients can tap into that data stream.

There’s a libsysmon.dylib system library that contains an API to communicate with sysmond. It has a fairly simple API surface:

$ dyld_info -exports /usr/lib/libsysmon.dylib /usr/lib/libsysmon.dylib [arm64e]: -exports: offset symbol 0x335BDDB0 _OBJC_CLASS_$_OS_sysmon_object 0x335BDD10 _OBJC_CLASS_$_OS_sysmon_request 0x335BDD60 _OBJC_CLASS_$_OS_sysmon_row 0x335BDD38 _OBJC_CLASS_$_OS_sysmon_table 0x335BDD88 _OBJC_METACLASS_$_OS_sysmon_object 0x335BDDD8 _OBJC_METACLASS_$_OS_sysmon_request 0x335BDE28 _OBJC_METACLASS_$_OS_sysmon_row 0x335BDE00 _OBJC_METACLASS_$_OS_sysmon_table 0x3176DBE8 _SYSMON_XPC_KEY_ATTRIBUTES 0x3176DBF0 _SYSMON_XPC_KEY_FLAGS 0x3176DBE0 _SYSMON_XPC_KEY_TYPE 0x3176DC00 _SYSMON_XPC_REPLY_KEY_HEADER 0x3176DC08 _SYSMON_XPC_REPLY_KEY_TABLE 0x3176DBF8 _SYSMON_XPC_REPLY_KEY_TIMESTAMP 0x3176DBD8 _SYSMON_XPC_SERVICE_NAME 0x00001908 __sysmon_build_reply_with_diff 0x00000A7C _sysmon_release 0x00000B90 _sysmon_request_add_attribute 0x00000CCC _sysmon_request_add_attributes 0x00001700 _sysmon_request_alloc 0x000012E0 _sysmon_request_cancel 0x00000A80 _sysmon_request_create 0x00000B58 _sysmon_request_create_with_error 0x00000D7C _sysmon_request_execute 0x00000D24 _sysmon_request_set_flags 0x00000D2C _sysmon_request_set_interval 0x00000A78 _sysmon_retain 0x00001768 _sysmon_row_alloc 0x000014E4 _sysmon_row_apply 0x0000141C _sysmon_row_get_value 0x00001734 _sysmon_table_alloc 0x000013B8 _sysmon_table_apply 0x000013AC _sysmon_table_copy_row 0x00001390 _sysmon_table_get_count 0x000013A0 _sysmon_table_get_row 0x00001398 _sysmon_table_get_timestamp

The key functions are:

  • sysmon_request_create() - Creates a new data request and accepts a callback that will be executed when the request completes
  • sysmon_request_add_attribute() - Specifies what data you want (memory, CPU, etc.)
  • sysmon_request_execute() - Fires off the request and gets results
  • sysmon_table_get_count() - Tells you how many processes were found
  • sysmon_row_get_value() - Extracts specific values from the results

You build a request by specifying which attributes you want for each process, execute it, and get back a table of results. By analyzing Activity Monitor, I discovered that our mysterious Memory and Real Memory metrics correspond to attribute codes 0x42 and 0x16, respectively. The full list of sysmond attributes and corresponding library calls is available on GitHub.

My first thought was that I could use those functions to read memory metrics directly from sysmond and bypass Activity Monitor entirely. And this is possible… sort of. I wrote a simple proof-of-concept to validate my theory.

$ ./sysmon PID comm Real Memory Memory [...] 639 sshd 15.0M 5.0M 641 sshd 3.6M 2.4M [...]

The numbers matched Activity Monitor perfectly, but there is a catch. Only processes with the special com.apple.sysmond.client entitlement can connect to sysmond. Even if I tried to sign my executable with this entitlement, macOS would unceremoniously kill it during loading:

Restricted entitlements not validated, bailing out. Error: Error Domain=AppleMobileFileIntegrityError Code=-420 "The signature on the file is invalid" UserInfo={ NSURL=<private>, NSLocalizedDescription=The signature on the file is invalid }

This was Apple’s security model doing its job – preventing arbitrary code from accessing system monitoring data. You need to disable SIP (System Integrity Protection) and AMFI (Apple Mobile File Integrity) to run this example.

Warning

Disabling SPI and AMFI is not something I’d recommend outside of a virtual machine.

The libsysmond route was effectively a dead-end, but there’s always another way in. First of all, if the only thing you need is a Memory value, you can use the footprint command. It reports exactly that, but may require sudo even if the process is running under the same user account. So I dug deeper into sysmond itself, and discovered something interesting: it wasn’t performing any magical kernel-level operations. Instead, it was using two standard library functions that any process can call: proc_pidinfo() and proc_pid_rusage().

MemoryReal Memory
sysmond code0x420x16
system library callproc_pid_rusage()proc_pidinfo()
data fieldrusage_info_v2::ri_phys_footprintproc_taskinfo::resident_size

These functions are available to any process for reading information about other processes running under the same user account – no special entitlements required. I could access the same memory data that Activity Monitor displays. Even better, proc_pid_rusage() alone provides both memory metrics. The footprint command is a convenient tool that uses this same function internally to display detailed memory breakdowns for running processes. It provides an interactive way to explore memory usage without writing code.

#include <mach/task_info.h> #include <sys/proc_info.h> #include <libproc.h> int main(void) { struct rusage_info_v6 rusage; proc_pid_rusage(pid, RUSAGE_INFO_V6, (rusage_info_t)&rusage); rusage.ri_resident_size // Real memory rusage.ri_phys_footprint // Memory }

When I found it and started searching for documentation on the proc_pid_rusage function, I realized I’d essentially “discovered” something that Apple has been publicly documenting and recommending for years. It was a classic case of reinventing the wheel, but the journey taught me far more than simply reading the documentation would have.

However, I still had the original question: what do these numbers actually represent?

Following the Trail to the Source Link to heading

To understand what these memory metrics actually represent, we need to go deeper – all the way to the kernel level. Fortunately, Apple open-sources significant portions of macOS, including the kernel’s memory management code.

In osfmk/kern/bsd_kern.c:

void fill_taskprocinfo(task_t task, struct proc_taskinfo_internal * ptinfo) { // ... ledger_get_balance(task->ledger, task_ledgers.phys_mem, (ledger_amount_t *) &ptinfo->pti_resident_size); // ... } // ... uint64_t get_task_phys_footprint(task_t task) { return get_task_ledger_balance(task, task_ledgers.phys_footprint); }

Real Memory and Memory are simply values read from two kernel ledgers called physmem.phys_mem and physmem.phys_footprint.

MemoryReal Memory
sysmond code0x420x16
ledgerphysmem.phys_footprintphysmem.phys_mem

But what exactly are these “ledgers”?

The Kernel’s Accounting System Link to heading

Ledgers are the kernel’s accounting system – every time your process allocates memory, maps a file, or performs certain operations, the kernel updates various ledgers using ledger_debit() and ledger_credit() functions. You can find some information about ledgers in a manual page of footprint command (MEMORY ACCOUNTING section). The full list of ledgers is available in the sources.

This system allows the kernel to answer questions like “How much memory is this process using?” in constant time – no need to walk through complex data structures or perform expensive calculations.

I discovered that there’s actually a ledger() syscall that can read these values directly. Apple’s engineers even left a comment in the code saying it “isn’t meant for civilians to use” – which naturally made me want to use it right away 😁.

I created a simple program that reads all ledger values for a given process:

$ ledger $$ GROUP LEDGER BALANCE CREDIT DEBIT UNITS sched cpu_time 4881264 4881264 0 ns physmem tkm_private 327680 327680 0 bytes physmem tkm_shared 0 0 0 bytes physmem phys_mem 8765440 68943872 60178432 bytes physmem wired_mem 0 0 0 bytes physmem conclave_mem 0 0 0 bytes physmem internal 6930432 6930432 0 bytes mappings iokit_mapped 0 0 0 bytes physmem alternate_accounting 16384 16384 0 bytes physmem alternate_accounting_compressed 0 0 0 bytes physmem page_table 345344 345344 0 bytes physmem phys_footprint 8783104 8783104 0 bytes physmem internal_compressed 1507328 6406144 4898816 bytes physmem reusable 49152 49152 0 bytes physmem external 1785856 1785856 0 bytes physmem purgeable_volatile 0 0 0 bytes physmem purgeable_nonvolatile 16384 16384 0 bytes physmem purgeable_volatile_compress 0 0 0 bytes physmem purgeable_nonvolatile_compress 0 0 0 bytes physmem pages_grabbed 3981 3981 0 count physmem pages_grabbed_kern 2 2 0 count physmem pages_grabbed_iopl 0 0 0 count physmem pages_grabbed_upl 102 102 0 count physmem tagged_nofootprint 0 0 0 bytes physmem tagged_footprint 0 0 0 bytes physmem tagged_nofootprint_compressed 0 0 0 bytes physmem tagged_footprint_compressed 0 0 0 bytes physmem network_volatile 0 0 0 bytes physmem network_nonvolatile 0 0 0 bytes physmem network_volatile_compressed 0 0 0 bytes physmem network_nonvolatile_compressed 0 0 0 bytes physmem media_nofootprint 0 0 0 bytes physmem media_footprint 0 0 0 bytes physmem media_nofootprint_compressed 0 0 0 bytes physmem media_footprint_compressed 0 0 0 bytes physmem graphics_nofootprint 0 0 0 bytes physmem graphics_footprint 0 0 0 bytes physmem graphics_nofootprint_compressed 0 0 0 bytes physmem graphics_footprint_compressed 0 0 0 bytes physmem neural_nofootprint 0 0 0 bytes physmem neural_footprint 0 0 0 bytes physmem neural_nofootprint_compressed 0 0 0 bytes physmem neural_footprint_compressed 0 0 0 bytes physmem neural_nofootprint_total 0 0 0 bytes power platform_idle_wakeups 0 0 0 count power interrupt_wakeups 3 5 2 count sfi SFI_CLASS_DARWIN_BG 0 0 0 MATUs sfi SFI_CLASS_APP_NAP 0 0 0 MATUs sfi SFI_CLASS_MANAGED 0 0 0 MATUs sfi SFI_CLASS_DEFAULT 0 0 0 MATUs sfi SFI_CLASS_OPTED_OUT 0 0 0 MATUs sfi SFI_CLASS_UTILITY 0 0 0 MATUs sfi SFI_CLASS_LEGACY 0 0 0 MATUs sfi SFI_CLASS_USER_INITIATED 0 0 0 MATUs sfi SFI_CLASS_USER_INTERACTIVE 0 0 0 MATUs sfi SFI_CLASS_MAINTENANCE 0 0 0 MATUs sched cpu_time_billed_to_me 24757 24757 0 ns sched cpu_time_billed_to_others 0 0 0 ns res physical_writes 57344 57344 0 bytes res logical_writes 229376 229376 0 bytes res logical_writes_to_external 0 0 0 bytes res fs_metadata_writes 114688 114688 0 bytes power energy_billed_to_me 136969 136969 0 nj power energy_billed_to_others 0 0 0 nj physmem memorystatus_dirty_time 0 0 0 ns physmem swapins 0 0 0 bytes

By examining the kernel’s memory management code in osfmk/arm64/sptm/pmap/pmap.c, I could finally understand what these ledgers actually track:

  • phys_mem gets updated every time a page is added or removed from physical RAM and the process’s page table. This is macOS’s version of Linux’s RSS (Resident Set Size) – memory that’s actually sitting in physical RAM right now. However, most of the shared system libraries (dyld cache) don’t count toward this ledger, except for dirty pages that the process has modified.

  • phys_footprint is far more complex. To understand it, I needed to step back and explain why phys_mem alone isn’t sufficient for understanding a process’s true memory impact.

One might think that Real Memory (macOS’s equivalent to Linux’s RSS) would be perfect for monitoring memory usage. It represents physical RAM that’s actually being used right now – what could be more straightforward? But there are two fundamental problems that make RSS-style metrics misleading:

Problem 1: Not all resident memory is truly “yours”

Some memory that appears in your process’s RSS is actually shared with other processes and therefore cannot be accounted solely to one process. Some memory is designed to be reclaimed by the system at any time; therefore, it does not contribute toward memory pressure on the system.

Problem 2: Not all your memory is resident

Some of your application’s memory might be swapped to disk, but it’s still your application’s memory – you just don’t see it in RSS.

Memory accounting becomes especially confusing when you consider several categories of “sort of yours” memory:

  • Memory-mapped files: When you map a file with MAP_SHARED, that memory can be shared across multiple processes. The OS technically owns it, even though your process is the only one using it.
  • Reusable memory: When you free memory, macOS might not immediately return it to the kernel. It leaves it in your process space for potential reuse, similar to Linux’s madvise(MADV_FREE).
  • Purgable memory: macOS has a concept of purgable memory, which the OS can discard if it experiences memory pressure. It’s resident in Real Memory but doesn’t contribute to memory footprint because it’s easily discardable. Applications can protect this memory from discarding temporarily (nonvolatile purgable memory). In this case, the OS will include it in a memory footprint until the nonvolatile mark is removed.
  • System libraries (dyld cache): This is the big one, and it deserves special attention.

The dyld Cache: macOS’s Memory Optimization Trick Link to heading

Here’s where macOS gets interesting. Unlike Linux, which provides a stable syscall interface, macOS provides stability through a collection of system libraries (libsystem_c.dylib, libsystem_malloc.dylib, etc.). Since every process needs these libraries, Apple devised an optimization called the dyld cache.

Instead of each process loading dozens of system libraries individually, the system pre-links all these libraries into a single, massive memory region (the dyld cache found in /System/Cryptexes/OS/System/Library/dyld/). This cache gets mapped into every new process in a single operation (this is rather a simplification; for more details, see Apple’s documentation).

Here’s the catch: this dyld cache is several hundred megabytes in size, and most of it is resident in physical RAM. This means that even a simple “Hello World” program appears to use over 100MB of RAM if you look at traditional RSS metrics – enough to sound alarm bells for any developer or user.

Apple’s solution was to exclude most of the dyld cache from the phys_mem ledger, counting only the dirty pages (like modified __DATA segments) that the process has actually changed. This makes perfect sense from a developer’s perspective – you don’t consider system libraries as “your” memory, so why should they count against your application’s memory usage?

Note

I found one interesting byproduct of how things are implemented in macOS: virtual addresses of all system libraries are the same across all macOS processes. This means that the dyld cache ASLR slide changes only after reboot. As far as I understand, this makes ASLR somewhat less effective, since it becomes easy to predict the address of any system function in any target process. To be fair, it can only be exploited if an attacker can already execute arbitrary code on a target machine, so this probably isn’t a huge sacrifice.

$ vmmap $(pgrep -u $(whoami) -x Dock) | grep 'libsystem_c.dylib' __TEXT 195af7000-195b79000 [ 520K 504K 0K 0K] r-x/r-x SM=COW /usr/lib/system/libsystem_c.dylib __DATA_CONST 1fd96a870-1fd96c0c8 [ 6K 6K 0K 0K] r--/rw- SM=COW /usr/lib/system/libsystem_c.dylib __AUTH_CONST 204e05938-204e062b8 [ 2432 2432 0K 0K] r--/rw- SM=COW /usr/lib/system/libsystem_c.dylib __DATA 2017679d0-201769be0 [ 9K 9K 9K 0K] rw-/rw- SM=COW /usr/lib/system/libsystem_c.dylib __DATA_DIRTY 203d7c6f0-203d7f260 [ 11K 11K 11K 0K] rw-/rw- SM=COW /usr/lib/system/libsystem_c.dylib __AUTH 204220070-204220120 [ 176 176 176 0K] rw-/rw- SM=COW /usr/lib/system/libsystem_c.dylib $ vmmap $(pgrep -u $(whoami) -x WindowManager) | grep 'libsystem_c.dylib' __TEXT 195af7000-195b79000 [ 520K 504K 0K 0K] r-x/r-x SM=COW /usr/lib/system/libsystem_c.dylib __DATA_CONST 1fd96a870-1fd96c0c8 [ 6K 6K 0K 0K] r--/rw- SM=COW /usr/lib/system/libsystem_c.dylib __AUTH_CONST 204e05938-204e062b8 [ 2432 2432 0K 0K] r--/rw- SM=COW /usr/lib/system/libsystem_c.dylib __DATA 2017679d0-201769be0 [ 9K 9K 9K 0K] rw-/rw- SM=COW /usr/lib/system/libsystem_c.dylib __DATA_DIRTY 203d7c6f0-203d7f260 [ 11K 11K 11K 0K] rw-/rw- SM=COW /usr/lib/system/libsystem_c.dylib __AUTH 204220070-204220120 [ 176 176 176 0K] rw-/rw- SM=COW /usr/lib/system/libsystem_c.dylib

The Swapped Memory Problem Link to heading

The other fundamental issue with phys_mem is that it completely ignores swapped memory. When the system runs low on RAM, it can swap out your application’s memory to disk. By definition, this memory is no longer resident, so it disappears from RSS-style metrics.

But here’s the thing: it’s still your application’s memory. As a developer, you should be aware of your total memory footprint, not just the portion that happens to be in RAM at any given moment. Under memory pressure, your seemingly “lightweight” application might actually be consuming significant resources through swapped memory.

This is why Apple developed the phys_footprint ledger – to give developers a more complete picture of their application’s true memory impact.

There’s a formula for the phys_footprint ledger in the kernel sources:

phys_footprint = + (internal - alternate_accounting) + (internal_compressed - alternate_accounting_compressed) + iokit_mapped + purgeable_nonvolatile + purgeable_nonvolatile_compressed + page_table

phys_footprint includes all your application’s memory, regardless of whether it’s currently in RAM, compressed, or swapped. It represents your true memory footprint.

The “alternate accounting” components exist to prevent double-counting. Since IOKit memory is also considered internal memory, adding them together would overcount the total allocation. The alternate accounting ledgers track this overlap, allowing the system to provide an accurate final number.

Here is the table that describes different types of memory and in which ledger they are present.

Memory typeReal MemoryMemory
dyld cache read-only segments (__TEXT)nono
Heap allocated memory (malloc(), mmap(MAP_ANONYMOUS))yes1yes
Stack allocated memory (alloca())yes1yes
Files mmapped with MAP_SHAREDyes1no
Files mmapped with MAP_PRIVATEyes1yes
dyld cache read-write segments (__DATA, __AUTH)yes, if changed (COW)yes, if changed (COW)
Reclaimed memoryyes1no
purgeable nonvolatileyes1yes
purgeable volatileyes1no

The Final Picture Link to heading

Real Memory corresponds to the phys_mem ledger and represents physical RAM currently occupied by your process. Think of it as macOS’s version of Linux’s RSS, but with a crucial improvement: it excludes most of the shared system libraries (dyld cache), counting only the dirty pages your process has actually modified.

Memory corresponds to the phys_footprint ledger and tells the complete story of your application’s memory impact. It includes:

  • All internal (anonymous) memory, whether resident, compressed, or swapped
  • IOKit-allocated memory on behalf of your process
  • Non-volatile purgeable memory
  • Page tables (usually negligible)

The key insight: Memory represents your application’s total memory footprint regardless of where that memory currently lives, while Real Memory only shows what’s physically in RAM right now. Both metrics exclude shared system components.

For developers: Focus on the Memory metric when optimizing. It provides the complete picture of your application’s memory consumption and won’t fluctuate based on system memory pressure.

Both metrics are accessible programmatically using proc_pid_rusage() with the ri_phys_footprint and ri_resident_size fields – no special entitlements required.


I’d like to express my deepest gratitude to Luna Razzaghipour and matklad for taking the time to review this article. Their feedback was invaluable!

Read Entire Article