From 033999583e02a48e68e488685c5673a153c231e0 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:30:45 -0400 Subject: [PATCH 01/24] Initial version of cpu_usage for Linux. --- lib/sys/linux/sys/cpu.rb | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lib/sys/linux/sys/cpu.rb b/lib/sys/linux/sys/cpu.rb index 8472610..527ce8d 100644 --- a/lib/sys/linux/sys/cpu.rb +++ b/lib/sys/linux/sys/cpu.rb @@ -110,6 +110,48 @@ def self.freq CPU_ARRAY.first['cpu_mhz'].to_f.round end + def self.cpu_usage(sample_time = 0) + if sample_time && sample_time > 0 + stats1 = cpu_stats + sleep(sample_time) + stats2 = cpu_stats + + total_diff = 0.0 + idle_diff = 0.0 + + # Use aggregate 'cpu' line if present, else sum all + 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) # idle is 4th field + total_diff += total + idle_diff += idle + end + + return nil if total_diff <= 0 + usage = (1.0 - (idle_diff / total_diff)) * 100 + usage.round + else + # Single snapshot, 100 - iowait% + stats = cpu_stats + total = 0.0 + iowait = 0.0 + stats.each_value do |arr| + total += arr.sum + iowait += (arr[4] || 0).to_f + end + return nil if total <= 0 + (100 - ((iowait / total) * 100)).to_i + end + rescue StandardError + nil + end + # Create singleton methods for each of the attributes. # def self.method_missing(id, arg = 0) From 4f00ac6ca95fd30ec82c42773a78fb2f3ada92c1 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:19:21 -0400 Subject: [PATCH 02/24] Add cpu_usage method. --- lib/sys/darwin/sys/cpu.rb | 48 ++++++++++++++++++++++++++++++++++++++ lib/sys/linux/sys/cpu.rb | 9 ++++++- lib/sys/unix/sys/cpu.rb | 48 ++++++++++++++++++++++++++++++++++++++ lib/sys/windows/sys/cpu.rb | 10 ++++++++ spec/sys_cpu_linux_spec.rb | 5 ++++ 5 files changed, 119 insertions(+), 1 deletion(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index ca34dc6..b1f0c27 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -208,5 +208,53 @@ 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 twice and calculates an + # average over that interval. If +sample_time+ is 0 (default), returns the + # current utilization estimate based on the last set of CPU times. + # + def self.cpu_usage(sample_time = 0) + 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) + } + + if sample_time && sample_time > 0 + 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 + + return nil if total_diff <= 0 + + ((1.0 - (idle_diff.to_f / total_diff)) * 100).round + else + # Fallback: use a single snapshot and interpret idle as the last element. + t = cp_time.call(FFI::MemoryPointer.new(:ulong, 5)) + total = t.sum + idle = t[4] || 0 + + return nil if total <= 0 + ((1.0 - (idle.to_f / total)) * 100).round + end + rescue StandardError + nil + end end end diff --git a/lib/sys/linux/sys/cpu.rb b/lib/sys/linux/sys/cpu.rb index 527ce8d..c2b864e 100644 --- a/lib/sys/linux/sys/cpu.rb +++ b/lib/sys/linux/sys/cpu.rb @@ -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. diff --git a/lib/sys/unix/sys/cpu.rb b/lib/sys/unix/sys/cpu.rb index 87de592..89e9a32 100644 --- a/lib/sys/unix/sys/cpu.rb +++ b/lib/sys/unix/sys/cpu.rb @@ -309,6 +309,54 @@ 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 twice and calculates an + # average over that interval. If +sample_time+ is 0 (default), returns the + # current utilization estimate based on the last set of CPU times. + # + def self.cpu_usage(sample_time = 0) + 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) + } + + if sample_time && sample_time > 0 + 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 + + return nil if total_diff <= 0 + + ((1.0 - (idle_diff.to_f / total_diff)) * 100).round + else + # Fallback: use a single snapshot and interpret idle as the last element. + t = cp_time.call(FFI::MemoryPointer.new(:ulong, 5)) + total = t.sum + idle = t[4] || 0 + + return nil if total <= 0 + ((1.0 - (idle.to_f / total)) * 100).round + end + rescue StandardError + nil + end + # Returns the floating point processor type. # # Not supported on all platforms. diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index cdc8584..57b12c6 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -117,6 +117,16 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) end end + # Returns CPU usage as a percentage. + # + # This currently delegates to Win32_Processor.LoadPercentage, which is + # already averaged over a short interval. The +sample_time+ parameter is + # accepted for compatibility with other platforms. + # + def self.cpu_usage(sample_time = 0, cpu_num = 0, host = Socket.gethostname) + load_avg(cpu_num, host) + end + # Returns a string indicating the cpu model, e.g. Intel Pentium 4. # def self.model(host = Socket.gethostname) diff --git a/spec/sys_cpu_linux_spec.rb b/spec/sys_cpu_linux_spec.rb index c30f986..691b7d6 100644 --- a/spec/sys_cpu_linux_spec.rb +++ b/spec/sys_cpu_linux_spec.rb @@ -49,6 +49,11 @@ 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 'bogus methods are not picked up by method_missing' do expect{ described_class.bogus }.to raise_error(NoMethodError) end From 76080554cedf674aa191713a96733c320624ed99 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:25:12 -0400 Subject: [PATCH 03/24] Add some specs for cpu_usage with values above zero, handle fractions. --- lib/sys/linux/sys/cpu.rb | 4 +++- spec/sys_cpu_linux_spec.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/sys/linux/sys/cpu.rb b/lib/sys/linux/sys/cpu.rb index c2b864e..ddbad53 100644 --- a/lib/sys/linux/sys/cpu.rb +++ b/lib/sys/linux/sys/cpu.rb @@ -217,7 +217,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 diff --git a/spec/sys_cpu_linux_spec.rb b/spec/sys_cpu_linux_spec.rb index 691b7d6..6e566a2 100644 --- a/spec/sys_cpu_linux_spec.rb +++ b/spec/sys_cpu_linux_spec.rb @@ -54,6 +54,14 @@ expect(described_class.cpu_usage).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(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 From f67c53dbdc32532a344f1e004c6dec91bfe3c9f0 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:26:18 -0400 Subject: [PATCH 04/24] Update windows comments. --- lib/sys/windows/sys/cpu.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index 57b12c6..3dfd0de 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -121,9 +121,9 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) # # This currently delegates to Win32_Processor.LoadPercentage, which is # already averaged over a short interval. The +sample_time+ parameter is - # accepted for compatibility with other platforms. + # accepted for interface compatibility with other platforms but does nothing. # - def self.cpu_usage(sample_time = 0, cpu_num = 0, host = Socket.gethostname) + def self.cpu_usage(_sample_time = 0, cpu_num = 0, host = Socket.gethostname) load_avg(cpu_num, host) end From d4a8accde98adca9d39a1cc2fdb1f2d3c2162e0b Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:07:41 -0400 Subject: [PATCH 05/24] Add specs for other platforms. --- spec/sys_cpu_bsd_spec.rb | 14 ++++++++++++++ spec/sys_cpu_hpux_spec.rb | 14 ++++++++++++++ spec/sys_cpu_windows_spec.rb | 15 +++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/spec/sys_cpu_bsd_spec.rb b/spec/sys_cpu_bsd_spec.rb index de0738f..ec92718 100644 --- a/spec/sys_cpu_bsd_spec.rb +++ b/spec/sys_cpu_bsd_spec.rb @@ -52,6 +52,20 @@ 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(0.1) }.not_to raise_error + expect(described_class.cpu_usage).to be_a(Numeric).or be_nil + end + + example 'cpu_usage sampling produces a valid range' do + result = described_class.cpu_usage(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 diff --git a/spec/sys_cpu_hpux_spec.rb b/spec/sys_cpu_hpux_spec.rb index 3ef05a9..bcf5a83 100644 --- a/spec/sys_cpu_hpux_spec.rb +++ b/spec/sys_cpu_hpux_spec.rb @@ -52,4 +52,18 @@ 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(0.1) }.not_to raise_error + expect(described_class.cpu_usage).to be_a(Numeric).or be_nil + end + + example 'cpu_usage sampling produces a valid range' do + result = described_class.cpu_usage(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 diff --git a/spec/sys_cpu_windows_spec.rb b/spec/sys_cpu_windows_spec.rb index 109704d..f655720 100644 --- a/spec/sys_cpu_windows_spec.rb +++ b/spec/sys_cpu_windows_spec.rb @@ -59,6 +59,21 @@ expect(described_class.load_avg).to be_a(Integer).or be_a(NilClass) 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(0.1, 0, host) }.not_to raise_error + expect(described_class.cpu_usage).to be_a(Integer).or be_a(NilClass) + 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(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 'processors' do expect(described_class).to respond_to(:processors) expect{ described_class.processors{} }.not_to raise_error From 9f52aabd6898d6f878e5aa7c89dd57e9c19a5e26 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:10:18 -0400 Subject: [PATCH 06/24] Update version to 1.3.0. --- lib/sys/cpu.rb | 2 +- spec/sys_cpu_shared.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sys/cpu.rb b/lib/sys/cpu.rb index 83201d3..433b215 100644 --- a/lib/sys/cpu.rb +++ b/lib/sys/cpu.rb @@ -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 diff --git a/spec/sys_cpu_shared.rb b/spec/sys_cpu_shared.rb index e3e5cca..ae3e38a 100644 --- a/spec/sys_cpu_shared.rb +++ b/spec/sys_cpu_shared.rb @@ -11,7 +11,7 @@ RSpec.shared_examples Sys::CPU do example 'version number is set to the expected value' do - expect(Sys::CPU::VERSION).to eq('1.2.0') + expect(Sys::CPU::VERSION).to eq('1.3.0') end example 'version number is frozen' do From 0dca7db289c7a617dad1bd3ecae32fe09de47e98 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:10:55 -0400 Subject: [PATCH 07/24] Update version to 1.3.0. --- sys-cpu.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sys-cpu.gemspec b/sys-cpu.gemspec index 7b1d4e2..e13bc94 100644 --- a/sys-cpu.gemspec +++ b/sys-cpu.gemspec @@ -2,7 +2,7 @@ require 'rubygems' Gem::Specification.new do |spec| spec.name = 'sys-cpu' - spec.version = '1.2.0' + spec.version = '1.3.0' spec.author = 'Daniel J. Berger' spec.email = 'djberg96@gmail.com' spec.license = 'Apache-2.0' From af5f5dff4763a567a2d4e1ea3cd0a346a95870d1 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:18:11 -0400 Subject: [PATCH 08/24] Use PerfFormattedData_PerfOS_Processor by default. --- lib/sys/windows/sys/cpu.rb | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index 3dfd0de..d734fed 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -119,12 +119,26 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) # Returns CPU usage as a percentage. # - # This currently delegates to Win32_Processor.LoadPercentage, which is - # already averaged over a short interval. The +sample_time+ parameter is - # accepted for interface compatibility with other platforms but does nothing. + # The Win32_Processor.LoadPercentage value is per-processor (usually per + # physical socket), while Windows Task Manager reports a total value. + # When no +cpu_num+ is given it uses the _Total performance counter, which + # better matches what Task Manager reports. + # + # The +sample_time+ parameter is accepted for interface compatibility with + # other platforms but does nothing in this implementation. # def self.cpu_usage(_sample_time = 0, cpu_num = 0, host = Socket.gethostname) - load_avg(cpu_num, host) + instance = cpu_num.zero? ? '_Total' : cpu_num.to_s + cs = BASE_CS + "//#{host}/root/cimv2:Win32_PerfFormattedData_PerfOS_Processor='#{instance}'" + + begin + wmi = WIN32OLE.connect(cs) + rescue WIN32OLERuntimeError + # fall back to the older Win32_Processor.LoadPercentage behavior + return load_avg(cpu_num, host) + else + wmi.PercentProcessorTime + end end # Returns a string indicating the cpu model, e.g. Intel Pentium 4. From f062caad5ffe5bae5ccfb791a236ccaf42052f52 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:23:27 -0400 Subject: [PATCH 09/24] Update comments for Windows. --- lib/sys/windows/sys/cpu.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index d734fed..d47e3e9 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -119,13 +119,16 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) # Returns CPU usage as a percentage. # - # The Win32_Processor.LoadPercentage value is per-processor (usually per - # physical socket), while Windows Task Manager reports a total value. - # When no +cpu_num+ is given it uses the _Total performance counter, which - # better matches what Task Manager reports. - # # The +sample_time+ parameter is accepted for interface compatibility with # other platforms but does nothing in this implementation. + #-- + # 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 that the Task Manager reports the total CPU usage across all cores. + # Win32_Processor.LoadPercentage is per-processor (usually per physical socket), + # so it can differ from what Task Manager shows if it falls back to that. # def self.cpu_usage(_sample_time = 0, cpu_num = 0, host = Socket.gethostname) instance = cpu_num.zero? ? '_Total' : cpu_num.to_s @@ -137,7 +140,8 @@ def self.cpu_usage(_sample_time = 0, cpu_num = 0, host = Socket.gethostname) # fall back to the older Win32_Processor.LoadPercentage behavior return load_avg(cpu_num, host) else - wmi.PercentProcessorTime + result = wmi.PercentProcessorTime + result.nil? ? nil : result.to_i end end From e1e4f4de878d7f54525ba71c8d61cbd2c4a13981 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:45:42 -0400 Subject: [PATCH 10/24] Update cpu_usage on Macs to use host statistics. --- lib/sys/darwin/sys/cpu.rb | 78 ++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index b1f0c27..33dc7e8 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -215,7 +215,51 @@ def self.load_avg # average over that interval. If +sample_time+ is 0 (default), returns the # current utilization estimate based on the last set of CPU times. # + 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, [], :int + attach_function :host_statistics, %i[int int pointer pointer], :int + + private_class_method :mach_host_self, :host_statistics + def self.cpu_usage(sample_time = 0) + ticks = if (t = cpu_ticks_sysctl) + t + else + cpu_ticks_host + end + + return nil unless ticks + + if sample_time && sample_time > 0 + sleep(sample_time) + ticks2 = if (t = cpu_ticks_sysctl) + t + else + cpu_ticks_host + end + + return nil unless ticks2 + + base = ticks + diff = ticks2.map.with_index { |v, i| v - base[i] } + else + diff = ticks + end + + total = diff.sum + idle = diff[3] || 0 + return nil if total <= 0 + + ((1.0 - (idle.to_f / total)) * 100).round + rescue StandardError + nil + end + + def self.cpu_ticks_sysctl cp_time = proc { |ptr| len = 5 size = FFI::MemoryPointer.new(:size_t) @@ -228,31 +272,21 @@ def self.cpu_usage(sample_time = 0) ptr.read_array_of_ulong(len) } - if sample_time && sample_time > 0 - 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 + cp_time.call(FFI::MemoryPointer.new(:ulong, 5)) + rescue StandardError + nil + end - return nil if total_diff <= 0 + def self.cpu_ticks_host + host = mach_host_self + info = FFI::MemoryPointer.new(:int, HOST_CPU_LOAD_INFO_COUNT) + count = FFI::MemoryPointer.new(:uint) + count.write_uint(HOST_CPU_LOAD_INFO_COUNT) - ((1.0 - (idle_diff.to_f / total_diff)) * 100).round - else - # Fallback: use a single snapshot and interpret idle as the last element. - t = cp_time.call(FFI::MemoryPointer.new(:ulong, 5)) - total = t.sum - idle = t[4] || 0 + kr = host_statistics(host, HOST_CPU_LOAD_INFO, info, count) + return nil unless kr == 0 - return nil if total <= 0 - ((1.0 - (idle.to_f / total)) * 100).round - end + info.read_array_of_int(HOST_CPU_LOAD_INFO_COUNT) rescue StandardError nil end From 9ae671fd8566ad45a716943c2a383dfe50b60fe4 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:58:56 -0400 Subject: [PATCH 11/24] Use uint not int. --- lib/sys/darwin/sys/cpu.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index 33dc7e8..a5809dd 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -220,8 +220,8 @@ def self.load_avg private_constant :HOST_CPU_LOAD_INFO, :HOST_CPU_LOAD_INFO_COUNT - attach_function :mach_host_self, [], :int - attach_function :host_statistics, %i[int int pointer pointer], :int + attach_function :mach_host_self, [], :uint + attach_function :host_statistics, %i[uint int pointer pointer], :int private_class_method :mach_host_self, :host_statistics @@ -251,7 +251,12 @@ def self.cpu_usage(sample_time = 0) end total = diff.sum - idle = diff[3] || 0 + idle = if diff.size >= 5 + diff[4] || 0 + else + diff[2] || 0 + end + return nil if total <= 0 ((1.0 - (idle.to_f / total)) * 100).round @@ -279,14 +284,14 @@ def self.cpu_ticks_sysctl def self.cpu_ticks_host host = mach_host_self - info = FFI::MemoryPointer.new(:int, HOST_CPU_LOAD_INFO_COUNT) + 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_int(HOST_CPU_LOAD_INFO_COUNT) + info.read_array_of_uint(HOST_CPU_LOAD_INFO_COUNT) rescue StandardError nil end From d4ab1ed6c5b2b8f821a9cf0c1c2813dafc2db804 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:06:26 -0400 Subject: [PATCH 12/24] Update to get more meaningful data. --- lib/sys/darwin/sys/cpu.rb | 53 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index a5809dd..0d87ca2 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -226,42 +226,47 @@ def self.load_avg private_class_method :mach_host_self, :host_statistics def self.cpu_usage(sample_time = 0) - ticks = if (t = cpu_ticks_sysctl) - t + # On modern macOS, tick counts are cumulative since boot. To get a meaningful + # CPU utilization percentage, we sample over a short interval and average. + if sample_time.nil? || sample_time <= 0 + sample_time = 0.2 + samples = 4 else - cpu_ticks_host + samples = 1 end - return nil unless ticks + usages = [] - if sample_time && sample_time > 0 + samples.times do + t1 = current_ticks sleep(sample_time) - ticks2 = if (t = cpu_ticks_sysctl) - t - else - cpu_ticks_host + t2 = current_ticks + next unless t1 && t2 + + if (u = usage_between_ticks(t1, t2)) + usages << u end + end - return nil unless ticks2 + return nil if usages.empty? - base = ticks - diff = ticks2.map.with_index { |v, i| v - base[i] } - else - diff = ticks - end + (usages.sum / usages.size.to_f).round(1) + rescue StandardError + nil + end - total = diff.sum - idle = if diff.size >= 5 - diff[4] || 0 - else - diff[2] || 0 - end + def self.current_ticks + cpu_ticks_sysctl || cpu_ticks_host + end + 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 - ((1.0 - (idle.to_f / total)) * 100).round - rescue StandardError - nil + # host_statistics returns [user, system, idle, nice] + idle = diff[2] || 0 + (1.0 - (idle.to_f / total)) * 100 end def self.cpu_ticks_sysctl From f8970cf24ab1b6c4eaa8b4b01bd38832c30b44a8 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:45:47 -0400 Subject: [PATCH 13/24] Default back to 1 second, add a samples argument. --- lib/sys/darwin/sys/cpu.rb | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index 0d87ca2..48c2178 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -211,9 +211,11 @@ def self.load_avg # Returns CPU usage as a percentage. # - # If +sample_time+ is positive, samples CPU times twice and calculates an - # average over that interval. If +sample_time+ is 0 (default), returns the - # current utilization estimate based on the last set of CPU times. + # 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. # HOST_CPU_LOAD_INFO = 3 HOST_CPU_LOAD_INFO_COUNT = 4 @@ -225,15 +227,12 @@ def self.load_avg private_class_method :mach_host_self, :host_statistics - def self.cpu_usage(sample_time = 0) + def self.cpu_usage(sample_time = 1.0, samples = 1) # On modern macOS, tick counts are cumulative since boot. To get a meaningful - # CPU utilization percentage, we sample over a short interval and average. - if sample_time.nil? || sample_time <= 0 - sample_time = 0.2 - samples = 4 - else - samples = 1 - end + # CPU utilization percentage, we sample over an interval and average. + # Default to a 1-second sample window when no duration is provided. + sample_time = 1.0 if sample_time.nil? || sample_time <= 0 + samples = 1 if samples.nil? || samples <= 0 usages = [] From 2b9115e5f3b146a700ff5f77315afd75da301915 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:52:17 -0400 Subject: [PATCH 14/24] Default to 2 samples. --- lib/sys/darwin/sys/cpu.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index 48c2178..5f51186 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -216,6 +216,7 @@ def self.load_avg # 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 @@ -227,12 +228,12 @@ def self.load_avg private_class_method :mach_host_self, :host_statistics - def self.cpu_usage(sample_time = 1.0, samples = 1) + def self.cpu_usage(sample_time = 1.0, samples = 2) # On modern macOS, tick counts are cumulative since boot. To get a meaningful # CPU utilization percentage, we sample over an interval and average. # Default to a 1-second sample window when no duration is provided. sample_time = 1.0 if sample_time.nil? || sample_time <= 0 - samples = 1 if samples.nil? || samples <= 0 + samples = 2 if samples.nil? || samples <= 0 usages = [] From 0ef2ed03bb49d81510f68fca485dc933c1988e0f Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:21:42 -0400 Subject: [PATCH 15/24] Update other methods to match MacOS. --- lib/sys/linux/sys/cpu.rb | 30 +++++++++++++----------------- lib/sys/unix/sys/cpu.rb | 24 +++++++++++------------- lib/sys/windows/sys/cpu.rb | 28 +++++++++++++++++++--------- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/lib/sys/linux/sys/cpu.rb b/lib/sys/linux/sys/cpu.rb index ddbad53..44a5522 100644 --- a/lib/sys/linux/sys/cpu.rb +++ b/lib/sys/linux/sys/cpu.rb @@ -117,8 +117,13 @@ def self.freq CPU_ARRAY.first['cpu_mhz'].to_f.round end - def self.cpu_usage(sample_time = 0) - if sample_time && sample_time > 0 + 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 @@ -126,7 +131,6 @@ def self.cpu_usage(sample_time = 0) total_diff = 0.0 idle_diff = 0.0 - # Use aggregate 'cpu' line if present, else sum all keys = stats1.key?('cpu') ? ['cpu'] : stats1.keys keys.each do |key| arr1 = stats1[key] @@ -135,26 +139,18 @@ def self.cpu_usage(sample_time = 0) t1 = arr1.sum t2 = arr2.sum total = t2 - t1 - idle = (arr2[3] || 0) - (arr1[3] || 0) # idle is 4th field + idle = (arr2[3] || 0) - (arr1[3] || 0) total_diff += total idle_diff += idle end - return nil if total_diff <= 0 - usage = (1.0 - (idle_diff / total_diff)) * 100 - usage.round - else - # Single snapshot, 100 - iowait% - stats = cpu_stats - total = 0.0 - iowait = 0.0 - stats.each_value do |arr| - total += arr.sum - iowait += (arr[4] || 0).to_f + if total_diff > 0 + usages << ((1.0 - (idle_diff / total_diff)) * 100) end - return nil if total <= 0 - (100 - ((iowait / total) * 100)).to_i end + + return nil if usages.empty? + (usages.sum / usages.size.to_f).round(1) rescue StandardError nil end diff --git a/lib/sys/unix/sys/cpu.rb b/lib/sys/unix/sys/cpu.rb index 89e9a32..f9ffa9d 100644 --- a/lib/sys/unix/sys/cpu.rb +++ b/lib/sys/unix/sys/cpu.rb @@ -315,7 +315,7 @@ def self.load_avg # average over that interval. If +sample_time+ is 0 (default), returns the # current utilization estimate based on the last set of CPU times. # - def self.cpu_usage(sample_time = 0) + def self.cpu_usage(sample_time = 1.0, samples = 2) cp_time = proc { |ptr| len = 5 size = FFI::MemoryPointer.new(:size_t) @@ -328,7 +328,12 @@ def self.cpu_usage(sample_time = 0) ptr.read_array_of_ulong(len) } - if sample_time && sample_time > 0 + 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)) @@ -341,18 +346,11 @@ def self.cpu_usage(sample_time = 0) total_diff = total2 - total1 idle_diff = idle2 - idle1 - return nil if total_diff <= 0 - - ((1.0 - (idle_diff.to_f / total_diff)) * 100).round - else - # Fallback: use a single snapshot and interpret idle as the last element. - t = cp_time.call(FFI::MemoryPointer.new(:ulong, 5)) - total = t.sum - idle = t[4] || 0 - - return nil if total <= 0 - ((1.0 - (idle.to_f / total)) * 100).round + 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 diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index d47e3e9..2aa6504 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -130,19 +130,29 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) # Win32_Processor.LoadPercentage is per-processor (usually per physical socket), # so it can differ from what Task Manager shows if it falls back to that. # - def self.cpu_usage(_sample_time = 0, cpu_num = 0, host = Socket.gethostname) + 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 instance = cpu_num.zero? ? '_Total' : cpu_num.to_s cs = BASE_CS + "//#{host}/root/cimv2:Win32_PerfFormattedData_PerfOS_Processor='#{instance}'" - begin - wmi = WIN32OLE.connect(cs) - rescue WIN32OLERuntimeError - # fall back to the older Win32_Processor.LoadPercentage behavior - return load_avg(cpu_num, host) - else - result = wmi.PercentProcessorTime - result.nil? ? nil : result.to_i + 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. From bba553560472a36186dacec2b5bf17c42af2ace9 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:41:52 -0400 Subject: [PATCH 16/24] Updates and fixes for Windows. --- lib/sys/windows/sys/cpu.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index 2aa6504..8cedee5 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -117,23 +117,23 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) end end - # Returns CPU usage as a percentage. + # Returns CPU usage as a percentage, averaged over multiple samples. # - # The +sample_time+ parameter is accepted for interface compatibility with - # other platforms but does nothing in this implementation. - #-- - # 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. + # The +sample_time+ parameter specifies the interval (in seconds) between samples. + # The +samples+ parameter specifies how many samples to take and average. + # The +cpu_num+ parameter selects which CPU to query (0 for total). + # The +host+ parameter specifies the target machine (defaults to local). # - # Note that the Task Manager reports the total CPU usage across all cores. - # Win32_Processor.LoadPercentage is per-processor (usually per physical socket), - # so it can differ from what Task Manager shows if it falls back to that. + # 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 - instance = cpu_num.zero? ? '_Total' : cpu_num.to_s + 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 = [] From dd7060e802f4e3c195105fa3a5594b7bf5959922 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:07:00 -0400 Subject: [PATCH 17/24] Update a windows spec. --- spec/sys_cpu_windows_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/sys_cpu_windows_spec.rb b/spec/sys_cpu_windows_spec.rb index f655720..6b516a9 100644 --- a/spec/sys_cpu_windows_spec.rb +++ b/spec/sys_cpu_windows_spec.rb @@ -63,7 +63,7 @@ expect(described_class).to respond_to(:cpu_usage) expect{ described_class.cpu_usage }.not_to raise_error expect{ described_class.cpu_usage(0.1, 0, host) }.not_to raise_error - expect(described_class.cpu_usage).to be_a(Integer).or be_a(NilClass) + expect(described_class.cpu_usage).to be_a(Numeric).or be_a(NilClass) end example 'cpu_usage sampling produces a valid range' do From 561fdeadad063a03eb4b533c8e5b17151d6a6e35 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:26:36 -0400 Subject: [PATCH 18/24] Make internal methods private, add comments. --- lib/sys/darwin/sys/cpu.rb | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index 5f51186..362c31d 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -228,12 +228,40 @@ def self.load_avg 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+ arguments. For example, +cpu_usage(0.5, 4)+ will take four samples, each 0.5 seconds apart, + # and return the average CPU usage over that period. + # + # If you pass 0 for either +sample_time+ or +samples+, the method returns the raw CPU tick counts + # since boot (as an array of counters), which can be used for advanced or custom calculations. + # + # Returns a Float (percentage) by default, or an Array of tick counts if 0 is passed for either argument. + # Returns nil if CPU usage cannot be determined. + # + # Example usage: + # Sys::CPU.cpu_usage #=> 12.3 + # Sys::CPU.cpu_usage(2, 3) #=> 10.7 + # Sys::CPU.cpu_usage(0, 0) #=> [123456, 78910, 1112, 1314] + # + #-- + # On modern macOS, tick counts are cumulative since boot. To get a meaningful + # CPU utilization percentage, you will generally want to sample over an + # interval and average. If sample_time or samples are nil, default to 1.0 + # and 2, respectively. If either is explicitly 0, return tick counts since boot. + # def self.cpu_usage(sample_time = 1.0, samples = 2) - # On modern macOS, tick counts are cumulative since boot. To get a meaningful - # CPU utilization percentage, we sample over an interval and average. - # Default to a 1-second sample window when no duration is provided. - sample_time = 1.0 if sample_time.nil? || sample_time <= 0 - samples = 2 if samples.nil? || samples <= 0 + if sample_time.nil? + sample_time = 1.0 + end + if samples.nil? + samples = 2 + end + if sample_time == 0 || samples == 0 + return current_ticks + end usages = [] @@ -259,6 +287,8 @@ 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 @@ -269,6 +299,8 @@ def self.usage_between_ticks(t1, t2) (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 @@ -287,6 +319,8 @@ def self.cpu_ticks_sysctl 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) @@ -300,5 +334,7 @@ def self.cpu_ticks_host rescue StandardError nil end + + private_class_method :cpu_ticks_host end end From eacf423bc6537f8f43bb0ee202f8751f0bdcd66b Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:23:45 -0400 Subject: [PATCH 19/24] Switch to one liners. --- lib/sys/darwin/sys/cpu.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index 362c31d..55ed7c1 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -253,15 +253,10 @@ def self.load_avg # and 2, respectively. If either is explicitly 0, return tick counts since boot. # def self.cpu_usage(sample_time = 1.0, samples = 2) - if sample_time.nil? - sample_time = 1.0 - end - if samples.nil? - samples = 2 - end - if sample_time == 0 || samples == 0 - return current_ticks - end + sample_time = 1.0 if sample_time.nil? + samples = 2 if samples.nil? + + return current_ticks if sample_time == 0 || samples == 0 usages = [] From 1b7a8efc6776351f1b28cc307727a69970de6bb0 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:01:54 -0400 Subject: [PATCH 20/24] Add documentation for Linux. --- lib/sys/linux/sys/cpu.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/sys/linux/sys/cpu.rb b/lib/sys/linux/sys/cpu.rb index 44a5522..9047cd1 100644 --- a/lib/sys/linux/sys/cpu.rb +++ b/lib/sys/linux/sys/cpu.rb @@ -117,6 +117,23 @@ 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+ arguments. For example, +cpu_usage(0.5, 4)+ will take four samples, each 0.5 seconds apart, + # and return the average CPU usage over that period. + # + # Unlike the macOS implementation, passing 0 or a negative value for either argument falls back to the + # defaults (1.0 seconds and 2 samples) rather than returning raw tick counts. + # + # 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(2, 3) #=> 10.7 + # Sys::CPU.cpu_usage(0, 0) #=> 12.3 # zeros fall back to defaults on Linux + # 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 From cdb446ffc723e2373efd13e2a895f172748f1665 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:07:18 -0400 Subject: [PATCH 21/24] Get rid of tick counts, just use defaults, to be consistent across platforms. --- lib/sys/darwin/sys/cpu.rb | 15 ++++++--------- lib/sys/linux/sys/cpu.rb | 6 +++--- lib/sys/unix/sys/cpu.rb | 8 ++++---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index 55ed7c1..a68cdbc 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -235,16 +235,15 @@ def self.load_avg # and +samples+ arguments. For example, +cpu_usage(0.5, 4)+ will take four samples, each 0.5 seconds apart, # and return the average CPU usage over that period. # - # If you pass 0 for either +sample_time+ or +samples+, the method returns the raw CPU tick counts - # since boot (as an array of counters), which can be used for advanced or custom calculations. + # 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) by default, or an Array of tick counts if 0 is passed for either argument. - # Returns nil if CPU usage cannot be determined. + # 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(2, 3) #=> 10.7 - # Sys::CPU.cpu_usage(0, 0) #=> [123456, 78910, 1112, 1314] + # Sys::CPU.cpu_usage(0, 0) #=> 12.3 # zeros fall back to defaults # #-- # On modern macOS, tick counts are cumulative since boot. To get a meaningful @@ -253,10 +252,8 @@ def self.load_avg # and 2, respectively. If either is explicitly 0, return tick counts since boot. # def self.cpu_usage(sample_time = 1.0, samples = 2) - sample_time = 1.0 if sample_time.nil? - samples = 2 if samples.nil? - - return current_ticks if sample_time == 0 || samples == 0 + sample_time = 1.0 if sample_time.nil? || sample_time <= 0 + samples = 2 if samples.nil? || samples <= 0 usages = [] diff --git a/lib/sys/linux/sys/cpu.rb b/lib/sys/linux/sys/cpu.rb index 9047cd1..db4a268 100644 --- a/lib/sys/linux/sys/cpu.rb +++ b/lib/sys/linux/sys/cpu.rb @@ -124,15 +124,15 @@ def self.freq # and +samples+ arguments. For example, +cpu_usage(0.5, 4)+ will take four samples, each 0.5 seconds apart, # and return the average CPU usage over that period. # - # Unlike the macOS implementation, passing 0 or a negative value for either argument falls back to the - # defaults (1.0 seconds and 2 samples) rather than returning raw tick counts. + # 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(2, 3) #=> 10.7 - # Sys::CPU.cpu_usage(0, 0) #=> 12.3 # zeros fall back to defaults on Linux + # Sys::CPU.cpu_usage(0, 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 diff --git a/lib/sys/unix/sys/cpu.rb b/lib/sys/unix/sys/cpu.rb index f9ffa9d..9d19639 100644 --- a/lib/sys/unix/sys/cpu.rb +++ b/lib/sys/unix/sys/cpu.rb @@ -309,11 +309,11 @@ def self.load_avg loadavg.get_array_of_double(0, 3) end - # Returns CPU usage as a percentage. + # Returns CPU usage as a percentage, averaged over a sampling interval. # - # If +sample_time+ is positive, samples CPU times twice and calculates an - # average over that interval. If +sample_time+ is 0 (default), returns the - # current utilization estimate based on the last set of CPU times. + # By default, samples CPU times twice, 1 second apart. Passing nil, 0, or a + # negative value for +sample_time+ or +samples+ falls back to these + # defaults for cross-platform consistency. # def self.cpu_usage(sample_time = 1.0, samples = 2) cp_time = proc { |ptr| From 6105626ebc4dbb6707212487cb0f3f3105eeeb83 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:11:40 -0400 Subject: [PATCH 22/24] Switch to keywords. --- lib/sys/darwin/sys/cpu.rb | 18 ++++++------------ lib/sys/linux/sys/cpu.rb | 11 ++++++----- lib/sys/unix/sys/cpu.rb | 8 ++++---- lib/sys/windows/sys/cpu.rb | 10 +++++----- spec/sys_cpu_bsd_spec.rb | 4 ++-- spec/sys_cpu_hpux_spec.rb | 4 ++-- spec/sys_cpu_linux_spec.rb | 2 +- spec/sys_cpu_windows_spec.rb | 4 ++-- 8 files changed, 28 insertions(+), 33 deletions(-) diff --git a/lib/sys/darwin/sys/cpu.rb b/lib/sys/darwin/sys/cpu.rb index a68cdbc..b91a32a 100644 --- a/lib/sys/darwin/sys/cpu.rb +++ b/lib/sys/darwin/sys/cpu.rb @@ -232,8 +232,8 @@ def self.load_avg # # 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+ arguments. For example, +cpu_usage(0.5, 4)+ will take four samples, each 0.5 seconds apart, - # and return the average CPU usage over that period. + # 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. @@ -241,17 +241,11 @@ def self.load_avg # 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(2, 3) #=> 10.7 - # Sys::CPU.cpu_usage(0, 0) #=> 12.3 # zeros fall back to defaults + # 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 # - #-- - # On modern macOS, tick counts are cumulative since boot. To get a meaningful - # CPU utilization percentage, you will generally want to sample over an - # interval and average. If sample_time or samples are nil, default to 1.0 - # and 2, respectively. If either is explicitly 0, return tick counts since boot. - # - def self.cpu_usage(sample_time = 1.0, samples = 2) + 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 diff --git a/lib/sys/linux/sys/cpu.rb b/lib/sys/linux/sys/cpu.rb index db4a268..afd3532 100644 --- a/lib/sys/linux/sys/cpu.rb +++ b/lib/sys/linux/sys/cpu.rb @@ -121,7 +121,8 @@ def self.freq # # 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+ arguments. For example, +cpu_usage(0.5, 4)+ will take four samples, each 0.5 seconds apart, + # 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 @@ -130,11 +131,11 @@ def self.freq # 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(2, 3) #=> 10.7 - # Sys::CPU.cpu_usage(0, 0) #=> 12.3 # zeros fall back to defaults + # 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) + 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 diff --git a/lib/sys/unix/sys/cpu.rb b/lib/sys/unix/sys/cpu.rb index 9d19639..88d9bcb 100644 --- a/lib/sys/unix/sys/cpu.rb +++ b/lib/sys/unix/sys/cpu.rb @@ -311,11 +311,11 @@ def self.load_avg # Returns CPU usage as a percentage, averaged over a sampling interval. # - # By default, samples CPU times twice, 1 second apart. Passing nil, 0, or a - # negative value for +sample_time+ or +samples+ falls back to these - # defaults for cross-platform consistency. + # 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) + def self.cpu_usage(sample_time: 1.0, samples: 2) cp_time = proc { |ptr| len = 5 size = FFI::MemoryPointer.new(:size_t) diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index 8cedee5..3abd5bd 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -119,17 +119,17 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) # Returns CPU usage as a percentage, averaged over multiple samples. # - # The +sample_time+ parameter specifies the interval (in seconds) between samples. - # The +samples+ parameter specifies how many samples to take and average. - # The +cpu_num+ parameter selects which CPU to query (0 for total). - # The +host+ parameter specifies the target machine (defaults to local). + # 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) + 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) diff --git a/spec/sys_cpu_bsd_spec.rb b/spec/sys_cpu_bsd_spec.rb index ec92718..59ae076 100644 --- a/spec/sys_cpu_bsd_spec.rb +++ b/spec/sys_cpu_bsd_spec.rb @@ -55,12 +55,12 @@ 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(0.1) }.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 sampling produces a valid range' do - result = described_class.cpu_usage(0.1) + 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 diff --git a/spec/sys_cpu_hpux_spec.rb b/spec/sys_cpu_hpux_spec.rb index bcf5a83..a4182a3 100644 --- a/spec/sys_cpu_hpux_spec.rb +++ b/spec/sys_cpu_hpux_spec.rb @@ -56,12 +56,12 @@ 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(0.1) }.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 sampling produces a valid range' do - result = described_class.cpu_usage(0.1) + 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 diff --git a/spec/sys_cpu_linux_spec.rb b/spec/sys_cpu_linux_spec.rb index 6e566a2..f239274 100644 --- a/spec/sys_cpu_linux_spec.rb +++ b/spec/sys_cpu_linux_spec.rb @@ -56,7 +56,7 @@ example 'cpu_usage sampling produces a valid range' do # Sampled usage should be a number between 0 and 100. - result = described_class.cpu_usage(0.1) + 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 diff --git a/spec/sys_cpu_windows_spec.rb b/spec/sys_cpu_windows_spec.rb index 6b516a9..54ae953 100644 --- a/spec/sys_cpu_windows_spec.rb +++ b/spec/sys_cpu_windows_spec.rb @@ -62,13 +62,13 @@ 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(0.1, 0, host) }.not_to raise_error + expect{ described_class.cpu_usage(sample_time: 0.1, samples: 0, host: host) }.not_to raise_error expect(described_class.cpu_usage).to be_a(Numeric).or be_a(NilClass) 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(0.1) + 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 From cfbc11ef5d212bc0117237b0ee0d9047868cffee Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:13:28 -0400 Subject: [PATCH 23/24] Add some specs for default values. --- spec/sys_cpu_bsd_spec.rb | 6 ++++++ spec/sys_cpu_hpux_spec.rb | 6 ++++++ spec/sys_cpu_linux_spec.rb | 6 ++++++ spec/sys_cpu_windows_spec.rb | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/spec/sys_cpu_bsd_spec.rb b/spec/sys_cpu_bsd_spec.rb index 59ae076..e44df77 100644 --- a/spec/sys_cpu_bsd_spec.rb +++ b/spec/sys_cpu_bsd_spec.rb @@ -59,6 +59,12 @@ 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 diff --git a/spec/sys_cpu_hpux_spec.rb b/spec/sys_cpu_hpux_spec.rb index a4182a3..59dba8f 100644 --- a/spec/sys_cpu_hpux_spec.rb +++ b/spec/sys_cpu_hpux_spec.rb @@ -60,6 +60,12 @@ 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 diff --git a/spec/sys_cpu_linux_spec.rb b/spec/sys_cpu_linux_spec.rb index f239274..61b504f 100644 --- a/spec/sys_cpu_linux_spec.rb +++ b/spec/sys_cpu_linux_spec.rb @@ -54,6 +54,12 @@ 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) diff --git a/spec/sys_cpu_windows_spec.rb b/spec/sys_cpu_windows_spec.rb index 54ae953..923c904 100644 --- a/spec/sys_cpu_windows_spec.rb +++ b/spec/sys_cpu_windows_spec.rb @@ -66,6 +66,12 @@ expect(described_class.cpu_usage).to be_a(Numeric).or be_a(NilClass) end + example 'cpu_usage falls back on non-positive values' do + expect{ described_class.cpu_usage(sample_time: 0, samples: 0, host: host) }.not_to raise_error + expect{ described_class.cpu_usage(sample_time: -1, samples: -1, host: host) }.not_to raise_error + expect(described_class.cpu_usage(sample_time: 0, samples: 0, host: host)).to be_a(Numeric).or be_a(NilClass) + 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) From cb5f55b2f78cc382202aaedd6e8ea808fc9c3d6d Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:54:26 -0400 Subject: [PATCH 24/24] Minor comment formatting for Windows. --- lib/sys/windows/sys/cpu.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/sys/windows/sys/cpu.rb b/lib/sys/windows/sys/cpu.rb index 3abd5bd..0e86ff7 100644 --- a/lib/sys/windows/sys/cpu.rb +++ b/lib/sys/windows/sys/cpu.rb @@ -124,11 +124,13 @@ def self.load_avg(cpu_num = 0, host = Socket.gethostname) # 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