You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
4.8 KiB
149 lines
4.8 KiB
//go:build !(js && wasm) |
|
|
|
package ratelimit |
|
|
|
import ( |
|
"errors" |
|
"runtime" |
|
|
|
"github.com/pbnjay/memory" |
|
) |
|
|
|
// MinimumMemoryMB is the minimum memory required to run the relay with rate limiting. |
|
const MinimumMemoryMB = 500 |
|
|
|
// AutoDetectMemoryFraction is the fraction of available memory to use when auto-detecting. |
|
const AutoDetectMemoryFraction = 0.66 |
|
|
|
// DefaultMaxMemoryMB is the default maximum memory target when auto-detecting. |
|
// This caps the auto-detected value to ensure optimal performance. |
|
const DefaultMaxMemoryMB = 1500 |
|
|
|
// ErrInsufficientMemory is returned when there isn't enough memory to run the relay. |
|
var ErrInsufficientMemory = errors.New("insufficient memory: relay requires at least 500MB of available memory") |
|
|
|
// ProcessMemoryStats contains memory statistics for the current process. |
|
// On Linux, these are read from /proc/self/status for accurate RSS values. |
|
// On other platforms, these are approximated from Go runtime stats. |
|
type ProcessMemoryStats struct { |
|
// VmRSS is the resident set size (total physical memory in use) in bytes |
|
VmRSS uint64 |
|
// RssShmem is the shared memory portion of RSS in bytes |
|
RssShmem uint64 |
|
// RssAnon is the anonymous (non-shared) memory in bytes |
|
RssAnon uint64 |
|
// VmHWM is the peak RSS (high water mark) in bytes |
|
VmHWM uint64 |
|
} |
|
|
|
// PhysicalMemoryBytes returns the actual physical memory usage (RSS - shared) |
|
func (p ProcessMemoryStats) PhysicalMemoryBytes() uint64 { |
|
if p.VmRSS > p.RssShmem { |
|
return p.VmRSS - p.RssShmem |
|
} |
|
return p.VmRSS |
|
} |
|
|
|
// PhysicalMemoryMB returns the actual physical memory usage in MB |
|
func (p ProcessMemoryStats) PhysicalMemoryMB() uint64 { |
|
return p.PhysicalMemoryBytes() / (1024 * 1024) |
|
} |
|
|
|
// DetectAvailableMemoryMB returns the available system memory in megabytes. |
|
// On Linux, this returns the actual available memory (free + cached). |
|
// On other systems, it returns total memory minus the Go runtime's current usage. |
|
func DetectAvailableMemoryMB() uint64 { |
|
// Use pbnjay/memory for cross-platform memory detection |
|
available := memory.FreeMemory() |
|
if available == 0 { |
|
// Fallback: use total memory |
|
available = memory.TotalMemory() |
|
} |
|
return available / (1024 * 1024) |
|
} |
|
|
|
// DetectTotalMemoryMB returns the total system memory in megabytes. |
|
func DetectTotalMemoryMB() uint64 { |
|
return memory.TotalMemory() / (1024 * 1024) |
|
} |
|
|
|
// CalculateTargetMemoryMB calculates the target memory limit based on configuration. |
|
// If configuredMB is 0, it auto-detects based on available memory (66% of available, capped at 1.5GB). |
|
// If configuredMB is non-zero, it validates that it's achievable. |
|
// Returns an error if there isn't enough memory. |
|
func CalculateTargetMemoryMB(configuredMB int) (int, error) { |
|
availableMB := int(DetectAvailableMemoryMB()) |
|
|
|
// If configured to auto-detect (0), calculate target |
|
if configuredMB == 0 { |
|
// First check if we have minimum available memory |
|
if availableMB < MinimumMemoryMB { |
|
return 0, ErrInsufficientMemory |
|
} |
|
|
|
// Calculate 66% of available |
|
targetMB := int(float64(availableMB) * AutoDetectMemoryFraction) |
|
|
|
// If 66% is less than minimum, use minimum (we've already verified we have enough) |
|
if targetMB < MinimumMemoryMB { |
|
targetMB = MinimumMemoryMB |
|
} |
|
|
|
// Cap at default maximum for optimal performance |
|
if targetMB > DefaultMaxMemoryMB { |
|
targetMB = DefaultMaxMemoryMB |
|
} |
|
|
|
return targetMB, nil |
|
} |
|
|
|
// If explicitly configured, validate it's achievable |
|
if configuredMB < MinimumMemoryMB { |
|
return 0, ErrInsufficientMemory |
|
} |
|
|
|
// Warn but allow if configured target exceeds available |
|
// (the PID controller will throttle as needed) |
|
return configuredMB, nil |
|
} |
|
|
|
// GetMemoryStats returns current memory statistics for logging. |
|
type MemoryStats struct { |
|
TotalMB uint64 |
|
AvailableMB uint64 |
|
TargetMB int |
|
GoAllocatedMB uint64 |
|
GoSysMB uint64 |
|
} |
|
|
|
// GetMemoryStats returns current memory statistics. |
|
func GetMemoryStats(targetMB int) MemoryStats { |
|
var m runtime.MemStats |
|
runtime.ReadMemStats(&m) |
|
|
|
return MemoryStats{ |
|
TotalMB: DetectTotalMemoryMB(), |
|
AvailableMB: DetectAvailableMemoryMB(), |
|
TargetMB: targetMB, |
|
GoAllocatedMB: m.Alloc / (1024 * 1024), |
|
GoSysMB: m.Sys / (1024 * 1024), |
|
} |
|
} |
|
|
|
// readProcessMemoryStatsFallback returns memory stats using Go runtime. |
|
// This is used on non-Linux platforms or when /proc is unavailable. |
|
// The values are approximations and may not accurately reflect OS-level metrics. |
|
func readProcessMemoryStatsFallback() ProcessMemoryStats { |
|
var m runtime.MemStats |
|
runtime.ReadMemStats(&m) |
|
|
|
// Use Sys as an approximation of RSS (includes all memory from OS) |
|
// HeapAlloc approximates anonymous memory (live heap objects) |
|
// We cannot determine shared memory from Go runtime, so leave it at 0 |
|
return ProcessMemoryStats{ |
|
VmRSS: m.Sys, |
|
RssAnon: m.HeapAlloc, |
|
RssShmem: 0, // Cannot determine shared memory from Go runtime |
|
VmHWM: 0, // Not available from Go runtime |
|
} |
|
}
|
|
|