process-metrics 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fc7fb298dde1bb1446b8223bf55be24f776fe8e042c0a5e2ee2cf3452e64383
4
- data.tar.gz: 340908860960f819a24c864e3d64bed4288840fad98dc4a0f33f37e622cc22ce
3
+ metadata.gz: bb30f9e1ffed610ad4a6735a8875f6438eae603ce53e6cb67274e7f893f1c4d5
4
+ data.tar.gz: 0474ced532e19377ee8b4b7003c21d1a098afc8df5fb3f9865ea163aa756eced
5
5
  SHA512:
6
- metadata.gz: d7bef8457eb65f378e63d65a768ac8ab4d5d4ae45bcb97f7489b9e06676afeb5fabe2e7b818d132fcae6436e51394ee810ade201f17f8ffd24211db42452b2a5
7
- data.tar.gz: 680700b1619d358370114cde3c8d1025c998b0e55ad01d5ee6a3c211857056865f9dd9f5d244ac20ebcb5ec6595f4e8439a2193c28f3765dae17b20df3158b64
6
+ metadata.gz: 898911e287967bb88596747da31bc6e956bdf4aa8bdea23b461df7998922155d72bd577ca0092320514aa41738547576abd379779a89cf66a22d5a63f0e279f8
7
+ data.tar.gz: 0f78a3b25a090f9823feae392358281e10f1828a0cb4d324ad880a0a877e0ae34d8cd9d4fbedf7018fe847b5dff1b2f2c6a27702eebb61d714afb46faf4eda7a
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
+
6
+ require "etc"
7
+
8
+ module Process
9
+ module Metrics
10
+ # General process information by reading /proc. Used on Linux to avoid spawning `ps`.
11
+ # We read directly from the kernel (proc(5)) so there is no subprocess and no parsing of
12
+ # external command output; same data source as the kernel uses for process accounting.
13
+ # Parses /proc/[pid]/stat and /proc/[pid]/cmdline for each process.
14
+ module General::Linux
15
+ # Clock ticks per second for /proc stat times (utime, stime, starttime).
16
+ CLK_TCK = Etc.sysconf(Etc::SC_CLK_TCK) rescue 100
17
+
18
+ # Page size in bytes for RSS (resident set size is in pages in /proc/pid/stat).
19
+ PAGE_SIZE = Etc.sysconf(Etc::SC_PAGESIZE) rescue 4096
20
+
21
+ # Whether /proc is available so we can list processes without ps.
22
+ def self.supported?
23
+ File.directory?("/proc") && File.readable?("/proc/self/stat")
24
+ end
25
+
26
+ # Capture process information from /proc. If given `pid`, captures only those process(es). If given `ppid`, captures that parent and all descendants. Both can be given to capture a process and its children.
27
+ # @parameter pid [Integer | Array(Integer)] Process ID(s) to capture.
28
+ # @parameter ppid [Integer | Array(Integer)] Parent process ID(s) to include children for.
29
+ # @parameter memory [Boolean] Whether to capture detailed memory metrics (default: Memory.supported?).
30
+ # @returns [Hash<Integer, General>] Map of PID to General instance.
31
+ def self.capture(pid: nil, ppid: nil, memory: Memory.supported?)
32
+ # When filtering by ppid we need the full process list to build the parent-child tree,
33
+ # so we enumerate all numeric /proc entries; when only pid is set we read just those.
34
+ pids_to_read = if pid && ppid.nil?
35
+ Array(pid)
36
+ else
37
+ Dir.children("/proc").filter{|e| e.match?(/\A\d+\z/)}.map(&:to_i)
38
+ end
39
+
40
+ uptime_jiffies = nil
41
+
42
+ processes = {}
43
+ pids_to_read.each do |pid|
44
+ stat_path = "/proc/#{pid}/stat"
45
+ next unless File.readable?(stat_path)
46
+
47
+ stat_content = File.read(stat_path)
48
+ # comm field can contain spaces and parentheses; find the closing ')' (proc(5)).
49
+ closing_paren_index = stat_content.rindex(")")
50
+ next unless closing_paren_index
51
+
52
+ executable_name = stat_content[1...closing_paren_index]
53
+ fields = stat_content[(closing_paren_index + 2)..].split(/\s+/)
54
+ # After comm: state(3), ppid(4), pgrp(5), ... utime(14), stime(15), ... starttime(22), vsz(23), rss(24). 0-based: ppid=1, pgrp=2, utime=11, stime=12, starttime=19, vsz=20, rss=21.
55
+ parent_process_id = fields[1].to_i
56
+ process_group_id = fields[2].to_i
57
+ utime = fields[11].to_i
58
+ stime = fields[12].to_i
59
+ starttime = fields[19].to_i
60
+ virtual_size = fields[20].to_i
61
+ resident_pages = fields[21].to_i
62
+
63
+ # Read /proc/uptime once per capture and reuse for every process (starttime is in jiffies since boot).
64
+ uptime_jiffies ||= begin
65
+ uptime_seconds = File.read("/proc/uptime").split(/\s+/).first.to_f
66
+ (uptime_seconds * CLK_TCK).to_i
67
+ end
68
+
69
+ processor_time = (utime + stime).to_f / CLK_TCK
70
+ elapsed_time = [(uptime_jiffies - starttime).to_f / CLK_TCK, 0.0].max
71
+
72
+ command = read_command(pid, executable_name)
73
+
74
+ processes[pid] = General.new(
75
+ pid,
76
+ parent_process_id,
77
+ process_group_id,
78
+ 0.0, # processor_utilization: would need two samples; not available from single stat read
79
+ virtual_size,
80
+ resident_pages * PAGE_SIZE,
81
+ processor_time,
82
+ elapsed_time,
83
+ command,
84
+ nil
85
+ )
86
+ rescue Errno::ENOENT, Errno::ESRCH, Errno::EACCES
87
+ # Process disappeared or we can't read it.
88
+ next
89
+ end
90
+
91
+ # Restrict to the requested pid/ppid subtree using the same tree logic as the ps backend.
92
+ if ppid
93
+ pids = Set.new
94
+ hierarchy = General.build_tree(processes)
95
+ General.expand_children(Array(pid), hierarchy, pids) if pid
96
+ General.expand_children(Array(ppid), hierarchy, pids)
97
+ processes.select!{|process_id, _| pids.include?(process_id)}
98
+ end
99
+
100
+ General.capture_memory(processes) if memory
101
+
102
+ processes
103
+ end
104
+
105
+ # Read command line from /proc/[pid]/cmdline; fall back to executable name from stat if empty.
106
+ # Use binread because cmdline is NUL-separated and may contain non-UTF-8 bytes; we split on NUL and join for display.
107
+ def self.read_command(pid, command_fallback)
108
+ path = "/proc/#{pid}/cmdline"
109
+ return command_fallback unless File.readable?(path)
110
+
111
+ cmdline_content = File.binread(path)
112
+ return command_fallback if cmdline_content.empty?
113
+
114
+ # cmdline is NUL-separated; replace with spaces for display.
115
+ cmdline_content.split("\0").join(" ").strip
116
+ rescue Errno::ENOENT, Errno::ESRCH, Errno::EACCES
117
+ command_fallback
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ if Process::Metrics::General::Linux.supported?
124
+ class << Process::Metrics::General
125
+ def capture(...)
126
+ Process::Metrics::General::Linux.capture(...)
127
+ end
128
+ end
129
+ else
130
+ require_relative "process_status"
131
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
+
6
+ module Process
7
+ module Metrics
8
+ # General process information via the process status command (`ps`). Used on non-Linux platforms (e.g. Darwin)
9
+ # where there is no /proc; ps is the portable way to get pid, ppid, times, and memory in one pass.
10
+ module General::ProcessStatus
11
+ PS = "ps"
12
+
13
+ # The fields that will be extracted from the `ps` command (order matches -o output).
14
+ FIELDS = {
15
+ pid: ->(value){value.to_i},
16
+ ppid: ->(value){value.to_i},
17
+ pgid: ->(value){value.to_i},
18
+ pcpu: ->(value){value.to_f},
19
+ vsz: ->(value){value.to_i * 1024},
20
+ rss: ->(value){value.to_i * 1024},
21
+ time: Process::Metrics.method(:duration),
22
+ etime: Process::Metrics.method(:duration),
23
+ command: ->(value){value},
24
+ }
25
+
26
+ # Whether process listing via ps is available on this system.
27
+ def self.supported?
28
+ system("which", PS, out: File::NULL, err: File::NULL)
29
+ end
30
+
31
+ # Capture process information using ps. If given a `pid`, captures that process; if given `ppid`, captures that process and all descendants. Specify both to capture a process and its children.
32
+ # @parameter pid [Integer | Array(Integer)] Process ID(s) to capture.
33
+ # @parameter ppid [Integer | Array(Integer)] Parent process ID(s) to include children for.
34
+ # @parameter memory [Boolean] Whether to capture detailed memory metrics (default: Memory.supported?).
35
+ # @returns [Hash<Integer, General>] Map of PID to General instance.
36
+ def self.capture(pid: nil, ppid: nil, memory: Memory.supported?)
37
+ spawned_pid = nil
38
+
39
+ header, *lines = IO.pipe do |input, output|
40
+ arguments = [PS]
41
+
42
+ # When filtering by ppid we need the full process list to build the tree, so use "ax"; otherwise limit to -p.
43
+ if pid && ppid.nil?
44
+ arguments.push("-p", Array(pid).join(","))
45
+ else
46
+ arguments.push("ax")
47
+ end
48
+
49
+ arguments.push("-o", FIELDS.keys.join(","))
50
+
51
+ spawned_pid = Process.spawn(*arguments, out: output)
52
+ output.close
53
+
54
+ input.readlines.map(&:strip)
55
+ ensure
56
+ input.close
57
+
58
+ # Always kill and reap the ps subprocess so we never leave it hanging if the pipe closes early.
59
+ if spawned_pid
60
+ begin
61
+ Process.kill(:KILL, spawned_pid)
62
+ Process.wait(spawned_pid)
63
+ rescue => error
64
+ warn "Failed to cleanup ps process #{spawned_pid}:\n#{error.full_message}"
65
+ end
66
+ end
67
+ end
68
+
69
+ processes = {}
70
+
71
+ lines.each do |line|
72
+ next if line.empty?
73
+
74
+ values = line.split(/\s+/, FIELDS.size)
75
+ next if values.size < FIELDS.size
76
+
77
+ record = FIELDS.keys.map.with_index{|key, i| FIELDS[key].call(values[i])}
78
+ instance = General.new(*record, nil)
79
+ processes[instance.process_id] = instance
80
+ end
81
+
82
+ # Restrict to the requested pid/ppid subtree; exclude our own ps process from the result.
83
+ if ppid
84
+ pids = Set.new
85
+ hierarchy = General.build_tree(processes)
86
+ General.expand_children(Array(pid), hierarchy, pids) if pid
87
+ General.expand_children(Array(ppid), hierarchy, pids)
88
+ processes.select!{|process_id, _| process_id != spawned_pid && pids.include?(process_id)}
89
+ else
90
+ processes.delete(spawned_pid) if spawned_pid
91
+ end
92
+
93
+ General.capture_memory(processes) if memory
94
+
95
+ processes
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Wire General.capture to this implementation when ProcessStatus is available and the Linux backend is not active (so Linux can load both for comparison tests).
102
+ linux_supported = defined?(Process::Metrics::General::Linux) && Process::Metrics::General::Linux.supported?
103
+ if Process::Metrics::General::ProcessStatus.supported? && !linux_supported
104
+ class << Process::Metrics::General
105
+ def capture(...)
106
+ Process::Metrics::General::ProcessStatus.capture(...)
107
+ end
108
+ end
109
+ end
@@ -9,8 +9,6 @@ require "json"
9
9
 
