Files
mesh-catalog/modules/memory-pressure/cmd/memory-pressure/tools.go
T
jochen b098b64b22 Phase 3: asus-zephyrus-g14 and memory-pressure, with Go tools and long-running code
The laptop model's hardware module and a memory-pressure module for any
machine (hq research 027/03, 026/05, to-be 42 phase 3). The predecessor's
polling auto-profile and mem-guard user scripts become each module's own
Go code launched by the node runtime (ADR 0198): a profile switcher woken
by the kernel's power-supply uevents, and a guard that warns on RAM, swap
or PSI before systemd-oomd acts, on the desktop over the account's bus and
always as an event. supergfxctl and triggerhappy are kept as found
(research 027 Q1).
2026-10-04 15:42:56 +02:00

146 lines
4.7 KiB
Go

package main
import (
"context"
"fmt"
"math"
"strconv"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Tools is the module's tools over one machine and its guard.
func Tools(m *Machine, g *Guard) []stdio.Tool {
ctx := context.Background
return []stdio.Tool{
{
Name: "memory_status",
Description: "Memory now: RAM total, available and used, swap and each swap device with its priority, the compressed swap in RAM " +
"(zram) and its ratio, and pressure stall (PSI some/full, avg10/60/300) — with the guard's verdict on it.",
Run: func(map[string]any) (any, error) {
s, err := m.Snapshot()
if err != nil {
return nil, err
}
return map[string]any{"memory": s, "verdict": Judge(s)}, nil
},
},
{
Name: "memory_top",
Description: "The largest processes by resident plus swapped memory, with the systemd unit each runs in; by=unit sums them per unit, which is what systemd-oomd chooses among.",
Input: map[string]any{
"limit": map[string]any{"type": "integer", "description": "how many (default 10, at most 50)"},
"by": map[string]any{"type": "string", "enum": []string{"process", "unit"}},
},
Run: func(args map[string]any) (any, error) {
limit, err := bounded(args, "limit", 10, 50)
if err != nil {
return nil, err
}
ps := m.Processes()
switch str(args, "by") {
case "", "process":
return map[string]any{"processes": orEmpty(Top(ps, limit))}, nil
case "unit":
return map[string]any{"units": orEmpty(ByUnit(ps, limit))}, nil
}
return nil, fmt.Errorf("by is process or unit")
},
},
{
Name: "memory_oom_history",
Description: "What was killed for memory, newest first, from the journal: the kernel's OOM killer, systemd-oomd, and units the service manager says lost a process to it.",
Input: map[string]any{
"since": map[string]any{"type": "string", "description": "-7d (default), 12h, today, yesterday or a date like 2026-10-01"},
"limit": map[string]any{"type": "integer", "description": "how many (default 20, at most 100)"},
},
Run: func(args map[string]any) (any, error) {
limit, err := bounded(args, "limit", 20, 100)
if err != nil {
return nil, err
}
since := str(args, "since")
if since == "" {
since = "-7d"
}
kills, err := m.OOMHistory(ctx(), since, limit)
if err != nil {
return nil, err
}
return map[string]any{"since": since, "kills": kills}, nil
},
},
{
Name: "memory_zram",
Description: "The compressed swap in RAM: each zram device's algorithm, size, what it stores, what that costs in RAM and the ratio, the generator's configuration, and the swap tunables beside it.",
Run: func(map[string]any) (any, error) {
return map[string]any{
"devices": m.Zram(),
"configuration": m.read("/etc/systemd/zram-generator.conf"),
"vm": map[string]string{
"swappiness": m.read("/proc/sys/vm/swappiness"),
"page-cluster": m.read("/proc/sys/vm/page-cluster"),
},
"note": "a change to the configuration applies at the next boot: swapping the device off to resize it would push what it holds back into RAM",
}, nil
},
},
{
Name: "memory_oomd",
Description: "systemd-oomd: whether it runs, and what it watches — the cgroups, their limits and their pressure, as oomctl reports them.",
Run: func(map[string]any) (any, error) { return m.Oomd(ctx()), nil },
},
{
Name: "memory_guard",
Description: "The module's guard that warns before the machine kills for memory: its thresholds, its last verdict, whether a warning stands, and whether the operator's desktop can be reached.",
Run: func(map[string]any) (any, error) {
if g == nil {
return nil, fmt.Errorf("the guard does not run in this process")
}
return g.Report(), nil
},
},
}
}
func str(args map[string]any, key string) string {
s, _ := args[key].(string)
return strings.TrimSpace(s)
}
// bounded is an integer argument, defaulted, at least 1 and at most most.
func bounded(args map[string]any, key string, fallback, most int) (int, error) {
v, given := args[key]
if !given || v == nil {
return fallback, nil
}
var n int
switch x := v.(type) {
case float64:
if x != math.Trunc(x) {
return 0, fmt.Errorf("%s must be a whole number, not %v", key, x)
}
n = int(x)
case string:
i, err := strconv.Atoi(strings.TrimSpace(x))
if err != nil {
return 0, fmt.Errorf("%s must be a whole number, not %q", key, x)
}
n = i
default:
return 0, fmt.Errorf("%s must be a whole number", key)
}
if n < 1 {
return 0, fmt.Errorf("%s must be at least 1", key)
}
return min(n, most), nil
}
func orEmpty[T any](s []T) []T {
if s == nil {
return []T{}
}
return s
}