Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0339995
Initial version of cpu_usage for Linux.
djberg96 Mar 18, 2026
4f00ac6
Add cpu_usage method.
djberg96 Mar 18, 2026
7608055
Add some specs for cpu_usage with values above zero, handle fractions.
djberg96 Mar 18, 2026
f67c53d
Update windows comments.
djberg96 Mar 18, 2026
d4a8acc
Add specs for other platforms.
djberg96 Mar 18, 2026
9f52aab
Update version to 1.3.0.
djberg96 Mar 18, 2026
0dca7db
Update version to 1.3.0.
djberg96 Mar 18, 2026
af5f5df
Use PerfFormattedData_PerfOS_Processor by default.
djberg96 Mar 18, 2026
f062caa
Update comments for Windows.
djberg96 Mar 18, 2026
e1e4f4d
Update cpu_usage on Macs to use host statistics.
djberg96 Mar 18, 2026
9ae671f
Use uint not int.
djberg96 Mar 18, 2026
d4ab1ed
Update to get more meaningful data.
djberg96 Mar 18, 2026
f8970cf
Default back to 1 second, add a samples argument.
djberg96 Mar 18, 2026
2b9115e
Default to 2 samples.
djberg96 Mar 18, 2026
0ef2ed0
Update other methods to match MacOS.
djberg96 Mar 19, 2026
bba5535
Updates and fixes for Windows.
djberg96 Mar 19, 2026
dd7060e
Update a windows spec.
djberg96 Mar 19, 2026
561fdea
Make internal methods private, add comments.
djberg96 Mar 19, 2026
eacf423
Switch to one liners.
djberg96 Mar 23, 2026
1b7a8ef
Add documentation for Linux.
djberg96 Mar 23, 2026
cdb446f
Get rid of tick counts, just use defaults, to be consistent across pl…
djberg96 Mar 23, 2026
6105626
Switch to keywords.
djberg96 Mar 23, 2026
cfbc11e
Add some specs for default values.
djberg96 Mar 23, 2026
cb5f55b
Minor comment formatting for Windows.
djberg96 Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/sys/cpu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module Sys
# This class is reopened for each of the supported platforms/operating systems.
class CPU
# The version of the sys-cpu gem.
VERSION = '1.2.0'
VERSION = '1.3.0'

private_class_method :new
end
Expand Down
114 changes: 114 additions & 0 deletions lib/sys/darwin/sys/cpu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,119 @@ def self.load_avg

loadavg.get_array_of_double(0, 3)
end

# Returns CPU usage as a percentage.
#
# If +sample_time+ is positive, samples CPU times and calculates an average
# over that interval. You can also specify +samples+ to average multiple
# consecutive measurements.
#
# If +sample_time+ is 0 (default), uses a 1-second sample window by default.
# Default value for +samples+ is 2 (averages two measurements).
#
HOST_CPU_LOAD_INFO = 3
HOST_CPU_LOAD_INFO_COUNT = 4

private_constant :HOST_CPU_LOAD_INFO, :HOST_CPU_LOAD_INFO_COUNT

attach_function :mach_host_self, [], :uint
attach_function :host_statistics, %i[uint int pointer pointer], :int

private_class_method :mach_host_self, :host_statistics

# Returns the current CPU usage as a percentage, averaged over a sampling interval.
#
# By default, this method samples CPU usage over a 1-second interval and averages two measurements.
# You can customize the interval and number of samples by passing the +sample_time+ (in seconds)
# and +samples+ keyword arguments. For example, +cpu_usage(sample_time: 0.5, samples: 4)+ will take four
# samples, each 0.5 seconds apart, and return the average CPU usage over that period.
#
# Passing nil, 0, or a negative value for either argument falls back to the defaults (1.0 seconds
# and 2 samples) to keep behavior consistent across platforms.
#
# Returns a Float (percentage), rounded to one decimal place, or nil if CPU usage cannot be determined.
#
# Example usage:
# Sys::CPU.cpu_usage #=> 12.3
# Sys::CPU.cpu_usage(sample_time: 2, samples: 3) #=> 10.7
# Sys::CPU.cpu_usage(sample_time: 0, samples: 0) #=> 12.3 # zeros fall back to defaults
#
def self.cpu_usage(sample_time: 1.0, samples: 2)
sample_time = 1.0 if sample_time.nil? || sample_time <= 0
samples = 2 if samples.nil? || samples <= 0

