From b393ec3e3f76b22719c639ad6bfe5502520b8885 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 1 Aug 2026 22:45:24 +1200 Subject: [PATCH] Normalize processor utilization --- context/getting-started.md | 25 ++++++++++++++++++- guides/getting-started/readme.md | 25 ++++++++++++++++++- lib/process/metrics/general.rb | 1 + lib/process/metrics/general/linux.rb | 7 +++++- lib/process/metrics/general/process_status.rb | 2 +- lib/process/metrics/processor.rb | 2 +- releases.md | 4 +++ test/process/general/linux.rb | 6 +++++ test/process/general/process_status.rb | 14 +++++++++++ 9 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 test/process/general/process_status.rb diff --git a/context/getting-started.md b/context/getting-started.md index f8aa0b1..a820021 100644 --- a/context/getting-started.md +++ b/context/getting-started.md @@ -21,6 +21,7 @@ $ gem install process-metrics The `process-metrics` gem provides a simple interface to collect and analyze process metrics. - {ruby Process::Metrics::General} is the main entry point for process metrics. Use {ruby Process::Metrics::General.capture} to collect metrics for one or more processes. +- {ruby Process::Metrics::Processor} measures processor utilization over intervals between samples. - {ruby Process::Metrics::Memory} provides additional methods for collecting memory metrics when the host operating system provides the necessary information. ## Usage @@ -67,7 +68,7 @@ The {ruby Process::Metrics::General} struct contains the following fields: - `process_id` - Process ID, a unique identifier for the process. - `parent_process_id` - Parent Process ID, the process ID of the process that started this process. - `process_group_id` - Process Group ID, the process group ID of the process, which can be shared by multiple processes. -- `processor_utilization` - Processor Utilization (%), the percentage of CPU time used by the process (over a system-specific duration). +- `processor_utilization` - Average processor utilization in core units over the system's observation period. `1.0` represents one fully occupied CPU core, and multi-threaded processes can exceed `1.0`. - `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). - `resident_size` - Resident (Set) Size (bytes), the amount of physical memory used by the process. - `processor_time` - CPU Time (s), the amount of CPU time used by the process. @@ -89,3 +90,25 @@ The {ruby Process::Metrics::Memory} struct contains the following fields: - `proportional_swap_size` - Proportional Swap Memory Size (bytes), the amount of memory that has been swapped to disk, excluding shared memory. 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. + +## Interval Processor Utilization + +The processor utilization reported by {ruby Process::Metrics::General} is a snapshot based on the operating system's observation period. Use {ruby Process::Metrics::Processor} when you need utilization over a specific sampling interval: + +``` ruby +processor = Process::Metrics::Processor.new + +# The first sample establishes a baseline: +processor.sample(Process.pid) + +sleep(10) +sample = processor.sample(Process.pid).fetch(Process.pid) + +sample.duration +# => approximately 10.0 + +sample.utilization +# => 1.0 means one fully occupied CPU core during the interval +``` + +Both interfaces use the same core-unit scale. A utilization of `0.5` means half of one core on average, while `2.0` means two cores were fully occupied. Utilization is therefore not limited to the range `0.0..1.0`. diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index f8aa0b1..a820021 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -21,6 +21,7 @@ $ gem install process-metrics The `process-metrics` gem provides a simple interface to collect and analyze process metrics. - {ruby Process::Metrics::General} is the main entry point for process metrics. Use {ruby Process::Metrics::General.capture} to collect metrics for one or more processes. +- {ruby Process::Metrics::Processor} measures processor utilization over intervals between samples. - {ruby Process::Metrics::Memory} provides additional methods for collecting memory metrics when the host operating system provides the necessary information. ## Usage @@ -67,7 +68,7 @@ The {ruby Process::Metrics::General} struct contains the following fields: - `process_id` - Process ID, a unique identifier for the process. - `parent_process_id` - Parent Process ID, the process ID of the process that started this process. - `process_group_id` - Process Group ID, the process group ID of the process, which can be shared by multiple processes. -- `processor_utilization` - Processor Utilization (%), the percentage of CPU time used by the process (over a system-specific duration). +- `processor_utilization` - Average processor utilization in core units over the system's observation period. `1.0` represents one fully occupied CPU core, and multi-threaded processes can exceed `1.0`. - `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). - `resident_size` - Resident (Set) Size (bytes), the amount of physical memory used by the process. - `processor_time` - CPU Time (s), the amount of CPU time used by the process. @@ -89,3 +90,25 @@ The {ruby Process::Metrics::Memory} struct contains the following fields: - `proportional_swap_size` - Proportional Swap Memory Size (bytes), the amount of memory that has been swapped to disk, excluding shared memory. 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. + +## Interval Processor Utilization + +The processor utilization reported by {ruby Process::Metrics::General} is a snapshot based on the operating system's observation period. Use {ruby Process::Metrics::Processor} when you need utilization over a specific sampling interval: + +``` ruby +processor = Process::Metrics::Processor.new + +# The first sample establishes a baseline: +processor.sample(Process.pid) + +sleep(10) +sample = processor.sample(Process.pid).fetch(Process.pid) + +sample.duration +# => approximately 10.0 + +sample.utilization +# => 1.0 means one fully occupied CPU core during the interval +``` + +Both interfaces use the same core-unit scale. A utilization of `0.5` means half of one core on average, while `2.0` means two cores were fully occupied. Utilization is therefore not limited to the range `0.0..1.0`. diff --git a/lib/process/metrics/general.rb b/lib/process/metrics/general.rb index 47c9c59..bad5c4c 100644 --- a/lib/process/metrics/general.rb +++ b/lib/process/metrics/general.rb @@ -35,6 +35,7 @@ def self.duration(value) end # General process information. + # @attribute [Float] The processor utilization in core units, where `1.0` represents one fully occupied CPU core. Multi-threaded processes may report values greater than `1.0`. class General < Struct.new(:process_id, :parent_process_id, :process_group_id, :processor_utilization, :virtual_size, :resident_size, :processor_time, :elapsed_time, :start_time, :command, :memory) # Convert the object to a JSON serializable hash. def as_json diff --git a/lib/process/metrics/general/linux.rb b/lib/process/metrics/general/linux.rb index 6417eff..7c6a651 100644 --- a/lib/process/metrics/general/linux.rb +++ b/lib/process/metrics/general/linux.rb @@ -73,6 +73,11 @@ def self.capture(pid: nil, ppid: nil, memory: Memory.supported?) end processor_time = (utime + stime).to_f / CLK_TCK elapsed_time = [(uptime_jiffies - start_time).to_f / CLK_TCK, 0.0].max + processor_utilization = if elapsed_time > 0.0 + processor_time.fdiv(elapsed_time) + else + 0.0 + end command = read_command(pid, executable_name) @@ -80,7 +85,7 @@ def self.capture(pid: nil, ppid: nil, memory: Memory.supported?) pid, parent_process_id, process_group_id, - 0.0, # processor_utilization: would need two samples; not available from single stat read + processor_utilization, virtual_size, resident_pages * PAGE_SIZE, processor_time, diff --git a/lib/process/metrics/general/process_status.rb b/lib/process/metrics/general/process_status.rb index a409cb7..4a6eb5e 100644 --- a/lib/process/metrics/general/process_status.rb +++ b/lib/process/metrics/general/process_status.rb @@ -17,7 +17,7 @@ module General::ProcessStatus pid: ->(values){values.shift.to_i}, ppid: ->(values){values.shift.to_i}, pgid: ->(values){values.shift.to_i}, - pcpu: ->(values){values.shift.to_f}, + pcpu: ->(values){values.shift.to_f / 100.0}, vsz: ->(values){values.shift.to_i * 1024}, rss: ->(values){values.shift.to_i * 1024}, time: ->(values){Process::Metrics.duration(values.shift)}, diff --git a/lib/process/metrics/processor.rb b/lib/process/metrics/processor.rb index 6f3570e..e17bb96 100644 --- a/lib/process/metrics/processor.rb +++ b/lib/process/metrics/processor.rb @@ -11,7 +11,7 @@ class Processor # @attribute [Integer] The process ID. # @attribute [Float] The elapsed monotonic time in seconds. # @attribute [Float] The CPU time consumed during the interval in seconds. - # @attribute [Float] The CPU utilization, where one fully occupied core is `1.0`. + # @attribute [Float] The CPU utilization in core units, where `1.0` represents one fully occupied core. Multi-threaded processes may report values greater than `1.0`. class Sample < Struct.new(:process_id, :duration, :processor_time, :utilization) end diff --git a/releases.md b/releases.md index c205174..9b785ba 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Normalize processor utilization to core units, where `1.0` represents one fully occupied CPU core, and restore Linux reporting. + ## v0.12.0 - Add `Process::Metrics::Processor` for measuring per-process CPU utilization over an interval. diff --git a/test/process/general/linux.rb b/test/process/general/linux.rb index 1714aef..c67a8d5 100644 --- a/test/process/general/linux.rb +++ b/test/process/general/linux.rb @@ -30,6 +30,12 @@ def assert_backends_match(linux_capture, process_status_capture) expect(linux_process.resident_size).to be_within(10.0).percent_of(process_status_process.resident_size) expect(linux_process.command).to be == process_status_process.command + expected_utilization = if linux_process.elapsed_time > 0.0 + linux_process.processor_time.fdiv(linux_process.elapsed_time) + else + 0.0 + end + expect(linux_process.processor_utilization).to be == expected_utilization expect((linux_process.processor_time - process_status_process.processor_time).abs).to be < 1.0 expect((linux_process.elapsed_time - process_status_process.elapsed_time).abs).to be < 1.0 expect((linux_process.start_time - process_status_process.start_time).abs).to be < 2.0 diff --git a/test/process/general/process_status.rb b/test/process/general/process_status.rb new file mode 100644 index 0000000..d5e91f8 --- /dev/null +++ b/test/process/general/process_status.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "process/metrics/general/process_status" + +describe Process::Metrics::General::ProcessStatus do + it "normalizes processor utilization to core units" do + values = ["125.0"] + + expect(subject::FIELDS[:pcpu].call(values)).to be == 1.25 + end +end