# ratatop day 2: the memory box, and the lie in `free -h`

> Source: <https://dev.to/lovestaco/ratatop-day-2-the-memory-box-and-the-lie-in-free-h-h67>
> Published: 2026-07-29 22:08:04+00:00

*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<br/>splits the terminal, paints background"]
    M --> C["ui/cpu.rs<br/>bar, cores, load"]
    M --> Mem["ui/mem.rs<br/>values, meters, formatting"]
    C --> W["widgets/<br/>BrailleGraph, Meter"]
    Mem --> W
    C --> Col["collect/cpu.rs"]
    Mem --> Col2["collect/mem.rs"]
```

The useful change was moving layout decisions *up*.

Previously `main.rs`

computed the box rectangle itself, which was fine when there was exactly one box and completely wrong the moment memory needed the leftover height.

Now `main.rs`

hands over the entire terminal and `ui::render`

decides who gets what:

```
let [cpu_area, rest] = Layout::vertical([
    Constraint::Length(cpu_height),
    Constraint::Min(0),
])
.areas(area);
```

CPU takes the top, memory takes a fixed width below it, and the rest stays empty for the network and process boxes.

Adding a box is now a matter of carving another `Rect`

out of `rest`

.

Network, probably. `/proc/net/dev`

is another counter file, so it needs the same delta treatment the CPU box uses, plus auto-scaling units because network speeds swing across five orders of magnitude.

Then processes, which is the big one. That is where `/proc/[pid]/stat`

lives, and where I suspect I will learn a great deal about how many edge cases a process table has.

Code so far: [lovestaco/ratatop](https://github.com/lovestaco/ratatop).

If you take one thing from this post, make it this: stop looking at `free`

. Look at `available`

. Your machine is fine.

AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:

| [🇩🇰 Dansk](https://github.com/HexmosTech/git-lrc/readme/README.da.md) | [🇪🇸 Español](https://github.com/HexmosTech/git-lrc/readme/README.es.md) | [🇮🇷 Farsi](https://github.com/HexmosTech/git-lrc/readme/README.fa.md) | [🇫🇮 Suomi](https://github.com/HexmosTech/git-lrc/readme/README.fi.md) | [🇯🇵 日本語](https://github.com/HexmosTech/git-lrc/readme/README.ja.md) | [🇳🇴 Norsk](https://github.com/HexmosTech/git-lrc/readme/README.nn.md) | [🇵🇹 Português](https://github.com/HexmosTech/git-lrc/readme/README.pt.md) | [🇷🇺 Русский](https://github.com/HexmosTech/git-lrc/readme/README.ru.md) | [🇦🇱 Shqip](https://github.com/HexmosTech/git-lrc/readme/README.sq.md) | [🇨🇳 中文](https://github.com/HexmosTech/git-lrc/readme/README.zh.md) | [🇮🇳 हिन्दी](https://github.com/HexmosTech/git-lrc/readme/README.hi.md) |

GenAI today is a **race car without brakes**. It accelerates fast -- you describe something, and large blocks of code appear instantly. But AI agents *silently break things*: they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior -- without telling you. You often find out in production.

** git-lrc is your braking system.** It hooks into

`git commit`

and runs an AI review on every diff In short, git-lrc helps **Prevent Outages, Breaches, and Technical Debt Before They Happen**

**At a glance:** [10 risk categories](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · [100+ failure patterns tracked](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · every commit…
