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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 213edfd5fbe960b9a75ac7574f17828f6b9f24ec5328ebf8c6ee85dcafeae0c2
4
- data.tar.gz: 8daf591b9691f058dbe87dd861c0d8ff2e2669edff94b098ad0de36440963b14
3
+ metadata.gz: 06e77d37339d28a6405332440e7049065b61defd68fb69c9b1e794f79aba8ac7
4
+ data.tar.gz: 6ac90b5a274be90958fb497fe2859807b21700dbd5a7a336278b5c7b2858e2d0
5
5
  SHA512:
6
- metadata.gz: 7cfafb180bf9286f5eb35dd2d5393b77ca88fc86f8b93509aed2f30870255dbba5e617ceceb69c17607bc3e620b7599e8c11ab960323033a97087a2acc45340c
7
- data.tar.gz: 78427458c5dd06674690a1fe3ffab4d1c85a6ce265defce5dc2191be874ee995c87299c86de9b83e88e84318425ca2b16361e69df4b9d26d4badca1dbb8078e7
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, free, swap_total, swap_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 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
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 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
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
- 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
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
- 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
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.new.capture
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 (total_size - free_size).
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
- Memory = Struct.new(:total_size, :used_size, :free_size, :swap_total_size, :swap_used_size) do
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, 0, nil, nil)
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
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require "json"
7
- require_relative "host/memory"
7
+ require_relative "host"
8
8
 
9
9
  module Process
10
10
  module Metrics
@@ -7,6 +7,6 @@
7
7
  module Process
8
8
  # @namespace
9
9
  module Metrics
10
- VERSION = "0.10.2"
10
+ VERSION = "0.11.0"
11
11
  end
12
12
  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.10.2
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
- - !ruby/object:Gem::Dependency
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