| #!/usr/bin/env python3 |
| # Copyright 2022 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """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}' |
| # Has an optional suffix, in which case it's a float |
| _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() |