From c0c1a81f084f949e271c17029ec1d586c91f94da Mon Sep 17 00:00:00 2001 From: Andre Nogueira Date: Fri, 3 Jul 2026 13:00:20 +0100 Subject: [PATCH] feat(memory): report real pressure signal and fix shared/cache accounting - shared: stop reporting the memory compressor as "shared" (semantically wrong); macOS exposes no system-wide shared-memory counter, so report 0 - buff/cache & available: include speculative (read-ahead) pages that were previously omitted, tightening the total = used + free + buff/cache identity - add -P/--pressure: prints the real kernel signal (kern.memorystatus_vm_pressure_level, the same one Activity Monitor uses), since macOS compresses in-RAM before swapping and the Linux-style columns cannot reveal pressure - update README to match the new behavior Signed-off-by: Andre Nogueira --- README.md | 34 ++++++++++++++++++++++++---------- src/display.c | 43 +++++++++++++++++++++++++++++++++++++++---- src/display.h | 7 +++++++ src/main.c | 6 +++++- src/memory.c | 21 +++++++++++++++++++-- src/memory.h | 34 ++++++++++++++++++++++++---------- src/utils.c | 15 +++++++++------ src/utils.h | 13 +++++++------ 8 files changed, 134 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index e011070..d5b94e3 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ free [options] | -t | --total | Show total for RAM + swap | | -s N | --seconds N | Repeat printing every N seconds | | -c N | --count N | Repeat printing N times, then exit | +| -P | --pressure | Show macOS memory pressure (kernel signal) | | -V | --version | Output version information and exit | | | --help | Display help and exit | @@ -83,7 +84,7 @@ free [options] ```txt $ free total used free shared buff/cache available -Mem: 16777216 8388608 2097152 524288 6291456 8388608 +Mem: 16777216 8388608 2097152 0 6291456 8388608 Swap: 2097152 524288 1572864 ``` @@ -92,7 +93,7 @@ Swap: 2097152 524288 1572864 ```shell $ free -h total used free shared buff/cache available -Mem: 16.0Gi 8.0Gi 2.0Gi 512.0Mi 6.0Gi 8.0Gi +Mem: 16.0Gi 8.0Gi 2.0Gi 0B 6.0Gi 8.0Gi Swap: 2.0Gi 512.0Mi 1.5Gi ``` @@ -117,12 +118,22 @@ free -h -s 2 free -h -s 1 -c 5 ``` +**Show macOS memory pressure (real kernel signal):** + +```txt +$ free -h -P + total used free shared buff/cache available +Mem: 16.0Gi 8.0Gi 2.0Gi 0B 6.0Gi 8.0Gi +Swap: 2.0Gi 512.0Mi 1.5Gi +Pressure: normal (50% available) +``` + **Show totals:** ```txt $ free -h -t total used free shared buff/cache available -Mem: 16.0Gi 8.0Gi 2.0Gi 512.0Mi 6.0Gi 8.0Gi +Mem: 16.0Gi 8.0Gi 2.0Gi 0B 6.0Gi 8.0Gi Swap: 2.0Gi 512.0Mi 1.5Gi Total: 18.0Gi 8.5Gi 3.5Gi ``` @@ -136,8 +147,8 @@ Total: 18.0Gi 8.5Gi 3.5Gi | **total** | Total installed physical memory | | **used** | Used memory(Active + Wired + Compressed) | | **free** | Completely unused memory | -| **shared** | Memory used by shared mappings | -| **buff/cache** | Memory used for file buffers and cache(mapped to Inactive on macOS) | +| **shared** | Always `0`: macOS exposes no system-wide shared-memory counter | +| **buff/cache** | Reclaimable file cache (Inactive + Purgeable + Speculative) | | **available** | Estimate of memory available for starting new applications | ### Wide Output(macOS - specific) @@ -155,24 +166,27 @@ This utility uses macOS - specific APIs to gather memory information: - **`host_statistics64()`** - Retrieves VM statistics including page counts for different memory states - **`sysctl()`** - Gets total physical memory and swap usage +- **`sysctlbyname("kern.memorystatus_vm_pressure_level")`** - Real kernel memory-pressure level (for `-P`) - **`vm_page_size`** - System page size for converting page counts to bytes ### Memory Calculation Notes -- **Used Memory** = Active + Wired + Compressed (speculative pages excluded) -- **Available Memory** = Free + Inactive (approximation, as macOS can reclaim inactive pages) -- **Shared Memory** = Pages marked as purgeable/shared in VM statistics +- **Used Memory** = Active + Wired + Compressed +- **Available Memory** = Free + Inactive + Purgeable + Speculative (approximation, as macOS can reclaim these pages) +- **Shared Memory** = reported as `0`; macOS has no cheap system-wide shared-memory counter (unlike Linux `Shmem`). Deriving a real value would require summing the shared footprint across every task (as `top`/`libtop` do). ## Differences from Linux `free` -1. **buff/cache**: On Linux, this shows buffer and cache memory separately. On macOS, we map this to inactive memory, which serves a similar purpose. +1. **buff/cache**: On Linux, this shows buffer and cache memory separately. On macOS, we sum the reclaimable file cache: inactive + purgeable + speculative pages. -2. **shared**: Linux tracks shared memory segments. On macOS, we report purgeable/shared page statistics. +2. **shared**: Linux tracks shared memory segments (`Shmem`). macOS exposes no equivalent system-wide counter, so this is reported as `0` rather than a misleading approximation. 3. **available**: Both systems estimate available memory, but the calculation differs due to different memory management strategies. 4. **Wide mode**: Shows macOS-specific categories (compressed memory is unique to macOS). +5. **Memory pressure** (`-P`): macOS relieves memory pressure with an in-RAM compressor *before* it touches swap, so the Linux-style `free`/`swap` columns can look healthy while the system is actually thrashing. `-P` prints the real kernel signal (`kern.memorystatus_vm_pressure_level` — the same one Activity Monitor uses): `normal`, `warning`, or `critical`. + ## Uninstall ```bash diff --git a/src/display.c b/src/display.c index db46e79..712af06 100644 --- a/src/display.c +++ b/src/display.c @@ -100,10 +100,15 @@ void print_numeric(const mem_info_t *mem, const swap_info_t *swap, } else { - /* "shared" - using compressed as approximation */ - print_value(mem->compressed, opts); - /* "buff/cache" */ - print_value(mem->cached + mem->inactive, opts); + /* + * "shared" - macOS has no simple equivalent of Linux Shmem/tmpfs + * accounting, so report 0 rather than a misleading value. Compressed + * memory (previously shown here) is anonymous, not shared, and is + * visible in the wide (-w) view. + */ + print_value(0, opts); + /* "buff/cache" - purgeable + inactive + speculative file cache */ + print_value(mem->cached + mem->inactive + mem->speculative, opts); } print_value(mem->available, opts); printf("\n"); @@ -189,6 +194,31 @@ void print_separator(const options_t *opts) putchar('\n'); } +void print_pressure(const mem_info_t *mem) +{ + const char *label; + switch (get_pressure_level()) + { + case 1: + label = "normal"; + break; + case 2: + label = "warning"; + break; + case 4: + label = "critical"; + break; + default: + label = "unknown"; + break; + } + + unsigned int avail_pct = + mem->total ? (unsigned int)((mem->available * 100) / mem->total) : 0; + + printf("Pressure: %s (%u%% available)\n", label, avail_pct); +} + void print_memory_info(const system_memory_t *sys_mem, const options_t *opts) { print_header(opts); @@ -206,4 +236,9 @@ void print_memory_info(const system_memory_t *sys_mem, const options_t *opts) { print_totals(&sys_mem->mem, &sys_mem->swap, opts); } + + if (opts->pressure) + { + print_pressure(&sys_mem->mem); + } } diff --git a/src/display.h b/src/display.h index 38f452a..c517e19 100644 --- a/src/display.h +++ b/src/display.h @@ -78,6 +78,13 @@ void print_memory_info(const system_memory_t *sys_mem, const options_t *opts); */ void print_separator(const options_t *opts); +/** + * Print the macOS memory pressure line (kernel signal + available %). + * + * @param mem Physical memory information + */ +void print_pressure(const mem_info_t *mem); + /** * Print totals row * diff --git a/src/main.c b/src/main.c index 4ce2c10..ef87650 100644 --- a/src/main.c +++ b/src/main.c @@ -66,6 +66,7 @@ static int parse_args(int argc, char *argv[], options_t *opts) {"seconds", required_argument, NULL, 's'}, {"count", required_argument, NULL, 'c'}, {"lohi", no_argument, NULL, 'l'}, + {"pressure", no_argument, NULL, 'P'}, {"help", no_argument, NULL, 'H'}, {"version", no_argument, NULL, 'V'}, {NULL, 0, NULL, 0}}; @@ -73,7 +74,7 @@ static int parse_args(int argc, char *argv[], options_t *opts) int opt; int option_index = 0; - while ((opt = getopt_long(argc, argv, "bkmghwts:c:lV", long_options, + while ((opt = getopt_long(argc, argv, "bkmghwts:c:lPV", long_options, &option_index)) != -1) { switch (opt) @@ -119,6 +120,9 @@ static int parse_args(int argc, char *argv[], options_t *opts) case 'l': opts->lohi = 1; break; + case 'P': + opts->pressure = 1; + break; case 'H': print_usage(argv[0]); exit(EXIT_SUCCESS); diff --git a/src/memory.c b/src/memory.c index 7a79cbe..725344e 100644 --- a/src/memory.c +++ b/src/memory.c @@ -104,6 +104,9 @@ int get_memory_info(mem_info_t *mem) /* Purgeable memory (cached) */ mem->cached = (uint64_t)vm_stats.purgeable_count * page_size; + /* Speculative read-ahead pages (reclaimable file cache) */ + mem->speculative = (uint64_t)vm_stats.speculative_count * page_size; + /* * Calculate used memory * Used = Active + Wired + Compressed @@ -113,10 +116,10 @@ int get_memory_info(mem_info_t *mem) /* * Calculate available memory - * Available = Free + Inactive + Cached + * Available = Free + Inactive + Cached + Speculative * This represents memory that can be reclaimed if needed */ - mem->available = mem->free + mem->inactive + mem->cached; + mem->available = mem->free + mem->inactive + mem->cached + mem->speculative; /* * App memory is roughly the active memory minus cached files @@ -178,6 +181,20 @@ int get_system_memory(system_memory_t *sys_mem) return 0; } +int get_pressure_level(void) +{ + int level = 0; + size_t length = sizeof(level); + + if (sysctlbyname("kern.memorystatus_vm_pressure_level", &level, &length, + NULL, 0) != 0) + { + return -1; + } + + return level; +} + double calculate_memory_pressure(const mem_info_t *mem) { if (mem == NULL || mem->total == 0) diff --git a/src/memory.h b/src/memory.h index eed8515..cc183b7 100644 --- a/src/memory.h +++ b/src/memory.h @@ -20,16 +20,17 @@ /* Physical memory information */ typedef struct { - uint64_t total; /* Total physical memory */ - uint64_t used; /* Used memory (active + wired + compressed) */ - uint64_t free; /* Free memory */ - uint64_t active; /* Active pages */ - uint64_t inactive; /* Inactive pages */ - uint64_t wired; /* Wired (non-pageable) pages */ - uint64_t compressed; /* Compressed pages */ - uint64_t cached; /* Cached/purgeable pages */ - uint64_t app_memory; /* Memory used by applications */ - uint64_t available; /* Available memory (free + inactive + cached) */ + uint64_t total; /* Total physical memory */ + uint64_t used; /* Used memory (active + wired + compressed) */ + uint64_t free; /* Free memory */ + uint64_t active; /* Active pages */ + uint64_t inactive; /* Inactive pages */ + uint64_t wired; /* Wired (non-pageable) pages */ + uint64_t compressed; /* Compressed pages */ + uint64_t cached; /* Cached/purgeable pages */ + uint64_t speculative; /* Speculative (read-ahead file cache) pages */ + uint64_t app_memory; /* Memory used by applications */ + uint64_t available; /* Available (reclaimable) memory */ } mem_info_t; /* Swap/virtual memory information */ @@ -108,4 +109,17 @@ int get_system_memory(system_memory_t *sys_mem); */ double calculate_memory_pressure(const mem_info_t *mem); +/** + * Get the kernel's memory pressure level. + * + * Reads the real signal macOS itself uses + * (kern.memorystatus_vm_pressure_level), the same one Activity Monitor's + * pressure graph is based on. This exists because macOS relieves pressure via + * the in-RAM compressor before touching swap, so the Linux-style free/swap + * columns cannot reveal it. + * + * @return 1 = normal, 2 = warning, 4 = critical, or -1 on error + */ +int get_pressure_level(void); + #endif /* MEMORY_H */ diff --git a/src/utils.c b/src/utils.c index 60e8ea4..63f8e73 100644 --- a/src/utils.c +++ b/src/utils.c @@ -110,12 +110,13 @@ void init_options(options_t *opts) return; } - opts->unit = UNIT_KIBI; - opts->wide = 0; - opts->seconds = 0; - opts->count = -1; - opts->totals = 0; - opts->lohi = 0; + opts->unit = UNIT_KIBI; + opts->wide = 0; + opts->seconds = 0; + opts->count = -1; + opts->totals = 0; + opts->lohi = 0; + opts->pressure = 0; } /* @@ -142,6 +143,8 @@ void print_usage(const char *prog_name) printf(" -t, --total Show total for RAM + swap\n"); printf(" -s N, --seconds N Repeat printing every N seconds\n"); printf(" -c N, --count N Repeat printing N times, then exit\n"); + printf( + " -P, --pressure Show macOS memory pressure (kernel signal)\n"); printf(" --help Display this help message\n"); printf(" -V, --version Display version information\n"); printf("\n"); diff --git a/src/utils.h b/src/utils.h index 41ac9d0..eb8453f 100644 --- a/src/utils.h +++ b/src/utils.h @@ -53,12 +53,13 @@ typedef enum /* Command-line options */ typedef struct { - unit_type_t unit; /* Output unit */ - int wide; /* Wide output mode */ - int seconds; /* Refresh interval (0 = no refresh) */ - int count; /* Number of iterations (-1 = infinite) */ - int totals; /* Show totals line */ - int lohi; /* Show low/high memory stats */ + unit_type_t unit; /* Output unit */ + int wide; /* Wide output mode */ + int seconds; /* Refresh interval (0 = no refresh) */ + int count; /* Number of iterations (-1 = infinite) */ + int totals; /* Show totals line */ + int lohi; /* Show low/high memory stats */ + int pressure; /* Show macOS memory pressure line */ } options_t; /*