#!/usr/bin/env ruby
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib")
require 'passenger/platform_info'
# Container for tabular data.
class Table
def initialize(column_names)
@column_names = column_names
@rows = []
end
def add_row(values)
@rows << values.to_a
end
def add_rows(list_of_rows)
list_of_rows.each do |row|
add_row(row)
end
end
def remove_column(name)
i = @column_names.index(name)
@column_names.delete_at(i)
@rows.each do |row|
row.delete_at(i)
end
end
def to_s(title = nil)
max_column_widths = [1] * @column_names.size
(@rows + [@column_names]).each do |row|
row.each_with_index do |value, i|
max_column_widths[i] = [value.to_s.size, max_column_widths[i]].max
end
end
format_string = max_column_widths.map{ |i| "%-#{i}s" }.join(" ")
header = sprintf(format_string, *@column_names).rstrip << "\n"
if title
free_space = header.size - title.size - 2
if free_space <= 0
left_bar_size = 3
right_bar_size = 3
else
left_bar_size = free_space / 2
right_bar_size = free_space - left_bar_size
end
result = "#{"-" * left_bar_size} #{title} #{"-" * right_bar_size}\n"
result << header
else
result = header.dup
end
result << ("-" * header.size) << "\n"
@rows.each do |row|
result << sprintf(format_string, *row).rstrip << "\n"
end
result
end
end
class MemoryStats
class Process
attr_accessor :pid
attr_accessor :ppid
attr_accessor :threads
attr_accessor :vm_size # in KB
attr_accessor :name
attr_accessor :private_dirty_rss # in KB
def vm_size_in_mb
return sprintf("%.1f MB", vm_size / 1024.0)
end
def private_dirty_rss_in_mb
if private_dirty_rss.is_a?(Numeric)
return sprintf("%.1f MB", private_dirty_rss / 1024.0)
else
return "?"
end
end
def to_a
return [pid, ppid, threads, vm_size_in_mb, private_dirty_rss_in_mb, name]
end
end
def start
apache_processes = list_processes(:exe => PlatformInfo::HTTPD)
print_process_list("Apache processes", apache_processes)
puts
passenger_processes = list_processes(:match => /(^Passenger |^Rails:|ApplicationPoolServerExecutable)/)
print_process_list("Passenger processes", passenger_processes, :show_ppid => false)
if RUBY_PLATFORM !~ /linux/
puts
puts "*** WARNING: The private dirty RSS can only be displayed " <<
"on Linux. You're currently using '#{RUBY_PLATFORM}'."
elsif ::Process.uid != 0 && (apache_processes + passenger_processes).any?{ |p| p.private_dirty_rss.nil? }
puts
puts "*** WARNING: Please run this tool as root. Otherwise the " <<
"private dirty RSS of processes cannot be determined."
end
end
# Returns a list of Process objects that match the given search criteria.
#
# # Search by executable path.
# list_processes(:exe => '/usr/sbin/apache2')
#
# # Search by executable name.
# list_processes(:name => 'ruby1.8')
#
# # Search by process name.
# list_processes(:match => 'Passenger FrameworkSpawner')
def list_processes(options)
if options[:exe]
name = options[:exe].sub(/.*\/(.*)/, '\1')
ps = "ps -C '#{name}'"
elsif options[:name]
ps = "ps -C '#{options[:name]}'"
elsif options[:match]
ps = "ps -A"
else
raise ArgumentError, "Invalid options."
end
processes = []
list = `#{ps} -o pid,ppid,nlwp,vsz,command`.split("\n")
list.shift
list.each do |line|
line.gsub!(/^ */, '')
line.gsub!(/ *$/, '')
p = Process.new
p.pid, p.ppid, p.threads, p.vm_size, p.name = line.split(/ +/, 5)
if p.name !~ /^ps/ && (!options[:match] || p.name.match(options[:match]))
[:pid, :ppid, :threads, :vm_size].each do |attr|
p.send("#{attr}=", p.send(attr).to_i)
end
p.private_dirty_rss = determine_private_dirty_rss(p.pid)
processes << p
end
end
return processes
end
private
# Returns the private dirty RSS for the given process, in KB.
def determine_private_dirty_rss(pid)
total = 0
File.read("/proc/#{pid}/smaps").split("\n").each do |line|
line =~ /^(Private)_Dirty: +(\d+)/
if $2
total += $2.to_i
end
end
return total
rescue Errno::EACCES, Errno::ENOENT
return nil
end
def print_process_list(title, processes, options = {})
table = Table.new(%w{PID PPID Threads VMSize Private Name})
table.add_rows(processes)
if options.has_key?(:show_ppid) && !options[:show_ppid]
table.remove_column('PPID')
end
puts table.to_s(title)
total_private_dirty_rss = 0
some_private_dirty_rss_cannot_be_determined = false
processes.each do |p|
if p.private_dirty_rss.is_a?(Numeric)
total_private_dirty_rss += p.private_dirty_rss
else
some_private_dirty_rss_cannot_be_determined = true
end
end
puts "### Processes: #{processes.size}"
printf "### Total private dirty RSS: %.2f MB", total_private_dirty_rss / 1024.0
if some_private_dirty_rss_cannot_be_determined
puts " (?)"
else
puts
end
end
end
MemoryStats.new.start