process-metrics 0.10.2 → 0.11.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/lib/process/metrics/host/memory/darwin.rb +1 -1
- data/lib/process/metrics/host/memory/linux/cgroup_v1.rb +91 -0
- data/lib/process/metrics/host/memory/linux/cgroup_v2.rb +88 -0
- data/lib/process/metrics/host/memory/linux/meminfo.rb +72 -0
- data/lib/process/metrics/host/memory/linux.rb +16 -102
- data/lib/process/metrics/host/memory.rb +19 -4
- data/lib/process/metrics/host.rb +20 -0
- data/lib/process/metrics/memory.rb +1 -1
- data/lib/process/metrics/version.rb +1 -1
- data/readme.md +4 -6
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +6 -21
- metadata.gz.sig +0 -0
- data/bin/process-metrics +0 -29
- data/lib/process/metrics/command/summary.rb +0 -217
- data/lib/process/metrics/command/top.rb +0 -40
- data/lib/process/metrics/command.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06e77d37339d28a6405332440e7049065b61defd68fb69c9b1e794f79aba8ac7
|
|
4
|
+
data.tar.gz: 6ac90b5a274be90958fb497fe2859807b21700dbd5a7a336278b5c7b2858e2d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04a5adc405715e2406a8601a58a65885f9026d0c5009822e23eb4c739e727265787eb5ed32218c1316f75a50eba4ac6e3dc6518176d539028bf00a18f9ccbbb9
|
|
7
|
+
data.tar.gz: dd436625c1ee3a6b3a8cb6424fb05ad35e5243adc961477632acd3ab9e006cd54435c7b662cc2698b06b6e5f4a61c4082c6957d67cb84306ae62d9f075f743c2
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -38,7 +38,7 @@ module Process
|
|
|
38
38
|
used = [total - free, 0].max
|
|
39
39
|
swap_total, swap_used = capture_swap
|
|
40
40
|
|
|
41
|
-
return Host::Memory.new(total, used,
|
|
41
|
+
return Host::Memory.new(total, used, swap_total, swap_used, nil)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Total physical RAM in bytes, from sysctl hw.memsize.
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
class Memory::Linux::CgroupV1
|
|
10
|
+
CGROUP_V1_UNLIMITED_THRESHOLD = 2**60
|
|
11
|
+
DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup"
|
|
12
|
+
|
|
13
|
+
def self.supported?(cgroup_root = DEFAULT_CGROUP_ROOT)
|
|
14
|
+
root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
15
|
+
|
|
16
|
+
if File.exist?("#{root}/memory/memory.limit_in_bytes") && File.exist?("#{root}/memory/memory.usage_in_bytes")
|
|
17
|
+
return true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
return false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(cgroup_root: nil)
|
|
24
|
+
@cgroup_root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def capture
|
|
28
|
+
total = read_total
|
|
29
|
+
return nil unless total && total.positive?
|
|
30
|
+
|
|
31
|
+
used = read_used
|
|
32
|
+
return nil unless used
|
|
33
|
+
used = 0 if used.negative?
|
|
34
|
+
used = [used, total].min
|
|
35
|
+
|
|
36
|
+
swap_total, swap_used = read_swap
|
|
37
|
+
reclaimable = read_reclaimable
|
|
38
|
+
|
|
39
|
+
return Host::Memory.new(total, used, swap_total, swap_used, reclaimable)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def path(name)
|
|
45
|
+
"#{@cgroup_root}/memory/#{name}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read_total
|
|
49
|
+
limit = File.read(path("memory.limit_in_bytes")).strip.to_i
|
|
50
|
+
|
|
51
|
+
if limit <= 0 || limit >= CGROUP_V1_UNLIMITED_THRESHOLD
|
|
52
|
+
return nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return limit
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def read_used
|
|
59
|
+
File.read(path("memory.usage_in_bytes")).strip.to_i
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def read_reclaimable
|
|
63
|
+
unless content = (File.read(path("memory.stat")) rescue nil)
|
|
64
|
+
return nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
match = content.match(/^cache\s+(\d+)/m)
|
|
68
|
+
|
|
69
|
+
return match ? match[1].to_i : nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def read_swap
|
|
73
|
+
unless content = (File.read("/proc/meminfo") rescue nil)
|
|
74
|
+
return [nil, nil]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
swap_total_kb = content[/SwapTotal:\s*(\d+)\s*kB/, 1]&.to_i
|
|
78
|
+
swap_free_kb = content[/SwapFree:\s*(\d+)\s*kB/, 1]&.to_i
|
|
79
|
+
|
|
80
|
+
unless swap_total_kb
|
|
81
|
+
return [nil, nil]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
swap_total = swap_total_kb * 1024
|
|
85
|
+
swap_used = (swap_total_kb - (swap_free_kb || 0)) * 1024
|
|
86
|
+
return swap_total, swap_used
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
class Memory::Linux::CgroupV2
|
|
10
|
+
DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup"
|
|
11
|
+
|
|
12
|
+
def self.supported?(cgroup_root = DEFAULT_CGROUP_ROOT)
|
|
13
|
+
root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
14
|
+
|
|
15
|
+
if File.exist?("#{root}/memory.current") && File.exist?("#{root}/memory.max")
|
|
16
|
+
return true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
return false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(cgroup_root: nil)
|
|
23
|
+
@cgroup_root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def capture
|
|
27
|
+
total = read_total
|
|
28
|
+
return nil unless total && total.positive?
|
|
29
|
+
|
|
30
|
+
used = read_used
|
|
31
|
+
used = 0 if used.nil? || used.negative?
|
|
32
|
+
used = [used, total].min
|
|
33
|
+
|
|
34
|
+
swap_total, swap_used = read_swap
|
|
35
|
+
reclaimable = read_reclaimable
|
|
36
|
+
|
|
37
|
+
return Host::Memory.new(total, used, swap_total, swap_used, reclaimable)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def path(name)
|
|
43
|
+
"#{@cgroup_root}/#{name}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def read_total
|
|
47
|
+
limit = File.read(path("memory.max")).strip
|
|
48
|
+
|
|
49
|
+
if limit == "max"
|
|
50
|
+
return nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return limit.to_i
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def read_used
|
|
57
|
+
File.read(path("memory.current")).strip.to_i
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_reclaimable
|
|
61
|
+
unless content = (File.read(path("memory.stat")) rescue nil)
|
|
62
|
+
return nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
match = content.match(/^file\s+(\d+)/m)
|
|
66
|
+
return match ? match[1].to_i : nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_swap
|
|
70
|
+
unless content = (File.read("/proc/meminfo") rescue nil)
|
|
71
|
+
return [nil, nil]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
swap_total_kb = content[/SwapTotal:\s*(\d+)\s*kB/, 1]&.to_i
|
|
75
|
+
swap_free_kb = content[/SwapFree:\s*(\d+)\s*kB/, 1]&.to_i
|
|
76
|
+
|
|
77
|
+
unless swap_total_kb
|
|
78
|
+
return [nil, nil]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
swap_total = swap_total_kb * 1024
|
|
82
|
+
swap_used = (swap_total_kb - (swap_free_kb || 0)) * 1024
|
|
83
|
+
return swap_total, swap_used
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
class Memory::Linux::Meminfo
|
|
10
|
+
def self.supported?
|
|
11
|
+
File.exist?("/proc/meminfo")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def capture
|
|
15
|
+
content = File.read("/proc/meminfo") rescue nil
|
|
16
|
+
return nil unless content
|
|
17
|
+
|
|
18
|
+
total = read_total(content)
|
|
19
|
+
return nil unless total && total.positive?
|
|
20
|
+
|
|
21
|
+
used = read_used(content, total)
|
|
22
|
+
used = 0 if used.nil? || used.negative?
|
|
23
|
+
used = [used, total].min
|
|
24
|
+
|
|
25
|
+
swap_total, swap_used = read_swap(content)
|
|
26
|
+
reclaimable = read_reclaimable(content)
|
|
27
|
+
|
|
28
|
+
return Host::Memory.new(total, used, swap_total, swap_used, reclaimable)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def read_total(content)
|
|
34
|
+
match = content.match(/MemTotal:\s*(?<total>\d+)\s*kB/m)
|
|
35
|
+
match ? match[:total].to_i * 1024 : nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def read_used(content, total)
|
|
39
|
+
available_kb = content[/MemAvailable:\s*(\d+)\s*kB/, 1]&.to_i
|
|
40
|
+
available_kb ||= content[/MemFree:\s*(\d+)\s*kB/, 1]&.to_i
|
|
41
|
+
|
|
42
|
+
unless available_kb
|
|
43
|
+
return nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return [total - (available_kb * 1024), 0].max
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def read_reclaimable(content)
|
|
50
|
+
cached_kb = content[/Cached:\s*(\d+)\s*kB/, 1]&.to_i || 0
|
|
51
|
+
buffers_kb = content[/Buffers:\s*(\d+)\s*kB/, 1]&.to_i || 0
|
|
52
|
+
sreclaimable_kb = content[/SReclaimable:\s*(\d+)\s*kB/, 1]&.to_i || 0
|
|
53
|
+
|
|
54
|
+
return (cached_kb + buffers_kb + sreclaimable_kb) * 1024
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def read_swap(content)
|
|
58
|
+
swap_total_kb = content[/SwapTotal:\s*(\d+)\s*kB/, 1]&.to_i
|
|
59
|
+
swap_free_kb = content[/SwapFree:\s*(\d+)\s*kB/, 1]&.to_i
|
|
60
|
+
|
|
61
|
+
unless swap_total_kb
|
|
62
|
+
return [nil, nil]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
swap_total = swap_total_kb * 1024
|
|
66
|
+
swap_used = (swap_total_kb - (swap_free_kb || 0)) * 1024
|
|
67
|
+
return swap_total, swap_used
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -6,124 +6,38 @@
|
|
|
6
6
|
module Process
|
|
7
7
|
module Metrics
|
|
8
8
|
module Host
|
|
9
|
-
# Linux
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
9
|
+
# Linux host memory: tries cgroup v2, then cgroup v1, then /proc/meminfo.
|
|
10
|
+
module Memory::Linux
|
|
11
|
+
DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup"
|
|
18
12
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
13
|
+
def self.capture(cgroup_root: DEFAULT_CGROUP_ROOT)
|
|
14
|
+
if Memory::Linux::CgroupV2.supported?(cgroup_root)
|
|
15
|
+
if capture = Memory::Linux::CgroupV2.new(cgroup_root: cgroup_root).capture
|
|
16
|
+
return capture
|
|
71
17
|
end
|
|
72
18
|
end
|
|
73
19
|
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
20
|
+
if Memory::Linux::CgroupV1.supported?(cgroup_root)
|
|
21
|
+
if capture = Memory::Linux::CgroupV1.new(cgroup_root: cgroup_root).capture
|
|
22
|
+
return capture
|
|
90
23
|
end
|
|
91
24
|
end
|
|
92
25
|
|
|
93
|
-
|
|
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
|
|
26
|
+
return Memory::Linux::Meminfo.new.capture if Memory::Linux::Meminfo.supported?
|
|
117
27
|
end
|
|
118
28
|
end
|
|
119
29
|
end
|
|
120
30
|
end
|
|
121
31
|
end
|
|
122
32
|
|
|
33
|
+
require_relative "linux/cgroup_v2"
|
|
34
|
+
require_relative "linux/cgroup_v1"
|
|
35
|
+
require_relative "linux/meminfo"
|
|
36
|
+
|
|
123
37
|
# Wire Host::Memory to this implementation on Linux.
|
|
124
38
|
class << Process::Metrics::Host::Memory
|
|
125
39
|
def capture
|
|
126
|
-
Process::Metrics::Host::Memory::Linux.
|
|
40
|
+
Process::Metrics::Host::Memory::Linux.capture
|
|
127
41
|
end
|
|
128
42
|
|
|
129
43
|
def supported?
|
|
@@ -8,22 +8,37 @@ module Process
|
|
|
8
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
9
|
module Host
|
|
10
10
|
# Struct for host memory snapshot. All sizes in bytes.
|
|
11
|
+
# Stored: total_size, used_size, swap_*, reclaimable_size. free_size and available_size are derived.
|
|
11
12
|
# @attribute total_size [Integer] Total memory (cgroup limit when in a container, else physical RAM).
|
|
12
|
-
# @attribute used_size [Integer] Memory in use (
|
|
13
|
-
# @attribute free_size [Integer] Available memory (MemAvailable-style: free + reclaimable).
|
|
13
|
+
# @attribute used_size [Integer] Memory in use (kernel/cgroup view; on Linux includes reclaimable e.g. page cache).
|
|
14
14
|
# @attribute swap_total_size [Integer, nil] Total swap, or nil if not available.
|
|
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
|
+
Memory = Struct.new(:total_size, :used_size, :swap_total_size, :swap_used_size, :reclaimable_size) do
|
|
18
|
+
def to_h
|
|
19
|
+
super.merge(free_size: free_size, available_size: available_size)
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
alias as_json to_h
|
|
18
23
|
|
|
19
24
|
def to_json(*arguments)
|
|
20
25
|
as_json.to_json(*arguments)
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
# Complement of used: total_size - used_size. Same meaning on all platforms.
|
|
29
|
+
def free_size
|
|
30
|
+
total_size - used_size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Memory that could be used: free_size plus reclaimable. Use this for "available" when reclaimable_size is set; otherwise equals free_size.
|
|
34
|
+
def available_size
|
|
35
|
+
free_size + (reclaimable_size || 0)
|
|
36
|
+
end
|
|
37
|
+
|
|
23
38
|
# Create a zero-initialized Host::Memory instance.
|
|
24
39
|
# @returns [Memory]
|
|
25
40
|
def self.zero
|
|
26
|
-
self.new(0, 0,
|
|
41
|
+
self.new(0, 0, nil, nil, nil)
|
|
27
42
|
end
|
|
28
43
|
|
|
29
44
|
# Whether host memory capture is supported on this platform.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "host/memory"
|
|
7
|
+
|
|
8
|
+
module Process
|
|
9
|
+
module Metrics
|
|
10
|
+
module Host
|
|
11
|
+
# System name from uname -a (kernel name, nodename, release, etc.). Returns nil if uname is not available.
|
|
12
|
+
# @returns [String, nil]
|
|
13
|
+
def self.name
|
|
14
|
+
IO.popen(["uname", "-a"], "r", &:read)&.strip
|
|
15
|
+
rescue Errno::ENOENT
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
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.11.0
|
|
20
|
+
|
|
21
|
+
- `process-metrics` command is removed, replaced with `bake process:metrics`.
|
|
22
|
+
|
|
19
23
|
### v0.10.2
|
|
20
24
|
|
|
21
25
|
- Add `Process::Metrics::Memory#private_size` for the sum of private (unshared) pages (Private\_Clean + Private\_Dirty); `#unique_size` is now an alias for `#private_size`.
|
|
@@ -56,12 +60,6 @@ Please see the [project releases](https://socketry.github.io/process-metrics/rel
|
|
|
56
60
|
|
|
57
61
|
- Fixed Linux memory usage capture to correctly read memory statistics.
|
|
58
62
|
|
|
59
|
-
### v0.5.0
|
|
60
|
-
|
|
61
|
-
- Added `--total-memory` option for scaling memory usage graphs, allowing users to set custom total memory values.
|
|
62
|
-
- Improved support for proportional memory usage (PSS).
|
|
63
|
-
- Exposed total system memory information.
|
|
64
|
-
|
|
65
63
|
## Contributing
|
|
66
64
|
|
|
67
65
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.11.0
|
|
4
|
+
|
|
5
|
+
- `process-metrics` command is removed, replaced with `bake process:metrics`.
|
|
6
|
+
|
|
3
7
|
## v0.10.2
|
|
4
8
|
|
|
5
9
|
- Add `Process::Metrics::Memory#private_size` for the sum of private (unshared) pages (Private\_Clean + Private\_Dirty); `#unique_size` is now an alias for `#private_size`.
|
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -67,38 +67,23 @@ dependencies:
|
|
|
67
67
|
- - "~>"
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
69
|
version: '2'
|
|
70
|
-
|
|
71
|
-
name: samovar
|
|
72
|
-
requirement: !ruby/object:Gem::Requirement
|
|
73
|
-
requirements:
|
|
74
|
-
- - "~>"
|
|
75
|
-
- !ruby/object:Gem::Version
|
|
76
|
-
version: '2.1'
|
|
77
|
-
type: :runtime
|
|
78
|
-
prerelease: false
|
|
79
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
-
requirements:
|
|
81
|
-
- - "~>"
|
|
82
|
-
- !ruby/object:Gem::Version
|
|
83
|
-
version: '2.1'
|
|
84
|
-
executables:
|
|
85
|
-
- process-metrics
|
|
70
|
+
executables: []
|
|
86
71
|
extensions: []
|
|
87
72
|
extra_rdoc_files: []
|
|
88
73
|
files:
|
|
89
|
-
- bin/process-metrics
|
|
90
74
|
- context/getting-started.md
|
|
91
75
|
- context/index.yaml
|
|
92
76
|
- lib/process/metrics.rb
|
|
93
|
-
- lib/process/metrics/command.rb
|
|
94
|
-
- lib/process/metrics/command/summary.rb
|
|
95
|
-
- lib/process/metrics/command/top.rb
|
|
96
77
|
- lib/process/metrics/general.rb
|
|
97
78
|
- lib/process/metrics/general/linux.rb
|
|
98
79
|
- lib/process/metrics/general/process_status.rb
|
|
80
|
+
- lib/process/metrics/host.rb
|
|
99
81
|
- lib/process/metrics/host/memory.rb
|
|
100
82
|
- lib/process/metrics/host/memory/darwin.rb
|
|
101
83
|
- lib/process/metrics/host/memory/linux.rb
|
|
84
|
+
- lib/process/metrics/host/memory/linux/cgroup_v1.rb
|
|
85
|
+
- lib/process/metrics/host/memory/linux/cgroup_v2.rb
|
|
86
|
+
- lib/process/metrics/host/memory/linux/meminfo.rb
|
|
102
87
|
- lib/process/metrics/memory.rb
|
|
103
88
|
- lib/process/metrics/memory/darwin.rb
|
|
104
89
|
- lib/process/metrics/memory/linux.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|
data/bin/process-metrics
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
5
|
-
#
|
|
6
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
-
# of this software and associated documentation files (the "Software"), to deal
|
|
8
|
-
# in the Software without restriction, including without limitation the rights
|
|
9
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
-
# copies of the Software, and to permit persons to whom the Software is
|
|
11
|
-
# furnished to do so, subject to the following conditions:
|
|
12
|
-
#
|
|
13
|
-
# The above copyright notice and this permission notice shall be included in
|
|
14
|
-
# all copies or substantial portions of the Software.
|
|
15
|
-
#
|
|
16
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
-
# THE SOFTWARE.
|
|
23
|
-
|
|
24
|
-
require_relative "../lib/process/metrics/command"
|
|
25
|
-
|
|
26
|
-
begin
|
|
27
|
-
Process::Metrics::Command.call
|
|
28
|
-
rescue Interrupt
|
|
29
|
-
end
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-2026, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
require "samovar"
|
|
7
|
-
|
|
8
|
-
require_relative "../general"
|
|
9
|
-
|
|
10
|
-
require "console/terminal"
|
|
11
|
-
|
|
12
|
-
module Process
|
|
13
|
-
module Metrics
|
|
14
|
-
module Command
|
|
15
|
-
# Helper module for rendering horizontal progress bars using Unicode block characters.
|
|
16
|
-
module Bar
|
|
17
|
-
BLOCK = [
|
|
18
|
-
" ",
|
|
19
|
-
"▏",
|
|
20
|
-
"▎",
|
|
21
|
-
"▍",
|
|
22
|
-
"▌",
|
|
23
|
-
"▋",
|
|
24
|
-
"▊",
|
|
25
|
-
"▉",
|
|
26
|
-
"█",
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
# Format a fractional value as a horizontal bar.
|
|
30
|
-
# @parameter value [Float] A value between 0.0 and 1.0 representing the fill level.
|
|
31
|
-
# @parameter width [Integer] The width of the bar in characters.
|
|
32
|
-
# @returns [String] A string of Unicode block characters representing the filled bar.
|
|
33
|
-
def self.format(value, width)
|
|
34
|
-
blocks = width * value
|
|
35
|
-
full_blocks = blocks.floor
|
|
36
|
-
partial_block = ((blocks - full_blocks) * BLOCK.size).floor
|
|
37
|
-
|
|
38
|
-
if partial_block.zero?
|
|
39
|
-
BLOCK.last * full_blocks
|
|
40
|
-
else
|
|
41
|
-
"#{BLOCK.last * full_blocks}#{BLOCK[partial_block]}"
|
|
42
|
-
end.ljust(width)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Command that displays a formatted summary of memory usage statistics for processes.
|
|
47
|
-
class Summary < Samovar::Command
|
|
48
|
-
self.description = "Display a summary of memory usage statistics."
|
|
49
|
-
|
|
50
|
-
options do
|
|
51
|
-
option "--pid <integer>", "Report on a single process id.", type: Integer
|
|
52
|
-
option "-p/--ppid <integer>", "Report on all children of this process id.", type: Integer
|
|
53
|
-
|
|
54
|
-
option "--total-memory <integer>", "Set the total memory relative to the usage (MiB).", type: Integer
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Get the configured terminal for styled output.
|
|
58
|
-
# @returns [Console::Terminal] A terminal object with color/style definitions.
|
|
59
|
-
def terminal
|
|
60
|
-
terminal = Console::Terminal.for($stdout)
|
|
61
|
-
|
|
62
|
-
# terminal[:pid] = terminal.style(:blue)
|
|
63
|
-
terminal[:command] = terminal.style(nil, nil, :bold)
|
|
64
|
-
terminal[:key] = terminal.style(:cyan)
|
|
65
|
-
|
|
66
|
-
terminal[:low] = terminal.style(:green)
|
|
67
|
-
terminal[:medium] = terminal.style(:yellow)
|
|
68
|
-
terminal[:high] = terminal.style(:red)
|
|
69
|
-
|
|
70
|
-
return terminal
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Format a processor utilization percentage with color-coded bar.
|
|
74
|
-
# @parameter value [Float] The CPU utilization percentage (0.0-100.0).
|
|
75
|
-
# @parameter terminal [Console::Terminal] The terminal to output styled text.
|
|
76
|
-
def format_processor_utilization(value, terminal)
|
|
77
|
-
if value > 80.0
|
|
78
|
-
intensity = :high
|
|
79
|
-
elsif value > 50.0
|
|
80
|
-
intensity = :medium
|
|
81
|
-
else
|
|
82
|
-
intensity = :low
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
formatted = "%5.1f%% " % value
|
|
86
|
-
|
|
87
|
-
terminal.print(formatted.rjust(10), intensity, "[", Bar.format(value / 100.0, 60), "]", :reset)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
UNITS = ["KiB", "MiB", "GiB"]
|
|
91
|
-
|
|
92
|
-
# Format a memory size value in human-readable units.
|
|
93
|
-
# @parameter value [Numeric] The size value in bytes.
|
|
94
|
-
# @parameter units [Array(String)] The unit labels to use for scaling.
|
|
95
|
-
# @returns [String] A formatted string with value and unit (e.g., "512KiB", "1.5MiB").
|
|
96
|
-
def format_size(value, units: UNITS)
|
|
97
|
-
unit = -1
|
|
98
|
-
|
|
99
|
-
while value >= 1024.0 && unit < units.size - 1
|
|
100
|
-
value /= 1024.0
|
|
101
|
-
unit += 1
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
if unit < 0
|
|
105
|
-
# Value is less than 1 KiB, show in bytes
|
|
106
|
-
return "#{value.round(0)}B"
|
|
107
|
-
else
|
|
108
|
-
return "#{value.round(unit)}#{units[unit]}"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Format a memory value with a horizontal bar showing utilization relative to total.
|
|
113
|
-
# @parameter value [Numeric] The memory value in bytes.
|
|
114
|
-
# @parameter total [Numeric] The total memory available in bytes.
|
|
115
|
-
# @parameter terminal [Console::Terminal] The terminal to output styled text.
|
|
116
|
-
def format_memory(value, total, terminal)
|
|
117
|
-
if value > (total * 0.8)
|
|
118
|
-
intensity = :high
|
|
119
|
-
elsif value > (total * 0.5)
|
|
120
|
-
intensity = :medium
|
|
121
|
-
else
|
|
122
|
-
intensity = :low
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
formatted = (format_size(value) + " ").rjust(10)
|
|
126
|
-
|
|
127
|
-
terminal.print(formatted, intensity, "[", Bar.format(value / total.to_f, 60), "]", :reset)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Get the total memory to use for percentage calculations.
|
|
131
|
-
# @returns [Integer] Total memory in bytes.
|
|
132
|
-
def total_memory
|
|
133
|
-
if total_memory = @options[:total_memory]
|
|
134
|
-
# Convert from MiB to bytes
|
|
135
|
-
return total_memory * 1024 * 1024
|
|
136
|
-
else
|
|
137
|
-
return Process::Metrics::Memory.total_size
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Execute the summary command, capturing and displaying process metrics.
|
|
142
|
-
def call
|
|
143
|
-
# Validate required arguments: at least one of --pid or --ppid must be provided:
|
|
144
|
-
unless @options[:pid] || @options[:ppid]
|
|
145
|
-
raise Samovar::MissingValueError.new(self, "pid or ppid")
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
terminal = self.terminal
|
|
149
|
-
|
|
150
|
-
summary = Process::Metrics::General.capture(pid: @options[:pid], ppid: @options[:ppid])
|
|
151
|
-
|
|
152
|
-
format_memory = self.method(:format_memory).curry
|
|
153
|
-
shared_memory = 0
|
|
154
|
-
private_memory = 0
|
|
155
|
-
total_memory = self.total_memory
|
|
156
|
-
|
|
157
|
-
proportional = true
|
|
158
|
-
|
|
159
|
-
summary.each do |pid, general|
|
|
160
|
-
terminal.print_line(:pid, pid, :reset, " ", :command, general[:command])
|
|
161
|
-
|
|
162
|
-
terminal.print(:key, "Processor Usage: ".rjust(20), :reset)
|
|
163
|
-
format_processor_utilization(general.processor_utilization, terminal)
|
|
164
|
-
terminal.print_line
|
|
165
|
-
|
|
166
|
-
if memory = general.memory
|
|
167
|
-
shared_memory += memory.proportional_size
|
|
168
|
-
private_memory += memory.unique_size
|
|
169
|
-
|
|
170
|
-
terminal.print_line(
|
|
171
|
-
:key, "Memory: ".rjust(20), :reset,
|
|
172
|
-
format_memory[memory.proportional_size, total_memory]
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
terminal.print_line(
|
|
176
|
-
:key, "Private Memory: ".rjust(20), :reset,
|
|
177
|
-
format_memory[memory.unique_size, total_memory]
|
|
178
|
-
)
|
|
179
|
-
else
|
|
180
|
-
shared_memory += general.resident_size
|
|
181
|
-
proportional = false
|
|
182
|
-
|
|
183
|
-
terminal.print_line(
|
|
184
|
-
:key, "Memory: ".rjust(20), :reset,
|
|
185
|
-
format_memory[general.resident_size, total_memory]
|
|
186
|
-
)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
terminal.print_line("Summary")
|
|
191
|
-
|
|
192
|
-
if proportional
|
|
193
|
-
terminal.print_line(
|
|
194
|
-
:key, "Memory: ".rjust(20), :reset,
|
|
195
|
-
format_memory[shared_memory, total_memory]
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
terminal.print_line(
|
|
199
|
-
:key, "Private Memory: ".rjust(20), :reset,
|
|
200
|
-
format_memory[private_memory, total_memory]
|
|
201
|
-
)
|
|
202
|
-
else
|
|
203
|
-
terminal.print_line(
|
|
204
|
-
:key, "Memory: ".rjust(20), :reset,
|
|
205
|
-
format_memory[memory, total_memory]
|
|
206
|
-
)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
terminal.print_line(
|
|
210
|
-
:key, "Memory (Total): ".rjust(20), :reset,
|
|
211
|
-
format_memory[shared_memory + private_memory, total_memory]
|
|
212
|
-
)
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-2025, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
require "samovar"
|
|
7
|
-
|
|
8
|
-
require_relative "summary"
|
|
9
|
-
require_relative "../version"
|
|
10
|
-
|
|
11
|
-
module Process
|
|
12
|
-
module Metrics
|
|
13
|
-
module Command
|
|
14
|
-
# Top-level command entry point for the process-metrics CLI.
|
|
15
|
-
class Top < Samovar::Command
|
|
16
|
-
self.description = "Collect memory usage statistics."
|
|
17
|
-
|
|
18
|
-
options do
|
|
19
|
-
option "-h/--help", "Print out help information."
|
|
20
|
-
option "-v/--version", "Print out the application version."
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
nested :command, {
|
|
24
|
-
"summary" => Summary,
|
|
25
|
-
}, default: "summary"
|
|
26
|
-
|
|
27
|
-
# Execute the top command, dispatching to nested commands.
|
|
28
|
-
def call
|
|
29
|
-
if @options[:version]
|
|
30
|
-
puts "#{self.name} v#{VERSION}"
|
|
31
|
-
elsif @options[:help]
|
|
32
|
-
self.print_usage
|
|
33
|
-
else
|
|
34
|
-
@command.call
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-2025, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
require_relative "command/top"
|
|
7
|
-
|
|
8
|
-
module Process
|
|
9
|
-
module Metrics
|
|
10
|
-
# @namespace
|
|
11
|
-
module Command
|
|
12
|
-
# Call the default command (Top).
|
|
13
|
-
# @parameter arguments [Array] Command-line arguments to pass through.
|
|
14
|
-
def self.call(*arguments)
|
|
15
|
-
Top.call(*arguments)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|