Skip to content

Commit 2a9bd05

Browse files
committed
Add support for page faults.
1 parent 76d2df6 commit 2a9bd05

File tree

5 files changed

+116
-4
lines changed

5 files changed

+116
-4
lines changed

examples/cow.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
# A small demo to visualize COW-related minor page fault trends.
4+
# - Allocates page-sized strings in an array.
5+
# - Forks a child that mutates one byte of a different string each second.
6+
# - Parent samples Process::Metrics and prints minor/major fault deltas and unique memory growth.
7+
8+
require "process/metrics"
9+
10+
PAGE_SIZE = Integer(ENV.fetch("PAGE_SIZE", "4096")) # bytes
11+
PAGES = Integer(ENV.fetch("PAGES", "128")) # number of page-sized strings
12+
DURATION = Integer(ENV.fetch("DURATION", "30")) # seconds to run/monitor
13+
14+
# Allocate an array of page-sized mutable strings:
15+
array = Array.new(PAGES) {"\x00" * PAGE_SIZE}
16+
17+
child_pid = fork do
18+
$0 = "cow-child"
19+
i = 0
20+
start = Time.now
21+
while (Time.now - start) < DURATION
22+
idx = i % PAGES
23+
s = array[idx]
24+
# Mutate a single byte; this should trigger a COW on the underlying page on first write:
25+
s.setbyte(0, (s.getbyte(0) + 1) & 0xFF)
26+
i += 1
27+
sleep 1
28+
end
29+
end
30+
31+
puts "Monitoring child PID=#{child_pid} for #{DURATION}s (PAGE_SIZE=#{PAGE_SIZE}, PAGES=#{PAGES})"
32+
33+
last_minor = nil
34+
last_major = nil
35+
last_unique = nil
36+
37+
DURATION.times do |t|
38+
procs = Process::Metrics::General.capture(pid: child_pid)
39+
proc = procs[child_pid]
40+
unless proc
41+
puts "[%2ds] child process is no longer listed" % t
42+
break
43+
end
44+
mem = proc.memory
45+
unless mem
46+
puts "[%2ds] detailed memory metrics not available on this platform" % t
47+
break
48+
end
49+
50+
minor = mem.minor_faults.to_i
51+
major = mem.major_faults.to_i
52+
unique = mem.unique_size.to_i # kB
53+
54+
delta_minor = last_minor ? (minor - last_minor) : 0
55+
delta_major = last_major ? (major - last_major) : 0
56+
delta_unique = last_unique ? (unique - last_unique) : 0
57+
58+
puts "[%2ds] minor: %d (+%d), major: %d (+%d), unique_kB: %d (+%d)" % [t, minor, delta_minor, major, delta_major, unique, delta_unique]
59+
60+
last_minor = minor
61+
last_major = major
62+
last_unique = unique
63+
64+
sleep 1
65+
end
66+
67+
begin
68+
Process.kill(:TERM, child_pid)
69+
rescue Errno::ESRCH
70+
# already exited
71+
end
72+
73+
begin
74+
_, status = Process.wait2(child_pid)
75+
if status
76+
if status.signaled?
77+
puts "Child terminated by signal #{status.termsig}"
78+
else
79+
puts "Child exited with status #{status.exitstatus}"
80+
end
81+
end
82+
rescue Errno::ECHILD
83+
# already reaped
84+
end

lib/process/metrics/memory.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
module Process
99
module Metrics
1010
# Represents memory usage for a process, sizes are in kilobytes.
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)
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)
1212

1313
alias as_json to_h
1414

@@ -28,7 +28,7 @@ def unique_size
2828
end
2929

3030
def self.zero
31-
self.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
31+
self.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
3232
end
3333

3434
# Whether the memory usage can be captured on this system.

lib/process/metrics/memory/linux.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
module Process
77
module Metrics
88
class Memory::Linux
9+
# Extract minor/major page fault counters from /proc/[pid]/stat and assign to usage.
10+
def self.capture_faults(pid, usage)
11+
begin
12+
stat = File.read("/proc/#{pid}/stat")
13+
# The comm field can contain spaces and parentheses; find the closing ')':
14+
rparen_index = stat.rindex(")")
15+
return unless rparen_index
16+
fields = stat[(rparen_index+2)..-1].split(/\s+/)
17+
# proc(5): field 10=minflt, 12=majflt; our fields array is 0-indexed from field 3.
18+
usage.minor_faults = fields[10-3].to_i
19+
usage.major_faults = fields[12-3].to_i
20+
rescue Errno::ENOENT, Errno::EACCES
21+
# The process may have exited or permissions are insufficient; ignore.
22+
rescue => error
23+
# Be robust to unexpected formats; ignore errors silently.
24+
end
25+
end
26+
927
# @returns [Numeric] Total memory size in kilobytes.
1028
def self.total_size
1129
File.read("/proc/meminfo").each_line do |line|
@@ -49,6 +67,8 @@ def self.capture(pid, **options)
4967
end
5068

5169
usage.map_count += File.readlines("/proc/#{pid}/maps").size
70+
# Also capture fault counters:
71+
self.capture_faults(pid, usage)
5272
rescue Errno::ENOENT => error
5373
# Ignore.
5474
end
@@ -79,6 +99,8 @@ def self.capture(pid, **options)
7999
usage.map_count += 1
80100
end
81101
end
102+
# Also capture fault counters:
103+
self.capture_faults(pid, usage)
82104
rescue Errno::ENOENT => error
83105
# Ignore.
84106
end

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- 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).
6+
37
## v0.5.1
48

59
- Fixed Linux memory usage capture to correctly read memory statistics.

test/process/memory.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
referenced_size: be >= 0,
3434
anonymous_size: be >= 0,
3535
swap_size: be >= 0,
36-
proportional_swap_size: be >= 0
36+
proportional_swap_size: be >= 0,
37+
minor_faults: be >= 0,
38+
major_faults: be >= 0
3739
)
3840
end
3941

@@ -46,7 +48,7 @@
4648
json = JSON.parse(json_string)
4749

4850
expect(json).to have_keys(
49-
"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"
51+
"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"
5052
)
5153
end
5254
end

0 commit comments

Comments
 (0)