usages = []

samples.times do
t1 = current_ticks
sleep(sample_time)
t2 = current_ticks
next unless t1 && t2

if (u = usage_between_ticks(t1, t2))
usages << u
end
end

return nil if usages.empty?

(usages.sum / usages.size.to_f).round(1)
rescue StandardError
nil
end

def self.current_ticks
cpu_ticks_sysctl || cpu_ticks_host
end

private_class_method :current_ticks

def self.usage_between_ticks(t1, t2)
diff = t2.map.with_index { |v, i| v - t1[i] }
total = diff.sum
return nil if total <= 0

# host_statistics returns [user, system, idle, nice]
idle = diff[2] || 0
(1.0 - (idle.to_f / total)) * 100
end

private_class_method :usage_between_ticks

def self.cpu_ticks_sysctl
cp_time = proc { |ptr|
len = 5
size = FFI::MemoryPointer.new(:size_t)
size.write_ulong(ptr.size)

if sysctlbyname('kern.cp_time', ptr, size, nil, 0) < 0
raise Error, 'sysctlbyname failed'
end

ptr.read_array_of_ulong(len)
}

cp_time.call(FFI::MemoryPointer.new(:ulong, 5))
rescue StandardError
nil
end

private_class_method :cpu_ticks_sysctl

def self.cpu_ticks_host
host = mach_host_self
info = FFI::MemoryPointer.new(:uint, HOST_CPU_LOAD_INFO_COUNT)
count = FFI::MemoryPointer.new(:uint)
count.write_uint(HOST_CPU_LOAD_INFO_COUNT)

kr = host_statistics(host, HOST_CPU_LOAD_INFO, info, count)
return nil unless kr == 0

info.read_array_of_uint(HOST_CPU_LOAD_INFO_COUNT)
rescue StandardError
nil
end

private_class_method :cpu_ticks_host
end
end
69 changes: 67 additions & 2 deletions lib/sys/linux/sys/cpu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,15 @@ def self.architecture

# Returns a string indicating the CPU model.
#
# Some systems may use slightly different keys in /proc/cpuinfo, so
# we fall back to other common names and ensure we always return a
# String.
def self.model
CPU_ARRAY.first['model_name']
CPU_ARRAY.first['model_name'] ||
CPU_ARRAY.first['model'] ||
CPU_ARRAY.first['cpu'] ||
CPU_ARRAY.first['processor'] ||
''.dup
end

# Returns an integer indicating the speed of the CPU.
Expand All @@ -110,6 +117,62 @@ def self.freq
CPU_ARRAY.first['cpu_mhz'].to_f.round
end

# Returns the current CPU usage as a percentage, averaged over a sampling interval.
#
# By default, this method samples CPU usage over a 1-second interval and averages two measurements.
# You can customize the interval and number of samples by passing the +sample_time+ (in seconds)
# and +samples+ keyword arguments. For example, +cpu_usage(sample_time: 0.5, samples: 4)+ will take four
# samples, each 0.5 seconds apart,
# and return the average CPU usage over that period.
#
# Passing nil, 0, or a negative value for either argument falls back to the defaults (1.0 seconds and
# 2 samples) for cross-platform consistency.
#
# Returns a Float (percentage), rounded to one decimal place, or nil if CPU usage cannot be determined.
#
# Example usage:
# Sys::CPU.cpu_usage #=> 12.3
# Sys::CPU.cpu_usage(sample_time: 2, samples: 3) #=> 10.7
# Sys::CPU.cpu_usage(sample_time: 0, samples: 0) #=> 12.3 # zeros fall back to defaults
#
def self.cpu_usage(sample_time: 1.0, samples: 2)
sample_time = 1.0 if sample_time.nil? || sample_time <= 0
samples = 2 if samples.nil? || samples <= 0

usages = []

samples.times do
stats1 = cpu_stats
sleep(sample_time)
stats2 = cpu_stats

