#!/usr/bin/env ruby
# frozen_string_literal: true

# from: https://gist.github.com/kenn/5105061/raw/ac7ebc6be7008c35b72560cc4e05b7cc14eb4919/memstats.rb

#------------------------------------------------------------------------------
# Aggregate Print useful information from /proc/[pid]/smaps
#
# pss  - Roughly the amount of memory that is "really" being used by the pid
# swap - Amount of swap this process is currently using
#
# Reference:
#  http://www.mjmwired.net/kernel/Documentation/filesystems/proc.txt#361
#
# Example:
#   # ./memstats.rb 4386
#   Process:             4386
#   Command Line:        /usr/bin/mongod -f /etc/mongo/mongod.conf
#   Memory Summary:
#     private_clean             107,132 kB
#     private_dirty           2,020,676 kB
#     pss                     2,127,860 kB
#     rss                     2,128,536 kB
#     shared_clean                  728 kB
#     shared_dirty                    0 kB
#     size                  149,281,668 kB
#     swap                    1,719,792 kB
#------------------------------------------------------------------------------

class Mapping
  FIELDS = %w[size rss shared_clean shared_dirty private_clean private_dirty swap pss]
  attr_reader :address_start
  attr_reader :address_end
  attr_reader :perms
  attr_reader :offset
  attr_reader :device_major
  attr_reader :device_minor
  attr_reader :inode
  attr_reader :region

  attr_accessor :size
  attr_accessor :rss
  attr_accessor :shared_clean
  attr_accessor :shared_dirty
  attr_accessor :private_dirty
  attr_accessor :private_clean
  attr_accessor :swap
  attr_accessor :pss

  def initialize(lines)
    FIELDS.each { |field| self.public_send("#{field}=", 0) }

    parse_first_line(lines.shift)
    lines.each { |l| parse_field_line(l) }
  end

  def parse_first_line(line)
    parts = line.strip.split
    @address_start, @address_end = parts[0].split("-")
    @perms = parts[1]
    @offset = parts[2]
    @device_major, @device_minor = parts[3].split(":")
    @inode = parts[4]
    @region = parts[5] || "anonymous"
  end

  def parse_field_line(line)
    parts = line.strip.split
    field = parts[0].downcase.sub(":", "")
    if respond_to? "#{field}="
      value = Float(parts[1]).to_i
      self.public_send("#{field}=", value)
    end
  end
end

def consume_mapping(map_lines, totals)
  m = Mapping.new(map_lines)

  Mapping::FIELDS.each { |field| totals[field] += m.public_send(field) }
  m
end

def create_memstats_not_available(totals)
  Mapping::FIELDS.each { |field| totals[field] += Float::NAN }
end

abort "usage: memstats [pid]" unless ARGV.first
pid = ARGV.shift.to_i
totals = Hash.new(0)
mappings = []

begin
  File.open("/proc/#{pid}/smaps") do |smaps|
    map_lines = []

    loop do
      break if smaps.eof?
      line = smaps.readline.strip
      case line
      when /\w+:\s+/
        map_lines << line
      when /[0-9a-f]+:[0-9a-f]+\s+/
        mappings << consume_mapping(map_lines, totals) if map_lines.size > 0
        map_lines.clear
        map_lines << line
      else
        break
      end
    end
  end
rescue StandardError
  create_memstats_not_available(totals)
end

# http://rubyforge.org/snippet/download.php?type=snippet&id=511
def format_number(n)
  n.to_s.gsub(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/, '\1,\2')
end

def get_commandline(pid)
  commandline = File.read("/proc/#{pid}/cmdline").split("\0")
  if commandline.first =~ /java$/
    loop { break if commandline.shift == "-jar" }
    return "[java] #{commandline.shift}"
  end
  commandline.join(" ")
end

if ARGV.include? "--yaml"
  require "yaml"
  puts Hash[*totals.map do |k, v| [k + "_kb", v] end.flatten].to_yaml
else
  puts "#{"Process:".ljust(20)} #{pid}"
  puts "#{"Command Line:".ljust(20)} #{get_commandline(pid)}"
  puts "Memory Summary:"
  totals.keys.sort.each { |k| puts "  #{k.ljust(20)} #{format_number(totals[k]).rjust(12)} kB" }
end