ratatop day 2: the memory box, and the lie in `free -h` A developer building ratatop, a Rust system monitor with Ratatui, discovered that parsing /proc/meminfo for memory stats is fraught with pitfalls. The MemFree field is misleading because Linux aggressively uses free RAM for caching, making it trend toward zero on healthy systems; the correct metric is MemAvailable. The developer also warns that SwapCached can be mistaken for Cached if parsing is not exact, and that the 'kB' suffix actually means kibibytes. Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback. Yesterday https://dev.to/lovestaco/building-ratatop-a-rust-system-monitor-with-ratatui-50n I got the CPU box working in ratatui https://ratatui.rs and wrote up my first impressions coming from Bubble Tea. Today: memory. I assumed this would be the easy one. CPU needed delta tracking between samples, per-core bookkeeping, braille bit twiddling. Memory is just a file with numbers in it. Read it, print it, go home. Reader, it was not the easy one. Look at that box again. Total 13.5 GiB. Free: 271 MiB. Two percent free. That sounds like a machine about to fall over. It is completely fine. I have 7.10 GiB available right now. This is the thing that trips up everyone reading /proc/meminfo for the first time, and it tripped me up too. MemFree means memory the kernel is doing absolutely nothing with. Not caching a file, not buffering a write, nothing. Just sitting there. And the kernel hates that. Unused RAM is wasted RAM. So it fills spare memory with page cache, and hands it back the instant a process actually needs it. So MemFree on a healthy machine trends toward zero, by design. It is a measure of how much RAM your kernel is failing to put to work. The number you actually want is MemAvailable , added to the kernel in 3.14 precisely because everyone was misreading MemFree . It is an estimate of how much you can allocate right now without swapping, and it accounts for the fact that most of that cache is reclaimable. Note that Used and Available add up to Total , but Cached and Free do not slot in neatly beside them. Cached memory is counted inside available. They overlap. Trying to make all four sum to total is a trap, and I definitely tried for a bit. Meme 1 Template:Bernie Sanders "I am once again asking" Text: "I am once again asking you to stop reading MemFree" Here is where I went looking at how btop https://github.com/aristocratos/btop does it instead of inventing my own. The obvious approach is to add up the parts. Total minus free minus cached minus buffers, something like that. This is what a lot of tools did historically, and it is where the old "Linux ate my RAM" confusion comes from. btop does the simple thing instead: used = MemTotal - MemAvailable That is it. Let the kernel do the hard estimate, because it is the only thing that actually knows what is reclaimable. In Rust, my collector boils down to this: pub struct Memory { pub total: u64, pub used: u64, pub available: u64, pub cached: u64, pub free: u64, } Plain u64 bytes, no units baked in. Formatting is the UI's problem, not the collector's. /proc/meminfo looks like this: MemTotal: 14177796 kB MemFree: 497404 kB MemAvailable: 7490900 kB Buffers: 1235656 kB Cached: 5146132 kB SwapCached: 81068 kB Spot the landmine. SwapCached contains the substring Cached . If you parse this with a contains or a loose prefix match, SwapCached arrives four lines later and quietly overwrites your real cached value with something roughly 60 times smaller. Your box renders. No error. No panic. Just a wrong number sitting there looking plausible. The fix is boring: split on the colon, match the label exactly. js let Some label, rest = line.split once ':' else { continue; }; match label { "MemTotal" = mem.total = bytes, "MemFree" = mem.free = bytes, "MemAvailable" = available = Some bytes , "Cached" = mem.cached = bytes, = {} } I wrote a test named swap cached does not get mistaken for cached purely so the next person who "simplifies" this parser gets caught. Also worth knowing: the kB suffix in that file is a lie too. Those are KiB, 1024 bytes, not 1000. So the multiply is << 10 , not 1000 . Look at the value column again: 13.5 GiB 6.42 GiB 271 MiB Different decimal counts. That is deliberate, and it is what btop does. The rule is three significant figures, always. Under 10, you get two decimals. Between 10 and 100, one. Above 100, none. The number is always exactly three digits wide, so the column stays aligned no matter what the values do. js let decimals = if unit == 0 || value = 100.0 { 0 } else if value = 10.0 { 1 } else { 2 }; Small thing. But a column that jitters between 6.4 GiB and 13.52 GiB looks amateurish in a way you feel before you can name it. One thing I got wrong on the first pass: my loop divided by 1024 while the value was too large, with no guard on the unit table. Feed that a u64::MAX and it walks straight off the end of the array. Now it stops at TiB, with a test that passes u64::MAX specifically. Two things stood out building a second box. Each field gets its own gradient. btop's theme gives used, available, cached and free their own three-stop colour ramps, and I lifted the exact hex values out of btop theme.cpp . Because a Gradient is just a value in my code, colouring a meter is passing a different one in: for label, value, gradient in "Used:", mem.used, theme::MEM USED , "Available:", mem.available, theme::MEM AVAILABLE , "Cached:", mem.cached, theme::MEM CACHED , "Free:", mem.free, theme::MEM FREE , { render value inner, &mut y, label, value, buf ; render meter inner, &mut y, mem.percent value , gradient, buf ; } Immediate mode really pays off here. There is no widget to construct, register, style and later mutate. You are in a loop, you have a Rect , you draw. Four fields, four iterations, done. The meter widget was already written. I built it for the CPU bar on day one, and reusing it for memory took zero changes. That is the nice consequence of ratatui's Widget trait being tiny: a widget is a struct plus one render method, so a generic one you wrote yesterday just works today. I did briefly add a Meter::braille method to make the memory bars look different. Then I noticed it returned self completely unchanged, doing nothing but making the call site read fancier. Deleted it. A method that lies about doing something is worse than no method. Day one had everything in one ui.rs . That was fine for one box and immediately wrong for two. flowchart TD M "ui/mod.rs