| #!/usr/bin/python |
| # Copyright 2018 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Parses /proc/[pid]/smaps on a device and shows the total amount of swap used. |
| """ |
| |
| from __future__ import print_function |
| |
| import argparse |
| import collections |
| import logging |
| import os |
| import re |
| import sys |
| |
| _SRC_PATH = os.path.join( |
| os.path.dirname(__file__), os.pardir, os.pardir, os.pardir) |
| sys.path.append(os.path.join(_SRC_PATH, 'third_party', 'catapult', 'devil')) |
| from devil.android import device_utils |
| |
| |
| class Mapping(object): |
| """A single entry (mapping) in /proc/[pid]/smaps.""" |
| |
| def __init__(self, start, end, permissions, offset, pathname): |
| """Initializes an instance. |
| |
| Args: |
| start: (str) Start address of the mapping. |
| end: (str) End address of the mapping. |
| permissions: (str) Permission string, e.g. r-wp. |
| offset: (str) Offset into the file or 0 if this is not a file mapping. |
| pathname: (str) Path name, or pseudo-path, e.g. [stack] |
| """ |
| self.start = int(start, 16) |
| self.end = int(end, 16) |
| self.permissions = permissions |
| self.offset = int(offset, 16) |
| self.pathname = pathname.strip() |
| self.fields = collections.OrderedDict() |
| |
| def AddField(self, line): |
| """Adds a field to an entry. |
| |
| Args: |
| line: (str) As it appears in /proc/[pid]/smaps. |
| """ |
| assert ':' in line |
| split_index = line.index(':') |
| k, v = line[:split_index].strip(), line[split_index + 1:].strip() |
| assert k not in self.fields |
| if v.endswith('kB'): |
| v = int(v[:-2]) |
| self.fields[k] = v |
| |
| def ToString(self): |
| """Returns a string representation of a mapping. |
| |
| The returned string is similar (but not identical) to the /proc/[pid]/smaps |
| entry it was generated from. |
| """ |
| lines = [] |
| lines.append('%x-%x %s %x %s' % ( |
| self.start, self.end, self.permissions, self.offset, self.pathname)) |
| for name in self.fields: |
| format_str = None |
| if isinstance(self.fields[name], int): |
| format_str = '%s: %d kB' |
| else: |
| format_str = '%s: %s' |
| lines.append(format_str % (name, self.fields[name])) |
| return '\n'.join(lines) |
| |
| |
| def _ParseProcSmapsLines(lines): |
| SMAPS_ENTRY_START_RE = ( |
| # start-end |
| '^([0-9a-f]{1,16})-([0-9a-f]{1,16}) ' |
| # Permissions |
| '([r\-][w\-][x\-][ps]) ' |
| # Offset |
| '([0-9a-f]{1,16}) ' |
| # Device |
| '([0-9a-f]{2,3}:[0-9a-f]{2,3}) ' |
| # Inode |
| '([0-9]*) ' |
| # Pathname |
| '(.*)') |
| assert re.search(SMAPS_ENTRY_START_RE, |
| '35b1800000-35b1820000 r-xp 00000000 08:02 135522 ' |
| '/usr/lib64/ld-2.15.so') |
| entry_re = re.compile(SMAPS_ENTRY_START_RE) |
| |
| mappings = [] |
| for line in lines: |
| match = entry_re.search(line) |
| if match: |
| (start, end, perms, offset, _, _, pathname) = match.groups() |
| mappings.append(Mapping(start, end, perms, offset, pathname)) |
| else: |
| mappings[-1].AddField(line) |
| return mappings |
| |
| |
| def ParseProcSmaps(device, pid, store_file=False): |
| """Parses /proc/[pid]/smaps on a device, and returns a list of Mapping. |
| |
| Args: |
| device: (device_utils.DeviceUtils) device to parse the file from. |
| pid: (int) PID of the process. |
| store_file: (bool) Whether to also write the file to disk. |
| |
| Returns: |
| [Mapping] all the mappings in /proc/[pid]/smaps. |
| """ |
| command = ['cat', '/proc/%d/smaps' % pid] |
| lines = device.RunShellCommand(command, check_return=True) |
| if store_file: |
| with open('smaps-%d' % pid, 'w') as f: |
| f.write('\n'.join(lines)) |
| return _ParseProcSmapsLines(lines) |
| |
| |
| def _GetPageTableFootprint(device, pid): |
| """Returns the page table footprint for a process in kiB.""" |
| command = ['cat', '/proc/%d/status' % pid] |
| lines = device.RunShellCommand(command, check_return=True) |
| for line in lines: |
| if line.startswith('VmPTE:'): |
| value = int(line[len('VmPTE: '):line.index('kB')]) |
| return value |
| |
| |
| def _SummarizeMapping(mapping, metric): |
| return '%s %s %s: %d kB (Total Size: %d kB)' % ( |
| hex(mapping.start), |
| mapping.pathname, mapping.permissions, metric, |
| (mapping.end - mapping.start) / 1024) |
| |
| |
| def _PrintMappingsMetric(mappings, field_name): |
| """Shows a summary of mappings for a given metric. |
| |
| For the given field, compute its aggregate value over all mappings, and |
| prints the mappings sorted by decreasing metric value. |
| |
| Args: |
| mappings: ([Mapping]) all process mappings. |
| field_name: (str) Mapping field to process. |
| """ |
| total_kb = sum(m.fields[field_name] for m in mappings) |
| print('Total Size (kB) = %d' % total_kb) |
| sorted_by_metric = sorted(mappings, |
| key=lambda m: m.fields[field_name], reverse=True) |
| for mapping in sorted_by_metric: |
| metric = mapping.fields[field_name] |
| if not metric: |
| break |
| print(_SummarizeMapping(mapping, metric)) |
| |
| |
| def _PrintSwapStats(mappings): |
| print('SWAP:') |
| _PrintMappingsMetric(mappings, 'Swap') |
| |
| |
| def _FootprintForAnonymousMapping(mapping): |
| assert mapping.pathname.startswith('[anon:') |
| if (mapping.pathname == '[anon:libc_malloc]' |
| and mapping.fields['Shared_Dirty'] != 0): |
| # libc_malloc mappings can come from the zygote. In this case, the shared |
| # dirty memory is likely dirty in the zygote, don't count it. |
| return mapping.fields['Rss'] |
| else: |
| return mapping.fields['Private_Dirty'] |
| |
| |
| def _PrintEstimatedFootprintStats(mappings, page_table_kb): |
| print('Private Dirty:') |
| _PrintMappingsMetric(mappings, 'Private_Dirty') |
| print('\n\nShared Dirty:') |
| _PrintMappingsMetric(mappings, 'Shared_Dirty') |
| print('\n\nPrivate Clean:') |
| _PrintMappingsMetric(mappings, 'Private_Clean') |
| print('\n\nShared Clean:') |
| _PrintMappingsMetric(mappings, 'Shared_Clean') |
| print('\n\nSwap PSS:') |
| _PrintMappingsMetric(mappings, 'SwapPss') |
| print('\n\nPage table = %d kiB' % page_table_kb) |
| |
| |
| def _ComputeEstimatedFootprint(mappings, page_table_kb): |
| """Returns the estimated footprint in kiB. |
| |
| Args: |
| mappings: ([Mapping]) all process mappings. |
| page_table_kb: (int) Sizeof the page tables in kiB. |
| """ |
| footprint = page_table_kb |
| for mapping in mappings: |
| # Chrome shared memory. |
| # |
| # Even though it is shared memory, it exists because the process exists, so |
| # account for its entirety. |
| if mapping.pathname.startswith('/dev/ashmem/shared_memory'): |
| footprint += mapping.fields['Rss'] |
| elif mapping.pathname.startswith('[anon'): |
| footprint += _FootprintForAnonymousMapping(mapping) |
| # Mappings without a name are most likely Chrome's native memory allocators: |
| # v8, PartitionAlloc, Oilpan. |
| # All of it should be charged to our process. |
| elif mapping.pathname.strip() == '': |
| footprint += mapping.fields['Rss'] |
| # Often inherited from the zygote, only count the private dirty part, |
| # especially as the swap part likely comes from the zygote. |
| elif mapping.pathname.startswith('['): |
| footprint += mapping.fields['Private_Dirty'] |
| # File mappings. Can be a real file, and/or Dalvik/ART. |
| else: |
| footprint += mapping.fields['Private_Dirty'] |
| return footprint |
| |
| |
| def _ShowAllocatorFootprint(mappings, allocator): |
| """Shows the total footprint from a specific allocator. |
| |
| Args: |
| mappings: ([Mapping]) all process mappings. |
| allocator: (str) Allocator name. |
| """ |
| total_footprint = 0 |
| pathname = '[anon:%s]' % allocator |
| for mapping in mappings: |
| if mapping.pathname == pathname: |
| total_footprint += _FootprintForAnonymousMapping(mapping) |
| print('\tFootprint from %s: %d kB' % (allocator, total_footprint)) |
| |
| |
| def _CreateArgumentParser(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--pid', help='PID.', required=True, type=int) |
| parser.add_argument('--estimate-footprint', |
| help='Show the estimated memory foootprint', |
| action='store_true') |
| parser.add_argument('--store-smaps', help='Store the smaps file locally', |
| action='store_true') |
| parser.add_argument('--show-allocator-footprint', |
| help='Show the footprint from a given allocator', |
| choices=['v8', 'libc_malloc', 'partition_alloc'], |
| nargs='+') |
| parser.add_argument( |
| '--device', help='Device to use', type=str, default='default') |
| return parser |
| |
| |
| def main(): |
| parser = _CreateArgumentParser() |
| args = parser.parse_args() |
| devices = device_utils.DeviceUtils.HealthyDevices(device_arg=args.device) |
| if not devices: |
| logging.error('No connected devices') |
| return |
| |
| device = devices[0] |
| if not device.HasRoot(): |
| device.EnableRoot() |
| # Enable logging after device handling as devil is noisy at INFO level. |
| logging.basicConfig(level=logging.INFO) |
| mappings = ParseProcSmaps(device, args.pid, args.store_smaps) |
| if args.estimate_footprint: |
| page_table_kb = _GetPageTableFootprint(device, args.pid) |
| _PrintEstimatedFootprintStats(mappings, page_table_kb) |
| footprint = _ComputeEstimatedFootprint(mappings, page_table_kb) |
| print('\n\nEstimated Footprint = %d kiB' % footprint) |
| else: |
| _PrintSwapStats(mappings) |
| |
| if args.show_allocator_footprint: |
| print('\n\nMemory Allocators footprint:') |
| for allocator in args.show_allocator_footprint: |
| _ShowAllocatorFootprint(mappings, allocator) |
| |
| |
| if __name__ == '__main__': |
| main() |