process-metrics 0.8.0 → 0.9.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: b288419449c98ce2b53cc2ecb8d3d2660a473c17677b66e6f18015b803db1482
4
- data.tar.gz: 67b4d639f7744814e47e61aac54ef7c588557a94ca8454926594d742f9cf88ce
3
+ metadata.gz: 7fc7fb298dde1bb1446b8223bf55be24f776fe8e042c0a5e2ee2cf3452e64383
4
+ data.tar.gz: 340908860960f819a24c864e3d64bed4288840fad98dc4a0f33f37e622cc22ce
5
5
  SHA512:
6
- metadata.gz: 0a1686b0609d8009a2d5a26b659876c29c5378d89b35b39777b5d55d53413b2a10558470075ce0303959f77213ceef101cad19547dc95743440d0243af2746e1
7
- data.tar.gz: 13a3e9bbc06eb5412a6b37c499f531eebf58317c1944d0e679dd68d8b87abc4450e62b7166c435334745555d26bb873b9ddc47d65324bdf17832b27865f60a2c
6
+ metadata.gz: d7bef8457eb65f378e63d65a768ac8ab4d5d4ae45bcb97f7489b9e06676afeb5fabe2e7b818d132fcae6436e51394ee810ade201f17f8ffd24211db42452b2a5
7
+ data.tar.gz: 680700b1619d358370114cde3c8d1025c998b0e55ad01d5ee6a3c211857056865f9dd9f5d244ac20ebcb5ec6595f4e8439a2193c28f3765dae17b20df3158b64
checksums.yaml.gz.sig CHANGED
Binary file
@@ -32,29 +32,30 @@ Process::Metrics::General.capture(pid: Process.pid)
32
32
  # =>
33
33
  # {3517456=>
34
34
  # #<struct Process::Metrics::General
35
- # pid=3517456,
36
- # ppid=3517432,
37
- # pgid=3517456,
35
+ # process_id=3517456,
36
+ # parent_process_id=3517432,
37
+ # process_group_id=3517456,
38
38
  # processor_utilization=0.0,
39
- # vsz=0,
40
- # rss=486892,
41
- # time=29928,
42
- # etime=2593,
39
+ # virtual_size=445768278528,
40
+ # resident_size=20348928,
41
+ # processor_time=0.05,
42
+ # elapsed_time=2.0,
43
43
  # command="irb",
44
44
  # memory=
45
45
  # #<struct Process::Metrics::Memory
46
46
  # map_count=193,
47
- # total_size=486896,
48
- # resident_size=30556,
49
- # proportional_size=25672,
50
- # shared_clean_size=5008,
47
+ # resident_size=31289344,
48
+ # proportional_size=26288128,
49
+ # shared_clean_size=5128192,
51
50
  # shared_dirty_size=0,
52
- # private_clean_size=5180,
53
- # private_dirty_size=20368,
54
- # referenced_size=30548,
55
- # anonymous_size=20376,
51
+ # private_clean_size=5304320,
52
+ # private_dirty_size=20856832,
53
+ # referenced_size=31281152,
54
+ # anonymous_size=20865024,
56
55
  # swap_size=0,