10
10
  module Process
11
11
  module Metrics
12
- PS = "ps"
13
-
14
12
  DURATION = /\A
15
13
  (?:(?<days>\d+)-)? # Optional days (e.g., '2-')
16
14
  (?:(?<hours>\d+):)? # Optional hours (e.g., '1:')
@@ -36,19 +34,6 @@ module Process
36
34
  end
37
35
  end
38
36
 
39
- # The fields that will be extracted from the `ps` command.
40
- FIELDS = {
41
- pid: ->(value){value.to_i}, # Process ID
42
- ppid: ->(value){value.to_i}, # Parent Process ID
43
- pgid: ->(value){value.to_i}, # Process Group ID
44
- pcpu: ->(value){value.to_f}, # Percentage CPU
45
- vsz: ->(value){value.to_i * 1024}, # Virtual Size (convert from KiB to bytes)
46
- rss: ->(value){value.to_i * 1024}, # Resident Size (convert from KiB to bytes)
47
- time: self.method(:duration), # CPU Time (seconds)
48
- etime: self.method(:duration), # Elapsed Time (seconds)
49
- command: ->(value){value}, # Command (name of the process)
50
- }
51
-
52
37
  # General process information.
53
38
  class General < Struct.new(:process_id, :parent_process_id, :process_group_id, :processor_utilization, :virtual_size, :resident_size, :processor_time, :elapsed_time, :command, :memory)
