3
# smem - a tool for meaningful memory reporting
5
# Copyright 2008-2009 Matt Mackall <mpm@selenic.com>
7
# This software may be used and distributed according to the terms of
8
# the GNU General Public License version 2 or later, incorporated
11
import re, os, sys, pwd, optparse, errno, tarfile
15
class procdata(object):
16
def __init__(self, source):
19
self.source = source and source or ""
22
return os.listdir(self.source + "/proc")
24
return open(self.source + '/proc/' + f).read()
25
def _readlines(self, f):
26
return self._read(f).splitlines(True)
28
return os.stat(self.source + "/proc/" + f)
31
'''get a list of processes'''
32
return [int(e) for e in self._list()
33
if e.isdigit() and not iskernel(e)]
34
def mapdata(self, pid):
35
return self._readlines('%s/smaps' % pid)
37
if self._memdata is None:
38
self._memdata = self._readlines('meminfo')
41
return self._readlines('version')[0]
42
def pidname(self, pid):
44
l = self.pidcmd(pid).split(' ')[0]
45
return os.path.basename(l)
48
def pidcmd(self, pid):
50
c = self._read('%s/cmdline' % pid)[:-1]
51
return c.replace('\0', ' ')
54
def piduser(self, pid):
56
return self._stat('%d' % pid).st_uid
59
def pidgroup(self, pid):
61
return self._stat('%d' % pid).st_gid
64
def username(self, uid):
67
if uid not in self._ucache:
69
self._ucache[uid] = pwd.getpwuid(uid)[0]
71
self._ucache[uid] = str(uid)
72
return self._ucache[uid]
73
def groupname(self, gid):
76
if gid not in self._gcache:
78
self._gcache[gid] = pwd.getgrgid(gid)[0]
80
self._gcache[gid] = str(gid)
81
return self._gcache[gid]
83
class tardata(procdata):
84
def __init__(self, source):
85
procdata.__init__(self, source)
86
self.tar = tarfile.open(source)
89
if ti.name.endswith('/smaps'):
90
d,f = ti.name.split('/')
93
return self.tar.extractfile(f).read()
94
def _readlines(self, f):
95
return self.tar.extractfile(f).readlines()
97
t = self.tar.getmember("%d" % p)
99
self._ucache[t.uid] = t.uname
101
def pidgroup(self, p):
102
t = self.tar.getmember("%d" % p)
104
self._gcache[t.gid] = t.gname
106
def username(self, u):
107
return self._ucache.get(u, str(u))
108
def groupname(self, g):
109
return self._gcache.get(g, str(g))
116
_totalmem = fromunits(options.realmem) / 1024
118
_totalmem = memory()['memtotal']
124
if not _kernelsize and options.kernel:
126
d = os.popen("size %s" % options.kernel).readlines()[1]
127
_kernelsize = int(d.split()[3]) / 1024
130
# try some heuristic to find gzipped part in kernel image
131
packedkernel = open(options.kernel).read()
132
pos = packedkernel.find('\x1F\x8B')
133
if pos >= 0 and pos < 25000:
134
sys.stderr.write("Maybe uncompressed kernel can be extracted by the command:\n"
135
" dd if=%s bs=1 skip=%d | gzip -d >%s.unpacked\n\n" % (options.kernel, pos, options.kernel))
138
sys.stderr.write("Parameter '%s' should be an original uncompressed compiled kernel file.\n\n" % options.kernel)
146
for l in src.mapdata(pid):
149
if f[0].startswith('Pss'):
151
maps[start][f[0][:-1].lower()] = int(f[1])
152
elif f[0] == 'VmFlags:':
155
start, end = f[0].split('-')
156
start = int(start, 16)
160
maps[start] = dict(end=int(end, 16), mode=f[1],
161
offset=int(f[2], 16),
162
device=f[3], inode=f[4], name=name)
164
if not seen and not warned:
165
sys.stderr.write('warning: kernel does not appear to support PSS measurement\n')
170
if options.mapfilter:
173
if not list(filter(options.mapfilter, m, lambda x: maps[x]['name'])):
179
def sortmaps(totals, key):
182
l.append((totals[pid][key], pid))
184
return [pid for pid,key in l]
187
return src.pidcmd(pid) == ""
191
f = re.compile('(\\S+):\\s+(\\d+) kB')
192
for l in src.memdata():
195
t[m.group(1).lower()] = int(m.group(2))
202
for s in ('', 'K', 'M', 'G', 'T'):
206
return "%.1f%s" % (x, s)
209
s = dict(k=2**10, K=2**10, kB=2**10, KB=2**10,
210
M=2**20, MB=2**20, G=2**30, GB=2**30,
212
for k,v in list(s.items()):
214
return int(float(x[:-len(k)])*v)
215
sys.stderr.write("Memory size should be written with units, for example 1024M\n")
218
def pidusername(pid):
219
return src.username(src.piduser(pid))
221
def showamount(a, total):
222
if options.abbreviate:
223
return units(a * 1024)
224
elif options.percent:
225
return "%.2f%%" % (100.0 * a / total)
228
def filter(opt, arg, *sources):
233
if re.search(opt, f(arg)):
239
t = dict(size=0, rss=0, pss=0, shared_clean=0, shared_dirty=0,
240
private_clean=0, private_dirty=0, referenced=0, swap=0)
241
for m in maps.keys():
243
t[k] += maps[m].get(k, 0)
245
t['uss'] = t['private_clean'] + t['private_dirty']
246
t['maps'] = len(maps)
250
def processtotals(pids):
253
if (filter(options.processfilter, pid, src.pidname, src.pidcmd) or
254
filter(options.userfilter, pid, pidusername)):
266
pt = processtotals(p)
270
return src.piduser(p)
271
return pidusername(p)
274
pid=('PID', lambda n: n, '% 5s', lambda x: len(pt),
276
user=('User', showuser, '%-8s', lambda x: len(dict.fromkeys(x)),
278
name=('Name', src.pidname, '%s', None,
280
command=('Command', src.pidcmd, '%s', None,
281
'process command line'),
282
maps=('Maps',lambda n: pt[n]['maps'], '% 5s', sum,
283
'total number of mappings'),
284
swap=('Swap',lambda n: pt[n]['swap'], '% 8a', sum,
285
'amount of swap space consumed (ignoring sharing)'),
286
uss=('USS', lambda n: pt[n]['uss'], '%a', sum,
288
rss=('RSS', lambda n: pt[n]['rss'], '%a', sum,
289
'resident set size (ignoring sharing)'),
290
pss=('PSS', lambda n: pt[n]['pss'], '%a', sum,
291
'proportional set size (including sharing)'),
292
vss=('VSS', lambda n: pt[n]['size'], '%a', sum,
293
'virtual set size (total virtual memory mapped)'),
295
columns = options.columns or 'pid user command swap uss pss rss'
297
showtable(list(pt.keys()), fields, columns.split(), options.sort or 'pss')
302
if (list(filter(options.processfilter, pid, src.pidname, src.pidcmd)) or
303
list(filter(options.userfilter, pid, pidusername))):
308
for m in maps.keys():
309
name = maps[m]['name']
310
if name not in totals:
311
t = dict(size=0, rss=0, pss=0, shared_clean=0,
312
shared_dirty=0, private_clean=0, count=0,
313
private_dirty=0, referenced=0, swap=0, pids=0)
318
t[k] += maps[m].get(k, 0)
324
except EnvironmentError:
333
map=('Map', lambda n: n, '%-40.40s', len,
335
count=('Count', lambda n: pt[n]['count'], '% 5s', sum,
336
'number of mappings found'),
337
pids=('PIDs', lambda n: pt[n]['pids'], '% 5s', sum,
338
'number of PIDs using mapping'),
339
swap=('Swap',lambda n: pt[n]['swap'], '% 8a', sum,
340
'amount of swap space consumed (ignoring sharing)'),
341
uss=('USS', lambda n: pt[n]['private_clean']
342
+ pt[n]['private_dirty'], '% 8a', sum,
344
rss=('RSS', lambda n: pt[n]['rss'], '% 8a', sum,
345
'resident set size (ignoring sharing)'),
346
pss=('PSS', lambda n: pt[n]['pss'], '% 8a', sum,
347
'proportional set size (including sharing)'),
348
vss=('VSS', lambda n: pt[n]['size'], '% 8a', sum,
349
'virtual set size (total virtual address space mapped)'),
350
avgpss=('AVGPSS', lambda n: int(1.0 * pt[n]['pss']/pt[n]['pids']),
352
'average PSS per PID'),
353
avguss=('AVGUSS', lambda n: int(1.0 * pt[n]['uss']/pt[n]['pids']),
355
'average USS per PID'),
356
avgrss=('AVGRSS', lambda n: int(1.0 * pt[n]['rss']/pt[n]['pids']),
358
'average RSS per PID'),
360
columns = options.columns or 'map pids avgpss pss'
362
showtable(list(pt.keys()), fields, columns.split(), options.sort or 'pss')
364
def usertotals(pids):
367
if (list(filter(options.processfilter, pid, src.pidname, src.pidcmd)) or
368
list(filter(options.userfilter, pid, pidusername))):
374
except EnvironmentError:
376
user = src.piduser(pid)
377
if user not in totals:
378
t = dict(size=0, rss=0, pss=0, shared_clean=0,
379
shared_dirty=0, private_clean=0, count=0,
380
private_dirty=0, referenced=0, swap=0)
384
for m in maps.keys():
386
t[k] += maps[m].get(k, 0)
399
return src.username(u)
402
user=('User', showuser, '%-8s', None,
404
count=('Count', lambda n: pt[n]['count'], '% 5s', sum,
405
'number of processes'),
406
swap=('Swap',lambda n: pt[n]['swap'], '% 8a', sum,
407
'amount of swapspace consumed (ignoring sharing)'),
408
uss=('USS', lambda n: pt[n]['private_clean']
409
+ pt[n]['private_dirty'], '% 8a', sum,
411
rss=('RSS', lambda n: pt[n]['rss'], '% 8a', sum,
412
'resident set size (ignoring sharing)'),
413
pss=('PSS', lambda n: pt[n]['pss'], '% 8a', sum,
414
'proportional set size (including sharing)'),
415
vss=('VSS', lambda n: pt[n]['pss'], '% 8a', sum,
416
'virtual set size (total virtual memory mapped)'),
418
columns = options.columns or 'user count swap uss pss rss'
420
showtable(list(pt.keys()), fields, columns.split(), options.sort or 'pss')
430
# total amount used by hardware
431
fh = max(t - mt - ki, 0)
433
# total amount mapped into userspace (ie mapped an unmapped pages)
434
u = m['anonpages'] + m['mapped']
436
# total amount allocated by kernel not for userspace
439
# total amount in kernel caches
440
kdc = m['buffers'] + m['sreclaimable'] + (m['cached'] - m['mapped'])
442
l = [("firmware/hardware", fh, 0),
443
("kernel image", ki, 0),
444
("kernel dynamic memory", kd, kdc),
445
("userspace memory", u, m['mapped']),
446
("free memory", f, f)]
449
order=('Order', lambda n: n, '% 1s', lambda x: '',
450
'hierarchical order'),
451
area=('Area', lambda n: l[n][0], '%-24s', lambda x: '',
453
used=('Used', lambda n: l[n][1], '%10a', sum,
455
cache=('Cache', lambda n: l[n][2], '%10a', sum,
456
'area used as reclaimable cache'),
457
noncache=('Noncache', lambda n: l[n][1] - l[n][2], '%10a', sum,
458
'area not reclaimable'))
460
columns = options.columns or 'area used cache noncache'
461
showtable(list(range(len(l))), fields, columns.split(), options.sort or 'order')
463
def showfields(fields, f):
465
print("unknown field", f)
466
print("known fields:")
467
for l in sorted(fields.keys()):
468
print("%-8s %s" % (l, fields[l][-1]))
470
def showtable(rows, fields, columns, sort):
475
if sort not in fields:
476
showfields(fields, sort)
480
columns.append(options.pie)
482
columns.append(options.bar)
485
st = memory()['swaptotal']
489
showfields(fields, n)
495
formatter.append(lambda x: showamount(x, st))
497
formatter.append(lambda x: showamount(x, mt))
498
f = f.replace('a', 's')
500
formatter.append(lambda x: x)
502
header += f % fields[n][0] + "\t"
506
r = [fields[c][1](n) for c in columns]
507
l.append((fields[sort][1](n), r))
509
l.sort(reverse=bool(options.reverse))
515
showbar(l, columns, sort)
518
if not options.no_header:
522
print(format % tuple([f(v) for f,v in zip(formatter, r)]))
530
t.append(f([fields[c][1](n) for n in rows]))
534
print("-" * len(header))
535
print(format % tuple([f(v) for f,v in zip(formatter, t)]))
537
def showpie(l, sort):
541
sys.stderr.write("pie chart requires matplotlib\n")
544
if (l[0][0] < l[-1][0]):
547
labels = [r[1][-1] for r in l]
548
values = [r[0] for r in l] # sort field
554
while values and (t + values[-1] < (tm * .02) or
555
values[-1] < (tm * .005)):
561
labels.append('other')
563
explode = [0] * len(values)
565
values.insert(0, unused)
566
labels.insert(0, 'unused')
567
explode.insert(0, .05)
569
pylab.figure(1, figsize=(6,6))
570
ax = pylab.axes([0.1, 0.1, 0.8, 0.8])
571
pylab.pie(values, explode = explode, labels=labels,
572
autopct="%.2f%%", shadow=True)
573
pylab.title('%s by %s' % (options.pie, sort))
576
def showbar(l, columns, sort):
580
sys.stderr.write("bar chart requires matplotlib\n")
583
if (l[0][0] < l[-1][0]):
588
for n in range(len(columns) - 1):
590
if columns[n] in 'pid user group'.split():
594
key.append(columns[n])
598
width = 1.0 / (len(rc) + 1)
602
return 'bgrcmyw'[n % 7]
605
ind = numpy.arange(len(l))
606
for n in range(len(rc)):
607
pl.append(pylab.bar(ind + offset + width * n,
608
[x[1][rc[n]] for x in l], width, color=gc(n)))
610
#plt.xticks(ind + .5, )
611
pylab.gca().set_xticks(ind + .5)
612
pylab.gca().set_xticklabels([x[1][-1] for x in l], rotation=45)
613
pylab.legend([p[0] for p in pl], key)
617
parser = optparse.OptionParser("%prog [options]")
618
parser.add_option("-H", "--no-header", action="store_true",
619
help="disable header line")
620
parser.add_option("-c", "--columns", type="str",
621
help="columns to show")
622
parser.add_option("-t", "--totals", action="store_true",
625
parser.add_option("-R", "--realmem", type="str",
626
help="amount of physical RAM")
627
parser.add_option("-K", "--kernel", type="str",
628
help="path to kernel image")
630
parser.add_option("-m", "--mappings", action="store_true",
631
help="show mappings")
632
parser.add_option("-u", "--users", action="store_true",
634
parser.add_option("-w", "--system", action="store_true",
635
help="show whole system")
637
parser.add_option("-P", "--processfilter", type="str",
638
help="process filter regex")
639
parser.add_option("-M", "--mapfilter", type="str",
640
help="map filter regex")
641
parser.add_option("-U", "--userfilter", type="str",
642
help="user filter regex")
644
parser.add_option("-n", "--numeric", action="store_true",
645
help="numeric output")
646
parser.add_option("-s", "--sort", type="str",
647
help="field to sort on")
648
parser.add_option("-r", "--reverse", action="store_true",
651
parser.add_option("-p", "--percent", action="store_true",
652
help="show percentage")
653
parser.add_option("-k", "--abbreviate", action="store_true",
654
help="show unit suffixes")
656
parser.add_option("", "--pie", type='str',
657
help="show pie graph")
658
parser.add_option("", "--bar", type='str',
659
help="show bar graph")
661
parser.add_option("-S", "--source", type="str",
662
help="/proc data source")
666
parser.set_defaults(**defaults)
667
(options, args) = parser.parse_args()
670
src = tardata(options.source)
672
src = procdata(options.source)
684
if e.errno == errno.EPIPE:
686
except KeyboardInterrupt: