Skip to content

Commit a5b6f10

Browse files
authored
Add support for memory detection on Windows (#159)
* feat: add windows memory I have tested locally and I'm unaware of a way to do this without `unsafe` as the native Go libraries do not support it. * fix: remove redundant tests
1 parent 4501d83 commit a5b6f10

File tree

3 files changed

+143
-18
lines changed

3 files changed

+143
-18
lines changed

.github/workflows/go.yml

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
timeout-minutes: 15
1616
strategy:
1717
matrix:
18-
os: [ubuntu-latest, macos-latest]
18+
os: [ubuntu-latest, macos-latest, windows-latest]
1919
steps:
2020
- uses: actions/checkout@v5
2121

@@ -32,37 +32,30 @@ jobs:
3232
continue-on-error: true
3333
run: go test -c -o tests && for test in $(go test -list . | grep -E "^(Test|Example)"); do ./tests -test.run "^$test\$" &>/dev/null && echo -e "$test passed\n" || echo -e "$test failed\n"; done
3434

35-
- name: Test (Full Suite)
35+
- name: Test (Full Suite including spooledtempfile)
3636
if: matrix.os == 'ubuntu-latest'
3737
run: go test -race -v ./...
3838

39-
- name: Test (spooledtempfile only)
39+
- name: Test (spooledtempfile only for macos)
4040
if: matrix.os == 'macos-latest'
4141
run: go test -race -v ./pkg/spooledtempfile/...
42+
43+
- name: Test (spooledtempfile only for windows)
44+
if: matrix.os == 'windows-latest'
45+
run: go test -race -v ./pkg/spooledtempfile/...
4246

4347
- name: Benchmarks
4448
if: matrix.os == 'ubuntu-latest'
4549
run: go test -bench=. -benchmem -run=^$ ./...
4650

47-
# Platform-specific test verification
48-
- name: Test Linux-specific memory implementation
49-
if: matrix.os == 'ubuntu-latest'
50-
run: |
51-
echo "Running Linux-specific memory tests..."
52-
cd pkg/spooledtempfile
53-
go test -v -run "TestCgroup|TestHostMeminfo|TestRead"
54-
55-
- name: Test macOS-specific memory implementation
56-
if: matrix.os == 'macos-latest'
57-
run: |
58-
echo "Running macOS-specific memory tests..."
59-
cd pkg/spooledtempfile
60-
go test -v -run "TestGetSystemMemoryUsedFraction|TestSysctlMemoryValues|TestMemoryFractionConsistency"
61-
6251
# Cross-compilation verification
6352
- name: Cross-compile for macOS (from Linux)
6453
if: matrix.os == 'ubuntu-latest'
6554
run: GOOS=darwin GOARCH=amd64 go build ./...
55+
56+
- name: Cross-compile for windows (from Linux)
57+
if: matrix.os == 'ubuntu-latest'
58+
run: GOOS=windows GOARCH=amd64 go build ./...
6659

6760
- name: Cross-compile for Linux (from macOS)
6861
if: matrix.os == 'macos-latest'
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//go:build windows
2+
3+
package spooledtempfile
4+
5+
import (
6+
"fmt"
7+
"unsafe"
8+
9+
"golang.org/x/sys/windows"
10+
)
11+
12+
// globalMemoryStatusEx calls the Windows API function to retrieve memory status.
13+
// This is not currently implemented by the Golang native Windows libraries.
14+
func globalMemoryStatusEx() (totalPhys, availPhys uint64, err error) {
15+
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
16+
proc := kernel32.NewProc("GlobalMemoryStatusEx")
17+
18+
// Define the MEMORYSTATUSEX structure matching the Windows API
19+
// See: https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-memorystatusex
20+
type memoryStatusEx struct {
21+
dwLength uint32
22+
dwMemoryLoad uint32
23+
ullTotalPhys uint64
24+
ullAvailPhys uint64
25+
ullTotalPageFile uint64
26+
ullAvailPageFile uint64
27+
ullTotalVirtual uint64
28+
ullAvailVirtual uint64
29+
ullAvailExtendedVirtual uint64
30+
}
31+
32+
var memStatus memoryStatusEx
33+
memStatus.dwLength = 64
34+
35+
ret, _, err := proc.Call(uintptr(unsafe.Pointer(&memStatus)))
36+
if ret == 0 {
37+
return 0, 0, fmt.Errorf("GlobalMemoryStatusEx failed: %w", err)
38+
}
39+
40+
return memStatus.ullTotalPhys, memStatus.ullAvailPhys, nil
41+
}
42+
43+
// getSystemMemoryUsedFraction returns the fraction of physical memory currently in use on Windows.
44+
// It uses the GlobalMemoryStatusEx Windows API to query system memory statistics.
45+
var getSystemMemoryUsedFraction = func() (float64, error) {
46+
totalPhys, availPhys, err := globalMemoryStatusEx()
47+
if err != nil {
48+
return 0, err
49+
}
50+
51+
if totalPhys == 0 {
52+
return 0, fmt.Errorf("total physical memory is 0")
53+
}
54+
55+
// Calculate used memory from total and available
56+
usedPhys := totalPhys - availPhys
57+
fraction := float64(usedPhys) / float64(totalPhys)
58+
59+
// Sanity check: fraction should be between 0 and 1
60+
if fraction < 0 || fraction > 1 {
61+
return 0, fmt.Errorf("calculated memory fraction out of range: %v (used: %d, total: %d)",
62+
fraction, usedPhys, totalPhys)
63+
}
64+
65+
return fraction, nil
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//go:build windows
2+
3+
package spooledtempfile
4+
5+
import (
6+
"testing"
7+
)
8+
9+
// TestGetSystemMemoryUsedFraction verifies that the Windows implementation returns a valid memory fraction between 0 and 1.
10+
func TestGetSystemMemoryUsedFraction(t *testing.T) {
11+
fraction, err := getSystemMemoryUsedFraction()
12+
if err != nil {
13+
t.Fatalf("getSystemMemoryUsedFraction() failed: %v", err)
14+
}
15+
16+
if fraction < 0 || fraction > 1 {
17+
t.Fatalf("memory fraction out of range: got %v, want 0.0-1.0", fraction)
18+
}
19+
20+
t.Logf("Current system memory usage: %.2f%%", fraction*100)
21+
}
22+
23+
// TestGlobalMemoryStatusEx verifies that we can successfully retrieve memory values via globalMemoryStatusEx.
24+
func TestGlobalMemoryStatusEx(t *testing.T) {
25+
totalPhys, availPhys, err := globalMemoryStatusEx()
26+
if err != nil {
27+
t.Fatalf("globalMemoryStatusEx failed: %v", err)
28+
}
29+
30+
if totalPhys == 0 {
31+
t.Fatal("total physical memory is 0")
32+
}
33+
34+
t.Logf("Total physical memory: %d bytes (%.2f GB)", totalPhys, float64(totalPhys)/(1024*1024*1024))
35+
t.Logf("Available physical memory: %d bytes (%.2f GB)", availPhys, float64(availPhys)/(1024*1024*1024))
36+
37+
usedPhys := totalPhys - availPhys
38+
t.Logf("Used physical memory: %d bytes (%.2f GB)", usedPhys, float64(usedPhys)/(1024*1024*1024))
39+
40+
usedPercent := float64(usedPhys) / float64(totalPhys) * 100
41+
t.Logf("Memory usage: %.2f%%", usedPercent)
42+
}
43+
44+
// TestMemoryFractionConsistency verifies that multiple calls return consistent values.
45+
func TestMemoryFractionConsistency(t *testing.T) {
46+
const calls = 5
47+
var fractions [calls]float64
48+
49+
for i := 0; i < calls; i++ {
50+
frac, err := getSystemMemoryUsedFraction()
51+
if err != nil {
52+
t.Fatalf("call %d failed: %v", i, err)
53+
}
54+
fractions[i] = frac
55+
}
56+
57+
// Check that all values are within a reasonable range of each other
58+
// Memory usage shouldn't vary wildly between consecutive calls
59+
for i := 1; i < calls; i++ {
60+
diff := fractions[i] - fractions[i-1]
61+
if diff < -0.2 || diff > 0.2 {
62+
t.Errorf("memory fraction changed too much between calls: %v -> %v (diff: %v)",
63+
fractions[i-1], fractions[i], diff)
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)