54
39
  # Convert the object to a JSON serializable hash.
@@ -132,77 +117,13 @@ module Process
132
117
  process.memory = Memory.capture(pid, count: count)
133
118
  end
134
119
  end
135
-
136
- # Capture process information. If given a `pid`, it will capture the details of that process. If given a `ppid`, it will capture the details of all child processes. Specify both `pid` and `ppid` if you want to capture a process and all its children.
137
- #
138
- # @parameter pid [Integer] The process ID to capture.
139
- # @parameter ppid [Integer] The parent process ID to capture.
140
- def self.capture(pid: nil, ppid: nil, memory: Memory.supported?)
141
- ps_pid = nil
142
-
143
- # Extract the information from the `ps` command:
144
- header, *lines = IO.pipe do |input, output|
145
- arguments = [PS]
146
-
147
- if pid && ppid.nil?
148
- arguments.push("-p", Array(pid).join(","))
149
- else
150
- arguments.push("ax")
151
- end
152
-
153
- arguments.push("-o", FIELDS.keys.join(","))
154
-
155
- ps_pid = Process.spawn(*arguments, out: output)
156
- output.close
157
-
158
- input.readlines.map(&:strip)
159
- ensure
160
- input.close
161
-
162
- if ps_pid
163
- begin
164
- # Make sure to kill the ps process if it's still running:
165
- Process.kill(:KILL, ps_pid)
166
- # Reap the process:
167
- Process.wait(ps_pid)
168
- rescue => error
169
- warn "Failed to cleanup ps process #{ps_pid}:\n#{error.full_message}"
170
- end
171
- end
172
- end
173
-
174
- processes = {}
175
-
176
- lines.map do |line|
177
- record = FIELDS.
178
- zip(line.split(/\s+/, FIELDS.size)).
179
- map{|(key, type), value| type.call(value)}
180
- instance = self.new(*record)
181
-
182
- processes[instance.process_id] = instance
183
- end
184
-
185
- if ppid
186
- pids = Set.new
187
-
188
- hierarchy = self.build_tree(processes)
189
-
190
- self.expand_children(Array(pid), hierarchy, pids)
191
- self.expand_children(Array(ppid), hierarchy, pids)
192
-
193
- processes.select! do |pid, process|
194
- if pid != ps_pid
195
- pids.include?(pid)
196
- end
197
- end
198
- end
199
-
200
- if memory
201
- self.capture_memory(processes)
202
- end
203
-
204
- return processes
205
- end
206
120
  end
207
121
  end
208
122
  end
