mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 07:15:01 +03:00
746bdbe9f4
In an LXC container with LXCFS the Overview showed the container's RAM beside the host's swap, the host's CPU topology and a CPU percentage from the host's view. Swap and uptime now come from LXCFS's virtualised files and CPU from the container's cgroup v2 counter, normalised by the visible vCPUs and any quota; container CPU and swap history is kept apart from the earlier host samples. A value the reader cannot measure (the first CPU sample, or a layout it cannot verify such as cgroup v1) is reported as unavailable instead of 0: the dashboard, the node list, node CPU history and the Telegram and Discord uptime show a dash, and nodes keep the flags in a new unavailable_metrics column.
146 lines
6.6 KiB
Go
146 lines
6.6 KiB
Go
//go:build linux
|
|
|
|
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/sys"
|
|
)
|
|
|
|
func TestContainerStatusOnlyOverridesCPUSwapAndUptime(t *testing.T) {
|
|
root := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", root)
|
|
dbtest.InitDB(t, filepath.Join(root, "test.db"))
|
|
sys.SelfRSS()
|
|
proc, cg := filepath.Join(root, "proc"), filepath.Join(root, "cg")
|
|
fixtures := map[string]string{
|
|
filepath.Join(proc, "self/mountinfo"): "1 0 0:92 /proc/meminfo /proc/meminfo rw - fuse.lxcfs lxcfs rw\n2 0 0:92 /proc/cpuinfo /proc/cpuinfo rw - fuse.lxcfs lxcfs rw\n3 0 0:92 /proc/uptime /proc/uptime rw - fuse.lxcfs lxcfs rw\n4 0 0:28 / " + cg + " rw - cgroup2 cgroup rw\n",
|
|
filepath.Join(proc, "1/cgroup"): "0::/init.scope\n",
|
|
filepath.Join(proc, "meminfo"): "MemTotal: 2097152 kB\nMemAvailable: 1966080 kB\nSwapTotal: 524288 kB\nSwapFree: 505384 kB\n",
|
|
filepath.Join(proc, "cpuinfo"): "processor : 0\ncpu cores : 16\nprocessor : 1\ncpu cores : 16\n",
|
|
filepath.Join(proc, "uptime"): "52624.93 52624.93\n",
|
|
filepath.Join(proc, "loadavg"): "1.25 2.50 3.75 1/5 1\n",
|
|
filepath.Join(proc, "diskstats"): "8 0 vda 1 0 2 0 1 0 4 0 0 0 0\n",
|
|
filepath.Join(proc, "stat"): "cpu 100 0 50 1000 0 0 0 0\n",
|
|
filepath.Join(proc, "net/dev"): "Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n",
|
|
filepath.Join(cg, "cpu.stat"): "usage_usec 222772076\n",
|
|
filepath.Join(cg, "cpu.max"): "max 100000\n",
|
|
filepath.Join(cg, "io.stat"): "259:0 rbytes=999999999 wbytes=999999999\n",
|
|
}
|
|
for path, content := range fixtures {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
t.Setenv("HOST_PROC", proc)
|
|
t.Setenv("HOST_SYS", filepath.Join(root, "sys"))
|
|
svc := &ServerService{cachedIPv4: "192.0.2.1", noIPv6: true, cachedCpuSpeedMhz: 5000, lxcMetrics: sys.LXCReader{ProcRoot: proc, CgroupRoot: cg}}
|
|
previous := &Status{T: time.Now().Add(-time.Second)}
|
|
got := svc.GetStatus(previous)
|
|
if got.ResourceScope != "container" || got.CpuCores != 2 || got.LogicalPro != 2 || got.CpuSpeedMhz != 0 || got.Swap.Total != 512<<20 || got.Swap.Current != 19357696 || got.Uptime != 52624 {
|
|
t.Fatalf("incorrect corrected resources: %+v", got)
|
|
}
|
|
if got.Mem.Total != 2<<30 || got.Mem.Current != 128<<20 || !reflect.DeepEqual(got.Loads, []float64{1.25, 2.5, 3.75}) || got.DiskTraffic.Read != 1024 || got.DiskTraffic.Write != 2048 || got.DiskIO.Read == 0 || got.DiskIO.Write == 0 {
|
|
t.Fatalf("unrelated metric path changed: %+v", got)
|
|
}
|
|
if !slices.Equal(got.UnavailableMetrics, []string{"cpu"}) {
|
|
t.Fatalf("unexpected unavailable metrics: %v", got.UnavailableMetrics)
|
|
}
|
|
raw, err := json.Marshal(got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wire map[string]json.RawMessage
|
|
if err = json.Unmarshal(raw, &wire); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"cpu", "cpuCores", "logicalPro", "swap", "uptime", "mem", "loads", "diskIO", "diskTraffic", "resourceScope", "unavailableMetrics"} {
|
|
if _, ok := wire[key]; !ok {
|
|
t.Fatalf("API field lost: %s", key)
|
|
}
|
|
}
|
|
if err = os.WriteFile(filepath.Join(proc, "self/mountinfo"), []byte("1 0 0:1 / / rw - ext4 /dev/root rw\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
native := svc.GetStatus(previous)
|
|
if native.ResourceScope != "" || len(native.UnavailableMetrics) != 0 || native.CpuSpeedMhz != 5000 || native.Mem != got.Mem || !reflect.DeepEqual(native.Loads, got.Loads) || native.DiskTraffic != got.DiskTraffic {
|
|
t.Fatalf("native regression: %+v", native)
|
|
}
|
|
}
|
|
|
|
func TestScopedResourcesLeaveUnrelatedStatusUntouched(t *testing.T) {
|
|
status := &Status{Loads: []float64{1, 2, 3}, CpuSpeedMhz: 0}
|
|
status.Mem.Current, status.Mem.Total = 123, 456
|
|
status.DiskTraffic.Read, status.DiskTraffic.Write = 99, 101
|
|
status.DiskIO.Read, status.DiskIO.Write = 11, 12
|
|
before := *status
|
|
applyLXCStats(status, &sys.LXCStats{Scope: "container", CPUs: 2, SwapUsed: 8, SwapTotal: 512, Uptime: 15})
|
|
if status.Mem != before.Mem || status.DiskTraffic != before.DiskTraffic || status.DiskIO != before.DiskIO || !reflect.DeepEqual(status.Loads, before.Loads) {
|
|
t.Fatalf("unrelated status overwritten: %+v", status)
|
|
}
|
|
}
|
|
|
|
func TestContainerHistorySeparatesOnlyCPUAndSwap(t *testing.T) {
|
|
old := systemMetrics
|
|
systemMetrics = newMetricHistory()
|
|
defer func() { systemMetrics = old }()
|
|
now := time.Now()
|
|
systemMetrics.append("cpu", now, 90)
|
|
systemMetrics.append("swap", now, 62)
|
|
for key, value := range map[string]float64{"mem": 6, "load1": 1.25, "diskRead": 1024, "netUp": 7} {
|
|
systemMetrics.append(key, now, value)
|
|
}
|
|
status := &Status{T: now, ResourceScope: "container", Cpu: 10, Loads: []float64{1.25, 2.5, 3.75}, UnavailableMetrics: []string{"swap"}}
|
|
status.Mem.Total, status.Mem.Current = 100, 6
|
|
status.DiskIO.Read, status.NetIO.Up = 1024, 7
|
|
svc := &ServerService{lastStatus: status}
|
|
svc.AppendStatusSample(now, status)
|
|
cpu := svc.AggregateCpuHistory(2, 60)
|
|
if len(cpu) != 1 || cpu[0]["cpu"] != float64(10) {
|
|
t.Fatalf("CPU history=%v", cpu)
|
|
}
|
|
native := &ServerService{lastStatus: &Status{}}
|
|
if host := native.AggregateCpuHistory(2, 60); len(host) != 1 || host[0]["cpu"] != float64(90) {
|
|
t.Fatalf("legacy CPU changed: %v", host)
|
|
}
|
|
if points := svc.AggregateSystemMetric("swap", 2, 60); len(points) != 0 {
|
|
t.Fatalf("missing swap became zero: %v", points)
|
|
}
|
|
for key, want := range map[string]float64{"mem": 6, "load1": 1.25, "diskRead": 1024, "netUp": 7} {
|
|
if svc.systemMetricKey(key) != key {
|
|
t.Fatalf("unrelated history renamed: %s", key)
|
|
}
|
|
points := svc.AggregateSystemMetric(key, 2, 60)
|
|
if len(points) != 1 || points[0]["v"] != want {
|
|
t.Fatalf("unrelated %s history=%v", key, points)
|
|
}
|
|
}
|
|
status.UnavailableMetrics = nil
|
|
svc.AppendStatusSample(now, status)
|
|
if points := svc.AggregateSystemMetric("swap", 2, 60); len(points) != 1 || points[0]["v"] != float64(0) {
|
|
t.Fatalf("disabled swap lost: %v", points)
|
|
}
|
|
snapshot := systemMetrics.snapshot()
|
|
for _, key := range []string{"cpu", "swap", containerHistoryPrefix + "cpu", containerHistoryPrefix + "swap"} {
|
|
if _, ok := snapshot[key]; !ok {
|
|
t.Fatalf("missing preserved series: %s", key)
|
|
}
|
|
}
|
|
for key := range snapshot {
|
|
if len(key) > len(containerHistoryPrefix) && key[:len(containerHistoryPrefix)] == containerHistoryPrefix && key != containerHistoryPrefix+"cpu" && key != containerHistoryPrefix+"swap" {
|
|
t.Fatalf("unexpected scoped metric: %s", key)
|
|
}
|
|
}
|
|
}
|