From ba626aac5078850cfa494493a271c19c13345ccf Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 12 Mar 2026 15:29:22 +0100 Subject: [PATCH] Use dynamic GOMEMLIMIT: 25% of system RAM, clamped to [2GB, 8GB] Replace static 2GB default with platform-aware auto-detection: - Linux: syscall.Sysinfo - macOS: sysctl hw.memsize - Windows: GlobalMemoryStatusEx Falls back to 4GB if detection fails. User config still overrides. Prevents both OOM kills (upper clamp) and excessive GC pressure (lower clamp). --- cmd/codebase-memory-mcp/main.go | 17 +++++----- cmd/codebase-memory-mcp/memlimit.go | 30 ++++++++++++++++++ cmd/codebase-memory-mcp/memlimit_darwin.go | 14 +++++++++ cmd/codebase-memory-mcp/memlimit_linux.go | 14 +++++++++ cmd/codebase-memory-mcp/memlimit_windows.go | 35 +++++++++++++++++++++ 5 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 cmd/codebase-memory-mcp/memlimit.go create mode 100644 cmd/codebase-memory-mcp/memlimit_darwin.go create mode 100644 cmd/codebase-memory-mcp/memlimit_linux.go create mode 100644 cmd/codebase-memory-mcp/memlimit_windows.go diff --git a/cmd/codebase-memory-mcp/main.go b/cmd/codebase-memory-mcp/main.go index 2aa4d2434..894b67483 100644 --- a/cmd/codebase-memory-mcp/main.go +++ b/cmd/codebase-memory-mcp/main.go @@ -67,14 +67,17 @@ func main() { log.Fatalf("config err=%v", err) } - // Apply GOMEMLIMIT from config (e.g. "2G", "512M"), default 2GB. - // Prevents OOM kills on systems without user-configured limits. - memLimitStr := cfg.Get(store.ConfigMemLimit, "2G") - if limit, parseErr := parseByteSize(memLimitStr); parseErr != nil { - log.Printf("config: invalid mem_limit %q: %v", memLimitStr, parseErr) - debug.SetMemoryLimit(2 << 30) // fallback: 2GB + // Apply GOMEMLIMIT from config (e.g. "4G", "512M"). + // If not configured, auto-detect: 25% of system memory, clamped to [2GB, 8GB]. + // Prevents OOM kills while avoiding unnecessary GC pressure. + if memLimitStr := cfg.Get(store.ConfigMemLimit, ""); memLimitStr != "" { + if limit, parseErr := parseByteSize(memLimitStr); parseErr != nil { + log.Printf("config: invalid mem_limit %q: %v", memLimitStr, parseErr) + } else { + debug.SetMemoryLimit(limit) + } } else { - debug.SetMemoryLimit(limit) + debug.SetMemoryLimit(autoMemLimit()) } srv := tools.NewServer(router, tools.WithConfig(cfg)) diff --git a/cmd/codebase-memory-mcp/memlimit.go b/cmd/codebase-memory-mcp/memlimit.go new file mode 100644 index 000000000..279fa9181 --- /dev/null +++ b/cmd/codebase-memory-mcp/memlimit.go @@ -0,0 +1,30 @@ +package main + +import "log" + +const ( + minMemLimit int64 = 2 << 30 // 2 GB + maxMemLimit int64 = 8 << 30 // 8 GB + memFraction int64 = 4 // use 1/4 of system memory +) + +// autoMemLimit returns a GOMEMLIMIT value based on available system memory. +// Uses 25% of total physical RAM, clamped to [2GB, 8GB]. +// Falls back to 4GB if detection fails. +func autoMemLimit() int64 { + total := totalSystemMemory() + if total <= 0 { + log.Printf("mem_limit: auto-detect unavailable, default=4GB") + return 4 << 30 + } + + limit := total / memFraction + if limit < minMemLimit { + limit = minMemLimit + } + if limit > maxMemLimit { + limit = maxMemLimit + } + log.Printf("mem_limit: system=%dGB limit=%dGB", total>>30, limit>>30) + return limit +} diff --git a/cmd/codebase-memory-mcp/memlimit_darwin.go b/cmd/codebase-memory-mcp/memlimit_darwin.go new file mode 100644 index 000000000..eb9f722ab --- /dev/null +++ b/cmd/codebase-memory-mcp/memlimit_darwin.go @@ -0,0 +1,14 @@ +//go:build darwin + +package main + +import "golang.org/x/sys/unix" + +// totalSystemMemory returns total physical memory in bytes via sysctl hw.memsize. +func totalSystemMemory() int64 { + mem, err := unix.SysctlUint64("hw.memsize") + if err != nil { + return 0 + } + return int64(mem) +} diff --git a/cmd/codebase-memory-mcp/memlimit_linux.go b/cmd/codebase-memory-mcp/memlimit_linux.go new file mode 100644 index 000000000..42041de9b --- /dev/null +++ b/cmd/codebase-memory-mcp/memlimit_linux.go @@ -0,0 +1,14 @@ +//go:build linux + +package main + +import "syscall" + +// totalSystemMemory returns total physical memory in bytes via Sysinfo. +func totalSystemMemory() int64 { + var si syscall.Sysinfo_t + if err := syscall.Sysinfo(&si); err != nil { + return 0 + } + return int64(si.Totalram) * int64(si.Unit) +} diff --git a/cmd/codebase-memory-mcp/memlimit_windows.go b/cmd/codebase-memory-mcp/memlimit_windows.go new file mode 100644 index 000000000..92b8b3718 --- /dev/null +++ b/cmd/codebase-memory-mcp/memlimit_windows.go @@ -0,0 +1,35 @@ +//go:build windows + +package main + +import ( + "syscall" + "unsafe" +) + +// memoryStatusEx mirrors the Windows MEMORYSTATUSEX struct. +type memoryStatusEx struct { + length uint32 + memoryLoad uint32 + totalPhys uint64 + availPhys uint64 + totalPageFile uint64 + availPageFile uint64 + totalVirtual uint64 + availVirtual uint64 + availExtendedVirtual uint64 +} + +// totalSystemMemory returns total physical memory via GlobalMemoryStatusEx. +func totalSystemMemory() int64 { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + proc := kernel32.NewProc("GlobalMemoryStatusEx") + + var ms memoryStatusEx + ms.length = uint32(unsafe.Sizeof(ms)) //nolint:gosec // standard Windows API pattern + r, _, _ := proc.Call(uintptr(unsafe.Pointer(&ms))) + if r == 0 { + return 0 + } + return int64(ms.totalPhys) +}