@@ -15,12 +15,14 @@ class Memory::Linux
1515 # Any value >= 2^60 (1 exabyte) is treated as unlimited and we fall back to /proc/meminfo.
1616 # Reference: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt
1717 CGROUP_V1_UNLIMITED_THRESHOLD = 2 **60
18+ DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup"
1819
19- def initialize
20+ def initialize ( cgroup_root : nil )
21+ @cgroup_root = ( cgroup_root || DEFAULT_CGROUP_ROOT ) . to_s . chomp ( "/" )
2022 @meminfo = false
2123 end
2224
23- # Capture current host memory. Reads total and used (from cgroup or meminfo), computes free, and parses swap from meminfo.
25+ # Capture current host memory. Reads total and used (from cgroup or meminfo), computes free, parses swap and reclaimable from meminfo/cgroup .
2426 # @returns [Host::Memory | Nil]
2527 def capture
2628 total = capture_total
@@ -29,15 +31,25 @@ def capture
2931 used = capture_used ( total )
3032 used = 0 if used . nil? || used . negative?
3133 used = [ used , total ] . min
32- free = total - used
3334
3435 swap_total , swap_used = capture_swap
36+ reclaimable = capture_reclaimable
3537
36- return Host ::Memory . new ( total , used , free , swap_total , swap_used )
38+ return Host ::Memory . new ( total , used , swap_total , swap_used , reclaimable )
3739 end
3840
3941 private
4042
43+ # Path for cgroups v2 (unified): e.g. /sys/fs/cgroup/memory.stat
44+ def cgroup_v2_path ( name )
45+ "#{ @cgroup_root } /#{ name } "
46+ end
47+
48+ # Path for cgroups v1 (memory controller): e.g. /sys/fs/cgroup/memory/memory.stat
49+ def cgroup_v1_path ( name )
50+ "#{ @cgroup_root } /memory/#{ name } "
51+ end
52+
4153 # Memoized /proc/meminfo contents. Used for total (MemTotal), used (via MemAvailable), and swap when not in a cgroup.
4254 # @returns [String | Nil]
4355 def meminfo
@@ -51,13 +63,13 @@ def meminfo
5163 # Total memory in bytes: cgroups v2 memory.max, cgroups v1 memory.limit_in_bytes (if < threshold), else MemTotal from meminfo.
5264 # @returns [Integer | Nil]
5365 def capture_total
54- if File . exist? ( "/sys/fs/cgroup/ memory.max")
55- limit = File . read ( "/sys/fs/cgroup/ memory.max") . strip
66+ if File . exist? ( cgroup_v2_path ( " memory.max") )
67+ limit = File . read ( cgroup_v2_path ( " memory.max") ) . strip
5668 return limit . to_i if limit != "max"
5769 end
5870
59- if File . exist? ( "/sys/fs/cgroup/ memory/memory .limit_in_bytes")
60- limit = File . read ( "/sys/fs/cgroup/ memory/memory .limit_in_bytes") . strip . to_i
71+ if File . exist? ( cgroup_v1_path ( " memory.limit_in_bytes") )
72+ limit = File . read ( cgroup_v1_path ( " memory.limit_in_bytes") ) . strip . to_i
6173 return limit if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD
6274 end
6375
@@ -78,15 +90,15 @@ def capture_total
7890 # @parameter total [Integer] Total memory (used to compute used from MemAvailable when not in cgroup).
7991 # @returns [Integer | Nil]
8092 def capture_used ( total )
81- if File . exist? ( "/sys/fs/cgroup/ memory.current")
82- current = File . read ( "/sys/fs/cgroup/ memory.current") . strip . to_i
93+ if File . exist? ( cgroup_v2_path ( " memory.current") )
94+ current = File . read ( cgroup_v2_path ( " memory.current") ) . strip . to_i
8395 return current
8496 end
8597
86- if 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
98+ if File . exist? ( cgroup_v1_path ( " memory.usage_in_bytes") )
99+ limit = File . read ( cgroup_v1_path ( " memory.limit_in_bytes") ) . strip . to_i
88100 if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD
89- return File . read ( "/sys/fs/cgroup/ memory/memory .usage_in_bytes") . strip . to_i
101+ return File . read ( cgroup_v1_path ( " memory.usage_in_bytes") ) . strip . to_i
90102 end
91103 end
92104
@@ -115,6 +127,37 @@ def capture_swap
115127
116128 return swap_total_bytes , swap_used_bytes
117129 end
130+
131+ # Reclaimable memory in bytes (page cache, buffers, reclaimable slab). Included in used_size.
132+ # From cgroups v2 memory.stat "file", cgroups v1 memory.stat "cache", or meminfo Cached + Buffers + SReclaimable.
133+ # @returns [Integer | Nil]
134+ def capture_reclaimable
135+ if File . exist? ( cgroup_v2_path ( "memory.stat" ) )
136+ # cgroups v2: "file" is file-backed (page cache), in bytes
137+ content = File . read ( cgroup_v2_path ( "memory.stat" ) ) rescue nil
138+ if content && ( m = content . match ( /^file\s +(\d +)/m ) )
139+ return m [ 1 ] . to_i
140+ end
141+ end
142+
143+ if File . exist? ( cgroup_v1_path ( "memory.stat" ) )
144+ # cgroups v1: "cache" is page cache, in bytes
145+ content = File . read ( cgroup_v1_path ( "memory.stat" ) ) rescue nil
146+ if content && ( m = content . match ( /^cache\s +(\d +)/m ) )
147+ return m [ 1 ] . to_i
148+ end
149+ end
150+
151+ # meminfo: Cached + Buffers + SReclaimable (kB)
152+ unless meminfo_content = self . meminfo
153+ return nil
154+ end
155+ cached_kb = meminfo_content [ /Cached:\s *(\d +)\s *kB/ , 1 ] &.to_i || 0
156+ buffers_kb = meminfo_content [ /Buffers:\s *(\d +)\s *kB/ , 1 ] &.to_i || 0
157+ sreclaimable_kb = meminfo_content [ /SReclaimable:\s *(\d +)\s *kB/ , 1 ] &.to_i || 0
158+ reclaimable_kb = cached_kb + buffers_kb + sreclaimable_kb
159+ return reclaimable_kb * 1024
160+ end
118161 end
119162 end
120163 end
0 commit comments