"""Parses the output of vmmap to query IOSurface memory usage.
"""
import argparse
import collections
import logging
import operator
import re
import subprocess
import sys
IOSurface = collections.namedtuple('IOSurface', [
'start', 'end', 'virtual', 'resident', 'dirty', 'swapped', 'width',
'height', 'size'
])
def _FormatSize(n: int):
_KIB = 1024
_MIB = 1024 * _KIB
result = ''
if n > _MIB:
result = '%.1fMiB' % (n / _MIB)
else:
result = '%0.1fkiB' % (n / _KIB)
return result.rjust(8)
def IOSurfaceToString(iosurface: IOSurface):
return ('%x-%x\tVirtual Size: %s\tResident: %s\tDirty: %s\tSwapped: %s'
'\tDimensions: %dx%d') % (
iosurface.start, iosurface.end, _FormatSize(iosurface.virtual),
_FormatSize(iosurface.resident), _FormatSize(iosurface.dirty),
_FormatSize(iosurface.swapped), iosurface.width, iosurface.height)
def ExecuteVmmap(pid: int) -> str:
"""Runs vmmap PID and returns its output."""
ret = subprocess.run(['vmmap', str(pid)], capture_output=True)
ret.check_returncode()
stdout = ret.stdout.decode('utf-8')
return stdout
def _ParseSize(size):
suffix = size[-1]
if suffix == 'K':
return int(1024 * float(size[:-1]))
elif suffix == 'M':
return int(1024 * 1024 * float(size[:-1]))
else:
return int(size)
def _PrettyPrint(size: int) -> str:
_KIB = 1024
_MIB = 1024 * _KIB
_GIB = 1024 * _MIB
if size > _GIB:
return '%.1fGiB' % (size / _GIB)
elif size > _MIB:
return '%.1fMiB' % (size / _MIB)
elif size > _KIB:
return '%.1fkiB' % (size / _KIB)
else:
return str(size)
def ParseIOSurface(contents: str, quiet=False) -> list:
"""From the content of a vmmap file, returns a list of IOSurfaces."""
io_surfaces = []
for line in contents.split('\n'):
_ADDRESS_RE = '[0-9a-f]{1,16}'
_SIZE_RE = '[0-9\.KM]*'
_SIZE_DECIMAL = '[0-9]*'
_RE = ('^IOSurface *'
'(?P<start>%(address)s)-(?P<end>%(address)s) *'
'\[ *(?P<virtual>%(size)s) *(?P<resident>%(size)s) '
'*(?P<dirty>%(size)s) *(?P<swapped>%(size)s)\]'
'.*SurfaceID: 0x[0-9a-f]* *'
'(?P<width>%(size_decimal)s)x(?P<height>%(size_decimal)s)'
' *\([^\(]*\) *'
'(?P<size>%(size)s)') % {
'address': _ADDRESS_RE,
'size': _SIZE_RE,
'size_decimal': _SIZE_DECIMAL
}
regexp = re.compile(_RE)
_SAMPLE_LINE = ('IOSurface 2a7d38000-2aae10000 '
'[ 48.8M 0K 0K 48.8M] rw-/rw- SM=SHM PURGE=N'
' SurfaceID: 0x5f 2440x5196 (BGRA) 48.8M')
assert regexp.match(_SAMPLE_LINE)
_SAMPLE_LINE2 = ('IOSurface 10c2d0000-10c2d4000 '
'[ 16K 0K 0K 0K] rw-/rw- SM=SHM PURGE=N'
' SurfaceID: 0x22e 19x19 (BGRA) 2560')
assert regexp.match(_SAMPLE_LINE2)
matches = regexp.match(line)
if matches:
if not quiet:
print(line)
start = int(matches.group('start'), 16)
end = int(matches.group('end'), 16)
virtual = _ParseSize(matches.group('virtual'))
resident = _ParseSize(matches.group('resident'))
dirty = _ParseSize(matches.group('dirty'))
swapped = _ParseSize(matches.group('swapped'))
width = int(matches.group('width'))
height = int(matches.group('height'))
size = _ParseSize(matches.group('size'))
io_surface = IOSurface(start, end, virtual, resident, dirty, swapped,
width, height, size)
io_surfaces.append(io_surface)
return io_surfaces
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--pid',
help='PID of the process to get the maps of',
type=int)
parser.add_argument('--filename',
help='Path to existing output of vmmap',
type=str)
args = parser.parse_args()
contents = None
if args.pid:
contents = ExecuteVmmap(args.pid)
else:
assert args.filename
with open(args.filename, 'r') as f:
contents = f.read()
io_surfaces = ParseIOSurface(contents)
io_surfaces.sort(key=operator.attrgetter('virtual'))
print('\nIOSurfaces sorted by virtual size:')
for io_surface in io_surfaces:
print('\t' + IOSurfaceToString(io_surface))
io_surfaces.sort(key=operator.attrgetter('width'))
print('\nIOSurfaces sorted by width:')
for io_surface in io_surfaces:
print('\t' + IOSurfaceToString(io_surface))
io_surfaces.sort(key=lambda x: x.dirty + x.swapped)
print('\nIOSurfaces sorted by dirty/swapped:')
for io_surface in io_surfaces:
print('\t' + IOSurfaceToString(io_surface))
lost_to_paging = sum(io_surface.virtual - io_surface.size
for io_surface in io_surfaces)
dirty = sum(io_surface.dirty for io_surface in io_surfaces)
swapped = sum(io_surface.swapped for io_surface in io_surfaces)
print('\nMemory lost due to page rounding = %s' %
_PrettyPrint(lost_to_paging))
print('Dirty Memory = %s' % _PrettyPrint(dirty))
print('Swapped Memory = %s' % _PrettyPrint(swapped))
if __name__ == '__main__':
main()