123
+
124
+ # One backend provides General.capture: Linux uses /proc (no subprocess); other platforms use ps.
125
+ if RUBY_PLATFORM.include?("linux")
126
+ require_relative "general/linux"
127
+ else
128
+ require_relative "general/process_status"
129
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
+
6
+ module Process
7
+ module Metrics
8
+ module Host
9
+ # Darwin (macOS) implementation of host memory metrics.
10
+ # Uses sysctl (hw.memsize), vm_stat (free + inactive pages), and vm.swapusage for swap.
11
+ class Memory::Darwin
12
+ # Parse a size string from vm.swapusage (e.g. "1024.00M", "512.00K") into bytes.
13
+ # @parameter size_string [String | Nil] The size string from sysctl vm.swapusage.
14
+ # @returns [Integer | Nil] Size in bytes, or nil if size_string is nil/empty.
15
+ def self.parse_swap_size(size_string)
16
+ return nil unless size_string
17
+
18
+ size_string = size_string.strip
19
+
20
+ case size_string
21
+ when /([\d.]+)M/i then ($1.to_f * 1024 * 1024).round
22
+ when /([\d.]+)G/i then ($1.to_f * 1024 * 1024 * 1024).round
23
+ when /([\d.]+)K/i then ($1.to_f * 1024).round
24
+ else size_string.to_f.round
25
+ end
26
+ end
27
+
28
+ # Capture current host memory. Reads total (hw.memsize), free (vm_stat), and swap (vm.swapusage).
29
+ # @returns [Host::Memory | Nil] A Host::Memory instance, or nil if capture fails.
30
+ def self.capture
31
+ total = capture_total
32
+ return nil unless total && total.positive?
33
+
34
+ free = capture_free
35
+ return nil unless free
36
+
37
+ free = 0 if free.negative?
38
+ used = [total - free, 0].max
39
+ swap_total, swap_used = capture_swap
40
+
41
+ return Host::Memory.new(total, used, free, swap_total, swap_used)
42
+ end
43
+
44
+ # Total physical RAM in bytes, from sysctl hw.memsize.
45
+ # @returns [Integer | Nil]
46
+ def self.capture_total
47
+ IO.popen(["sysctl", "-n", "hw.memsize"], "r", &:read)&.strip&.to_i
48
+ end
49
+
50
+ # Free + inactive (reclaimable) memory in bytes, from vm_stat. Matches Linux MemAvailable semantics.
51
+ # @returns [Integer | Nil]
52
+ def self.capture_free
53
+ output = IO.popen(["vm_stat"], "r", &:read)
54
+ page_size = output[/page size of (\d+) bytes/, 1]&.to_i
55
+ return nil unless page_size && page_size.positive?
56
+
57
+ pages_free = output[/Pages free:\s*(\d+)/, 1]&.to_i || 0
58
+ pages_inactive = output[/Pages inactive:\s*(\d+)/, 1]&.to_i || 0
59
+ return (pages_free + pages_inactive) * page_size
60
+ end
61
+
62
+ # Swap total and used in bytes, from sysctl vm.swapusage (e.g. "total = 64.00M used = 32.00M free = 32.00M").
63
+ # @returns [Array(Integer | Nil, Integer | Nil)] [swap_total_bytes, swap_used_bytes], or [nil, nil] if unavailable.
64
+ def self.capture_swap
65
+ output = IO.popen(["sysctl", "-n", "vm.swapusage"], "r", &:read)
66
+ return [nil, nil] unless output
67
+
68
+ total_string = output[/total\s*=\s*([\d.]+\s*[KMG]?)/i, 1]
69
+ used_string = output[/used\s*=\s*([\d.]+\s*[KMG]?)/i, 1]
70
+ swap_total = total_string ? parse_swap_size(total_string) : nil
71
+ swap_used = used_string ? parse_swap_size(used_string) : nil
72
+
73
+ return swap_total, swap_used
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # Wire Host::Memory to this implementation on Darwin.
81
+ class << Process::Metrics::Host::Memory
82
+ def capture
83
+ Process::Metrics::Host::Memory::Darwin.capture
84
+ end
85
+
86
+ def supported?
87
+ File.exist?("/usr/bin/vm_stat")
88
+ end
89
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
+
6
+ module Process
7
+ module Metrics
8
+ module Host
9
+ # Linux implementation of host memory metrics.
10
+ # Uses cgroups v2 (memory.max, memory.current) or cgroups v1 (memory.limit_in_bytes, memory.usage_in_bytes) when in a container;
11
+ # otherwise reads /proc/meminfo (MemTotal, MemAvailable/MemFree, SwapTotal/SwapFree). Parses meminfo once per capture and reuses it.
12
+ class Memory::Linux
13
+ # Threshold for distinguishing actual memory limits from "unlimited" sentinel values in cgroups v1.
14
+ # In cgroups v1, when memory.limit_in_bytes is set to unlimited (by writing -1), the kernel stores a very large sentinel near 2^63.
15
+ # Any value >= 2^60 (1 exabyte) is treated as unlimited and we fall back to /proc/meminfo.
16
+ # Reference: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt
17
+ CGROUP_V1_UNLIMITED_THRESHOLD = 2**60
18
+
19
+ def initialize
20
+ @meminfo = false
21
+ end
22
+
23
+ # Capture current host memory. Reads total and used (from cgroup or meminfo), computes free, and parses swap from meminfo.
24
+ # @returns [Host::Memory | Nil]
25
+ def capture
26
+ total = capture_total
27
+ return nil unless total && total.positive?
28
+
29
+ used = capture_used(total)
30
+ used = 0 if used.nil? || used.negative?
31
+ used = [used, total].min
32
+ free = total - used
33
+
34
+ swap_total, swap_used = capture_swap
35
+
36
+ return Host::Memory.new(total, used, free, swap_total, swap_used)
37
+ end
38
+
39
+ private
40
+
41
+ # Memoized /proc/meminfo contents. Used for total (MemTotal), used (via MemAvailable), and swap when not in a cgroup.
42
+ # @returns [String | Nil]
43
+ def meminfo
44
+ if @meminfo == false
45
+ @meminfo = File.read("/proc/meminfo") rescue nil
46
+ end
47
+
48
+ return @meminfo
49
+ end
50
+
51
+ # Total memory in bytes: cgroups v2 memory.max, cgroups v1 memory.limit_in_bytes (if < threshold), else MemTotal from meminfo.
52
+ # @returns [Integer | Nil]
53
+ def capture_total
54
+ if File.exist?("/sys/fs/cgroup/memory.max")
55
+ limit = File.read("/sys/fs/cgroup/memory.max").strip
56
+ return limit.to_i if limit != "max"
57
+ end
58
+
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
61
+ return limit if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD
62
+ end
63
+
64
+ unless meminfo_content = self.meminfo
65
+ return nil
66
+ end
67
+
68
+ meminfo_content.each_line do |line|
69
+ if /MemTotal:\s*(?<total>\d+)\s*kB/ =~ line
70
+ return $~[:total].to_i * 1024
71
+ end
72
+ end
73
+
74
+ return nil
75
+ end
76
+
77
+ # Current memory usage in bytes: cgroups v2 memory.current, cgroups v1 memory.usage_in_bytes, or total - MemAvailable from meminfo.
78
+ # @parameter total [Integer] Total memory (used to compute used from MemAvailable when not in cgroup).
79
+ # @returns [Integer | Nil]
80
+ 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
83
+ return current
84
+ end
85
+
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
88
+ if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD
89
+ return File.read("/sys/fs/cgroup/memory/memory.usage_in_bytes").strip.to_i
90
+ end
91
+ end
92
+
93
+ unless meminfo_content = self.meminfo
94
+ return nil
95
+ end
96
+
97
+ available_kilobytes = meminfo_content[/MemAvailable:\s*(\d+)\s*kB/, 1]&.to_i
98
+ available_kilobytes ||= meminfo_content[/MemFree:\s*(\d+)\s*kB/, 1]&.to_i
99
+ return nil unless available_kilobytes
100
+
101
+ return [total - (available_kilobytes * 1024), 0].max
102
+ end
103
+
104
+ # Swap total and used in bytes from meminfo (SwapTotal, SwapFree).
105
+ # @returns [Array(Integer, Integer)] [swap_total_bytes, swap_used_bytes], or [nil, nil] if no swap.
106
+ def capture_swap
107
+ return [nil, nil] unless meminfo_content = self.meminfo
108
+ swap_total_kilobytes = meminfo_content[/SwapTotal:\s*(\d+)\s*kB/, 1]&.to_i
109
+ swap_free_kilobytes = meminfo_content[/SwapFree:\s*(\d+)\s*kB/, 1]&.to_i
110
+
111
+ return [nil, nil] unless swap_total_kilobytes
112
+
113
+ swap_total_bytes = swap_total_kilobytes * 1024
114
+ swap_used_bytes = (swap_total_kilobytes - (swap_free_kilobytes || 0)) * 1024
115
+
116
+ return swap_total_bytes, swap_used_bytes
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ # Wire Host::Memory to this implementation on Linux.
124
+ class << Process::Metrics::Host::Memory
125
+ def capture
126
+ Process::Metrics::Host::Memory::Linux.new.capture
127
+ end
128
+
129
+ def supported?
130
+ File.exist?("/proc/meminfo")
131
+ end
132
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
+
6
+ module Process
7
+ module Metrics
8
+ # Per-host (system-wide) memory metrics. Use Host::Memory for total/used/free and swap; use Process::Metrics::Memory for per-process metrics.
9
+ module Host
10
+ # Struct for host memory snapshot. All sizes in bytes.
11
+ # @attribute total [Integer] Total memory (cgroup limit when in a container, else physical RAM).
12
+ # @attribute used [Integer] Memory in use (total - free).
13
+ # @attribute free [Integer] Available memory (MemAvailable-style: free + reclaimable).
14
+ # @attribute swap_total [Integer, nil] Total swap, or nil if not available.
15
+ # @attribute swap_used [Integer, nil] Swap in use, or nil if not available.
16
+ Memory = Struct.new(:total, :used, :free, :swap_total, :swap_used) do
17
+ alias as_json to_h
18
+
19
+ def to_json(*arguments)
20
+ as_json.to_json(*arguments)
21
+ end
22
+
23
+ # Create a zero-initialized Host::Memory instance.
24
+ # @returns [Memory]
25
+ def self.zero
26
+ self.new(0, 0, 0, nil, nil)
27
+ end
28
+
29
+ # Whether host memory capture is supported on this platform.
30
+ # @returns [Boolean]
31
+ def self.supported?
32
+ false
33
+ end
34
+
35
+ # Capture current host memory. Implemented by Host::Memory::Linux or Host::Memory::Darwin (in host/memory/linux.rb, host/memory/darwin.rb).
36
+ # @returns [Memory | Nil] A Host::Memory instance, or nil if not supported or capture failed.
37
+ def self.capture
38
+ return nil
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ if RUBY_PLATFORM.include?("linux")
46
+ require_relative "memory/linux"
47
+ elsif RUBY_PLATFORM.include?("darwin")
48
+ require_relative "memory/darwin"
49
+ end
@@ -5,7 +5,8 @@
5
5
 
