process-metrics 0.11.0 → 0.12.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/process/metrics.rb +218 -0
- data/lib/process/metrics/general/linux.rb +12 -6
- data/lib/process/metrics/general/process_status.rb +16 -14
- data/lib/process/metrics/general.rb +2 -1
- data/lib/process/metrics/host/memory/darwin.rb +1 -1
- data/lib/process/metrics/host/memory/linux/cgroup_v1.rb +9 -1
- data/lib/process/metrics/host/memory/linux/cgroup_v2.rb +9 -1
- data/lib/process/metrics/host/memory/linux/meminfo.rb +6 -1
- data/lib/process/metrics/host/memory/linux.rb +4 -1
- data/lib/process/metrics/host/memory.rb +6 -1
- data/lib/process/metrics/host.rb +1 -1
- data/lib/process/metrics/processor.rb +81 -0
- data/lib/process/metrics/version.rb +2 -2
- data/lib/process/metrics.rb +2 -1
- data/readme.md +20 -4
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +5 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4692590f9565bbf61e0854c736a4225749d1e12021b0fa769fdc6cab6cee7002
|
|
4
|
+
data.tar.gz: 138b82d832444faa4eafe5137ae4f25ffd4652b5d2b313fd698b1a11c871cb09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be2a880bd7a6285d5472425860ab1cdf1f031e0aca0e0565e849825e51449a9f1278646a34796f6b43f6e9c19cc7b120850e1470e1cc8bc86d9114e27709b779
|
|
7
|
+
data.tar.gz: 6c149bd8d92118add7f24d2a28d6c9b1ac22e97bd048fb5e30f3ed9e93f4b88bccf3be428255469e566d538ee81ebea72aaa82e7b7db0dce7841e89d8fcba5e0
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
def capture(pid: nil, ppid: nil)
|
|
7
|
+
require "process/metrics/general"
|
|
8
|
+
|
|
9
|
+
Process::Metrics::General.capture(pid: pid, ppid: ppid)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Print metrics for a process or processes.
|
|
13
|
+
#
|
|
14
|
+
# @parameter pid [Integer] The process ID to capture.
|
|
15
|
+
# @parameter ppid [Integer] The parent process ID to capture.
|
|
16
|
+
def metrics(pid: nil, ppid: nil)
|
|
17
|
+
require "process/metrics/general"
|
|
18
|
+
require "process/metrics/host"
|
|
19
|
+
|
|
20
|
+
terminal = self.terminal
|
|
21
|
+
format_memory = self.method(:format_memory).curry
|
|
22
|
+
|
|
23
|
+
# Host name (uname -a) as first line
|
|
24
|
+
if host_name = Process::Metrics::Host.name
|
|
25
|
+
terminal.print_line(host_name, :reset)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
host = Process::Metrics::Host::Memory.capture
|
|
29
|
+
terminal.print_line(
|
|
30
|
+
:key, "Total Memory: ".rjust(20), :reset,
|
|
31
|
+
format_size(host.total_size).rjust(9)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
terminal.print_line(
|
|
35
|
+
:key, "Used Memory: ".rjust(20), :reset,
|
|
36
|
+
format_memory[host.used_size, host.total_size]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
summary = Process::Metrics::General.capture(pid: pid, ppid: ppid)
|
|
40
|
+
|
|
41
|
+
shared_memory = 0
|
|
42
|
+
private_memory = 0
|
|
43
|
+
total_memory = host.total_size
|
|
44
|
+
|
|
45
|
+
proportional = true
|
|
46
|
+
|
|
47
|
+
summary.each do |pid, general|
|
|
48
|
+
terminal.print_line(:pid, pid, :reset, " ", :command, general[:command])
|
|
49
|
+
|
|
50
|
+
terminal.print(:key, "Processor Usage: ".rjust(20), :reset)
|
|
51
|
+
format_processor_utilization(general.processor_utilization, terminal)
|
|
52
|
+
terminal.print_line
|
|
53
|
+
|
|
54
|
+
if memory = general.memory
|
|
55
|
+
shared_memory += memory.proportional_size
|
|
56
|
+
private_memory += memory.unique_size
|
|
57
|
+
|
|
58
|
+
terminal.print_line(
|
|
59
|
+
:key, "Memory: ".rjust(20), :reset,
|
|
60
|
+
format_memory[memory.proportional_size, total_memory]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
terminal.print_line(
|
|
64
|
+
:key, "Private Memory: ".rjust(20), :reset,
|
|
65
|
+
format_memory[memory.unique_size, total_memory]
|
|
66
|
+
)
|
|
67
|
+
else
|
|
68
|
+
shared_memory += general.resident_size
|
|
69
|
+
proportional = false
|
|
70
|
+
|
|
71
|
+
terminal.print_line(
|
|
72
|
+
:key, "Memory: ".rjust(20), :reset,
|
|
73
|
+
format_memory[general.resident_size, total_memory]
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
terminal.print_line("Summary")
|
|
79
|
+
|
|
80
|
+
if proportional
|
|
81
|
+
terminal.print_line(
|
|
82
|
+
:key, "Memory: ".rjust(20), :reset,
|
|
83
|
+
format_memory[shared_memory, total_memory]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
terminal.print_line(
|
|
87
|
+
:key, "Private Memory: ".rjust(20), :reset,
|
|
88
|
+
format_memory[private_memory, total_memory]
|
|
89
|
+
)
|
|
90
|
+
else
|
|
91
|
+
terminal.print_line(
|
|
92
|
+
:key, "Memory: ".rjust(20), :reset,
|
|
93
|
+
format_memory[shared_memory, total_memory]
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
terminal.print_line(
|
|
98
|
+
:key, "Memory (Total): ".rjust(20), :reset,
|
|
99
|
+
format_memory[shared_memory + private_memory, total_memory]
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
protected
|
|
104
|
+
|
|
105
|
+
# Helper module for rendering horizontal progress bars using Unicode block characters.
|
|
106
|
+
module Bar
|
|
107
|
+
BLOCK = [
|
|
108
|
+
" ",
|
|
109
|
+
"▏",
|
|
110
|
+
"▎",
|
|
111
|
+
"▍",
|
|
112
|
+
"▌",
|
|
113
|
+
"▋",
|
|
114
|
+
"▊",
|
|
115
|
+
"▉",
|
|
116
|
+
"█",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# Format a fractional value as a horizontal bar.
|
|
120
|
+
# @parameter value [Float] A value between 0.0 and 1.0 representing the fill level.
|
|
121
|
+
# @parameter width [Integer] The width of the bar in characters.
|
|
122
|
+
# @returns [String] A string of Unicode block characters representing the filled bar.
|
|
123
|
+
def self.format(value, width)
|
|
124
|
+
blocks = width * value
|
|
125
|
+
full_blocks = blocks.floor
|
|
126
|
+
partial_block = ((blocks - full_blocks) * BLOCK.size).floor
|
|
127
|
+
|
|
128
|
+
if partial_block.zero?
|
|
129
|
+
BLOCK.last * full_blocks
|
|
130
|
+
else
|
|
131
|
+
"#{BLOCK.last * full_blocks}#{BLOCK[partial_block]}"
|
|
132
|
+
end.ljust(width)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get the configured terminal for styled output.
|
|
137
|
+
# @returns [Console::Terminal] A terminal object with color/style definitions.
|
|
138
|
+
def terminal
|
|
139
|
+
terminal = Console::Terminal.for($stdout)
|
|
140
|
+
|
|
141
|
+
# terminal[:pid] = terminal.style(:blue)
|
|
142
|
+
terminal[:command] = terminal.style(nil, nil, :bold)
|
|
143
|
+
terminal[:key] = terminal.style(:cyan)
|
|
144
|
+
|
|
145
|
+
terminal[:low] = terminal.style(:green)
|
|
146
|
+
terminal[:medium] = terminal.style(:yellow)
|
|
147
|
+
terminal[:high] = terminal.style(:red)
|
|
148
|
+
|
|
149
|
+
return terminal
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Width in characters for progress bars (label + value + " []" ≈ 33 chars).
|
|
153
|
+
DEFAULT_BAR_WIDTH = 60
|
|
154
|
+
|
|
155
|
+
def bar_width(terminal, prefix_width: 33)
|
|
156
|
+
if width = terminal.width
|
|
157
|
+
return width - prefix_width if width > prefix_width
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
return DEFAULT_BAR_WIDTH
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Format a processor utilization percentage with color-coded bar.
|
|
164
|
+
# @parameter value [Float] The CPU utilization percentage (0.0-100.0).
|
|
165
|
+
# @parameter terminal [Console::Terminal] The terminal to output styled text.
|
|
166
|
+
def format_processor_utilization(value, terminal)
|
|
167
|
+
if value > 80.0
|
|
168
|
+
intensity = :high
|
|
169
|
+
elsif value > 50.0
|
|
170
|
+
intensity = :medium
|
|
171
|
+
else
|
|
172
|
+
intensity = :low
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
formatted = "%5.1f%% " % value
|
|
176
|
+
|
|
177
|
+
terminal.print(formatted.rjust(10), intensity, "[", Bar.format(value / 100.0, bar_width(terminal)), "]", :reset)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
UNITS = ["KiB", "MiB", "GiB"]
|
|
181
|
+
|
|
182
|
+
# Format a memory size value in human-readable units.
|
|
183
|
+
# @parameter value [Numeric] The size value in bytes.
|
|
184
|
+
# @parameter units [Array(String)] The unit labels to use for scaling.
|
|
185
|
+
# @returns [String] A formatted string with value and unit (e.g., "512KiB", "1.5MiB").
|
|
186
|
+
def format_size(value, units: UNITS)
|
|
187
|
+
unit = -1
|
|
188
|
+
|
|
189
|
+
while value >= 1024.0 && unit < units.size - 1
|
|
190
|
+
value /= 1024.0
|
|
191
|
+
unit += 1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if unit < 0
|
|
195
|
+
# Value is less than 1 KiB, show in bytes
|
|
196
|
+
return "#{value.round(0)}B"
|
|
197
|
+
else
|
|
198
|
+
return "#{value.round(unit)}#{units[unit]}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Format a memory value with a horizontal bar showing utilization relative to total.
|
|
203
|
+
# @parameter value [Numeric] The memory value in bytes.
|
|
204
|
+
# @parameter total [Numeric] The total memory available in bytes.
|
|
205
|
+
# @parameter terminal [Console::Terminal] The terminal to output styled text.
|
|
206
|
+
def format_memory(value, total, terminal)
|
|
207
|
+
if value > (total * 0.8)
|
|
208
|
+
intensity = :high
|
|
209
|
+
elsif value > (total * 0.5)
|
|
210
|
+
intensity = :medium
|
|
211
|
+
else
|
|
212
|
+
intensity = :low
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
formatted = (format_size(value) + " ").rjust(10)
|
|
216
|
+
|
|
217
|
+
terminal.print(formatted, intensity, "[", Bar.format(value / total.to_f, bar_width(terminal)), "]", :reset)
|
|
218
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "etc"
|
|
7
7
|
|
|
@@ -18,9 +18,16 @@ module Process
|
|
|
18
18
|
# Page size in bytes for RSS (resident set size is in pages in /proc/pid/stat).
|
|
19
19
|
PAGE_SIZE = Etc.sysconf(Etc::SC_PAGESIZE) rescue 4096
|
|
20
20
|
|
|
21
|
+
# The Unix timestamp when the system booted.
|
|
22
|
+
BOOT_TIME = if File.readable?("/proc/stat")
|
|
23
|
+
File.foreach("/proc/stat") do |line|
|
|
24
|
+
break line.split.last.to_f if line.start_with?("btime ")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
# Whether /proc is available so we can list processes without ps.
|
|
22
29
|
def self.supported?
|
|
23
|
-
File.directory?("/proc") && File.readable?("/proc/self/stat")
|
|
30
|
+
File.directory?("/proc") && File.readable?("/proc/self/stat") && File.readable?("/proc/uptime") && !BOOT_TIME.nil?
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
# 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.
|
|
@@ -38,7 +45,6 @@ module Process
|
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
uptime_jiffies = nil
|
|
41
|
-
|
|
42
48
|
processes = {}
|
|
43
49
|
pids_to_read.each do |pid|
|
|
44
50
|
stat_path = "/proc/#{pid}/stat"
|
|
@@ -56,7 +62,7 @@ module Process
|
|
|
56
62
|
process_group_id = fields[2].to_i
|
|
57
63
|
utime = fields[11].to_i
|
|
58
64
|
stime = fields[12].to_i
|
|
59
|
-
|
|
65
|
+
start_time = fields[19].to_i
|
|
60
66
|
virtual_size = fields[20].to_i
|
|
61
67
|
resident_pages = fields[21].to_i
|
|
62
68
|
|
|
@@ -65,9 +71,8 @@ module Process
|
|
|
65
71
|
uptime_seconds = File.read("/proc/uptime").split(/\s+/).first.to_f
|
|
66
72
|
(uptime_seconds * CLK_TCK).to_i
|
|
67
73
|
end
|
|
68
|
-
|
|
69
74
|
processor_time = (utime + stime).to_f / CLK_TCK
|
|
70
|
-
elapsed_time = [(uptime_jiffies -
|
|
75
|
+
elapsed_time = [(uptime_jiffies - start_time).to_f / CLK_TCK, 0.0].max
|
|
71
76
|
|
|
72
77
|
command = read_command(pid, executable_name)
|
|
73
78
|
|
|
@@ -80,6 +85,7 @@ module Process
|
|
|
80
85
|
resident_pages * PAGE_SIZE,
|
|
81
86
|
processor_time,
|
|
82
87
|
elapsed_time,
|
|
88
|
+
BOOT_TIME + start_time.to_f / CLK_TCK,
|
|
83
89
|
command,
|
|
84
90
|
nil
|
|
85
91
|
)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "time"
|
|
5
7
|
|
|
6
8
|
module Process
|
|
7
9
|
module Metrics
|
|
@@ -12,15 +14,16 @@ module Process
|
|
|
12
14
|
|
|
13
15
|
# The fields that will be extracted from the `ps` command (order matches -o output).
|
|
14
16
|
FIELDS = {
|
|
15
|
-
pid: ->(
|
|
16
|
-
ppid: ->(
|
|
17
|
-
pgid: ->(
|
|
18
|
-
pcpu: ->(
|
|
19
|
-
vsz: ->(
|
|
20
|
-
rss: ->(
|
|
21
|
-
time: Process::Metrics.
|
|
22
|
-
etime: Process::Metrics.
|
|
23
|
-
|
|
17
|
+
pid: ->(values){values.shift.to_i},
|
|
18
|
+
ppid: ->(values){values.shift.to_i},
|
|
19
|
+
pgid: ->(values){values.shift.to_i},
|
|
20
|
+
pcpu: ->(values){values.shift.to_f},
|
|
21
|
+
vsz: ->(values){values.shift.to_i * 1024},
|
|
22
|
+
rss: ->(values){values.shift.to_i * 1024},
|
|
23
|
+
time: ->(values){Process::Metrics.duration(values.shift)},
|
|
24
|
+
etime: ->(values){Process::Metrics.duration(values.shift)},
|
|
25
|
+
lstart: ->(values){Time.strptime(values.shift(5).join(" "), "%a %b %e %H:%M:%S %Y").to_f},
|
|
26
|
+
command: ->(values){values.join(" ")},
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
# Whether process listing via ps is available on this system.
|
|
@@ -48,7 +51,7 @@ module Process
|
|
|
48
51
|
|
|
49
52
|
arguments.push("-o", FIELDS.keys.join(","))
|
|
50
53
|
|
|
51
|
-
spawned_pid = Process.spawn(*arguments, out: output)
|
|
54
|
+
spawned_pid = Process.spawn({"LC_ALL" => "C"}, *arguments, out: output)
|
|
52
55
|
output.close
|
|
53
56
|
|
|
54
57
|
input.readlines.map(&:strip)
|
|
@@ -71,10 +74,9 @@ module Process
|
|
|
71
74
|
lines.each do |line|
|
|
72
75
|
next if line.empty?
|
|
73
76
|
|
|
74
|
-
values = line.split(/\s
|
|
75
|
-
|
|
77
|
+
values = line.split(/\s+/)
|
|
78
|
+
record = FIELDS.values.map{|parser| parser.call(values)}
|
|
76
79
|
|
|
77
|
-
record = FIELDS.keys.map.with_index{|key, i| FIELDS[key].call(values[i])}
|
|
78
80
|
instance = General.new(*record, nil)
|
|
79
81
|
processes[instance.process_id] = instance
|
|
80
82
|
end
|
|
@@ -35,7 +35,7 @@ module Process
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# General process information.
|
|
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)
|
|
38
|
+
class General < Struct.new(:process_id, :parent_process_id, :process_group_id, :processor_utilization, :virtual_size, :resident_size, :processor_time, :elapsed_time, :start_time, :command, :memory)
|
|
39
39
|
# Convert the object to a JSON serializable hash.
|
|
40
40
|
def as_json
|
|
41
41
|
{
|
|
@@ -48,6 +48,7 @@ module Process
|
|
|
48
48
|
resident_size: self.resident_size,
|
|
49
49
|
processor_time: self.processor_time,
|
|
50
50
|
elapsed_time: self.elapsed_time,
|
|
51
|
+
start_time: self.start_time,
|
|
51
52
|
command: self.command,
|
|
52
53
|
memory: self.memory&.as_json,
|
|
53
54
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Process
|
|
7
7
|
module Metrics
|
|
8
8
|
module Host
|
|
9
|
+
# Captures host memory limits and usage from a cgroup v1 filesystem.
|
|
9
10
|
class Memory::Linux::CgroupV1
|
|
10
11
|
CGROUP_V1_UNLIMITED_THRESHOLD = 2**60
|
|
11
12
|
DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup"
|
|
12
13
|
|
|
14
|
+
# Whether cgroup v1 memory metrics are available.
|
|
15
|
+
# @parameter cgroup_root [String | Nil] The root of the cgroup filesystem.
|
|
16
|
+
# @returns [Boolean] Whether the required cgroup v1 files exist.
|
|
13
17
|
def self.supported?(cgroup_root = DEFAULT_CGROUP_ROOT)
|
|
14
18
|
root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
15
19
|
|
|
@@ -20,10 +24,14 @@ module Process
|
|
|
20
24
|
return false
|
|
21
25
|
end
|
|
22
26
|
|
|
27
|
+
# Initialize the cgroup v1 memory reader.
|
|
28
|
+
# @parameter cgroup_root [String | Nil] The root of the cgroup filesystem.
|
|
23
29
|
def initialize(cgroup_root: nil)
|
|
24
30
|
@cgroup_root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
25
31
|
end
|
|
26
32
|
|
|
33
|
+
# Capture host memory from the cgroup v1 filesystem.
|
|
34
|
+
# @returns [Host::Memory | Nil] The captured host memory, if the cgroup has a finite limit.
|
|
27
35
|
def capture
|
|
28
36
|
total = read_total
|
|
29
37
|
return nil unless total && total.positive?
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Process
|
|
7
7
|
module Metrics
|
|
8
8
|
module Host
|
|
9
|
+
# Captures host memory limits and usage from a cgroup v2 filesystem.
|
|
9
10
|
class Memory::Linux::CgroupV2
|
|
10
11
|
DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup"
|
|
11
12
|
|
|
13
|
+
# Whether cgroup v2 memory metrics are available.
|
|
14
|
+
# @parameter cgroup_root [String | Nil] The root of the cgroup filesystem.
|
|
15
|
+
# @returns [Boolean] Whether the required cgroup v2 files exist.
|
|
12
16
|
def self.supported?(cgroup_root = DEFAULT_CGROUP_ROOT)
|
|
13
17
|
root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
14
18
|
|
|
@@ -19,10 +23,14 @@ module Process
|
|
|
19
23
|
return false
|
|
20
24
|
end
|
|
21
25
|
|
|
26
|
+
# Initialize the cgroup v2 memory reader.
|
|
27
|
+
# @parameter cgroup_root [String | Nil] The root of the cgroup filesystem.
|
|
22
28
|
def initialize(cgroup_root: nil)
|
|
23
29
|
@cgroup_root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
24
30
|
end
|
|
25
31
|
|
|
32
|
+
# Capture host memory from the cgroup v2 filesystem.
|
|
33
|
+
# @returns [Host::Memory | Nil] The captured host memory, if the cgroup has a finite limit.
|
|
26
34
|
def capture
|
|
27
35
|
total = read_total
|
|
28
36
|
return nil unless total && total.positive?
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Process
|
|
7
7
|
module Metrics
|
|
8
8
|
module Host
|
|
9
|
+
# Captures host memory from Linux `/proc/meminfo`.
|
|
9
10
|
class Memory::Linux::Meminfo
|
|
11
|
+
# Whether `/proc/meminfo` is available.
|
|
12
|
+
# @returns [Boolean] Whether the procfs memory information exists.
|
|
10
13
|
def self.supported?
|
|
11
14
|
File.exist?("/proc/meminfo")
|
|
12
15
|
end
|
|
13
16
|
|
|
17
|
+
# Capture host memory from `/proc/meminfo`.
|
|
18
|
+
# @returns [Host::Memory | Nil] The captured host memory, if available.
|
|
14
19
|
def capture
|
|
15
20
|
content = File.read("/proc/meminfo") rescue nil
|
|
16
21
|
return nil unless content
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Process
|
|
7
7
|
module Metrics
|
|
@@ -10,6 +10,9 @@ module Process
|
|
|
10
10
|
module Memory::Linux
|
|
11
11
|
DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup"
|
|
12
12
|
|
|
13
|
+
# Capture host memory from the available Linux interface.
|
|
14
|
+
# @parameter cgroup_root [String] The root of the cgroup filesystem.
|
|
15
|
+
# @returns [Host::Memory | Nil] The captured host memory, if available.
|
|
13
16
|
def self.capture(cgroup_root: DEFAULT_CGROUP_ROOT)
|
|
14
17
|
if Memory::Linux::CgroupV2.supported?(cgroup_root)
|
|
15
18
|
if capture = Memory::Linux::CgroupV2.new(cgroup_root: cgroup_root).capture
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Process
|
|
7
7
|
module Metrics
|
|
@@ -15,12 +15,17 @@ module Process
|
|
|
15
15
|
# @attribute swap_used_size [Integer, nil] Swap in use, or nil if not available.
|
|
16
16
|
# @attribute reclaimable_size [Integer, nil] Reclaimable memory (e.g. page cache, slab), or nil. Included in used_size.
|
|
17
17
|
Memory = Struct.new(:total_size, :used_size, :swap_total_size, :swap_used_size, :reclaimable_size) do
|
|
18
|
+
# Convert the memory snapshot to a hash, including derived sizes.
|
|
19
|
+
# @returns [Hash(Symbol, Integer | Nil)] The memory snapshot fields.
|
|
18
20
|
def to_h
|
|
19
21
|
super.merge(free_size: free_size, available_size: available_size)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
alias as_json to_h
|
|
23
25
|
|
|
26
|
+
# Convert the memory snapshot to JSON.
|
|
27
|
+
# @parameter arguments [Array] Additional arguments passed to the JSON encoder.
|
|
28
|
+
# @returns [String] The encoded memory snapshot.
|
|
24
29
|
def to_json(*arguments)
|
|
25
30
|
as_json.to_json(*arguments)
|
|
26
31
|
end
|
data/lib/process/metrics/host.rb
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Process
|
|
7
|
+
module Metrics
|
|
8
|
+
# Computes interval CPU utilization from cumulative process metrics.
|
|
9
|
+
class Processor
|
|
10
|
+
# An immutable measurement of process CPU usage over an interval.
|
|
11
|
+
# @attribute [Integer] The process ID.
|
|
12
|
+
# @attribute [Float] The elapsed monotonic time in seconds.
|
|
13
|
+
# @attribute [Float] The CPU time consumed during the interval in seconds.
|
|
14
|
+
# @attribute [Float] The CPU utilization, where one fully occupied core is `1.0`.
|
|
15
|
+
class Sample < Struct.new(:process_id, :duration, :processor_time, :utilization)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @private
|
|
19
|
+
Snapshot = Struct.new(:start_time, :processor_time, :timestamp)
|
|
20
|
+
|
|
21
|
+
# Initialize a process metrics sampler.
|
|
22
|
+
# @parameter capture [Interface(:call)] The process snapshot capture callable.
|
|
23
|
+
def initialize(capture: General.method(:capture))
|
|
24
|
+
@capture = capture
|
|
25
|
+
@snapshots = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sample CPU utilization for the given processes.
|
|
29
|
+
# The first observation of each process establishes a baseline and does not produce a sample.
|
|
30
|
+
# @parameter process_ids [Integer | Array(Integer)] The process IDs to sample.
|
|
31
|
+
# @returns [Hash(Integer, Processor::Sample)] The valid interval samples keyed by process ID.
|
|
32
|
+
def sample(process_ids)
|
|
33
|
+
processes = @capture.call(pid: process_ids, memory: false)
|
|
34
|
+
timestamp = now
|
|
35
|
+
return {} unless finite?(timestamp)
|
|
36
|
+
|
|
37
|
+
samples = {}
|
|
38
|
+
snapshots = {}
|
|
39
|
+
|
|
40
|
+
processes.each do |process_id, process|
|
|
41
|
+
start_time = process.start_time
|
|
42
|
+
processor_time = process.processor_time
|
|
43
|
+
next unless finite?(start_time) && finite?(processor_time)
|
|
44
|
+
|
|
45
|
+
current = Snapshot.new(start_time, processor_time, timestamp)
|
|
46
|
+
snapshots[process_id] = current
|
|
47
|
+
|
|
48
|
+
if previous = @snapshots[process_id]
|
|
49
|
+
next unless previous.start_time == current.start_time
|
|
50
|
+
|
|
51
|
+
duration = current.timestamp - previous.timestamp
|
|
52
|
+
processor_time = current.processor_time - previous.processor_time
|
|
53
|
+
next unless finite?(duration) && duration > 0.0
|
|
54
|
+
next unless finite?(processor_time) && processor_time >= 0.0
|
|
55
|
+
|
|
56
|
+
utilization = processor_time / duration
|
|
57
|
+
next unless finite?(utilization)
|
|
58
|
+
|
|
59
|
+
samples[process_id] = Sample.new(process_id, duration, processor_time, utilization).freeze
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@snapshots = snapshots
|
|
64
|
+
|
|
65
|
+
return samples
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Get the current monotonic time.
|
|
71
|
+
def now
|
|
72
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Whether the value is a finite number.
|
|
76
|
+
def finite?(value)
|
|
77
|
+
value.is_a?(Numeric) && value.finite?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2019-
|
|
4
|
+
# Copyright, 2019-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
# @namespace
|
|
7
7
|
module Process
|
|
8
8
|
# @namespace
|
|
9
9
|
module Metrics
|
|
10
|
-
VERSION = "0.
|
|
10
|
+
VERSION = "0.12.0"
|
|
11
11
|
end
|
|
12
12
|
end
|
data/lib/process/metrics.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2019-
|
|
4
|
+
# Copyright, 2019-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "metrics/version"
|
|
7
7
|
require_relative "metrics/general"
|
|
8
|
+
require_relative "metrics/processor"
|
data/readme.md
CHANGED
|
@@ -16,6 +16,10 @@ 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.12.0
|
|
20
|
+
|
|
21
|
+
- Add `Process::Metrics::Processor` for measuring per-process CPU utilization over an interval.
|
|
22
|
+
|
|
19
23
|
### v0.11.0
|
|
20
24
|
|
|
21
25
|
- `process-metrics` command is removed, replaced with `bake process:metrics`.
|
|
@@ -56,10 +60,6 @@ Please see the [project releases](https://socketry.github.io/process-metrics/rel
|
|
|
56
60
|
|
|
57
61
|
- Add support for major and minor page faults on Linux: `Process::Metrics::Memory#major_faults` and `#minor_faults`. Unfortunately these metrics are not available on Darwin (macOS).
|
|
58
62
|
|
|
59
|
-
### v0.5.1
|
|
60
|
-
|
|
61
|
-
- Fixed Linux memory usage capture to correctly read memory statistics.
|
|
62
|
-
|
|
63
63
|
## Contributing
|
|
64
64
|
|
|
65
65
|
We welcome contributions to this project.
|
|
@@ -70,6 +70,22 @@ We welcome contributions to this project.
|
|
|
70
70
|
4. Push to the branch (`git push origin my-new-feature`).
|
|
71
71
|
5. Create new Pull Request.
|
|
72
72
|
|
|
73
|
+
### Running Tests
|
|
74
|
+
|
|
75
|
+
To run the test suite:
|
|
76
|
+
|
|
77
|
+
``` shell
|
|
78
|
+
bundle exec sus
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Making Releases
|
|
82
|
+
|
|
83
|
+
To make a new release:
|
|
84
|
+
|
|
85
|
+
``` shell
|
|
86
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
87
|
+
```
|
|
88
|
+
|
|
73
89
|
### Developer Certificate of Origin
|
|
74
90
|
|
|
75
91
|
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
data/releases.md
CHANGED
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.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -71,6 +71,7 @@ executables: []
|
|
|
71
71
|
extensions: []
|
|
72
72
|
extra_rdoc_files: []
|
|
73
73
|
files:
|
|
74
|
+
- bake/process/metrics.rb
|
|
74
75
|
- context/getting-started.md
|
|
75
76
|
- context/index.yaml
|
|
76
77
|
- lib/process/metrics.rb
|
|
@@ -87,6 +88,7 @@ files:
|
|
|
87
88
|
- lib/process/metrics/memory.rb
|
|
88
89
|
- lib/process/metrics/memory/darwin.rb
|
|
89
90
|
- lib/process/metrics/memory/linux.rb
|
|
91
|
+
- lib/process/metrics/processor.rb
|
|
90
92
|
- lib/process/metrics/version.rb
|
|
91
93
|
- license.md
|
|
92
94
|
- readme.md
|
|
@@ -105,14 +107,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
105
107
|
requirements:
|
|
106
108
|
- - ">="
|
|
107
109
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '3.
|
|
110
|
+
version: '3.3'
|
|
109
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
112
|
requirements:
|
|
111
113
|
- - ">="
|
|
112
114
|
- !ruby/object:Gem::Version
|
|
113
115
|
version: '0'
|
|
114
116
|
requirements: []
|
|
115
|
-
rubygems_version: 4.0.
|
|
117
|
+
rubygems_version: 4.0.10
|
|
116
118
|
specification_version: 4
|
|
117
119
|
summary: Provide detailed OS-specific process metrics.
|
|
118
120
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|