All pastes #2093161 Raw Edit

memory_stats

public ruby v1 · immutable
#2093161 ·published 2011-11-08 19:16 UTC
rendered paste body
#!/usr/bin/env ruby#  Adopted from Phusion Passenger - http://www.modrails.com/#  Copyright (c) 2008, 2009 Phusion, 37signals##  "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.##  Permission is hereby granted, free of charge, to any person obtaining a copy#  of this software and associated documentation files (the "Software"), to deal#  in the Software without restriction, including without limitation the rights#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell#  copies of the Software, and to permit persons to whom the Software is#  furnished to do so, subject to the following conditions:##  The above copyright notice and this permission notice shall be included in#  all copies or substantial portions of the Software.##  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN#  THE SOFTWARE.# ANSI color codesRESET   = "\e[0m"BOLD    = "\e[1m"WHITE   = "\e[37m"YELLOW  = "\e[33m"BLUE_BG = "\e[44m"# 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 = "#{BLUE_BG}#{BOLD}#{YELLOW}"      result << "#{"-" * left_bar_size} #{title} #{"-" * right_bar_size}\n"      if !@rows.empty?        result << WHITE        result << header      end    else      result = header.dup    end    if @rows.empty?      result << RESET    else      result << ("-" * header.size) << "#{RESET}\n"      @rows.each do |row|        result << sprintf(format_string, *row).rstrip << "\n"      end    end    result  endendclass MemoryStats  class Process    attr_accessor :pid    attr_accessor :ppid    attr_accessor :threads    attr_accessor :vm_size              # in KB    attr_accessor :rss                  # 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 rss_in_mb      return sprintf("%.1f MB", rss / 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, rss_in_mb, name]    end  end  def start(match = 'rails')    procs = list_processes(:match => match).delete_if {|p| p.pid == ::Process.pid }    if !procs.empty?      if ::Process.uid != 0 && procs.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      print_process_list("#{match}", procs)    else      puts "No processes found matching '#{match}'"    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')      if RUBY_PLATFORM =~ /linux/        ps = "ps -C '#{name}'"      else        ps = "ps -A"        options[:match] = Regexp.new(Regexp.escape(name))      end    elsif options[:name]      if RUBY_PLATFORM =~ /linux/        ps = "ps -C '#{options[:name]}'"      else        ps = "ps -A"        options[:match] = Regexp.new(" #{Regexp.escape(options[:name])}")      end    elsif options[:match]      ps = "ps -A"    else      raise ArgumentError, "Invalid options."    end        processes = []    case RUBY_PLATFORM    when /solaris/      list = `#{ps} -o pid,ppid,nlwp,vsz,rss,comm`.split("\n")      threads_known = true    when /darwin/      list = `#{ps} -w -o pid,ppid,vsz,rss,command`.split("\n")      threads_known = false    else      list = `#{ps} -w -o pid,ppid,nlwp,vsz,rss,command`.split("\n")      threads_known = true    end    list.shift    list.each do |line|      line.gsub!(/^ */, '')      line.gsub!(/ *$/, '')            p = Process.new      if threads_known        p.pid, p.ppid, p.threads, p.vm_size, p.rss, p.name = line.split(/ +/, 6)      else        p.pid, p.ppid, p.vm_size, p.rss, p.name = line.split(/ +/, 5)        p.threads = "?"      end      p.name.sub!(/\Aruby: /, '')      p.name.sub!(/ \(ruby\)\Z/, '')      if p.name !~ /^ps/ && (!options[:match] || p.name.match(options[:match]))        # Convert some values to integer.        [:pid, :ppid, :vm_size, :rss].each do |attr|          p.send("#{attr}=", p.send(attr).to_i)        end        p.threads = p.threads.to_i if threads_known        if platform_provides_private_dirty_rss_information?          p.private_dirty_rss = determine_private_dirty_rss(p.pid)        end        processes << p      end    end    return processes  endprivate  def platform_provides_private_dirty_rss_information?    return RUBY_PLATFORM =~ /linux/  end  # 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    if total == 0      return nil    else      return total    end  rescue Errno::EACCES, Errno::ENOENT    return nil  end    def print_process_list(title, processes, options = {})    table = Table.new(%w{PID PPID Threads VMSize Private Resident Name})    table.add_rows(processes)    if options.has_key?(:show_ppid) && !options[:show_ppid]      table.remove_column('PPID')    end    if platform_provides_private_dirty_rss_information?      table.remove_column('Resident')    else      table.remove_column('Private')    end    puts table.to_s(title)        if platform_provides_private_dirty_rss_information?      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  endendif !ARGV[0]  puts "Prints memory usage stats for processes whose title matches <filter>"  puts "Usage: memory_stats <filter>"  exit 1endMemoryStats.new.start(ARGV[0])