total_diff = 0.0
idle_diff = 0.0

keys = stats1.key?('cpu') ? ['cpu'] : stats1.keys
keys.each do |key|
arr1 = stats1[key]
arr2 = stats2[key]
next unless arr1 && arr2
t1 = arr1.sum
t2 = arr2.sum
total = t2 - t1
idle = (arr2[3] || 0) - (arr1[3] || 0)
total_diff += total
idle_diff += idle
end

if total_diff > 0
usages << ((1.0 - (idle_diff / total_diff)) * 100)
end
end

return nil if usages.empty?
(usages.sum / usages.size.to_f).round(1)
rescue StandardError
nil
end

# Create singleton methods for each of the attributes.
#
def self.method_missing(id, arg = 0)
Expand Down Expand Up @@ -168,7 +231,9 @@ def self.cpu_stats
next
end

vals = array[1..-1].map{ |e| e.to_i / 100 } # 100 jiffies/sec.
# Keep raw jiffies counts (do not scale by hz) so deltas over short
# intervals still produce meaningful values.
vals = array[1..-1].map{ |e| e.to_i }
hash[array[0]] = vals
end

Expand Down
46 changes: 46 additions & 0 deletions lib/sys/unix/sys/cpu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,52 @@ def self.load_avg
loadavg.get_array_of_double(0, 3)
end

# Returns CPU usage as a percentage, averaged over a sampling interval.
#
# By default, samples CPU times twice, 1 second apart. Arguments are keyword-based
# (+sample_time:+, +samples:+). Passing nil, 0, or a negative value for either
# falls back to these defaults for cross-platform consistency.
#
def self.cpu_usage(sample_time: 1.0, samples: 2)
cp_time = proc { |ptr|
len = 5
size = FFI::MemoryPointer.new(:size_t)
size.write_ulong(ptr.size)

if sysctlbyname('kern.cp_time', ptr, size, nil, 0) < 0
raise Error, 'sysctlbyname failed'
end

ptr.read_array_of_ulong(len)
}

sample_time = 1.0 if sample_time.nil? || sample_time <= 0
samples = 2 if samples.nil? || samples <= 0

usages = []

samples.times do
t1 = cp_time.call(FFI::MemoryPointer.new(:ulong, 5))
sleep(sample_time)
t2 = cp_time.call(FFI::MemoryPointer.new(:ulong, 5))

total1 = t1.sum
total2 = t2.sum
idle1 = t1[4] || 0
idle2 = t2[4] || 0

total_diff = total2 - total1
idle_diff = idle2 - idle1

usages << ((1.0 - (idle_diff.to_f / total_diff)) * 100) if total_diff > 0
end

return nil if usages.empty?
(usages.sum / usages.size.to_f).round(1)
rescue StandardError
nil
end

# Returns the floating point processor type.
#
# Not supported on all platforms.
Expand Down
40 changes: 40 additions & 0 deletions lib/sys/windows/sys/cpu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,46 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname)
end
end

# Returns CPU usage as a percentage, averaged over multiple samples.
#
# The +sample_time+ keyword specifies the interval (in seconds) between samples.
# The +samples+ keyword specifies how many samples to take and average.
# The +cpu_num+ keyword selects which CPU to query (0 for total).
# The +host+ keyword specifies the target machine (defaults to local).
#
#--
# This method uses the _Total Win32_PerfFormattedData_PerfOS_Processor instance
# (unless a specific +cpu_num+ is requested) to better match Task Manager's total view.
#
# Note: Task Manager reports total CPU usage across all cores. Win32_Processor.LoadPercentage
# is per-processor (usually per physical socket), so it can differ from Task Manager if it falls back.
#
def self.cpu_usage(sample_time: 1.0, samples: 2, cpu_num: 0, host: Socket.gethostname)
sample_time = 1.0 if sample_time.nil? || sample_time <= 0
samples = 2 if samples.nil? || samples <= 0
cpu_num = cpu_num.to_i if cpu_num.respond_to?(:to_i)
instance = cpu_num == 0 ? '_Total' : cpu_num.to_s
cs = BASE_CS + "//#{host}/root/cimv2:Win32_PerfFormattedData_PerfOS_Processor='#{instance}'"