57
- # proportional_swap_size=0>>}
56
+ # proportional_swap_size=0,
57
+ # minor_faults=0,
58
+ # major_faults=0>>}
58
59
  ```
59
60
 
60
61
  If you want to capture a tree of processes, you can specify the `ppid:` option instead.
@@ -67,8 +68,8 @@ The {ruby Process::Metrics::General} struct contains the following fields:
67
68
  - `parent_process_id` - Parent Process ID, the process ID of the process that started this process.
68
69
  - `process_group_id` - Process Group ID, the process group ID of the process, which can be shared by multiple processes.
69
70
  - `processor_utilization` - Processor Utilization (%), the percentage of CPU time used by the process (over a system-specific duration).
70
- - `total_size` - Memory Size (KB), the total size of the process's memory space (usually over-estimated as it doesn't take into account shared memory).
71
- - `resident_size` - Resident (Set) Size (KB), the amount of physical memory used by the process.
71
+ - `total_size` - Memory Size (bytes), the total size of the process's memory space (usually over-estimated as it doesn't take into account shared memory).
72
+ - `resident_size` - Resident (Set) Size (bytes), the amount of physical memory used by the process.
72
73
  - `processor_time` - CPU Time (s), the amount of CPU time used by the process.
73
74
  - `elapsed_time` - Elapsed Time (s), the amount of time the process has been running.
74
75
  - `command` - Command Name, the name of the command that started the process.
@@ -76,15 +77,15 @@ The {ruby Process::Metrics::General} struct contains the following fields:
76
77
  The {ruby Process::Metrics::Memory} struct contains the following fields:
77
78
 
78
79
  - `map_count` - Number of Memory Mappings, e.g. number of thread stacks, fiber stacks, shared libraries, memory mapped files, etc.
79
- - `resident_size` - Resident Memory Size (KB), the amount of physical memory used by the process.
80
- - `proportional_size` - Proportional Memory Size (KB), the amount of memory that the process is using, taking into account shared memory.
81
- - `shared_clean_size` - Shared Clean Memory Size (KB), the amount of shared memory that is clean (not modified).
82
- - `shared_dirty_size` - Shared Dirty Memory Size (KB), the amount of shared memory that is dirty (modified).
83
- - `private_clean_size` - Private Clean Memory Size (KB), the amount of private memory that is clean (not modified).
84
- - `private_dirty_size` - Private Dirty Memory Size (KB), the amount of private memory that is dirty (modified).
85
- - `referenced_size` - Referenced Memory Size (KB), active page-cache that isn't going to be reclaimed any time soon.
86
- - `anonymous_size` - Anonymous Memory Size (KB), mapped memory that isn't backed by a file.
87
- - `swap_size` - Swap Memory Size (KB), the amount of memory that has been swapped to disk.
88
- - `proportional_swap_size` - Proportional Swap Memory Size (KB), the amount of memory that has been swapped to disk, excluding shared memory.
80
+ - `resident_size` - Resident Memory Size (bytes), the amount of physical memory used by the process.
81
+ - `proportional_size` - Proportional Memory Size (bytes), the amount of memory that the process is using, taking into account shared memory.
82
+ - `shared_clean_size` - Shared Clean Memory Size (bytes), the amount of shared memory that is clean (not modified).
83
+ - `shared_dirty_size` - Shared Dirty Memory Size (bytes), the amount of shared memory that is dirty (modified).
84
+ - `private_clean_size` - Private Clean Memory Size (bytes), the amount of private memory that is clean (not modified).
85
+ - `private_dirty_size` - Private Dirty Memory Size (bytes), the amount of private memory that is dirty (modified).
86
+ - `referenced_size` - Referenced Memory Size (bytes), active page-cache that isn't going to be reclaimed any time soon.
87
+ - `anonymous_size` - Anonymous Memory Size (bytes), mapped memory that isn't backed by a file.
88
+ - `swap_size` - Swap Memory Size (bytes), the amount of memory that has been swapped to disk.
89
+ - `proportional_swap_size` - Proportional Swap Memory Size (bytes), the amount of memory that has been swapped to disk, excluding shared memory.
89
90
 
90
91
  In general, the interpretation of these fields is operating system specific. At best, they provide a rough estimate of the process's memory usage, but you should consult the documentation for your operating system for more details on exactly what each field represents.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2025, by Samuel Williams.
4
+ # Copyright, 2020-2026, by Samuel Williams.
5
5
 
6
6
  require "samovar"
7
7
 
@@ -90,23 +90,28 @@ module Process
90
90
  UNITS = ["KiB", "MiB", "GiB"]
91
91
 
92
92
  # Format a memory size value in human-readable units.
93
- # @parameter value [Numeric] The size value in kilobytes.
93
+ # @parameter value [Numeric] The size value in bytes.
94
94
  # @parameter units [Array(String)] The unit labels to use for scaling.
95
95
  # @returns [String] A formatted string with value and unit (e.g., "512KiB", "1.5MiB").
96
96
  def format_size(value, units: UNITS)
97
- unit = 0
97
+ unit = -1
98
98
 
99
- while value > 1024.0 && unit < units.size
99
+ while value >= 1024.0 && unit < units.size - 1
100
100
  value /= 1024.0
101
101
  unit += 1
102
102
  end
103
103
 
104
- return "#{value.round(unit)}#{units[unit]}"
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
105
110
  end
106
111
 
107
112
  # Format a memory value with a horizontal bar showing utilization relative to total.
108
- # @parameter value [Numeric] The memory value in kilobytes.
109
- # @parameter total [Numeric] The total memory available in kilobytes.
113
+ # @parameter value [Numeric] The memory value in bytes.
114
+ # @parameter total [Numeric] The total memory available in bytes.
110
115
  # @parameter terminal [Console::Terminal] The terminal to output styled text.
111
116
  def format_memory(value, total, terminal)
112
117
  if value > (total * 0.8)
@@ -123,10 +128,11 @@ module Process
123
128
  end
124
129
 
125
130
  # Get the total memory to use for percentage calculations.
126
- # @returns [Integer] Total memory in kilobytes.
131
+ # @returns [Integer] Total memory in bytes.
127
132
  def total_memory
128
133
  if total_memory = @options[:total_memory]
129
- return total_memory * 1024
134
+ # Convert from MiB to bytes
135
+ return total_memory * 1024 * 1024
130
136
  else
131
137
  return Process::Metrics::Memory.total_size
132
138
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2025, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "memory"
7
7
  require "set"
@@ -42,8 +42,8 @@ module Process
42
42
  ppid: ->(value){value.to_i}, # Parent Process ID
43
43
  pgid: ->(value){value.to_i}, # Process Group ID
44
44
  pcpu: ->(value){value.to_f}, # Percentage CPU
45
- vsz: ->(value){value.to_i}, # Virtual Size (KiB)
46
- rss: ->(value){value.to_i}, # Resident Size (KiB)
45
+ vsz: ->(value){value.to_i * 1024}, # Virtual Size (convert from KiB to bytes)
46
+ rss: ->(value){value.to_i * 1024}, # Resident Size (convert from KiB to bytes)
47
47
  time: self.method(:duration), # CPU Time (seconds)
48
48
  etime: self.method(:duration), # Elapsed Time (seconds)
49
49
  command: ->(value){value}, # Command (name of the process)
@@ -73,7 +73,7 @@ module Process
73
73
  as_json.to_json(*arguments)
74
74
  end
75
75
 
76
- # The total size of the process in memory, in kilobytes.
76
+ # The total size of the process in memory, in bytes.
77
77
  def total_size
78
78
  if memory = self.memory
79
79
  memory.proportional_size
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  module Process
7
7
  module Metrics
@@ -14,29 +14,29 @@ module Process
14
14
  File.executable?(VMMAP)
15
15
  end
16
16
 
17
- # @returns [Numeric] Total memory size in kilobytes.
17
+ # @returns [Numeric] Total memory size in bytes.
18
18
  def self.total_size
19
19
  # sysctl hw.memsize
20
20
  IO.popen(["sysctl", "hw.memsize"], "r") do |io|
21
21
  io.each_line do |line|
22
22
  if line =~ /hw.memsize: (\d+)/
23
- return $1.to_i / 1024
23
+ return $1.to_i
24
24
  end
25
25
  end
26
26
  end
27
27
  end
28
28
 
29
- # Parse a size string from vmmap output into kilobytes.
29
+ # Parse a size string from vmmap output into bytes.
30
30
  # @parameter string [String | Nil] The size string (e.g., "4K", "1.5M", "2G").
31
- # @returns [Integer] The size in kilobytes.
31
+ # @returns [Integer] The size in bytes.
32
32
  def self.parse_size(string)
33
33
  return 0 unless string
34
34
 
35
35
  case string.strip
36
- when /([\d\.]+)K/i then ($1.to_f).round
37
- when /([\d\.]+)M/i then ($1.to_f * 1024).round
38
- when /([\d\.]+)G/i then ($1.to_f * 1024 * 1024).round
39
- else (string.to_f / 1024).ceil
36
+ when /([\d\.]+)K/i then ($1.to_f * 1024).round
37
+ when /([\d\.]+)M/i then ($1.to_f * 1024 * 1024).round
38
+ when /([\d\.]+)G/i then ($1.to_f * 1024 * 1024 * 1024).round
39
+ else (string.to_f).ceil
40
40
  end
41
41
  end
42
42
 
@@ -111,7 +111,7 @@ module Process
111
111
  end
112
112
 
113
113
  # Get total system memory size.
114
- # @returns [Integer] Total memory in kilobytes.
114
+ # @returns [Integer] Total memory in bytes.
115
115
  def total_size
116
116
  return Memory::Darwin.total_size
117
117
  end
@@ -1,12 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  module Process
7
7
  module Metrics
8
8
  # Linux implementation of memory metrics using `/proc/[pid]/smaps` and `/proc/[pid]/stat`.
9
9
  class Memory::Linux
10
+ # Threshold for distinguishing actual memory limits from "unlimited" sentinel values in cgroups v1.
11
+ #
12
+ # In cgroups v1, when memory.limit_in_bytes is set to unlimited (by writing -1),
13
+ # the kernel stores a very large sentinel value close to 2^63 (approximately 9,223,372,036,854,771,712 bytes).
14
+ # Since no real system would have 1 exabyte (2^60 bytes) of RAM, any value >= this threshold
15
+ # indicates an "unlimited" configuration and should be treated as if no limit is set.
16
+ #
17
+ # This allows us to distinguish between:
18
+ # - Actual container memory limits: typically in GB-TB range (< 1 EB)
19
+ # - Unlimited sentinel values: near 2^63 (>> 1 EB)
20
+ #
21
+ # Reference: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt
22
+ CGROUP_V1_UNLIMITED_THRESHOLD = 2**60 # ~1 exabyte
23
+
10
24
  # Extract minor/major page fault counters from `/proc/[pid]/stat` and assign to usage.
11
25
  # @parameter pid [Integer] The process ID.
12
26
  # @parameter usage [Memory] The Memory instance to populate with fault counters.
@@ -23,11 +37,34 @@ module Process
23
37
  # Ignore.
24
38
  end
25
39
 
26
- # @returns [Numeric] Total memory size in kilobytes.
40
+ # Determine the total memory size in bytes. This is the maximum amount of memory that can be used by the current process. If running in a container, this may be limited by the container runtime (e.g. cgroups).
41
+ #
42
+ # @returns [Integer] The total memory size in bytes.
27
43
  def self.total_size
28
- File.read("/proc/meminfo").each_line do |line|
29
- if /MemTotal:\s+(?<total>\d+) kB/ =~ line
30
- return total.to_i
44
+ # Check for Kubernetes/cgroup memory limit first (cgroups v2):
45
+ if File.exist?("/sys/fs/cgroup/memory.max")
46
+ limit = File.read("/sys/fs/cgroup/memory.max").strip
47
+ # "max" means unlimited, fall through to other methods:
48
+ if limit != "max"
49
+ return limit.to_i
50
+ end
51
+ end
52
+
53
+ # Check for Kubernetes/cgroup memory limit (cgroups v1):
54
+ if File.exist?("/sys/fs/cgroup/memory/memory.limit_in_bytes")
55
+ limit = File.read("/sys/fs/cgroup/memory/memory.limit_in_bytes").strip.to_i
56
+ # A very large number means unlimited, fall through:
57
+ if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD
58
+ return limit
59
+ end
60
+ end
61
+
62
+ # Fall back to Linux system memory detection:
63
+ if File.exist?("/proc/meminfo")
64
+ File.foreach("/proc/meminfo") do |line|
65
+ if /MemTotal:\s*(?<total>\d+)\s*kB/ =~ line
66
+ return total.to_i * 1024
67
+ end
31
68
  end
32
69
  end
33
70
  end
@@ -53,21 +90,27 @@ module Process
53
90
  end
54
91
 
55
92
  # Capture memory usage for the given process IDs.
56
- def self.capture(pid, **options)
93
+ # @parameter pid [Integer] The process ID.
94
+ # @parameter faults [Boolean] Whether to capture fault counters (default: true).
95
+ # @parameter options [Hash] Additional options.
96
+ def self.capture(pid, faults: true, **options)
57
97
  File.open("/proc/#{pid}/smaps_rollup") do |file|
58
98
  usage = Memory.zero
59
99
 
60
100
  file.each_line do |line|
61
101
  if /(?<name>.*?):\s+(?<value>\d+) kB/ =~ line
62
102
  if key = SMAP[name]
63
- usage[key] += value.to_i
103
+ # Convert from kilobytes to bytes
104
+ usage[key] += value.to_i * 1024
64
105
  end
65
106
  end
66
107
  end
67
108
 
68
109
  usage.map_count += File.readlines("/proc/#{pid}/maps").size
69
- # Also capture fault counters:
70
- self.capture_faults(pid, usage)
110
+ # Also capture fault counters if requested:
111
+ if faults
112
+ self.capture_faults(pid, usage)
113
+ end
71
114
 
72
115
  return usage
73
116
  end
@@ -82,7 +125,10 @@ module Process
82
125
  end
83
126
 
84
127
  # Capture memory usage for the given process IDs.
85
- def self.capture(pid, **options)
128
+ # @parameter pid [Integer] The process ID.
129
+ # @parameter faults [Boolean] Whether to capture fault counters (default: true).
130
+ # @parameter options [Hash] Additional options.
131
+ def self.capture(pid, faults: true, **options)
86
132
  File.open("/proc/#{pid}/smaps") do |file|
87
133
  usage = Memory.zero
88
134
 
@@ -91,7 +137,8 @@ module Process
91
137
  # https://github.com/torvalds/linux/blob/351c8a09b00b5c51c8f58b016fffe51f87e2d820/fs/proc/task_mmu.c#L804-L814
92
138
  if /(?<name>.*?):\s+(?<value>\d+) kB/ =~ line
93
139
  if key = SMAP[name]
94
- usage[key] += value.to_i
140
+ # Convert from kilobytes to bytes
141
+ usage[key] += value.to_i * 1024
95
142
  end
96
143
  elsif /VmFlags:\s+(?<flags>.*)/ =~ line
97
144
  # It should be possible to extract the number of fibers and each fiber's memory usage.
@@ -100,8 +147,8 @@ module Process
100
147
  end
101
148
  end
102
149
 
103
- # Also capture fault counters:
104
- self.capture_faults(pid, usage)
150
+ # Also capture fault counters if requested:
151
+ self.capture_faults(pid, usage) if faults
105
152
 
106
153
  return usage
107
154
  end
@@ -125,13 +172,14 @@ module Process
125
172
  end
126
173
 
127
174
  # Get total system memory size.
128
- # @returns [Integer] Total memory in kilobytes.
175
+ # @returns [Integer] Total memory in bytes.
129
176
  def total_size
130
177
  return Memory::Linux.total_size
131
178
  end
132
179
 
133
180
  # Capture memory metrics for a process.
134
181
  # @parameter pid [Integer] The process ID.
182
+ # @parameter faults [Boolean] Whether to capture fault counters (default: true).
135
183
  # @parameter options [Hash] Additional options.
136
184
  # @returns [Memory] A Memory instance with captured metrics.
137
185
  def capture(...)
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2025, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require "json"
7
7
 
8
8
  module Process
9
9
  module Metrics
10
- # Represents memory usage for a process, sizes are in kilobytes.
10
+ # Represents memory usage for a process, sizes are in bytes.
11
11
  class Memory < Struct.new(:map_count, :resident_size, :proportional_size, :shared_clean_size, :shared_dirty_size, :private_clean_size, :private_dirty_size, :referenced_size, :anonymous_size, :swap_size, :proportional_swap_size, :minor_faults, :major_faults)
12
12
 
13
13
  alias as_json to_h
@@ -7,6 +7,6 @@
7
7
  module Process
8
8
  # @namespace
9
9
  module Metrics
10
- VERSION = "0.8.0"
10
+ VERSION = "0.9.0"
11
11
  end
12
12
  end
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2019-2025, by Samuel Williams.
3
+ Copyright, 2019-2026, by Samuel Williams.
4
4
  Copyright, 2024, by Adam Daniels.
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
data/readme.md CHANGED
@@ -16,6 +16,12 @@ 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.9.0
20
+
21
+ - `Process::Metrics::Memory.total_size` takes into account cgroup limits.
22
+ - On Linux, capturing faults is optional, controlled by `capture(faults: true/false)`.
23
+ - Report all sizes in bytes for consistency.
24
+
19
25
  ### v0.7.0
20
26
 
21
27
  - Be more proactive about returning nil if memory capture failed.
@@ -67,12 +73,6 @@ Please see the [project releases](https://socketry.github.io/process-metrics/rel
67
73
  - Implemented structured data using Ruby structs for better performance and clarity.
68
74
  - Added documentation about PSS (Proportional Set Size) and USS (Unique Set Size) metrics.
69
75
 
70
- ### v0.1.1
71
-
72
- - Removed `Gemfile.lock` from version control.
73
- - Fixed process metrics to exclude the `ps` command itself from measurements.
74
- - Fixed documentation formatting issues.
75
-
76
76
  ## Contributing
77
77
 
78
78
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Releases
2
2
 
3
+ ## v0.9.0
4
+
5
+ - `Process::Metrics::Memory.total_size` takes into account cgroup limits.
6
+ - On Linux, capturing faults is optional, controlled by `capture(faults: true/false)`.
7
+ - Report all sizes in bytes for consistency.
8
+
3
9
  ## v0.7.0
4
10
 
5
11
  - Be more proactive about returning nil if memory capture failed.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process-metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -122,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
124
  requirements: []
125
- rubygems_version: 3.6.9
125
+ rubygems_version: 4.0.3
126
126
  specification_version: 4
127
127
  summary: Provide detailed OS-specific process metrics.
128
128
  test_files: []
metadata.gz.sig CHANGED
Binary file