6
6
  module Process
7
7
  module Metrics
8
- # Darwin (macOS) implementation of memory metrics using vmmap.
8
+ # Darwin (macOS) implementation of per-process memory metrics using vmmap(1).
9
+ # Parses vmmap output for virtual/resident/dirty/swap per region and maps sharing mode (PRV, COW, SHM) to private/shared fields.
9
10
  class Memory::Darwin
10
11
  VMMAP = "/usr/bin/vmmap"
11
12
 
@@ -14,32 +15,21 @@ module Process
14
15
  File.executable?(VMMAP)
15
16
  end
16
17
 
17
- # @returns [Numeric] Total memory size in bytes.
18
- def self.total_size
19
- # sysctl hw.memsize
20
- IO.popen(["sysctl", "hw.memsize"], "r") do |io|
21
- io.each_line do |line|
22
- if line =~ /hw.memsize: (\d+)/
23
- return $1.to_i
24
- end
25
- end
26
- end
27
- end
28
-
29
- # Parse a size string from vmmap output into bytes.
30
- # @parameter string [String | Nil] The size string (e.g., "4K", "1.5M", "2G").
31
- # @returns [Integer] The size in bytes.
32
- def self.parse_size(string)
33
- return 0 unless string
18
+ # Parse a size string from vmmap (e.g. "4K", "1.5M", "2G") into bytes.
19
+ # @parameter size_string [String | Nil]
20
+ # @returns [Integer]
21
+ def self.parse_size(size_string)
22
+ return 0 unless size_string
34
23
 
35
- case string.strip
24
+ case size_string.strip
36
25
  when /([\d\.]+)K/i then ($1.to_f * 1024).round
37
26
  when /([\d\.]+)M/i then ($1.to_f * 1024 * 1024).round
38
27
  when /([\d\.]+)G/i then ($1.to_f * 1024 * 1024 * 1024).round
39
- else (string.to_f).ceil
28
+ else (size_string.to_f).ceil
40
29
  end
41
30
  end
42
31
 
32
+ # Regex for vmmap region lines: region name, address range, [virtual resident dirty swap], permissions, SM=sharing_mode.
43
33
  LINE = /\A
44
34
  \s*
45
35
  (?<region_name>.+?)\s+
@@ -49,26 +39,24 @@ module Process
49
39
  SM=(?<sharing_mode>\w+)
50
40
  /x
51
41
 
52
- # Capture memory usage for the given process IDs.
42
+ # Capture memory usage by running vmmap for the given pid and summing region sizes. Proportional size is estimated as resident_size / count (Darwin has no PSS).
43
+ # @parameter pid [Integer] Process ID.
44
+ # @parameter count [Integer] Number of processes for proportional estimate (default: 1).
45
+ # @returns [Memory | Nil]
53
46
  def self.capture(pid, count: 1, **options)
54
47
  IO.popen(["vmmap", pid.to_s], "r") do |io|
55
48
  usage = Memory.zero
56
-
57
49
  io.each_line do |line|
