Skip to content

Commit 1a3cb19

Browse files
authored
fix(agent): report RAM usage excluding reclaimable cache (#513) (#516)
Co-authored-by: gnacho <hnacho@proton.me>
1 parent c6df3a1 commit 1a3cb19

8 files changed

Lines changed: 151 additions & 12 deletions

File tree

‎agent/probe/local.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ func (p *Prober) probeSystem(ctx context.Context) *SystemData {
154154
if out := p.runBest(ctx, CmdUbusSystemInfo, 0); out != "" {
155155
var si SysInfo
156156
if err := json.Unmarshal([]byte(out), &si); err == nil {
157+
// ubus system info no expone la caché de ficheros (Cached), que
158+
// la fórmula de uso clásica (total-free-buffers-cached) necesita
159+
// para no contar RAM reclamable como usada (#513). /proc/meminfo
160+
// local la da; fallback silencioso si no está (host sin proc).
161+
if cached := readMeminfoCachedKB(); cached > 0 {
162+
si.Memory.Cached = float64(cached) * 1024 // ubus va en bytes
163+
}
157164
sd.SysInfo = &si
158165
any = true
159166
}

‎agent/probe/meminfo.go‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// meminfo.go — lectura local de /proc/meminfo en el agente (#513).
2+
//
3+
// ubus system info no expone la caché de ficheros; la fórmula de uso clásica
4+
// (usado = total - free - buffers - cached) la necesita para no contar RAM
5+
// reclamable como consumida. El agente corre EN el router: /proc/meminfo
6+
// siempre está. Fuera de OpenWrt (hosts de desarrollo/tests) la lectura
7+
// falla y el agente simplemente no rellena Cached (fallback del servidor).
8+
package probe
9+
10+
import (
11+
"os"
12+
"strconv"
13+
"strings"
14+
)
15+
16+
// readMeminfoCachedKB devuelve el campo Cached de /proc/meminfo en kB, o 0
17+
// si el fichero no existe o no se puede leer.
18+
func readMeminfoCachedKB() int64 {
19+
data, err := os.ReadFile("/proc/meminfo")
20+
if err != nil {
21+
return 0
22+
}
23+
return parseMeminfoCachedKB(string(data))
24+
}
25+
26+
// parseMeminfoCachedKB extrae "Cached: N kB" del contenido de /proc/meminfo.
27+
func parseMeminfoCachedKB(data string) int64 {
28+
for _, line := range strings.Split(data, "\n") {
29+
if !strings.HasPrefix(line, "Cached:") {
30+
continue
31+
}
32+
fields := strings.Fields(line)
33+
if len(fields) >= 2 {
34+
if n, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
35+
return n
36+
}
37+
}
38+
}
39+
return 0
40+
}

‎agent/probe/meminfo_test.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package probe
2+
3+
import "testing"
4+
5+
func TestParseMeminfoCachedKB(t *testing.T) {
6+
fixture := `MemTotal: 414264 kB
7+
MemFree: 45780 kB
8+
MemAvailable: 88088 kB
9+
Buffers: 0 kB
10+
Cached: 90464 kB
11+
SwapCached: 0 kB
12+
SReclaimable: 14356 kB
13+
`
14+
if got := parseMeminfoCachedKB(fixture); got != 90464 {
15+
t.Fatalf("Cached: got %d, esperado 90464", got)
16+
}
17+
if got := parseMeminfoCachedKB("MemTotal: 1 kB\n"); got != 0 {
18+
t.Fatalf("sin Cached debería dar 0, got %d", got)
19+
}
20+
if got := parseMeminfoCachedKB("Cached: 123 kB"); got != 123 {
21+
t.Fatalf("solo línea Cached: %d", got)
22+
}
23+
}

‎agent/probe/probe.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ type SysInfo struct {
135135
Free float64 `json:"free"`
136136
Buffered float64 `json:"buffered"`
137137
Available float64 `json:"available"`
138+
// Cached: caché de ficheros (Cached de /proc/meminfo). ubus system
139+
// info no la trae; el agente local la rellena para que el servidor
140+
// calcule el uso excluyendo RAM reclamable (#513).
141+
Cached float64 `json:"cached,omitempty"`
138142
} `json:"memory"`
139143
}
140144

‎server-go/internal/adapters/agent.go‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ package adapters
1212

1313
import (
1414
"fmt"
15-
"math"
1615
"strings"
1716
"sync"
1817
"time"
@@ -515,11 +514,7 @@ func (l *Live) polledFromAgent(cfg RouterConfig, p *probe.Payload) *routerPolled
515514
}
516515
mem := sysInfo.Memory
517516
if mem.Total > 0 {
518-
avail := mem.Available
519-
if avail == 0 {
520-
avail = mem.Free + mem.Buffered
521-
}
522-
ramPct = int(math.Round((mem.Total - avail) / mem.Total * 100))
517+
ramPct = memUsagePct(mem.Total, mem.Free, mem.Buffered, mem.Cached, mem.Available)
523518
}
524519
out.sysInfo = sysInfo
525520
out.ram = ramPct

‎server-go/internal/adapters/live.go‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -935,15 +935,12 @@ func (l *Live) pollRouter(ctx context.Context, cfg RouterConfig) (*routerPolled,
935935
l.extrasCache[cfg.ID] = &extrasSnapshot{ports: portsGood, radios: radiosGood, wireless: wirelessGood, fdb: fdbGood, luci: luciGood, vlans: vlansGood}
936936
l.mu.Unlock()
937937

938-
// Uso real de RAM como en la UI del router: used = total − available
938+
// Uso de RAM: #513 (con Cached del agente, fórmula clásica sin caché
939+
// reclamable; sin Cached, MemAvailable). Ver memUsagePct.
939940
mem := sysInfo.Memory
940941
ramPct := 0
941942
if mem.Total > 0 {
942-
avail := mem.Available
943-
if avail == 0 {
944-
avail = mem.Free + mem.Buffered
945-
}
946-
ramPct = int(math.Round((mem.Total - avail) / mem.Total * 100))
943+
ramPct = memUsagePct(mem.Total, mem.Free, mem.Buffered, mem.Cached, mem.Available)
947944
}
948945

949946
isGw := gw != nil && cfg.ID == gw.ID

‎server-go/internal/adapters/mem.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// mem.go — cálculo del uso de RAM de un router a partir de los campos de
2+
// memoria (issue #513).
3+
package adapters
4+
5+
import "math"
6+
7+
// memUsagePct devuelve el % de RAM en uso.
8+
//
9+
// Con Cached > 0 (el agente lo rellena desde /proc/meminfo; ubus system info
10+
// no lo da) usa la fórmula clásica que no cuenta la caché de ficheros como
11+
// consumida (paridad LuCI/htop): used = total - free - buffered - cached.
12+
// Sin Cached (payloads viejos o sondeo SSH de ubus) cae a la fórmula previa
13+
// basada en MemAvailable, que en root ubifs/overlay tiende a sobreestimar el
14+
// uso porque el kernel cuenta poca caché como reclamable a corto plazo.
15+
func memUsagePct(total, free, buffered, cached, available float64) int {
16+
if total <= 0 {
17+
return 0
18+
}
19+
if cached > 0 {
20+
used := total - free - buffered - cached
21+
if used < 0 {
22+
used = 0
23+
}
24+
return int(math.Round(used / total * 100))
25+
}
26+
avail := available
27+
if avail == 0 {
28+
avail = free + buffered
29+
}
30+
used := total - avail
31+
if used < 0 {
32+
used = 0
33+
}
34+
return int(math.Round(used / total * 100))
35+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// mem_test.go — contraste de memUsagePct con mediciones reales (issue #513).
2+
package adapters
3+
4+
import "testing"
5+
6+
func TestMemUsagePct(t *testing.T) {
7+
cases := []struct {
8+
name string
9+
total, free, buff, cach, av float64
10+
want int
11+
}{
12+
{
13+
// rt3 AX6 real: con Cached (fórmula clásica) baja del 79% al 67%.
14+
name: "rt3 con cached", total: 414264, free: 45780, buff: 0, cach: 90464, av: 88088, want: 67,
15+
},
16+
{
17+
// Sin Cached (payload viejo/ubus): se mantiene el comportamiento
18+
// anterior basado en MemAvailable.
19+
name: "rt3 fallback available", total: 414264, free: 45780, buff: 0, cach: 0, av: 88088, want: 79,
20+
},
21+
{
22+
// rt4 AX6: 60% con la fórmula previa; la clásica baja.
23+
name: "rt4 con cached", total: 414264, free: 120000, buff: 0, cach: 48876, av: 165268, want: 59,
24+
},
25+
{
26+
// Sin available, cae a free+buffered.
27+
name: "sin available", total: 1000, free: 100, buff: 50, cach: 0, av: 0, want: 85,
28+
},
29+
{name: "cached mayor que el resto", total: 100, free: 0, buff: 0, cach: 500, av: 0, want: 0},
30+
{name: "total cero", total: 0, free: 0, buff: 0, cach: 0, av: 0, want: 0},
31+
}
32+
for _, tc := range cases {
33+
got := memUsagePct(tc.total, tc.free, tc.buff, tc.cach, tc.av)
34+
if got != tc.want {
35+
t.Errorf("%s: got %d, want %d", tc.name, got, tc.want)
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)