Skip to content

Commit acb5507

Browse files
Add Memory.free_size.
1 parent e49483e commit acb5507

File tree

5 files changed

+91
-1
lines changed

5 files changed

+91
-1
lines changed

lib/process/metrics/memory.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ def self.zero
3333
self.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
3434
end
3535

36+
# Get free (available) system memory in bytes. Overridden on Linux; returns nil when not available.
37+
# @returns [Integer, nil]
38+
def self.free_size
39+
nil
40+
end
41+
3642
# Whether the memory usage can be captured on this system.
3743
def self.supported?
3844
false

lib/process/metrics/memory/darwin.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ def self.total_size
2626
end
2727
end
2828

29+
# Free (available) memory in bytes. Uses vm_stat: free + inactive pages (reclaimable), matching Linux MemAvailable semantics.
30+
# @returns [Integer, nil] Free memory in bytes, or nil if vm_stat is unavailable.
31+
def self.free_size
32+
output = IO.popen(["vm_stat"], "r", &:read)
33+
page_size = output[/page size of (\d+) bytes/, 1]&.to_i
34+
return nil unless page_size && page_size.positive?
35+
pages_free = output[/Pages free:\s*(\d+)/, 1]&.to_i || 0
36+
pages_inactive = output[/Pages inactive:\s*(\d+)/, 1]&.to_i || 0
37+
(pages_free + pages_inactive) * page_size
38+
end
39+
2940
# Parse a size string from vmmap output into bytes.
3041
# @parameter string [String | Nil] The size string (e.g., "4K", "1.5M", "2G").
3142
# @returns [Integer] The size in bytes.
@@ -116,6 +127,12 @@ def total_size
116127
return Memory::Darwin.total_size
117128
end
118129

130+
# Get free (available) memory size.
131+
# @returns [Integer, nil] Free memory in bytes, or nil if not determinable.
132+
def free_size
133+
return Memory::Darwin.free_size
134+
end
135+
119136
# Capture memory metrics for a process.
120137
# @parameter pid [Integer] The process ID.
121138
# @parameter options [Hash] Additional options (e.g., count for proportional estimates).

lib/process/metrics/memory/linux.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,43 @@ def self.total_size
6969
end
7070
end
7171

72+
# Determine the free (available) memory size in bytes. This is memory that can be used without reclaiming. When in a container, this is limit minus current usage; otherwise it uses MemAvailable from /proc/meminfo (includes reclaimable cache).
73+
#
74+
# @returns [Integer | Nil] The free memory size in bytes, or nil if not determinable.
75+
def self.free_size
76+
# cgroups v2: free = max - current (when max is set):
77+
if File.exist?("/sys/fs/cgroup/memory.max")
78+
limit = File.read("/sys/fs/cgroup/memory.max").strip
79+
if limit != "max" && File.exist?("/sys/fs/cgroup/memory.current")
80+
current = File.read("/sys/fs/cgroup/memory.current").strip.to_i
81+
return [limit.to_i - current, 0].max
82+
end
83+
end
84+
85+
# cgroups v1: free = limit - usage (when limit is set):
86+
if File.exist?("/sys/fs/cgroup/memory/memory.limit_in_bytes") && File.exist?("/sys/fs/cgroup/memory/memory.usage_in_bytes")
87+
limit = File.read("/sys/fs/cgroup/memory/memory.limit_in_bytes").strip.to_i
88+
if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD
89+
usage = File.read("/sys/fs/cgroup/memory/memory.usage_in_bytes").strip.to_i
90+
return [limit - usage, 0].max
91+
end
92+
end
93+
94+
# Fall back to /proc/meminfo: MemAvailable (reclaimable + free) or MemFree:
95+
if File.exist?("/proc/meminfo")
96+
meminfo = File.read("/proc/meminfo")
97+
if /MemAvailable:\s*(?<avail>\d+)\s*kB/ =~ meminfo
98+
return avail.to_i * 1024
99+
end
100+
if /MemFree:\s*(?<free>\d+)\s*kB/ =~ meminfo
101+
return free.to_i * 1024
102+
end
103+
end
104+
105+
# Unknown, return nil:
106+
return nil
107+
end
108+
72109
# The fields that will be extracted from the `smaps` data.
73110
SMAP = {
74111
"Rss" => :resident_size,
@@ -177,6 +214,12 @@ def total_size
177214
return Memory::Linux.total_size
178215
end
179216

217+
# Get free (available) memory size.
218+
# @returns [Integer, nil] Free memory in bytes, or nil if not determinable.
219+
def free_size
220+
return Memory::Linux.free_size
221+
end
222+
180223
# Capture memory metrics for a process.
181224
# @parameter pid [Integer] The process ID.
182225
# @parameter faults [Boolean] Whether to capture fault counters (default: true).

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Added `Process::Metrics::Memory.free_size` for free/available system memory in bytes.
6+
37
## v0.9.0
48

59
- `Process::Metrics::Memory.total_size` takes into account cgroup limits.

test/process/memory.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,29 @@
77

88
describe Process::Metrics::Memory do
99
with ".total_size" do
10-
it "can get the total available memory" do
10+
it "returns total available memory in bytes" do
1111
expect(Process::Metrics::Memory.total_size).to be > 0
1212
end
13+
14+
it "returns an integer" do
15+
expect(Process::Metrics::Memory.total_size).to be_a(Integer)
16+
end
17+
end
18+
19+
with ".free_size" do
20+
it "returns free/available memory in bytes when supported" do
21+
free = Process::Metrics::Memory.free_size
22+
skip "free_size is not available on this platform" if free.nil?
23+
expect(free).to be_a(Integer)
24+
expect(free).to be >= 0
25+
end
26+
27+
it "returns free size less than or equal to total size when both are available" do
28+
total = Process::Metrics::Memory.total_size
29+
free = Process::Metrics::Memory.free_size
30+
skip "free_size is not available on this platform" if free.nil?
31+
expect(free).to be <= total
32+
end
1333
end
1434

1535
with ".capture" do

0 commit comments

Comments
 (0)