class Processor

Computes interval CPU utilization from cumulative process metrics.

Nested Classes and Modules

module Linux
class Sample

An immutable measurement of process CPU usage over an interval.

Definitions

def self.quota

The processor capacity available to the current process. This takes cgroup v2 CPU bandwidth limits into account, and otherwise returns Process::Metrics::Processor.count as a Float.

Signature

returns Float

The available processor capacity in core units.

Implementation

def self.quota
	count = self.count.to_f
	
	if quota = Linux.quota
		if quota < count
			return quota
		end
	end
	
	return count
end

def self.quota

The processor capacity available to the current process. On Linux, this takes cgroup v2 CPU bandwidth limits into account. Otherwise, it returns Process::Metrics::Processor.count as a Float.

Signature

returns Float

The available processor capacity in core units.

Implementation

def self.quota
	return self.count.to_f
end

def self.count

The number of processors available to the current process. On Linux, this takes the process CPU affinity into account.

Signature

returns Integer

The number of available processors.

Implementation

def self.count
	Etc.nprocessors
end

Snapshot = Struct.new(:start_time, :processor_time, :timestamp)

  • private

Signature

private

def initialize(capture: General.method(:capture))

Initialize a process metrics sampler.

Signature

parameter capture Interface(:call)

The process snapshot capture callable.

Implementation

def initialize(capture: General.method(:capture))
	@capture = capture
	@snapshots = {}
end

def sample(process_ids)

Sample CPU utilization for the given processes. The first observation of each process establishes a baseline and does not produce a sample.

Signature

parameter process_ids Integer | Array(Integer)

The process IDs to sample.

returns Hash(Integer, Processor::Sample)

The valid interval samples keyed by process ID.

Implementation

def sample(process_ids)
	processes = @capture.call(pid: process_ids, memory: false)
	timestamp = now
	return {} unless finite?(timestamp)
	
	samples = {}
	snapshots = {}
	
	processes.each do |process_id, process|
		start_time = process.start_time
		processor_time = process.processor_time
		next unless finite?(start_time) && finite?(processor_time)
		
		current = Snapshot.new(start_time, processor_time, timestamp)
		snapshots[process_id] = current
		
		if previous = @snapshots[process_id]
			next unless previous.start_time == current.start_time
			
			duration = current.timestamp - previous.timestamp
			processor_time = current.processor_time - previous.processor_time
			next unless finite?(duration) && duration > 0.0
			next unless finite?(processor_time) && processor_time >= 0.0
			
			utilization = processor_time / duration
			next unless finite?(utilization)
			
			samples[process_id] = Sample.new(process_id, duration, processor_time, utilization).freeze
		end
	end
	
	@snapshots = snapshots
	
	return samples
end

def now

Get the current monotonic time.

Implementation

def now
	Process.clock_gettime(Process::CLOCK_MONOTONIC)
end

def finite?(value)

Whether the value is a finite number.

Implementation

def finite?(value)
	value.is_a?(Numeric) && value.finite?
end