With THP 'always' journald's mmapped journal files sit in 2 MiB folios, so each small journal write dirties and writes back 2 MiB; on the control node that saturated the root mirror holding the bus store and the store, and the controller lost its lease. The module sets the mode from a per-machine setting (default always, the kernel's own, so assigning it changes nothing until a machine sets madvise), verifies it took, rotates the journal once so new files are mapped under the new mode, gives back what it found when stopped, and serves kernel_huge_pages to measure file_thp per unit.
114 lines
4.9 KiB
Go
114 lines
4.9 KiB
Go
package main
|
|
|
|
// Over a stand-in tree holding novox's own files of 2026-10-14 23:25 (read through node-reads for
|
|
// novox/hq issue 746): the mode, the per-size controls, meminfo's huge page lines, and fail2ban's and
|
|
// journald's memory.stat and io.stat, cut to the lines the tool reads.
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func tree(t *testing.T, files map[string]string) *Machine {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
for p, content := range files {
|
|
full := filepath.Join(root, p)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return &Machine{Root: root}
|
|
}
|
|
|
|
func novox(t *testing.T) *Machine {
|
|
thp := "sys/kernel/mm/transparent_hugepage/"
|
|
cg := "sys/fs/cgroup/system.slice/"
|
|
files := map[string]string{
|
|
thp + "enabled": "[always] madvise never\n",
|
|
thp + "defrag": "always defer defer+madvise [madvise] never\n",
|
|
thp + "shmem_enabled": "always within_size [advise] never deny force\n",
|
|
thp + "hugepages-2048kB/enabled": "always [inherit] madvise never\n",
|
|
thp + "hugepages-64kB/enabled": "always inherit madvise [never]\n",
|
|
thp + "hugepages-1024kB/enabled": "always inherit madvise [never]\n",
|
|
"proc/meminfo": "MemTotal: 65748808 kB\nAnonHugePages: 3579904 kB\nShmemHugePages: 4096 kB\n" +
|
|
"ShmemPmdMapped: 4096 kB\nFileHugePages: 1693696 kB\nFilePmdMapped: 882688 kB\nHugepagesize: 2048 kB\n",
|
|
cg + "fail2ban.service/memory.stat": "anon 21815296\nfile 204623872\nfile_mapped 130736128\nfile_dirty 27262976\n" +
|
|
"file_writeback 4096\nanon_thp 0\nfile_thp 197132288\nthp_fault_alloc 0\nthp_collapse_alloc 550\n",
|
|
cg + "fail2ban.service/io.stat": "9:0 \n8:16 rbytes=3223552 wbytes=67723415552 rios=240 wios=281159 dbytes=0 dios=0\n" +
|
|
"8:0 rbytes=6152192 wbytes=67723415552 rios=832 wios=281159 dbytes=0 dios=0\n" +
|
|
"9:2 rbytes=9375744 wbytes=67723415552 rios=1072 wios=281159 dbytes=0 dios=0\n",
|
|
cg + "systemd-journald.service/memory.stat": "anon 3227648\nfile 1032015872\nfile_mapped 131928064\nfile_dirty 622592\n" +
|
|
"file_writeback 159744\nanon_thp 2097152\nfile_thp 379584512\nthp_fault_alloc 0\nthp_collapse_alloc 11741\n",
|
|
cg + "systemd-journald.service/io.stat": "9:0 \n8:16 rbytes=55209984 wbytes=2498768035840 rios=1317 wios=24719004 dbytes=0 dios=0\n" +
|
|
"8:0 rbytes=72953856 wbytes=2498768039936 rios=2194 wios=24719005 dbytes=0 dios=0\n" +
|
|
"9:2 rbytes=128163840 wbytes=2498724843520 rios=3511 wios=24707873 dbytes=0 dios=0\n",
|
|
cg + "sshd.service/memory.stat": "anon 1000\nfile_thp 0\n",
|
|
}
|
|
return tree(t, files)
|
|
}
|
|
|
|
func TestNovoxBeforeTheChange(t *testing.T) {
|
|
a, err := novox(t).HugePages(10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Mode != "always" || a.Defrag != "madvise" || a.Shmem != "advise" {
|
|
t.Fatalf("modes: %+v", a)
|
|
}
|
|
if len(a.PerSize) != 3 || a.PerSize[0].Size != "64kB" || a.PerSize[2].Size != "2048kB" ||
|
|
a.PerSize[2].Enabled != "inherit" || a.PerSize[2].Means != "always" || a.PerSize[0].Means != "never" {
|
|
t.Fatalf("per size: %+v", a.PerSize)
|
|
}
|
|
if a.Meminfo["FileHugePages"] != 1654 || a.Meminfo["FilePmdMapped"] != 862 {
|
|
t.Fatalf("meminfo: %v", a.Meminfo)
|
|
}
|
|
if len(a.Units) != 2 || a.Units[0].Cgroup != "system.slice/systemd-journald.service" ||
|
|
a.Units[1].Cgroup != "system.slice/fail2ban.service" {
|
|
t.Fatalf("units: %+v", a.Units)
|
|
}
|
|
// The mirror and its two members each count the write: the most any one saw, never their sum.
|
|
if a.Units[1].Written != 67723415552 || a.Units[1].FileTHP != 197132288 || a.Units[1].CollapsedByKhug != 550 {
|
|
t.Fatalf("fail2ban: %+v", a.Units[1])
|
|
}
|
|
said := strings.Join(a.Said, "\n")
|
|
for _, want := range []string{"transparent huge pages: always, defrag madvise", "sizes not never: 2048kB always",
|
|
"file 1654 MiB (mapped at 2 MiB 862 MiB)", "system.slice/fail2ban.service: file in huge folios 188.0 MiB"} {
|
|
if !strings.Contains(said, want) {
|
|
t.Errorf("said lacks %q:\n%s", want, said)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnitsAreBoundedAndNoneIsSaid(t *testing.T) {
|
|
a, err := novox(t).HugePages(1)
|
|
if err != nil || len(a.Units) != 1 {
|
|
t.Fatalf("%v %+v", err, a)
|
|
}
|
|
b, err := tree(t, map[string]string{"sys/kernel/mm/transparent_hugepage/enabled": "always [madvise] never\n"}).HugePages(10)
|
|
if err != nil || b.Mode != "madvise" || len(b.Units) != 0 || !strings.Contains(strings.Join(b.Said, "\n"), "no unit holds") {
|
|
t.Fatalf("%v %+v", err, b)
|
|
}
|
|
}
|
|
|
|
func TestAKernelWithoutHugePagesIsAnError(t *testing.T) {
|
|
if _, err := tree(t, map[string]string{"proc/meminfo": ""}).HugePages(10); err == nil ||
|
|
!strings.Contains(err.Error(), "built without transparent huge pages") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheArgumentIsBounded(t *testing.T) {
|
|
if _, err := whole(map[string]any{"units": -1.0}, "units", 10, 0, 100); err == nil {
|
|
t.Error("a negative count was accepted")
|
|
}
|
|
if n, _ := whole(map[string]any{"units": 1000.0}, "units", 10, 0, 100); n != 100 {
|
|
t.Errorf("not bounded: %d", n)
|
|
}
|
|
}
|