FreeBSD: incorrectly shows RAM in FreeBSD from version 3.4.0
incorrectly shows RAM in FreeBSD from version 3.4.0
will it take long to get fixed? :)
Without the full map of what memory was used for, it's hard to tell if this is a bug or actually intentional.
maybe this will help? maybe someone will fix it in FreeBSD ... :(
The problem is that htop was designed for Linux, and FreeBSD has a significantly different way of handling memory. As a consequence, htop's memory display on FreeBSD has been wrong from day one of the FreeBSD port.
The only relevant way I can see to fix this would be to rewrite the way htop collects memory stats on FreeBSD, and use a different color scheme on FreeBSD to represent wired, buffered, active, laundry, and inactive memory (the "free" memory may be left with having no color).
Basically, on FreeBSD, the ideal situation of the system is: all memory is used, and only a small set is left free (unallocated). In that "used" lot, the subclasses are:
- "wired": these are memory pages that must remain mapped to physical memory. You can find there pages used by the FreeBSD kernel and its modules, and pages that are used by VMs and must remain mapped to physical memory for the CPU to find them when it switches in and out of a virtualized context.
- "buffered": AFAIU, these are physical pages FreeBSD uses for caching disk I/O operations (filesystem tables, various metadata).
- "active": these are pages of virtual memory that are currently mapped to physical memory, i.e. actively being used right now by userland processes. These correspond to the "used" memory on other systems such as Windows and Linux.
- "laundry": these are pages that were active recently, but need to be flushed to disk before they can be considered disposable.
- "inactive": these are pages that were active at some point in the past, and don't (or no longer) need to be flushed anywhere. They can be disposed of. But unlike on other systems such as Windows and Linux where they would be counted as "free memory", on FreeBSD these pages are kept resident, so that they can be instantly reactivated if they are to be reallocated again. The FreeBSD kernel has extensive caching mechanisms that make it able to tell which pages could be optimally reallocated in which context and for which program.
- aside that is the real "free" memory, i.e. pages that the kernel has currently not allocated. It strives to keep a minimal amount of these (and this may seem a waste from FreeBSD's point of view), but it needs to have some for the edge case where e.g. "inactive" memory would be small, "laundry" memory would be maximal, and a userland program would require some fast allocation that would exceed the available "inactive" size. In this situation, the kernel has to quickly cobble together physical pages in a virtual allocated space and return its address faster than all of the disposable memory can be flushed, hence the constant need for a small set of free pages.
This is why, for Linux-centered tools such as "htop", almost all memory appears "used", and why this is quite incorrect.
I'm a long-time user of htop on all my systems and I admit it's a pity that the FreeBSD memory stats are still irrelevant in this program. I haven't been annoyed by this enough to submit my own patch to "fix" it, but that might change in the future.
htop has similar categories:
- Used: Memory in active use by a program (should map to FreeBSD's "wired"+"active"
- Buffers: Pages used for Disk I/O that needs write-back to disk before being claimable ("buffered"+"laundry")
- Cached: Filesystem caches that can be dropped without write-back ("inactive")
- Shared: Memory segments shared between processes (e.g. shmem, shared libraries)
- Free: Actually free memory (this is rare even on Linux)
Even on Linux we don't map the memory information 1:1 to htop's output, as that would be much more complex to explain in a portable way.
Any changing the assignment, which category of memory is treated as the categories that htop understands is not too complicated, once there is agreement how to handle the various values.
There will be a lot of argument with these sort of classifications (I, for example, would disagree: buffered memory and laundry aren't the same thing at all : buffered memory is a permanent cache ; and I would separate wired [kernel] memory and active [userland] memory). But the point I want to make is not here.
In my opinion, trying to stuff FreeBSD's (or whatever system) memory classes into htop's categories is wrong design.
htop's purpose is to be an "improved top", and top is a very system-specific utility. There already exists portability bindings in the htop's source code (I gave it a spin some time ago for a QNX port), it would make sense to extend these to let the memory bar graph display system-specific memory classes as well.
This way, htop on Linux would display the same Linux memory classes as Linux's top, htop on FreeBSD would display the same classes as FreeBSD's top, htop on QNX would display the same classes as the (pretty rudimentary) QNX top, etc.
Each system-specific flavor of htop would expose a variable list of memory classes, an API to probe each of them, and a color for each. I believe that would be the right way of doing things.
Of course, I don't want to force this opinion on anybody, but I think the core point that memory representation should be a system-specific thing, should at least be considered.
The free screenshot shows the common memory categories as used by htop. We should just make the calculations (esp. of what is counted as free memory) match.
Following the details of each memory allocator kernel folks can come up with would increase our complexity and kernel version dependency a lot. I'd not want that. /DLange
FYI, "free" is not a base FreeBSD command, but something from the Linux world. The OP posted the output of a Linux tool ported to FreeBSD.
I think you largely overestimate the complexity of all this. AFAIK htop only supports Linux, *BSDs (Darwin included), and Solaris. Each of these OSes exposes one interface or another (/proc, sysctl, ...) to obtain memory information. Actually, the current calculations as they are done now, i.e. trying to map everything correctly into htop's own classes, actually look much more convoluted to me : more math, many assumptions, and the risk to get some of them wrong.
In case you really want to preserve htop's categories, here's my take on how they should map on FreeBSD:
static void FreeBSDMachine_scanMemoryInfo(Machine* super) {
FreeBSDMachine* this = (FreeBSDMachine*) super;
// comment by Pierre-Marie Baty <[email protected]>
//
// FreeBSD has the following memory classes:
// active: userland pages currently mapped to physical memory (i.e. in use)
// inactive: userland pages that are no longer active, can be (re)allocated to processes
// laundry: userland pages that were just released, now being flushed, will become inactive
// wired: kernel pages currently mapped to physical memory, cannot be paged out nor swapped
// buffers: subcategory of 'wired' corresponding to the filesystem caches
// free: pages that haven't been allocated yet, or have been released
//
// htop has the following memory classes:
// super->usedMem: can be mapped to FreeBSD's ('wired' - 'buffers') + 'laundry' + ('active' - shared)
// super->buffersMem: can be mapped to FreeBSD's 'buffers'
// super->cachedMem: can be mapped to FreeBSD's 'inactive'. Inactive pages are cached allocations.
// super->sharedMem: must be read separately and deduced from the 'active' set
//
// With ZFS ARC, 'buffers' being a subset of 'wired', both these classes will grow in FreeBSD's 'top'.
// In htop, since only the non-'buffers' part of 'wired' is counted as usedMem, the result is that
// only the buffersMem category will grow, which is consistent with what ZFS users would expect.
u_long totalMem;
u_int memActive, memWire, memInactive, memLaundry;
long buffersMem, sharedMem;
size_t len;
struct vmtotal vmtotal;
// total memory
len = sizeof(totalMem);
sysctl(MIB_hw_physmem, 2, &(totalMem), &len, NULL, 0);
super->totalMem = totalMem / 1024;
// 'active' pages
len = sizeof(memActive);
sysctl(MIB_vm_stats_vm_v_active_count, 4, &(memActive), &len, NULL, 0);
memActive *= this->pageSizeKb;
// 'wired' pages
len = sizeof(memWire);
sysctl(MIB_vm_stats_vm_v_wire_count, 4, &(memWire), &len, NULL, 0);
memWire *= this->pageSizeKb;
// 'inactive' pages
len = sizeof(memInactive);
sysctl(MIB_vm_stats_vm_v_inactive_count, 4, &(memInactive), &len, NULL, 0);
memInactive *= this->pageSizeKb;
// 'laundry' pages
len = sizeof(memLaundry);
sysctl(MIB_vm_stats_vm_v_laundry_count, 4, &(memLaundry), &len, NULL, 0);
memLaundry *= this->pageSizeKb;
// 'buffers' pages (separate read, should be deduced from 'wired')
len = sizeof(buffersMem);
sysctl(MIB_vfs_bufspace, 2, &(buffersMem), &len, NULL, 0);
buffersMem /= 1024;
// 'shared' pages (separate read, should be deduced from 'active')
len = sizeof(vmtotal);
sysctl(MIB_vm_vmtotal, 2, &(vmtotal), &len, NULL, 0);
sharedMem = vmtotal.t_rmshr * this->pageSizeKb;
// now fill in the htop categories
super->usedMem = (memWire - buffersMem) + memLaundry + (memActive - sharedMem);
super->cachedMem = memInactive;
super->buffersMem = buffersMem;
super->sharedMem = sharedMem;
// swap
struct kvm_swap swap[16];
int nswap = kvm_getswapinfo(this->kd, swap, ARRAYSIZE(swap), 0);
super->totalSwap = 0;
super->usedSwap = 0;
for (int i = 0; i < nswap; i++) {
super->totalSwap += swap[i].ksw_total;
super->usedSwap += swap[i].ksw_used;
}
super->totalSwap *= this->pageSizeKb;
super->usedSwap *= this->pageSizeKb;
}
In FreeBSD, it must be known that the 'buffers' category reported by top is in fact a subset of the 'wired' category. Also, the 'shared' category in htop must be deduced from the 'active' pages. Hence the computation:
used = (wired - buffers) + laundry + (active - shared)
This shows nice categories in htop:
Unfortunately the "used" memory count (3.70G displayed here) is not right, because htop doesn't count "buffers" as used memory. This class might be disposable on Linux and reallocatable to userland programs, but FreeBSD is different. To fix this, either zero the "buffers" class in the above code, or patch the memory meter. E.g. in MemoryMeter.c, line 58:
#ifdef __FreeBSD__ // do note: a #ifndef __linux__ would probably be more suitable!
if (isPositive(this->values[MEMORY_METER_BUFFERS]))
used += this->values[MEMORY_METER_BUFFERS];
#endif // __FREEBSD__
After that, the readings
are consistent with top's
And there you go.
edit I quickly checked the source tree, and I see the same fix could be applied to DragonflyBSD and Darwin, all of which derive their VM from CMU Mach. I think (not sure) that the NetBSD and OpenBSD implementations are right, though.
I haven't been annoyed by this enough to submit my own patch to "fix" it, but that might change in the future.
This is The Way.
Of course, I don't want to force this opinion on anybody, but I think the core point that memory representation should be a system-specific thing, should at least be considered.
Your opinion is how htop works in practice - it tries to adapt to show the right thing for different operating systems but it looks like in this case its not quite right. Since you've got a good handle on what it should display, and good access to a BSD test system, please send a PR to improve it.
Look at the memory meter code here for example: https://github.com/htop-dev/htop/blob/8a87d3e6617059613ee2515579656853eeee0889/MemoryMeter.c#L87 ... and how htop adapts on-the-fly to provide the right information for each system. You can display whatever makes sense for your platform here.
Thanks for helping improve htop!
By personal policy, I don't want to populate my GitHub account and have each atom of my body be metered by Microsoft. I understand this is an inconvenience that prevents me from sending pull requests, but I posted a working change just above. I hope it'll be copy/pastable enough for the maintainers.
They just have to declare a (missed out for brevity) MIB_vm_stats_vm_v_laundry_count integer array at the top of freebsd/FreeBSD_Machine.c next to the other variables, and call sysctlnametomib() on it (length 4) in Machine_new() aside the other MIBs. Then replace FreeBSDMachine_scanMemoryInfo() in the same file with the implementation I posted above, and patch MemoryMeter.c as I indicated.
All the Mach-based OS-specific VMs should be fixed this way. There should be a 1:1 mapping for this on the DragonflyBSD code, since the latter is an early fork of FreeBSD. For Darwin, it should be relatively straightforward. The code is a bit different, the memory class fillings don't happen at the same place and they're read from a single VM statistics structure. But the idea is the same.
(edit) BTW, you're welcome! My pleasure.
OK, hmm - alot to unpack there.
I don't have easy access to a BSD machine, but I'm happy to help if I can. Could you try out your suggested changes locally and paste a patch inline here (or email it to me -> [email protected]) - I'll open a PR for you & take it through the process.
Yes, I already tried the changes and htop on my server has been replaced by this patched one.
Do you want a unified diff just for FreeBSD or for DragonflyBSD and Darwin as well?
All three would be delightful, thanks!
Hmmm... The DragonflyBSD build is broken:
root@:~/htop # ./configure
checking build system type... x86_64-unknown-dragonfly6.4
checking host system type... x86_64-unknown-dragonfly6.4
checking for a BSD-compatible install... /usr/bin/install -c
checking whether sleep supports fractional seconds... yes
checking filesystem timestamp resolution... 2
checking whether build environment is sane... yes
checking for a race-free mkdir -p... mkdir -p
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking xargs -n works... yes
checking whether make supports the include directive... yes (GNU style)
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables...
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether the compiler supports GNU C... yes
checking whether gcc accepts -g... yes
checking for gcc option to enable C11 features... none needed
checking whether gcc understands -c and -o together... yes
checking dependency style of gcc... gcc3
checking for stdio.h... yes
checking for stdlib.h... yes
checking for string.h... yes
checking for inttypes.h... yes
checking for stdint.h... yes
checking for strings.h... yes
checking for sys/stat.h... yes
checking for sys/types.h... yes
checking for unistd.h... yes
checking for wchar.h... yes
checking for minix/config.h... no
checking whether it is safe to define __EXTENSIONS__... yes
checking whether _XOPEN_SOURCE should be defined... no
checking for gcc option to enable large file support... none needed
checking for gcc... (cached) gcc
checking whether the compiler supports GNU C... (cached) yes
checking whether gcc accepts -g... (cached) yes
checking for gcc option to enable C11 features... (cached) none needed
checking whether gcc understands -c and -o together... (cached) yes
checking dependency style of gcc... (cached) gcc3
checking for dirent.h that defines DIR... yes
checking for library containing opendir... none required
checking for stdlib.h... (cached) yes
checking for string.h... (cached) yes
checking for strings.h... (cached) yes
checking for sys/param.h... yes
checking for sys/time.h... yes
checking for sys/utsname.h... yes
checking for unistd.h... (cached) yes
checking for sys/mkdev.h... no
checking for sys/sysmacros.h... no
checking for execinfo.h... yes
checking for mbstate_t... yes
checking for mode_t... yes
checking for off_t... yes
checking for pid_t... yes
checking for size_t... yes
checking for ssize_t... yes
checking for uid_t... yes
checking for gid_t... yes
checking for uint8_t... yes
checking for uint16_t... yes
checking for uint32_t... yes
checking for uint64_t... yes
checking for alloc_size... yes
checking for access... no
checking for nonnull... yes
checking for returns_nonnull... yes
checking for NaN support... yes
checking for __builtin_ctz... yes
checking for library containing ceil... -lm
checking for library containing kvm_open... -lkvm
checking for library containing getdevs... -ldevstat
checking for library containing clock_gettime... none required
checking for dladdr... yes
checking for faccessat... yes
checking for fstatat... yes
checking for host_get_clock_service... no
checking for memfd_create... no
checking for openat... yes
checking for readlinkat... yes
checking for sched_getscheduler... yes
checking for sched_setscheduler... yes
checking for strnlen... yes
checking for strchrnul... yes
checking for pkg-config... /usr/local/bin/pkg-config
checking pkg-config is at least version 0.9.0... yes
found ncursesw information through /usr/local/bin/pkg-config
checking for keypad in -rpath /usr/local/lib -lncurses -ltinfo... no
found ncurses information through /usr/local/bin/pkg-config
checking for keypad in -rpath /usr/local/lib -lncurses -ltinfo... no
found ncurses6 information through ncurses6-config
checking for keypad in -rpath /usr/local/lib -lncurses -ltinfo... no
no curses information found through '*-config' utilities; will guess the linker flags
checking for keypad in -lncursesw6... no
checking for keypad in -lncursesw6 -ltinfow6... no
checking for keypad in -lncursesw6 -ltinfo... no
checking for keypad in -lncursesw5... no
checking for keypad in -lncursesw5 -ltinfow5... no
checking for keypad in -lncursesw5 -ltinfo... no
checking for keypad in -lncursesw... no
checking for keypad in -lncursesw -ltinfow... no
checking for keypad in -lncursesw -ltinfo... no
checking for keypad in -lncursestw6... no
checking for keypad in -lncursestw6 -ltinfotw6... no
checking for keypad in -lncursestw6 -ltinfo... no
checking for keypad in -lncursestw5... no
checking for keypad in -lncursestw5 -ltinfotw5... no
checking for keypad in -lncursestw5 -ltinfo... no
checking for keypad in -lncursestw... no
checking for keypad in -lncursestw -ltinfotw... no
checking for keypad in -lncursestw -ltinfo... no
checking for keypad in -lncurses6... no
checking for keypad in -lncurses6 -ltinfo6... no
checking for keypad in -lncurses6 -ltinfo... no
checking for keypad in -lncurses5... no
checking for keypad in -lncurses5 -ltinfo5... no
checking for keypad in -lncurses5 -ltinfo... no
checking for keypad in -lncurses... no
checking for keypad in -lncurses -ltinfo... no
checking for keypad in -lncursest6... no
checking for keypad in -lncursest6 -ltinfot6... no
checking for keypad in -lncursest6 -ltinfo... no
checking for keypad in -lncursest5... no
checking for keypad in -lncursest5 -ltinfot5... no
checking for keypad in -lncursest5 -ltinfo... no
checking for keypad in -lncursest... no
checking for keypad in -lncursest -ltinfot... no
checking for keypad in -lncursest -ltinfo... no
checking for keypad in -lcurses... no
checking for keypad in -lcurses -ltinfo... no
configure: error: cannot find required curses/ncurses library
Yet:
root@:~/htop # find / -name '*ncurses*'
/usr/local/libdata/pkgconfig/ncursesw.pc
/usr/local/libdata/pkgconfig/ncurses++.pc
/usr/local/libdata/pkgconfig/ncurses.pc
/usr/local/include/ncurses
/usr/local/include/ncurses/ncurses_dll.h
/usr/local/include/ncurses/ncurses.h
/usr/local/bin/ncurses6-config
/usr/local/share/man/man3/ncurses.3x.gz
/usr/local/share/man/man1/ncurses6-config.1.gz
/usr/local/share/licenses/ncurses-6.5
/usr/local/share/doc/ncurses
/usr/local/share/doc/ncurses/ncurses-intro.doc
/usr/local/share/doc/ncurses/ncurses-intro.html
/usr/local/lib/libncurses.a
/usr/local/lib/libncurses.so
/usr/local/lib/libncurses.so.6.5
/usr/local/lib/libncurses++.a
/usr/local/lib/libncursesw.so
/usr/local/lib/libncurses.so.6
/usr/include/priv/ncurses
/usr/include/priv/ncurses/ncurses_dll.h
/usr/include/priv/ncurses/ncurses.h
/usr/lib/priv/libprivate_ncursesw.a
/usr/lib/priv/libprivate_ncursesw.so
/usr/lib/priv/libprivate_ncurses.a
/usr/lib/priv/libprivate_ncurses.so
/usr/lib/priv/profile/libprivate_ncursesw.a
/usr/lib/priv/profile/libprivate_ncurses.a
/lib/priv/libprivate_ncurses.so.60
/lib/priv/libprivate_ncursesw.so.60
I'll have to investigate this one first...
Check here too: https://github.com/htop-dev/htop/pull/1822
I needed to override the ncurses-related environment variables for the configure script to do its job on the latest Dragonfly. Someone should look at it.
export CURSES_CFLAGS=-I/usr/local/include
export CURSES_LIBS=-L/usr/local/lib -lncurses
After that I can compile. And it errors out, which tells me that contrarily to what I thought, DragonflyBSD simplified FreeBSD's VM model: they don't expose the "laundry" memory class, and there's no sysctl to get the "shared" memory pages count. So the DragonflyBSD computation was correct. The following comment in dragonflyBSD/DragonflyBSD_Machine.c
// @etosan:
// memory counter relationships seem to be these:
// total = active + wired + inactive + cache + free
// htop_used (unavail to anybody) = active + wired
// htop_cache (for cache meter) = buffers + cache
is right.
On to Darwin now...
Darwin is correct too. The memory count is right, and the VM stats interface doesn't expose as much details as with FreeBSD, so there's nothing to fix on that OS. It turns out that only FreeBSD was summing it the wrong way.
By the way, htop on macOS (Darwin) can be compiled straight from source, without homebrew or any other fancyware, as long as you have clang and autotools (they come with Xcode). To do so just define
export PKG_CONFIG=true
just before running ./configure on macOS.
I'll post the unified diff for FreeBSD in the next message.
So here's the unified diff for fixing the FreeBSD memory computations:
diff -ruN htop/MemoryMeter.c htop-mine/MemoryMeter.c
--- htop/MemoryMeter.c 2025-11-27 02:52:22.400100000 +0100
+++ htop-mine/MemoryMeter.c 2025-11-27 02:28:04.043246000 +0100
@@ -55,6 +55,10 @@
used += this->values[MEMORY_METER_SHARED];
if (isPositive(this->values[MEMORY_METER_COMPRESSED]))
used += this->values[MEMORY_METER_COMPRESSED];
+#if defined(__FreeBSD__)
+ if (isPositive(this->values[MEMORY_METER_BUFFERS]))
+ used += this->values[MEMORY_METER_BUFFERS];
+#endif // defined(__FreeBSD__)
written = Meter_humanUnit(buffer, used, size);
METER_BUFFER_CHECK(buffer, size, written);
diff -ruN htop/freebsd/FreeBSDMachine.c htop-mine/freebsd/FreeBSDMachine.c
--- htop/freebsd/FreeBSDMachine.c 2025-11-27 02:52:22.409113000 +0100
+++ htop-mine/freebsd/FreeBSDMachine.c 2025-11-27 01:25:22.084285000 +0100
@@ -43,9 +43,8 @@
static int MIB_vm_stats_vm_v_wire_count[4];
static int MIB_vm_stats_vm_v_active_count[4];
-static int MIB_vm_stats_vm_v_cache_count[4];
+static int MIB_vm_stats_vm_v_laundry_count[4];
static int MIB_vm_stats_vm_v_inactive_count[4];
-static int MIB_vm_stats_vm_v_free_count[4];
static int MIB_vm_vmtotal[2];
static int MIB_vfs_bufspace[2];
@@ -77,9 +76,8 @@
len = 4; sysctlnametomib("vm.stats.vm.v_wire_count", MIB_vm_stats_vm_v_wire_count, &len);
len = 4; sysctlnametomib("vm.stats.vm.v_active_count", MIB_vm_stats_vm_v_active_count, &len);
- len = 4; sysctlnametomib("vm.stats.vm.v_cache_count", MIB_vm_stats_vm_v_cache_count, &len);
+ len = 4; sysctlnametomib("vm.stats.vm.v_laundry_count", MIB_vm_stats_vm_v_laundry_count, &len);
len = 4; sysctlnametomib("vm.stats.vm.v_inactive_count", MIB_vm_stats_vm_v_inactive_count, &len);
- len = 4; sysctlnametomib("vm.stats.vm.v_free_count", MIB_vm_stats_vm_v_free_count, &len);
len = 2; sysctlnametomib("vm.vmtotal", MIB_vm_vmtotal, &len);
len = 2; sysctlnametomib("vfs.bufspace", MIB_vfs_bufspace, &len);
@@ -310,65 +308,74 @@
static void FreeBSDMachine_scanMemoryInfo(Machine* super) {
FreeBSDMachine* this = (FreeBSDMachine*) super;
- // @etosan:
- // memory counter relationships seem to be these:
- // total = active + wired + inactive + cache + free
- // htop_used (unavail to anybody) = active + wired + inactive - buffer
- // htop_cache (for cache meter) = buffer + cache
- // htop_user_free (avail to procs) = buffer + cache + free
- // htop_buffers (disk write buffer) = 0 (not applicable to FreeBSD)
+ // comment by Pierre-Marie Baty <[email protected]>
//
- // 'buffer' contain cache used by most file systems other than ZFS, and is
- // included in 'wired'
+ // FreeBSD has the following memory classes:
+ // active: userland pages currently mapped to physical memory (i.e. in use)
+ // inactive: userland pages that are no longer active, can be (re)allocated to processes
+ // laundry: userland pages that were just released, now being flushed, will become inactive
+ // wired: kernel pages currently mapped to physical memory, cannot be paged out nor swapped
+ // buffers: subcategory of 'wired' corresponding to the filesystem caches
+ // free: pages that haven't been allocated yet, or have been released
//
- // with ZFS ARC situation becomes bit muddled, as ARC behaves like "user_free"
- // and belongs into cache, but is reported as wired by kernel
+ // htop has the following memory classes:
+ // super->usedMem: can be mapped to FreeBSD's ('wired' - 'buffers') + 'laundry' + ('active' - shared)
+ // super->buffersMem: can be mapped to FreeBSD's 'buffers'
+ // super->cachedMem: can be mapped to FreeBSD's 'inactive'. Inactive pages are cached allocations.
+ // super->sharedMem: must be read separately and deduced from the 'active' set
//
- // htop_used = active + (wired - arc)
- // htop_cache = buffers + cache + arc
+ // With ZFS ARC, 'buffers' being a subset of 'wired', both these classes will grow in FreeBSD's 'top'.
+ // In htop, since only the non-'buffers' part of 'wired' is counted as usedMem, the result is that
+ // only the buffersMem category will grow, which is consistent with what ZFS users would expect.
+
u_long totalMem;
- u_int memActive, memWire, memInactive, cachedMem;
- long buffersMem;
+ u_int memActive, memWire, memInactive, memLaundry;
+ long buffersMem, sharedMem;
size_t len;
struct vmtotal vmtotal;
- //disabled for now, as it is always smaller than phycal amount of memory...
- //...to avoid "where is my memory?" questions
- //sysctl(MIB_vm_stats_vm_v_page_count, 4, &(super->totalMem), &len, NULL, 0);
- //super->totalMem *= this->pageSizeKb;
+ // total memory
len = sizeof(totalMem);
sysctl(MIB_hw_physmem, 2, &(totalMem), &len, NULL, 0);
- totalMem /= 1024;
- super->totalMem = totalMem;
+ super->totalMem = totalMem / 1024;
+ // 'active' pages
len = sizeof(memActive);
sysctl(MIB_vm_stats_vm_v_active_count, 4, &(memActive), &len, NULL, 0);
memActive *= this->pageSizeKb;
+ // 'wired' pages
len = sizeof(memWire);
sysctl(MIB_vm_stats_vm_v_wire_count, 4, &(memWire), &len, NULL, 0);
memWire *= this->pageSizeKb;
+ // 'inactive' pages
len = sizeof(memInactive);
sysctl(MIB_vm_stats_vm_v_inactive_count, 4, &(memInactive), &len, NULL, 0);
memInactive *= this->pageSizeKb;
+ // 'laundry' pages
+ len = sizeof(memLaundry);
+ sysctl(MIB_vm_stats_vm_v_laundry_count, 4, &(memLaundry), &len, NULL, 0);
+ memLaundry *= this->pageSizeKb;
+
+ // 'buffers' pages (separate read, should be deduced from 'wired')
len = sizeof(buffersMem);
sysctl(MIB_vfs_bufspace, 2, &(buffersMem), &len, NULL, 0);
buffersMem /= 1024;
- super->cachedMem = buffersMem;
- len = sizeof(cachedMem);
- sysctl(MIB_vm_stats_vm_v_cache_count, 4, &(cachedMem), &len, NULL, 0);
- cachedMem *= this->pageSizeKb;
- super->cachedMem += cachedMem;
-
+ // 'shared' pages (separate read, should be deduced from 'active')
len = sizeof(vmtotal);
sysctl(MIB_vm_vmtotal, 2, &(vmtotal), &len, NULL, 0);
- super->sharedMem = vmtotal.t_rmshr * this->pageSizeKb;
+ sharedMem = vmtotal.t_rmshr * this->pageSizeKb;
- super->usedMem = memActive + memWire + memInactive - buffersMem;
+ // now fill in the htop categories
+ super->usedMem = (memWire - buffersMem) + memLaundry + (memActive - sharedMem);
+ super->cachedMem = memInactive;
+ super->buffersMem = buffersMem;
+ super->sharedMem = sharedMem;
+ // swap
struct kvm_swap swap[16];
int nswap = kvm_getswapinfo(this->kd, swap, ARRAYSIZE(swap), 0);
super->totalSwap = 0;
Let me know if you need anything else.
Update: I tested on a ZFS-enabled FreeBSD system (my previous test was on a FreeBSD machine with a UFS root filesystem) and while the overall maths seem to work, there remains a glitch. Top reports:
Mem: 115M Active, 346M Inact, 21M Laundry, 6882M Wired, 104K Buf, 297M Free
ARC: 5323M Total, 3940M MFU, 642M MRU, 768K Anon, 91M Header, 634M Other
4127M Compressed, 12G Uncompressed, 2.90:1 Ratio
And the patched htop reports:
If I substract the ZFS ARC total (disposable and reallocatable to userland processes) from the Wired class I get 6882 - 5323 = 1559 ; to which I add the Active and Laundry classes and I get 1695M, this approaches what htop reports as "used" memory (1.90G). So far so good.
Yet, I was surprised because the bars don't have the colors I'd expect. I'd have expected ZFS ARC to be reported by FreeBSD in the "Buf" class and naturally be counted in the "buffers" htop category. It's not. This means that the following comment of mine is wrong:
// With ZFS ARC, 'buffers' being a subset of 'wired', both these classes will grow in FreeBSD's 'top'.
// In htop, since only the non-'buffers' part of 'wired' is counted as usedMem, the result is that
// only the buffersMem category will grow, which is consistent with what ZFS users would expect.
To make ZFS ARC appear in the "buffers" category, we need to substract the ARC total from the Wired class, and add it to the Buf class. This only affects ZFS-enabled FreeBSD systems. Let me see how the ARC math is done in htop to get this finishing touch done...
@PierreMarieBaty it would be preferable to not have any "defined(FreeBSD)" conditional logic in the core MemoryMeter.c file - can you rework that (perhaps by adding another category in the MemoryMeterValues enum)?
If we must have it, HTOP_FREEBSD is the more correct macro to be using in that situation FWIW.
Adding system-specific values to the MemoryMeterValues enum would defeat the purpose of portability. Wouldn't that be worse?
That's why I was advising something completely different in the first place:
htop's purpose is to be an "improved top", and top is a very system-specific utility. There already exists portability bindings in the htop's source code (I gave it a spin some time ago for a QNX port), it would make sense to extend these to let the memory bar graph display system-specific memory classes as well.
This way, htop on Linux would display the same Linux memory classes as Linux's top, htop on FreeBSD would display the same classes as FreeBSD's top, htop on QNX would display the same classes as the (pretty rudimentary) QNX top, etc.
Each system-specific flavor of htop would expose a variable list of memory classes, an API to probe each of them, and a color for each. I believe that would be the right way of doing things.
And I am forced to say again this is not the way htop works at the moment...
I'd stick to the preprocessor-based conditional logic instead of abusing the system-agnostic MemoryMeterValues enum with such a precedent.
BTW, no sadism intended, but here's another one I had forgotten, line 44 in the same file:
--- htop/MemoryMeter.c 2025-11-27 02:52:22.400100000 +0100
+++ htop-mine/MemoryMeter.c 2025-11-27 04:32:57.210600000 +0100
@@ -41,7 +41,9 @@
this->values[MEMORY_METER_AVAILABLE] = NAN;
Platform_setMemoryValues(this);
if ((this->mode == GRAPH_METERMODE || this->mode == BAR_METERMODE) && !settings->showCachedMemory) {
+#if !defined(HTOP_FREEBSD)
this->values[MEMORY_METER_BUFFERS] = 0;
+#endif // !defined(HTOP_FREEBSD)
this->values[MEMORY_METER_CACHE] = 0;
}
/* Do not print available memory in bar mode */
@@ -55,6 +57,10 @@
used += this->values[MEMORY_METER_SHARED];
if (isPositive(this->values[MEMORY_METER_COMPRESSED]))
used += this->values[MEMORY_METER_COMPRESSED];
+#if defined(HTOP_FREEBSD)
+ if (isPositive(this->values[MEMORY_METER_BUFFERS]))
+ used += this->values[MEMORY_METER_BUFFERS];
+#endif // defined(HTOP_FREEBSD)
written = Meter_humanUnit(buffer, used, size);
METER_BUFFER_CHECK(buffer, size, written);
Because when the checkbox "Show cached memory in graph and bar mode" is checked in Display options, you want the Buffered memory (used kernel memory) to stay displayed in FreeBSD.
I'm still puzzled about the ZFS ARC issue. My numbers are right (I output them in a debug log file), but the meter is wrong?
Top says:
Mem: 80M Active, 390M Inact, 21M Laundry, 6835M Wired, 104K Buf, 332M Free
ARC: 5337M Total, 3954M MFU, 643M MRU, 923K Anon, 91M Header, 634M Other
4141M Compressed, 12G Uncompressed, 2.90:1 Ratio
The numbers in htop are (the first line are the intermediary variables, the second line are the values that are written in htop's categories):
memWire 6835M buffersMem 0M ZFSARC 5337M memLaundry 20M memActive 81M sharedMem 34M
super->usedMem/cachedMem/buffersMem/sharedMem = 1565M/389M/5337M/34M
And htop displays (my comments in blue):
Do you have an idea?
Okay, problem solved. The computation I figured out about ZFS ARC was done a second time in freebsd/Platform.c. Now it's looking good.
FINAL unified diff in the next post...
diff -ruN htop/MemoryMeter.c htop-mine/MemoryMeter.c
--- htop/MemoryMeter.c 2025-11-27 02:52:22.400100000 +0100
+++ htop-mine/MemoryMeter.c 2025-11-27 04:53:05.658755000 +0100
@@ -41,7 +41,9 @@
this->values[MEMORY_METER_AVAILABLE] = NAN;
Platform_setMemoryValues(this);
if ((this->mode == GRAPH_METERMODE || this->mode == BAR_METERMODE) && !settings->showCachedMemory) {
+#if !defined(HTOP_FREEBSD)
this->values[MEMORY_METER_BUFFERS] = 0;
+#endif // !defined(HTOP_FREEBSD)
this->values[MEMORY_METER_CACHE] = 0;
}
/* Do not print available memory in bar mode */
@@ -55,6 +57,10 @@
used += this->values[MEMORY_METER_SHARED];
if (isPositive(this->values[MEMORY_METER_COMPRESSED]))
used += this->values[MEMORY_METER_COMPRESSED];
+#if defined(HTOP_FREEBSD)
+ if (isPositive(this->values[MEMORY_METER_BUFFERS]))
+ used += this->values[MEMORY_METER_BUFFERS];
+#endif // defined(HTOP_FREEBSD)
written = Meter_humanUnit(buffer, used, size);
METER_BUFFER_CHECK(buffer, size, written);
diff -ruN htop/freebsd/FreeBSDMachine.c htop-mine/freebsd/FreeBSDMachine.c
--- htop/freebsd/FreeBSDMachine.c 2025-11-27 02:52:22.409113000 +0100
+++ htop-mine/freebsd/FreeBSDMachine.c 2025-11-27 05:09:38.655228000 +0100
@@ -43,9 +43,8 @@
static int MIB_vm_stats_vm_v_wire_count[4];
static int MIB_vm_stats_vm_v_active_count[4];
-static int MIB_vm_stats_vm_v_cache_count[4];
+static int MIB_vm_stats_vm_v_laundry_count[4];
static int MIB_vm_stats_vm_v_inactive_count[4];
-static int MIB_vm_stats_vm_v_free_count[4];
static int MIB_vm_vmtotal[2];
static int MIB_vfs_bufspace[2];
@@ -77,9 +76,8 @@
len = 4; sysctlnametomib("vm.stats.vm.v_wire_count", MIB_vm_stats_vm_v_wire_count, &len);
len = 4; sysctlnametomib("vm.stats.vm.v_active_count", MIB_vm_stats_vm_v_active_count, &len);
- len = 4; sysctlnametomib("vm.stats.vm.v_cache_count", MIB_vm_stats_vm_v_cache_count, &len);
+ len = 4; sysctlnametomib("vm.stats.vm.v_laundry_count", MIB_vm_stats_vm_v_laundry_count, &len);
len = 4; sysctlnametomib("vm.stats.vm.v_inactive_count", MIB_vm_stats_vm_v_inactive_count, &len);
- len = 4; sysctlnametomib("vm.stats.vm.v_free_count", MIB_vm_stats_vm_v_free_count, &len);
len = 2; sysctlnametomib("vm.vmtotal", MIB_vm_vmtotal, &len);
len = 2; sysctlnametomib("vfs.bufspace", MIB_vfs_bufspace, &len);
@@ -310,65 +308,75 @@
static void FreeBSDMachine_scanMemoryInfo(Machine* super) {
FreeBSDMachine* this = (FreeBSDMachine*) super;
- // @etosan:
- // memory counter relationships seem to be these:
- // total = active + wired + inactive + cache + free
- // htop_used (unavail to anybody) = active + wired + inactive - buffer
- // htop_cache (for cache meter) = buffer + cache
- // htop_user_free (avail to procs) = buffer + cache + free
- // htop_buffers (disk write buffer) = 0 (not applicable to FreeBSD)
+ // comment by Pierre-Marie Baty <[email protected]>
//
- // 'buffer' contain cache used by most file systems other than ZFS, and is
- // included in 'wired'
+ // FreeBSD has the following memory classes:
+ // active: userland pages currently mapped to physical memory (i.e. in use)
+ // inactive: userland pages that are no longer active, can be (re)allocated to processes
+ // laundry: userland pages that were just released, now being flushed, will become inactive
+ // wired: kernel pages currently mapped to physical memory, cannot be paged out nor swapped
+ // buffers: subcategory of 'wired' corresponding to the filesystem caches
+ // free: pages that haven't been allocated yet, or have been released
//
- // with ZFS ARC situation becomes bit muddled, as ARC behaves like "user_free"
- // and belongs into cache, but is reported as wired by kernel
+ // htop has the following memory classes:
+ // super->usedMem: can be mapped to FreeBSD's ('wired' - 'buffers') + 'laundry' + ('active' - shared)
+ // super->buffersMem: can be mapped to FreeBSD's 'buffers'
+ // super->cachedMem: can be mapped to FreeBSD's 'inactive'. Inactive pages are cached allocations.
+ // super->sharedMem: must be read separately and deduced from the 'active' set
//
- // htop_used = active + (wired - arc)
- // htop_cache = buffers + cache + arc
+ // With ZFS, the ARC area is NOT counted in the 'buffers' class, but is still counted in the 'wired'
+ // class. The ARC total must thus be substracted from the 'wired' class AND added to the 'buffer' class,
+ // so that the result (ARC being shown in buffersMem) is consistent with what ZFS users would expect.
+ // This adjustment is done in Platform_setMemoryValues() in freebsd/Platform.c.
+
u_long totalMem;
- u_int memActive, memWire, memInactive, cachedMem;
- long buffersMem;
+ u_int memActive, memWire, memInactive, memLaundry;
+ long buffersMem, sharedMem;
size_t len;
struct vmtotal vmtotal;
- //disabled for now, as it is always smaller than phycal amount of memory...
- //...to avoid "where is my memory?" questions
- //sysctl(MIB_vm_stats_vm_v_page_count, 4, &(super->totalMem), &len, NULL, 0);
- //super->totalMem *= this->pageSizeKb;
+ // total memory
len = sizeof(totalMem);
sysctl(MIB_hw_physmem, 2, &(totalMem), &len, NULL, 0);
- totalMem /= 1024;
- super->totalMem = totalMem;
+ super->totalMem = totalMem / 1024;
+ // 'active' pages
len = sizeof(memActive);
sysctl(MIB_vm_stats_vm_v_active_count, 4, &(memActive), &len, NULL, 0);
memActive *= this->pageSizeKb;
+ // 'wired' pages
len = sizeof(memWire);
sysctl(MIB_vm_stats_vm_v_wire_count, 4, &(memWire), &len, NULL, 0);
memWire *= this->pageSizeKb;
+ // 'inactive' pages
len = sizeof(memInactive);
sysctl(MIB_vm_stats_vm_v_inactive_count, 4, &(memInactive), &len, NULL, 0);
memInactive *= this->pageSizeKb;
+ // 'laundry' pages
+ len = sizeof(memLaundry);
+ sysctl(MIB_vm_stats_vm_v_laundry_count, 4, &(memLaundry), &len, NULL, 0);
+ memLaundry *= this->pageSizeKb;
+
+ // 'buffers' pages (separate read, should be deduced from 'wired')
len = sizeof(buffersMem);
sysctl(MIB_vfs_bufspace, 2, &(buffersMem), &len, NULL, 0);
buffersMem /= 1024;
- super->cachedMem = buffersMem;
- len = sizeof(cachedMem);
- sysctl(MIB_vm_stats_vm_v_cache_count, 4, &(cachedMem), &len, NULL, 0);
- cachedMem *= this->pageSizeKb;
- super->cachedMem += cachedMem;
-
+ // 'shared' pages (separate read, should be deduced from 'active')
len = sizeof(vmtotal);
sysctl(MIB_vm_vmtotal, 2, &(vmtotal), &len, NULL, 0);
- super->sharedMem = vmtotal.t_rmshr * this->pageSizeKb;
+ sharedMem = vmtotal.t_rmshr * this->pageSizeKb;
- super->usedMem = memActive + memWire + memInactive - buffersMem;
+ // now fill in the htop categories
+ super->usedMem = (memWire - buffersMem) + memLaundry + (memActive - sharedMem);
+ super->cachedMem = memInactive;
+ super->buffersMem = buffersMem;
+ super->sharedMem = sharedMem;
+ // swap
struct kvm_swap swap[16];
int nswap = kvm_getswapinfo(this->kd, swap, ARRAYSIZE(swap), 0);
super->totalSwap = 0;
diff -ruN htop/freebsd/Platform.c htop-mine/freebsd/Platform.c
--- htop/freebsd/Platform.c 2025-11-27 02:52:22.409562000 +0100
+++ htop-mine/freebsd/Platform.c 2025-11-27 05:10:12.156937000 +0100
@@ -243,7 +243,7 @@
if (fhost->zfs.size > fhost->zfs.min)
shrinkableSize = fhost->zfs.size - fhost->zfs.min;
this->values[MEMORY_METER_USED] -= shrinkableSize;
- this->values[MEMORY_METER_CACHE] += shrinkableSize;
+ this->values[MEMORY_METER_BUFFERS] += shrinkableSize;
// this->values[MEMORY_METER_AVAILABLE] += shrinkableSize;
}
}
Here we are. At last...
| Here we are. At last...
Hmm, not quite. :) This new interpretation of "buffers" as something that is also "used" isn't really working out too well, IMO from looking at the patch - the increasing ifdef use is a big red flag. I'll update the PR and add some review comments - let's continue this discussion there.