58
50
  if match = LINE.match(line)
59
51
  usage.map_count += 1
60
-
61
52
  virtual_size = parse_size(match[:virtual_size])
62
53
  resident_size = parse_size(match[:resident_size])
63
54
  dirty_size = parse_size(match[:dirty_size])
64
55
  swap_size = parse_size(match[:swap_size])
65
-
66
56
  usage.resident_size += resident_size
67
57
  usage.swap_size += swap_size
68
58
 
69
- # Private vs. Shared memory
70
- # COW=copy_on_write PRV=private NUL=empty ALI=aliased
71
- # SHM=shared ZER=zero_filled S/A=shared_alias
59
+ # Private vs. Shared memory: COW=copy_on_write PRV=private NUL=empty ALI=aliased SHM=shared ZER=zero_filled S/A=shared_alias
72
60
  case match[:sharing_mode]
73
61
  when "PRV"
74
62
  usage.private_clean_size += resident_size - dirty_size
@@ -85,10 +73,8 @@ module Process
85
73
  end
86
74
  end
87
75
 
88
- if usage.map_count.zero?
89
- # vmap might not fail, but also might not return any data.
90
- return nil
91
- end
76
+ # vmmap might not fail, but also might not return any data.
77
+ return nil if usage.map_count.zero?
92
78
 
93
79
  # Darwin does not expose proportional memory usage, so we guess based on the number of processes. Yes, this is a terrible hack, but it's the most reasonable thing to do given the constraints:
94
80
  usage.proportional_size = usage.resident_size / count
@@ -101,29 +87,24 @@ module Process
101
87
  return nil
102
88
  end
103
89
  end
90
+ end
91
+ end
92
+
93
+ # Wire Memory.capture and Memory.supported? to this implementation when vmmap is executable.
94
+ if Process::Metrics::Memory::Darwin.supported?
95
+ class << Process::Metrics::Memory
96
+ # Whether memory capture is supported on this platform.
97
+ # @returns [Boolean] True if vmmap is available.
98
+ def supported?
99
+ true
100
+ end
104
101
 
105
- if Memory::Darwin.supported?
106
- class << Memory
107
- # Whether memory capture is supported on this platform.
108
- # @returns [Boolean] True if vmmap is available.
109
- def supported?
110
- return true
111
- end
112
-
113
- # Get total system memory size.
114
- # @returns [Integer] Total memory in bytes.
115
- def total_size
116
- return Memory::Darwin.total_size
117
- end
118
-
119
- # Capture memory metrics for a process.
120
- # @parameter pid [Integer] The process ID.
121
- # @parameter options [Hash] Additional options (e.g., count for proportional estimates).
122
- # @returns [Memory] A Memory instance with captured metrics.
123
- def capture(...)
124
- return Memory::Darwin.capture(...)
125
- end
126
- end
102
+ # Capture memory metrics for a process.
103
+ # @parameter pid [Integer] The process ID.
104
+ # @parameter options [Hash] Additional options (e.g. count for proportional estimates).
105
+ # @returns [Memory | Nil] A Memory instance with captured metrics.
106
+ def capture(...)
107
+ Process::Metrics::Memory::Darwin.capture(...)
127
108
  end
128
109
  end
129
110
  end
@@ -5,31 +5,18 @@
5
5
 
6
6
  module Process
7
7
  module Metrics
8
- # Linux implementation of memory metrics using `/proc/[pid]/smaps` and `/proc/[pid]/stat`.
8
+ # Linux implementation of per-process memory metrics using `/proc/[pid]/smaps` or `/proc/[pid]/smaps_rollup`, and `/proc/[pid]/stat` for fault counters.
9
+ # Prefers smaps_rollup when readable (single summary); otherwise falls back to full smaps and counts maps from /proc/[pid]/maps.
9
10
  class Memory::Linux
10
- # Threshold for distinguishing actual memory limits from "unlimited" sentinel values in cgroups v1.
11
- #
12
- # In cgroups v1, when memory.limit_in_bytes is set to unlimited (by writing -1),
13
- # the kernel stores a very large sentinel value close to 2^63 (approximately 9,223,372,036,854,771,712 bytes).
14
- # Since no real system would have 1 exabyte (2^60 bytes) of RAM, any value >= this threshold
15
- # indicates an "unlimited" configuration and should be treated as if no limit is set.
16
- #
17
- # This allows us to distinguish between:
18
- # - Actual container memory limits: typically in GB-TB range (< 1 EB)
19
- # - Unlimited sentinel values: near 2^63 (>> 1 EB)
20
- #
21
- # Reference: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt
22
- CGROUP_V1_UNLIMITED_THRESHOLD = 2**60 # ~1 exabyte
23
-
24
- # Extract minor/major page fault counters from `/proc/[pid]/stat` and assign to usage.
25
- # @parameter pid [Integer] The process ID.
26
- # @parameter usage [Memory] The Memory instance to populate with fault counters.
11
+ # Extract minor and major page fault counters from `/proc/[pid]/stat` (proc(5): fields 10=minflt, 12=majflt) and assign to usage.
12
+ # @parameter pid [Integer] Process ID.
13
+ # @parameter usage [Memory] Memory instance to populate with minor_faults and major_faults.
27
14
  def self.capture_faults(pid, usage)
28
- stat = File.read("/proc/#{pid}/stat")
15
+ stat_content = File.read("/proc/#{pid}/stat")
29
16
  # The comm field can contain spaces and parentheses; find the closing ')':
30
- rparen_index = stat.rindex(")")
31
- return unless rparen_index
32
- fields = stat[(rparen_index+2)..-1].split(/\s+/)
17
+ closing_paren_index = stat_content.rindex(")")
18
+ return unless closing_paren_index
19
+ fields = stat_content[(closing_paren_index + 2)..].split(/\s+/)
33
20
  # proc(5): field 10=minflt, 12=majflt; our fields array is 0-indexed from field 3.
34
21
  usage.minor_faults = fields[10-3].to_i
35
22
  usage.major_faults = fields[12-3].to_i
