milis/bin/smem

690 lines
21 KiB
Python
Executable File

#!/usr/bin/env python
#
# smem - a tool for meaningful memory reporting
#
# Copyright 2008-2009 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of
# the GNU General Public License version 2 or later, incorporated
# herein by reference.
import re, os, sys, pwd, optparse, errno, tarfile
warned = False
class procdata(object):
def __init__(self, source):
self._ucache = {}
self._gcache = {}
self.source = source and source or ""
self._memdata = None
def _list(self):
return os.listdir(self.source + "/proc")
def _read(self, f):
return file(self.source + '/proc/' + f).read()
def _readlines(self, f):
return self._read(f).splitlines(True)
def _stat(self, f):
return os.stat(self.source + "/proc/" + f)
def pids(self):
'''get a list of processes'''
return [int(e) for e in self._list()
if e.isdigit() and not iskernel(e)]
def mapdata(self, pid):
return self._readlines('%s/smaps' % pid)
def memdata(self):
if self._memdata is None:
self._memdata = self._readlines('meminfo')
return self._memdata
def version(self):
return self._readlines('version')[0]
def pidname(self, pid):
try:
l = self._read('%d/stat' % pid)
return l[l.find('(') + 1: l.find(')')]
except:
return '?'
def pidcmd(self, pid):
try:
c = self._read('%s/cmdline' % pid)[:-1]
return c.replace('\0', ' ')
except:
return '?'
def piduser(self, pid):
try:
return self._stat('%d' % pid).st_uid
except:
return -1
def pidgroup(self, pid):
try:
return self._stat('%d' % pid).st_gid
except:
return -1
def username(self, uid):
if uid == -1:
return '?'
if uid not in self._ucache:
try:
self._ucache[uid] = pwd.getpwuid(uid)[0]
except KeyError:
self._ucache[uid] = str(uid)
return self._ucache[uid]
def groupname(self, gid):
if gid == -1:
return '?'
if gid not in self._gcache:
try:
self._gcache[gid] = pwd.getgrgid(gid)[0]
except KeyError:
self._gcache[gid] = str(gid)
return self._gcache[gid]
class tardata(procdata):
def __init__(self, source):
procdata.__init__(self, source)
self.tar = tarfile.open(source)
def _list(self):
for ti in self.tar:
if ti.name.endswith('/smaps'):
d,f = ti.name.split('/')
yield d
def _read(self, f):
return self.tar.extractfile(f).read()
def _readlines(self, f):
return self.tar.extractfile(f).readlines()
def piduser(self, p):
t = self.tar.getmember("%d" % p)
if t.uname:
self._ucache[t.uid] = t.uname
return t.uid
def pidgroup(self, p):
t = self.tar.getmember("%d" % p)
if t.gname:
self._gcache[t.gid] = t.gname
return t.gid
def username(self, u):
return self._ucache.get(u, str(u))
def groupname(self, g):
return self._gcache.get(g, str(g))
_totalmem = 0
def totalmem():
global _totalmem
if not _totalmem:
if options.realmem:
_totalmem = fromunits(options.realmem) / 1024
else:
_totalmem = memory()['memtotal']
return _totalmem
_kernelsize = 0
def kernelsize():
global _kernelsize
if not _kernelsize and options.kernel:
try:
d = os.popen("size %s" % options.kernel).readlines()[1]
_kernelsize = int(d.split()[3]) / 1024
except:
try:
# try some heuristic to find gzipped part in kernel image
packedkernel = open(options.kernel).read()
pos = packedkernel.find('\x1F\x8B')
if pos >= 0 and pos < 25000:
sys.stderr.write("Maybe uncompressed kernel can be extracted by the command:\n"
" dd if=%s bs=1 skip=%d | gzip -d >%s.unpacked\n\n" % (options.kernel, pos, options.kernel))
except:
pass
sys.stderr.write("Parameter '%s' should be an original uncompressed compiled kernel file.\n\n" % options.kernel)
return _kernelsize
def pidmaps(pid):
global warned
maps = {}
start = None
seen = False
empty = True
for l in src.mapdata(pid):
empty = False
f = l.split()
if f[-1] == 'kB':
if f[0].startswith('Pss'):
seen = True
maps[start][f[0][:-1].lower()] = int(f[1])
elif '-' in f[0] and ':' not in f[0]: # looks like a mapping range
start, end = f[0].split('-')
start = int(start, 16)
name = "<anonymous>"
if len(f) > 5:
name = f[5]
maps[start] = dict(end=int(end, 16), mode=f[1],
offset=int(f[2], 16),
device=f[3], inode=f[4], name=name)
if not empty and not seen and not warned:
sys.stderr.write('warning: kernel does not appear to support PSS measurement\n')
warned = True
if not options.sort:
options.sort = 'rss'
if options.mapfilter:
f = {}
for m in maps:
if not filter(options.mapfilter, m, lambda x: maps[x]['name']):
f[m] = maps[m]
return f
return maps
def sortmaps(totals, key):
l = []
for pid in totals:
l.append((totals[pid][key], pid))
l.sort()
return [pid for pid,key in l]
def iskernel(pid):
return src.pidcmd(pid) == ""
def memory():
t = {}
f = re.compile('(\\S+):\\s+(\\d+) kB')
for l in src.memdata():
m = f.match(l)
if m:
t[m.group(1).lower()] = int(m.group(2))
return t
def units(x):
s = ''
if x == 0:
return '0'
for s in ('', 'K', 'M', 'G', 'T'):
if x < 1024:
break
x /= 1024.0
return "%.1f%s" % (x, s)
def fromunits(x):
s = dict(k=2**10, K=2**10, kB=2**10, KB=2**10,
M=2**20, MB=2**20, G=2**30, GB=2**30,
T=2**40, TB=2**40)
for k,v in s.items():
if x.endswith(k):
return int(float(x[:-len(k)])*v)
sys.stderr.write("Memory size should be written with units, for example 1024M\n")
sys.exit(-1)
def pidusername(pid):
return src.username(src.piduser(pid))
def showamount(a, total):
if options.abbreviate:
return units(a * 1024)
elif options.percent:
if total == 0:
return 'N/A'
return "%.2f%%" % (100.0 * a / total)
return a
def filter(opt, arg, *sources):
if not opt:
return False
for f in sources:
if re.search(opt, f(arg)):
return False
return True
def pidtotals(pid):
maps = pidmaps(pid)
t = dict(size=0, rss=0, pss=0, shared_clean=0, shared_dirty=0,
private_clean=0, private_dirty=0, referenced=0, swap=0)
for m in maps.iterkeys():
for k in t:
t[k] += maps[m].get(k, 0)
t['uss'] = t['private_clean'] + t['private_dirty']
t['maps'] = len(maps)
return t
def processtotals(pids):
totals = {}
for pid in pids:
if (filter(options.processfilter, pid, src.pidname, src.pidcmd) or
filter(options.userfilter, pid, pidusername)):
continue
try:
p = pidtotals(pid)
if p['maps'] != 0:
totals[pid] = p
except:
continue
return totals
def showpids():
p = src.pids()
pt = processtotals(p)
def showuser(p):
if options.numeric:
return src.piduser(p)
return pidusername(p)
fields = dict(
pid=('PID', lambda n: n, '% 5s', lambda x: len(pt),
'process ID'),
user=('User', showuser, '%-8s', lambda x: len(dict.fromkeys(x)),
'owner of process'),
name=('Name', src.pidname, '%-24.24s', None,
'name of process'),
command=('Command', src.pidcmd, '%-27.27s', None,
'process command line'),
maps=('Maps',lambda n: pt[n]['maps'], '% 5s', sum,
'total number of mappings'),
swap=('Swap',lambda n: pt[n]['swap'], '% 8a', sum,
'amount of swap space consumed (ignoring sharing)'),
uss=('USS', lambda n: pt[n]['uss'], '% 8a', sum,
'unique set size'),
rss=('RSS', lambda n: pt[n]['rss'], '% 8a', sum,
'resident set size (ignoring sharing)'),
pss=('PSS', lambda n: pt[n]['pss'], '% 8a', sum,
'proportional set size (including sharing)'),
vss=('VSS', lambda n: pt[n]['size'], '% 8a', sum,
'virtual set size (total virtual memory mapped)'),
)
columns = options.columns or 'pid user command swap uss pss rss'
showtable(pt.keys(), fields, columns.split(), options.sort or 'pss')
def maptotals(pids):
totals = {}
for pid in pids:
if (filter(options.processfilter, pid, src.pidname, src.pidcmd) or
filter(options.userfilter, pid, pidusername)):
continue
try:
maps = pidmaps(pid)
seen = {}
for m in maps.iterkeys():
name = maps[m]['name']
if name not in totals:
t = dict(size=0, rss=0, pss=0, shared_clean=0,
shared_dirty=0, private_clean=0, count=0,
private_dirty=0, referenced=0, swap=0, pids=0)
else:
t = totals[name]
for k in t:
t[k] += maps[m].get(k, 0)
t['count'] += 1
if name not in seen:
t['pids'] += 1
seen[name] = 1
totals[name] = t
except EnvironmentError:
continue
return totals
def showmaps():
p = src.pids()
pt = maptotals(p)
fields = dict(
map=('Map', lambda n: n, '%-40.40s', len,
'mapping name'),
count=('Count', lambda n: pt[n]['count'], '% 5s', sum,
'number of mappings found'),
pids=('PIDs', lambda n: pt[n]['pids'], '% 5s', sum,
'number of PIDs using mapping'),
swap=('Swap',lambda n: pt[n]['swap'], '% 8a', sum,
'amount of swap space consumed (ignoring sharing)'),
uss=('USS', lambda n: pt[n]['private_clean']
+ pt[n]['private_dirty'], '% 8a', sum,
'unique set size'),
rss=('RSS', lambda n: pt[n]['rss'], '% 8a', sum,
'resident set size (ignoring sharing)'),
pss=('PSS', lambda n: pt[n]['pss'], '% 8a', sum,
'proportional set size (including sharing)'),
vss=('VSS', lambda n: pt[n]['size'], '% 8a', sum,
'virtual set size (total virtual address space mapped)'),
avgpss=('AVGPSS', lambda n: int(1.0 * pt[n]['pss']/pt[n]['pids']),
'% 8a', sum,
'average PSS per PID'),
avguss=('AVGUSS', lambda n: int(1.0 * pt[n]['uss']/pt[n]['pids']),
'% 8a', sum,
'average USS per PID'),
avgrss=('AVGRSS', lambda n: int(1.0 * pt[n]['rss']/pt[n]['pids']),
'% 8a', sum,
'average RSS per PID'),
)
columns = options.columns or 'map pids avgpss pss'
showtable(pt.keys(), fields, columns.split(), options.sort or 'pss')
def usertotals(pids):
totals = {}
for pid in pids:
if (filter(options.processfilter, pid, src.pidname, src.pidcmd) or
filter(options.userfilter, pid, pidusername)):
continue
try:
maps = pidmaps(pid)
if len(maps) == 0:
continue
except EnvironmentError:
continue
user = src.piduser(pid)
if user not in totals:
t = dict(size=0, rss=0, pss=0, shared_clean=0,
shared_dirty=0, private_clean=0, count=0,
private_dirty=0, referenced=0, swap=0)
else:
t = totals[user]
for m in maps.iterkeys():
for k in t:
t[k] += maps[m].get(k, 0)
t['count'] += 1
totals[user] = t
return totals
def showusers():
p = src.pids()
pt = usertotals(p)
def showuser(u):
if options.numeric:
return u
return src.username(u)
fields = dict(
user=('User', showuser, '%-8s', None,
'user name or ID'),
count=('Count', lambda n: pt[n]['count'], '% 5s', sum,
'number of processes'),
swap=('Swap',lambda n: pt[n]['swap'], '% 8a', sum,
'amount of swapspace consumed (ignoring sharing)'),
uss=('USS', lambda n: pt[n]['private_clean']
+ pt[n]['private_dirty'], '% 8a', sum,
'unique set size'),
rss=('RSS', lambda n: pt[n]['rss'], '% 8a', sum,
'resident set size (ignoring sharing)'),
pss=('PSS', lambda n: pt[n]['pss'], '% 8a', sum,
'proportional set size (including sharing)'),
vss=('VSS', lambda n: pt[n]['pss'], '% 8a', sum,
'virtual set size (total virtual memory mapped)'),
)
columns = options.columns or 'user count swap uss pss rss'
showtable(pt.keys(), fields, columns.split(), options.sort or 'pss')
def showsystem():
t = totalmem()
ki = kernelsize()
m = memory()
mt = m['memtotal']
f = m['memfree']
# total amount used by hardware
fh = max(t - mt - ki, 0)
# total amount mapped into userspace (ie mapped an unmapped pages)
u = m['anonpages'] + m['mapped']
# total amount allocated by kernel not for userspace
kd = mt - f - u
# total amount in kernel caches
kdc = m['buffers'] + m['sreclaimable'] + (m['cached'] - m['mapped'])
l = [("firmware/hardware", fh, 0),
("kernel image", ki, 0),
("kernel dynamic memory", kd, kdc),
("userspace memory", u, m['mapped']),
("free memory", f, f)]
fields = dict(
order=('Order', lambda n: n, '% 1s', lambda x: '',
'hierarchical order'),
area=('Area', lambda n: l[n][0], '%-24s', lambda x: '',
'memory area'),
used=('Used', lambda n: l[n][1], '%10a', sum,
'area in use'),
cache=('Cache', lambda n: l[n][2], '%10a', sum,
'area used as reclaimable cache'),
noncache=('Noncache', lambda n: l[n][1] - l[n][2], '%10a', sum,
'area not reclaimable'))
columns = options.columns or 'area used cache noncache'
showtable(range(len(l)), fields, columns.split(), options.sort or 'order')
def showfields(fields, f):
if f != list:
print "unknown field", f
print "known fields:"
for l in sorted(fields.keys()):
print "%-8s %s" % (l, fields[l][-1])
def showtable(rows, fields, columns, sort):
header = ""
format = ""
formatter = []
if sort not in fields:
showfields(fields, sort)
sys.exit(-1)
if options.pie:
columns.append(options.pie)
if options.bar:
columns.append(options.bar)
mt = totalmem()
st = memory()['swaptotal']
for n in columns:
if n not in fields:
showfields(fields, n)
sys.exit(-1)
f = fields[n][2]
if 'a' in f:
if n == 'swap':
formatter.append(lambda x: showamount(x, st))
else:
formatter.append(lambda x: showamount(x, mt))
f = f.replace('a', 's')
else:
formatter.append(lambda x: x)
format += f + " "
header += f % fields[n][0] + " "
l = []
for n in rows:
r = [fields[c][1](n) for c in columns]
l.append((fields[sort][1](n), r))
l.sort(reverse=bool(options.reverse))
if options.pie:
showpie(l, sort)
return
elif options.bar:
showbar(l, columns, sort)
return
if not options.no_header:
print header
for k,r in l:
print format % tuple([f(v) for f,v in zip(formatter, r)])
if options.totals:
# totals
t = []
for c in columns:
f = fields[c][3]
if f:
t.append(f([fields[c][1](n) for n in rows]))
else:
t.append("")
print "-" * len(header)
print format % tuple([f(v) for f,v in zip(formatter, t)])
def showpie(l, sort):
try:
import pylab
except ImportError:
sys.stderr.write("pie chart requires matplotlib\n")
sys.exit(-1)
if (l[0][0] < l[-1][0]):
l.reverse()
labels = [r[1][-1] for r in l]
values = [r[0] for r in l] # sort field
tm = totalmem()
s = sum(values)
unused = tm - s
t = 0
while values and (t + values[-1] < (tm * .02) or
values[-1] < (tm * .005)):
t += values.pop()
labels.pop()
if t:
values.append(t)
labels.append('other')
explode = [0] * len(values)
if unused > 0:
values.insert(0, unused)
labels.insert(0, 'unused')
explode.insert(0, .05)
pylab.figure(1, figsize=(6,6))
ax = pylab.axes([0.1, 0.1, 0.8, 0.8])
pylab.pie(values, explode = explode, labels=labels,
autopct="%.2f%%", shadow=True)
pylab.title('%s by %s' % (options.pie, sort))
pylab.show()
def showbar(l, columns, sort):
try:
import pylab, numpy
except ImportError:
sys.stderr.write("bar chart requires matplotlib\n")
sys.exit(-1)
if (l[0][0] < l[-1][0]):
l.reverse()
rc = []
key = []
for n in range(len(columns) - 1):
try:
if columns[n] in 'pid user group'.split():
continue
float(l[0][1][n])
rc.append(n)
key.append(columns[n])
except:
pass
width = 1.0 / (len(rc) + 1)
offset = width / 2
def gc(n):
return 'bgrcmyw'[n % 7]
pl = []
ind = numpy.arange(len(l))
for n in xrange(len(rc)):
pl.append(pylab.bar(ind + offset + width * n,
[x[1][rc[n]] for x in l], width, color=gc(n)))
#plt.xticks(ind + .5, )
pylab.gca().set_xticks(ind + .5)
pylab.gca().set_xticklabels([x[1][-1] for x in l], rotation=45)
pylab.legend([p[0] for p in pl], key)
pylab.show()
parser = optparse.OptionParser("%prog [options]")
parser.add_option("-H", "--no-header", action="store_true",
help="disable header line")
parser.add_option("-c", "--columns", type="str",
help="columns to show")
parser.add_option("-t", "--totals", action="store_true",
help="show totals")
parser.add_option("-R", "--realmem", type="str",
help="amount of physical RAM")
parser.add_option("-K", "--kernel", type="str",
help="path to kernel image")
parser.add_option("-m", "--mappings", action="store_true",
help="show mappings")
parser.add_option("-u", "--users", action="store_true",
help="show users")
parser.add_option("-w", "--system", action="store_true",
help="show whole system")
parser.add_option("-P", "--processfilter", type="str",
help="process filter regex")
parser.add_option("-M", "--mapfilter", type="str",
help="map filter regex")
parser.add_option("-U", "--userfilter", type="str",
help="user filter regex")
parser.add_option("-n", "--numeric", action="store_true",
help="numeric output")
parser.add_option("-s", "--sort", type="str",
help="field to sort on")
parser.add_option("-r", "--reverse", action="store_true",
help="reverse sort")
parser.add_option("-p", "--percent", action="store_true",
help="show percentage")
parser.add_option("-k", "--abbreviate", action="store_true",
help="show unit suffixes")
parser.add_option("", "--pie", type='str',
help="show pie graph")
parser.add_option("", "--bar", type='str',
help="show bar graph")
parser.add_option("-S", "--source", type="str",
help="/proc data source")
defaults = {}
parser.set_defaults(**defaults)
(options, args) = parser.parse_args()
try:
src = tardata(options.source)
except:
src = procdata(options.source)
try:
if options.mappings:
showmaps()
elif options.users:
showusers()
elif options.system:
showsystem()
else:
showpids()
except IOError, e:
if e.errno == errno.EPIPE:
pass
except KeyboardInterrupt:
pass