diff --git a/mem/mem_aix.go b/mem/mem_aix.go index ac2c39dd38..eaded7effa 100644 --- a/mem/mem_aix.go +++ b/mem/mem_aix.go @@ -5,8 +5,6 @@ package mem import ( "context" - - "github.com/shirou/gopsutil/v4/internal/common" ) func VirtualMemory() (*VirtualMemoryStat, error) { @@ -18,5 +16,5 @@ func SwapMemory() (*SwapMemoryStat, error) { } func SwapDevices() ([]*SwapDevice, error) { - return nil, common.ErrNotImplementedError + return SwapDevicesWithContext(context.Background()) } diff --git a/mem/mem_aix_cgo.go b/mem/mem_aix_cgo.go index 2d03dd0c3f..90be98ac10 100644 --- a/mem/mem_aix_cgo.go +++ b/mem/mem_aix_cgo.go @@ -28,6 +28,22 @@ func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { return &ret, nil } +func SwapDevicesWithContext(_ context.Context) ([]*SwapDevice, error) { + ps, err := perfstat.PagingSpaceStat() + if err != nil { + return nil, err + } + var ret []*SwapDevice + for _, p := range ps { + ret = append(ret, &SwapDevice{ + Name: p.Name, + UsedBytes: uint64(p.MBUsed) * 1024 * 1024, + FreeBytes: uint64(p.MBSize-p.MBUsed) * 1024 * 1024, + }) + } + return ret, nil +} + func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { m, err := perfstat.MemoryTotalStat() if err != nil { diff --git a/mem/mem_aix_nocgo.go b/mem/mem_aix_nocgo.go index bc3c0ed3b4..7f97df4746 100644 --- a/mem/mem_aix_nocgo.go +++ b/mem/mem_aix_nocgo.go @@ -5,6 +5,8 @@ package mem import ( "context" + "fmt" + "os" "strconv" "strings" @@ -35,19 +37,91 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { return swap, nil } -func callSVMon(ctx context.Context, virt bool) (*VirtualMemoryStat, *SwapMemoryStat, error) { - out, err := invoke.CommandWithContext(ctx, "svmon", "-G") +func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) { + out, err := invoke.CommandWithContext(ctx, "lsps", "-a") if err != nil { - return nil, nil, err + return nil, err + } + return parseLspsOutput(string(out)) +} + +// parseLspsOutput parses the output of "lsps -a" into SwapDevice entries. +// +// lsps -a output format: +// +// Page Space Physical Volume Volume Group Size %Used Active Auto Type Chksum +// hd6 hdisk6 rootvg 512MB 3 yes yes lv 0 +func parseLspsOutput(output string) ([]*SwapDevice, error) { + var ret []*SwapDevice + for _, line := range strings.Split(output, "\n") { + fields := strings.Fields(line) + if len(fields) < 5 { + continue + } + // Skip header line + if fields[0] == "Page" { + continue + } + + totalBytes, err := parseLspsSize(fields[3]) + if err != nil { + continue + } + + // %Used may be "NaNQ" for NFS paging spaces — treat as 0 + pctUsed, err := strconv.ParseUint(fields[4], 10, 64) + if err != nil { + pctUsed = 0 + } + + usedBytes := totalBytes * pctUsed / 100 + ret = append(ret, &SwapDevice{ + Name: fields[0], + UsedBytes: usedBytes, + FreeBytes: totalBytes - usedBytes, + }) } + return ret, nil +} - pagesize := uint64(4096) +// parseLspsSize parses a size string from lsps output (e.g., "512MB", "4GB", "1TB"). +func parseLspsSize(s string) (uint64, error) { + units := []struct { + suffix string + multiplier uint64 + }{ + {"TB", 1024 * 1024 * 1024 * 1024}, + {"GB", 1024 * 1024 * 1024}, + {"MB", 1024 * 1024}, + } + for _, u := range units { + if strings.HasSuffix(s, u.suffix) { + val, err := strconv.ParseUint(strings.TrimSuffix(s, u.suffix), 10, 64) + if err != nil { + return 0, err + } + return val * u.multiplier, nil + } + } + return 0, fmt.Errorf("unsupported size unit in %q", s) +} + +// parseSVMonPages parses raw "svmon -G" output (page counts) into memory +// and swap statistics. Using raw page counts preserves full precision; +// the page size is provided by the caller (typically os.Getpagesize()). +// +// svmon -G output format: +// +// size inuse free pin virtual mmode +// memory 1048576 544457 504119 383555 425029 Ded +// pg space 131072 3644 +func parseSVMonPages(output string, pagesize uint64, parseMemory bool) (*VirtualMemoryStat, *SwapMemoryStat) { vmem := &VirtualMemoryStat{} swap := &SwapMemoryStat{} - for _, line := range strings.Split(string(out), "\n") { - if virt && strings.HasPrefix(line, "memory") { + for _, line := range strings.Split(output, "\n") { + if parseMemory && strings.HasPrefix(line, "memory") { p := strings.Fields(line) - if len(p) > 2 { + if len(p) > 5 { if t, err := strconv.ParseUint(p[1], 10, 64); err == nil { vmem.Total = t * pagesize } @@ -62,17 +136,82 @@ func callSVMon(ctx context.Context, virt bool) (*VirtualMemoryStat, *SwapMemoryS } } } else if strings.HasPrefix(line, "pg space") { + // "pg space" is two words, so fields split as: pg space total used p := strings.Fields(line) if len(p) > 3 { if t, err := strconv.ParseUint(p[2], 10, 64); err == nil { swap.Total = t * pagesize } if t, err := strconv.ParseUint(p[3], 10, 64); err == nil { - swap.Free = swap.Total - t*pagesize + swapUsed := t * pagesize + swap.Used = swapUsed + swap.Free = swap.Total - swapUsed + if swap.Total > 0 { + swap.UsedPercent = 100 * float64(swap.Used) / float64(swap.Total) + } } } break } } + return vmem, swap +} + +// parseSVMonAvailable extracts the Available memory value from +// "svmon -G -O unit=KB" output. The available column only exists in +// this format — it is a kernel-computed value that includes free pages +// plus reclaimable cache and cannot be derived from raw page counts. +// Returns the value in bytes. +// +// svmon -G -O unit=KB output format: +// +// size inuse free pin virtual available mmode +// memory 4194304 2177848 2016456 1534220 1700132 2126476 Ded +func parseSVMonAvailable(output string) uint64 { + for _, line := range strings.Split(output, "\n") { + if strings.HasPrefix(line, "memory") { + p := strings.Fields(line) + if len(p) > 6 { + if t, err := strconv.ParseUint(p[6], 10, 64); err == nil { + return t * 1024 + } + } + break + } + } + return 0 +} + +// callSVMon collects memory and swap statistics from svmon. +// +// Two separate svmon calls are used: +// 1. "svmon -G" (raw 4KB pages) — provides size, inuse, free, pin, virtual +// for both memory and paging space. Using raw page counts preserves full +// precision; the page size comes from os.Getpagesize(). +// 2. "svmon -G -O unit=KB" — only used when virt=true, to read the +// "available" column which does not exist in the raw page output. +// Available memory includes free pages plus reclaimable cache and is +// computed internally by the AIX kernel. +// +// This two-call approach avoids the precision loss from -O unit=KB (which +// truncates sub-KB fractions) while still providing the real available value. +func callSVMon(ctx context.Context, virt bool) (*VirtualMemoryStat, *SwapMemoryStat, error) { + pagesize := uint64(os.Getpagesize()) + + out, err := invoke.CommandWithContext(ctx, "svmon", "-G") + if err != nil { + return nil, nil, err + } + + vmem, swap := parseSVMonPages(string(out), pagesize, virt) + + // Separate call for the available column, which only exists in unit=KB output. + if virt { + kbOut, err := invoke.CommandWithContext(ctx, "svmon", "-G", "-O", "unit=KB") + if err == nil { + vmem.Available = parseSVMonAvailable(string(kbOut)) + } + } + return vmem, swap, nil } diff --git a/mem/mem_aix_nocgo_test.go b/mem/mem_aix_nocgo_test.go new file mode 100644 index 0000000000..756ca0f114 --- /dev/null +++ b/mem/mem_aix_nocgo_test.go @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build aix && !cgo + +package mem + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseLspsOutput(t *testing.T) { + tests := []struct { + name string + input string + expected []*SwapDevice + }{ + { + name: "MB size", + input: `Page Space Physical Volume Volume Group Size %Used Active Auto Type Chksum +hd6 hdisk6 rootvg 512MB 3 yes yes lv 0 +`, + expected: []*SwapDevice{ + { + Name: "hd6", + UsedBytes: 512 * 1024 * 1024 * 3 / 100, + FreeBytes: 512*1024*1024 - 512*1024*1024*3/100, + }, + }, + }, + { + name: "GB size", + input: `Page Space Physical Volume Volume Group Size %Used Active Auto Type Chksum +paging00 hdisk0 rootvg 4GB 10 yes yes lv 0 +`, + expected: []*SwapDevice{ + { + Name: "paging00", + UsedBytes: 4 * 1024 * 1024 * 1024 * 10 / 100, + FreeBytes: 4*1024*1024*1024 - 4*1024*1024*1024*10/100, + }, + }, + }, + { + name: "NaNQ percent used (NFS paging space)", + input: `Page Space Physical Volume Volume Group Size %Used Active Auto Type Chksum +nfspg - - 256MB NaNQ yes yes nfs 0 +`, + expected: []*SwapDevice{ + { + Name: "nfspg", + UsedBytes: 0, + FreeBytes: 256 * 1024 * 1024, + }, + }, + }, + { + name: "multiple devices mixed units", + input: `Page Space Physical Volume Volume Group Size %Used Active Auto Type Chksum +hd6 hdisk0 rootvg 512MB 5 yes yes lv 0 +paging01 hdisk1 datavg 2GB 20 yes yes lv 0 +`, + expected: []*SwapDevice{ + { + Name: "hd6", + UsedBytes: 512 * 1024 * 1024 * 5 / 100, + FreeBytes: 512*1024*1024 - 512*1024*1024*5/100, + }, + { + Name: "paging01", + UsedBytes: 2 * 1024 * 1024 * 1024 * 20 / 100, + FreeBytes: 2*1024*1024*1024 - 2*1024*1024*1024*20/100, + }, + }, + }, + { + name: "empty output", + input: "", + expected: nil, + }, + { + name: "header only", + input: `Page Space Physical Volume Volume Group Size %Used Active Auto Type Chksum +`, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + devices, err := parseLspsOutput(tt.input) + require.NoError(t, err) + assert.Equal(t, tt.expected, devices) + }) + } +} + +func TestParseLspsSize(t *testing.T) { + tests := []struct { + input string + expected uint64 + wantErr bool + }{ + {"512MB", 512 * 1024 * 1024, false}, + {"4GB", 4 * 1024 * 1024 * 1024, false}, + {"1TB", 1024 * 1024 * 1024 * 1024, false}, + {"0MB", 0, false}, + {"notasize", 0, true}, + {"MB", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := parseLspsSize(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +// Sample svmon -G output (raw pages, 4KB page size) +const testSVMonPages = ` size inuse free pin virtual mmode +memory 1048576 544457 504119 383555 425029 Ded +pg space 131072 3644 + + work pers clnt other +pin 275224 0 4203 104128 +in use 425029 0 119428 +` + +// Sample svmon -G -O unit=KB output +const testSVMonKB = `Unit: KB +-------------------------------------------------------------------------------------- + size inuse free pin virtual available mmode +memory 4194304 2177848 2016456 1534220 1700132 2126476 Ded +pg space 524288 14576 +` + +func TestParseSVMonPages(t *testing.T) { + pagesize := uint64(4096) + vmem, swap := parseSVMonPages(testSVMonPages, pagesize, true) + + // memory line: 1048576 pages * 4096 = 4294967296 bytes (4 GB) + assert.Equal(t, uint64(1048576*4096), vmem.Total) + assert.Equal(t, uint64(544457*4096), vmem.Used) + assert.Equal(t, uint64(504119*4096), vmem.Free) + assert.InDelta(t, 51.92, vmem.UsedPercent, 0.1) + // Available is not in raw page output — stays zero + assert.Equal(t, uint64(0), vmem.Available) + + // pg space: 131072 pages * 4096 = 536870912 bytes (512 MB) + assert.Equal(t, uint64(131072*4096), swap.Total) + assert.Equal(t, uint64(3644*4096), swap.Used) + assert.Equal(t, swap.Total-swap.Used, swap.Free) + assert.InDelta(t, 2.78, swap.UsedPercent, 0.1) +} + +func TestParseSVMonPagesSwapOnly(t *testing.T) { + pagesize := uint64(4096) + vmem, swap := parseSVMonPages(testSVMonPages, pagesize, false) + + // Memory fields should be zero when parseMemory=false + assert.Equal(t, uint64(0), vmem.Total) + + // Swap should still be parsed + assert.Equal(t, uint64(131072*4096), swap.Total) + assert.Equal(t, uint64(3644*4096), swap.Used) +} + +func TestParseSVMonAvailable(t *testing.T) { + available := parseSVMonAvailable(testSVMonKB) + // 2126476 KB * 1024 = 2177511424 bytes + assert.Equal(t, uint64(2126476*1024), available) +} + +func TestParseSVMonAvailableEmpty(t *testing.T) { + available := parseSVMonAvailable("") + assert.Equal(t, uint64(0), available) +} diff --git a/mem/mem_test.go b/mem/mem_test.go index 6e1c72bad2..ad8974829a 100644 --- a/mem/mem_test.go +++ b/mem/mem_test.go @@ -50,7 +50,9 @@ func TestVirtualMemory(t *testing.T) { "Total should be computable (%v): %v", totalStr, v) assert.True(t, runtime.GOOS == "windows" || v.Free > 0) - assert.Truef(t, runtime.GOOS == "windows" || v.Available > v.Free, + // On AIX, Available is typically equal to Free + // On other systems, Available should be >= Free + assert.Truef(t, runtime.GOOS == "windows" || v.Available >= v.Free, "Free should be a subset of Available: %v", v) inDelta := assert.InDelta