@@ -37,39 +24,7 @@ module Process
37
24
  # Ignore.
38
25
  end
39
26
 
40
- # Determine the total memory size in bytes. This is the maximum amount of memory that can be used by the current process. If running in a container, this may be limited by the container runtime (e.g. cgroups).
41
- #
42
- # @returns [Integer] The total memory size in bytes.
43
- def self.total_size
44
- # Check for Kubernetes/cgroup memory limit first (cgroups v2):
45
- if File.exist?("/sys/fs/cgroup/memory.max")
46
- limit = File.read("/sys/fs/cgroup/memory.max").strip
47
- # "max" means unlimited, fall through to other methods:
48
- if limit != "max"
49
- return limit.to_i
50
- end
51
- end
52
-
53
- # Check for Kubernetes/cgroup memory limit (cgroups v1):
54
- if File.exist?("/sys/fs/cgroup/memory/memory.limit_in_bytes")
55
- limit = File.read("/sys/fs/cgroup/memory/memory.limit_in_bytes").strip.to_i
56
- # A very large number means unlimited, fall through:
57
- if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD
58
- return limit
59
- end
60
- end
61
-
62
- # Fall back to Linux system memory detection:
63
- if File.exist?("/proc/meminfo")
64
- File.foreach("/proc/meminfo") do |line|
65
- if /MemTotal:\s*(?<total>\d+)\s*kB/ =~ line
66
- return total.to_i * 1024
67
- end
68
- end
69
- end
70
- end
71
-
72
- # The fields that will be extracted from the `smaps` data.
27
+ # Mapping from smaps/smaps_rollup line names to Memory struct members (values in kB, converted to bytes when parsing).
73
28
  SMAP = {
74
29
  "Rss" => :resident_size,
75
30
  "Pss" => :proportional_size,
@@ -89,24 +44,24 @@ module Process
89
44
  true
90
45
  end
91
46
 
92
- # Capture memory usage for the given process IDs.
93
- # @parameter pid [Integer] The process ID.
94
- # @parameter faults [Boolean] Whether to capture fault counters (default: true).
95
- # @parameter options [Hash] Additional options.
47
+ # Capture memory usage from /proc/[pid]/smaps_rollup and /proc/[pid]/maps. Optionally fill fault counters from /proc/[pid]/stat.
48
+ # @parameter pid [Integer] Process ID.
49
+ # @parameter faults [Boolean] Whether to capture minor_faults and major_faults (default: true).
50
+ # @returns [Memory | Nil]
96
51
  def self.capture(pid, faults: true, **options)
97
52
  File.open("/proc/#{pid}/smaps_rollup") do |file|
98
53
  usage = Memory.zero
99
-
100
54
  file.each_line do |line|
101
55
  if /(?<name>.*?):\s+(?<value>\d+) kB/ =~ line
102
56
  if key = SMAP[name]
103
- # Convert from kilobytes to bytes
57
+ # Convert from kilobytes to bytes:
104
58
  usage[key] += value.to_i * 1024
105
59
  end
106
60
  end
107
61
  end
108
62
 
109
63
  usage.map_count += File.readlines("/proc/#{pid}/maps").size
64
+
110
65
  # Also capture fault counters if requested:
111
66
  if faults
112
67
  self.capture_faults(pid, usage)
@@ -124,20 +79,19 @@ module Process
124
79
  true
125
80
  end
126
81
 
127
- # Capture memory usage for the given process IDs.
128
- # @parameter pid [Integer] The process ID.
129
- # @parameter faults [Boolean] Whether to capture fault counters (default: true).
130
- # @parameter options [Hash] Additional options.
82
+ # Capture memory usage from /proc/[pid]/smaps (and map count from VmFlags) and /proc/[pid]/maps. Optionally fill fault counters from /proc/[pid]/stat.
83
+ # @parameter pid [Integer] Process ID.
84
+ # @parameter faults [Boolean] Whether to capture minor_faults and major_faults (default: true).
85
+ # @returns [Memory | Nil]
131
86
  def self.capture(pid, faults: true, **options)
132
87
  File.open("/proc/#{pid}/smaps") do |file|
133
88
  usage = Memory.zero
134
-
135
89
  file.each_line do |line|
136
90
  # The format of this is fixed according to:
137
91
  # https://github.com/torvalds/linux/blob/351c8a09b00b5c51c8f58b016fffe51f87e2d820/fs/proc/task_mmu.c#L804-L814
138
92
  if /(?<name>.*?):\s+(?<value>\d+) kB/ =~ line
139
93
  if key = SMAP[name]
140
- # Convert from kilobytes to bytes
94
+ # Convert from kilobytes to bytes:
141
95
  usage[key] += value.to_i * 1024
142
96
  end
143
97
  elsif /VmFlags:\s+(?<flags>.*)/ =~ line
@@ -148,7 +102,9 @@ module Process
148
102
  end
149
103
 
150
104
  # Also capture fault counters if requested:
151
- self.capture_faults(pid, usage) if faults
105
+ if faults
106
+ self.capture_faults(pid, usage)
107
+ end
152
108
 
153
109
  return usage
154
110
  end
@@ -162,30 +118,25 @@ module Process
162
118
  end
163
119
  end
164
120
  end
121
+ end
122
+ end
123
+
124
+ # Wire Memory.capture and Memory.supported? to this implementation when smaps or smaps_rollup is readable.
125
+ if Process::Metrics::Memory::Linux.supported?
126
+ class << Process::Metrics::Memory
127
+ # Whether memory capture is supported on this platform.
128
+ # @returns [Boolean] True if /proc/[pid]/smaps or smaps_rollup is readable.
129
+ def supported?
130
+ true
131
+ end
165
132
 
166
- if Memory::Linux.supported?
167
- class << Memory
168
- # Whether memory capture is supported on this platform.
169
- # @returns [Boolean] True if /proc/[pid]/smaps or smaps_rollup is readable.
170
- def supported?
171
- return true
172
- end
173
-
174
- # Get total system memory size.
175
- # @returns [Integer] Total memory in bytes.
176
- def total_size
177
- return Memory::Linux.total_size
178
- end
179
-
180
- # Capture memory metrics for a process.
181
- # @parameter pid [Integer] The process ID.
182
- # @parameter faults [Boolean] Whether to capture fault counters (default: true).
183
- # @parameter options [Hash] Additional options.
184
- # @returns [Memory] A Memory instance with captured metrics.
185
- def capture(...)
186
- return Memory::Linux.capture(...)
187
- end
188
- end
133
+ # Capture memory metrics for a process.
134
+ # @parameter pid [Integer] The process ID.
135
+ # @parameter faults [Boolean] Whether to capture fault counters (default: true).
136
+ # @parameter options [Hash] Additional options.
137
+ # @returns [Memory | Nil] A Memory instance with captured metrics.
138
+ def capture(...)
139
+ Process::Metrics::Memory::Linux.capture(...)
189
140
  end
190
141
  end
191
142
  end
@@ -4,6 +4,7 @@
4
4
  # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require "json"
7
+ require_relative "host/memory"
7
8
 
8
9
  module Process
9
10
  module Metrics
@@ -33,6 +34,12 @@ module Process
33
34
  self.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
34
35
  end
35
36
 
37
+ # Total system/host memory in bytes. Delegates to Host::Memory.capture.
38
+ # @returns [Integer | Nil]
39
+ def self.total_size
40
+ Host::Memory.capture&.total
41
+ end
42
+
36
43
  # Whether the memory usage can be captured on this system.
37
44
  def self.supported?
38
45
  false
@@ -46,5 +53,8 @@ module Process
46
53
  end
47
54
  end
48
55
 
49
- require_relative "memory/linux"
50
- require_relative "memory/darwin"
56
+ if RUBY_PLATFORM.include?("linux")
57
+ require_relative "memory/linux"
58
+ elsif RUBY_PLATFORM.include?("darwin")
59
+ require_relative "memory/darwin"
60
+ end
@@ -7,6 +7,6 @@
7
7
  module Process
8
8
  # @namespace
9
9
  module Metrics
10
- VERSION = "0.9.0"
10
+ VERSION = "0.10.0"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -16,12 +16,22 @@ Please see the [project documentation](https://socketry.github.io/process-metric
16
16
 
17
17
  Please see the [project releases](https://socketry.github.io/process-metrics/releases/index) for all releases.
18
18
 
19
+ ### v0.10.0
20
+
21
+ - **Host::Memory**: New per-host struct `Process::Metrics::Host::Memory` with `total`, `used`, `free`, `swap_total`, `swap_used` (all bytes). Use `Host::Memory.capture` to get a snapshot; `.supported?` indicates platform support.
22
+
19
23
  ### v0.9.0
20
24
 
21
25
  - `Process::Metrics::Memory.total_size` takes into account cgroup limits.
22
26
  - On Linux, capturing faults is optional, controlled by `capture(faults: true/false)`.
23
27
  - Report all sizes in bytes for consistency.
24
28
 
29
+ ### v0.8.0
30
+
31
+ - Kill `ps` before waiting to avoid hanging when using the process-status backend.
32
+ - Ignore `Errno::EACCES` when reading process information.
33
+ - Cleaner process management for the `ps`-based capture path.
34
+
25
35
  ### v0.7.0
26
36
 
27
37
  - Be more proactive about returning nil if memory capture failed.
@@ -61,18 +71,6 @@ Please see the [project releases](https://socketry.github.io/process-metrics/rel
61
71
  - Added missing dependencies: `bake-test-external` and `json` gem.
62
72
  - Added summary lines for PSS (Proportional Set Size) and USS (Unique Set Size).
63
73
 
64
- ### v0.2.1
65
-
66
- - Added missing dependency to gemspec.
67
- - Added example of command line usage to documentation.
68
- - Renamed `rsz` to `rss` (Resident Set Size) for consistency across Darwin and Linux platforms.
69
-
70
- ### v0.2.0
71
-
72
- - Added `process-metrics` command line interface for monitoring processes.
73
- - Implemented structured data using Ruby structs for better performance and clarity.
74
- - Added documentation about PSS (Proportional Set Size) and USS (Unique Set Size) metrics.
75
-
76
74
  ## Contributing
77
75
 
78
76
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # Releases
2
2
 
3
+ ## v0.10.0
4
+
5
+ - **Host::Memory**: New per-host struct `Process::Metrics::Host::Memory` with `total`, `used`, `free`, `swap_total`, `swap_used` (all bytes). Use `Host::Memory.capture` to get a snapshot; `.supported?` indicates platform support.
6
+
3
7
  ## v0.9.0
4
8
 
5
9
  - `Process::Metrics::Memory.total_size` takes into account cgroup limits.
6
10
  - On Linux, capturing faults is optional, controlled by `capture(faults: true/false)`.
7
11
  - Report all sizes in bytes for consistency.
8
12
 
13
+ ## v0.8.0
14
+
15
+ - Kill `ps` before waiting to avoid hanging when using the process-status backend.
16
+ - Ignore `Errno::EACCES` when reading process information.
17
+ - Cleaner process management for the `ps`-based capture path.
18
+
9
19
  ## v0.7.0
10
20
 
11
21
  - Be more proactive about returning nil if memory capture failed.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process-metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -94,6 +94,11 @@ files:
94
94
  - lib/process/metrics/command/summary.rb
95
95
  - lib/process/metrics/command/top.rb
96
96
  - lib/process/metrics/general.rb
97
+ - lib/process/metrics/general/linux.rb
98
+ - lib/process/metrics/general/process_status.rb
99
+ - lib/process/metrics/host/memory.rb
100
+ - lib/process/metrics/host/memory/darwin.rb
101
+ - lib/process/metrics/host/memory/linux.rb
97
102
  - lib/process/metrics/memory.rb
98
103
  - lib/process/metrics/memory/darwin.rb
99
104
  - lib/process/metrics/memory/linux.rb
metadata.gz.sig CHANGED
Binary file