usages = []

samples.times do
begin
wmi = WIN32OLE.connect(cs)
rescue WIN32OLERuntimeError
usages << load_avg(cpu_num, host)
else
result = wmi.PercentProcessorTime
usages << result.to_i if result
end
sleep(sample_time)
end

usages.compact!
return nil if usages.empty?
(usages.sum / usages.size.to_f).round(1)
end

# Returns a string indicating the cpu model, e.g. Intel Pentium 4.
#
def self.model(host = Socket.gethostname)
Expand Down
20 changes: 20 additions & 0 deletions spec/sys_cpu_bsd_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@
expect{ described_class.load_avg(0) }.to raise_error(ArgumentError)
end

example 'cpu_usage works as expected' do
expect(described_class).to respond_to(:cpu_usage)
expect{ described_class.cpu_usage }.not_to raise_error
expect{ described_class.cpu_usage(sample_time: 0.1) }.not_to raise_error
expect(described_class.cpu_usage).to be_a(Numeric).or be_nil
end

example 'cpu_usage falls back on non-positive values' do
expect{ described_class.cpu_usage(sample_time: 0, samples: 0) }.not_to raise_error
expect{ described_class.cpu_usage(sample_time: -0.5, samples: -1) }.not_to raise_error
expect(described_class.cpu_usage(sample_time: 0, samples: 0)).to be_a(Numeric).or be_nil
end

example 'cpu_usage sampling produces a valid range' do
result = described_class.cpu_usage(sample_time: 0.1)
expect(result).to be_a(Numeric).or be_nil
expect(result).to be >= 0 if result
expect(result).to be <= 100 if result
end

example 'machine method basic functionality' do
expect(described_class).to respond_to(:machine)
expect{ described_class.machine }.not_to raise_error
Expand Down
20 changes: 20 additions & 0 deletions spec/sys_cpu_hpux_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,24 @@
expect(described_class.load_avg.length).to eq(3)
expect(described_class.load_avg(0).length).to eq(3)
end

example 'cpu_usage works as expected' do
expect(described_class).to respond_to(:cpu_usage)
expect{ described_class.cpu_usage }.not_to raise_error
expect{ described_class.cpu_usage(sample_time: 0.1) }.not_to raise_error
expect(described_class.cpu_usage).to be_a(Numeric).or be_nil
end

example 'cpu_usage falls back on non-positive values' do
expect{ described_class.cpu_usage(sample_time: 0, samples: 0) }.not_to raise_error
expect{ described_class.cpu_usage(sample_time: -1, samples: -1) }.not_to raise_error
expect(described_class.cpu_usage(sample_time: 0, samples: 0)).to be_a(Numeric).or be_nil
end

example 'cpu_usage sampling produces a valid range' do
result = described_class.cpu_usage(sample_time: 0.1)
expect(result).to be_a(Numeric).or be_nil
expect(result).to be >= 0 if result
expect(result).to be <= 100 if result
end
end
19 changes: 19 additions & 0 deletions spec/sys_cpu_linux_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@
expect(described_class.num_cpu).to be_a(Numeric)
end

example 'cpu_usage works as expected' do
expect{ described_class.cpu_usage }.not_to raise_error
expect(described_class.cpu_usage).to be_a(Numeric)
end

example 'cpu_usage falls back on non-positive values' do
expect{ described_class.cpu_usage(sample_time: 0, samples: 0) }.not_to raise_error
expect{ described_class.cpu_usage(sample_time: -1, samples: -2) }.not_to raise_error
expect(described_class.cpu_usage(sample_time: 0, samples: 0)).to be_a(Numeric)
end

example 'cpu_usage sampling produces a valid range' do
# Sampled usage should be a number between 0 and 100.
result = described_class.cpu_usage(sample_time: 0.1)
expect(result).to be_a(Numeric)
expect(result).to be >= 0
expect(result).to be <= 100
end

example 'bogus methods are not picked up by method_missing' do
expect{ described_class.bogus }.to raise_error(NoMethodError)
end
Expand Down
Loading
Loading