PSI
PSI is a pure Ruby interface to Linux Pressure Stall Information. It reads system-wide and cgroup v2 pressure metrics, calculates exact interval ratios, and monitors kernel PSI triggers without a C extension.
Requirements
- Ruby 3.2 or later
- Linux 4.20 or later with
CONFIG_PSI=yfor readings - Linux 5.2 or later and write permission on a pressure file for triggers
Some distributions built with CONFIG_PSI_DEFAULT_DISABLED=y also require the
psi=1 kernel command-line option.
Installation
bundle add psi
Read pressure
require "psi"
reading = PSI.read(:memory)
reading.some.avg10 # percentage over the last 10 seconds
reading.full.total # cumulative stalled microseconds
PSI.resources # => [:cpu, :memory, :io] plus :irq where available
PSI.read_all # => { cpu: PSI::Reading, ... }
some means at least one task was stalled. full means every non-idle task
was stalled. System-wide CPU pressure normally has only some; IRQ pressure,
available since Linux 6.1, has only full.
For an exact interval ratio, use cumulative totals instead of rolling averages:
sampler = PSI::Sampler.new(:memory)
sampler.sample # => nil
sleep 5
sampler.sample # => #<data PSI::Delta some_ratio=..., full_ratio=..., elapsed=...>
PSI::Sampler is intentionally not thread-safe; give each sampling thread its
own instance.
Read the current cgroup
Containers should prefer cgroup values because /proc/pressure can expose the
host's system-wide pressure:
cgroup = PSI.current_cgroup
PSI.read(:memory, cgroup: cgroup)
Wait for a trigger
Trigger files must be writable. /proc/pressure/* normally requires root;
delegated cgroup v2 pressure files can be used without root.
On current kernels, an unprivileged trigger's window must be a multiple of two
seconds; privileged triggers retain the full 0.5–10 second range.
PSI::Trigger.open(:memory, kind: :some, stall: 0.15, window: 1.0) do |trigger|
warn "memory pressure" if trigger.wait(timeout: 10)
end
window must be 0.5–10 seconds and stall cannot exceed it. Start with a
some trigger around 10–20% of the window for early warning and a higher
full trigger for load shedding, then tune from measurements on the real
workload. There is no portable 10–30 second warning threshold: reclaim,
working-set size, and cgroup limits determine the lead time.
Monitor several triggers
monitor = PSI::Monitor.new
monitor.on_error { |error| warn error. }
monitor.on(:memory, stall: 0.1, window: 1.0) { |event| warn event }
monitor.on(:io, stall: 0.3, window: 2.0) { |event| warn event }
monitor.start
# Later, during shutdown:
monitor.stop
Monitor uses one thread and IO.select's priority set. Callback exceptions are
sent to on_error and do not stop monitoring. stop wakes the thread and
closes every trigger.
See examples/load_shedding.rb for Rack/Puma-style 503 shedding,
examples/prometheus_exporter.rb for a dependency-free metrics endpoint, and
benchmark/monitor_idle.rb for idle CPU measurement.
Unsupported and constrained environments
Requiring the gem always succeeds; PSI.supported? reports whether the
system-wide PSI directory exists, and use on an unsupported kernel raises
PSI::UnsupportedError.
| Environment | Limitation |
|---|---|
| macOS and Windows | No Linux procfs PSI interface. |
| WSL2 with an old or PSI-disabled kernel | /proc/pressure is absent. |
| Docker Desktop and other containers | /proc/pressure may represent the host; trigger writes are commonly denied. Prefer a delegated cgroup. |
| GitHub-hosted runners | Readings usually work, but trigger tests require root and the host kernel cannot be changed. |
| Linux before 4.20 | PSI is unavailable. Linux before 5.2 supports readings but not triggers. |
Development
bundle install
bundle exec rake test:unit
bundle exec rbs validate
bundle exec yard
Linux system tests require PSI trigger write permission:
sudo --preserve-env=PATH,GEM_HOME,GEM_PATH bundle exec rake test:system
License
MIT. See LICENSE.txt.