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).
146 lines
4.7 KiB
Go
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
|
|
}
|