| #!/usr/bin/env python3 |
| # Copyright 2026 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Parses Mac vmmap output to group and count IOAccelerator allocations. |
| |
| This tool helps profile GPU memory allocations by reading vmmap output |
| directly from a running process (via PID) or from a saved text file. |
| """ |
| |
| import argparse |
| import logging |
| import re |
| import subprocess |
| import sys |
| from collections import Counter |
| |
| |
| def parse_size(size_str): |
| """Parses a size string with units (K, M, G) into bytes.""" |
| match = re.match(r'([0-9.]+)([KMG]?)', size_str) |
| if not match: |
| return 0 |
| val, unit = match.groups() |
| val = float(val) |
| if unit == 'K': |
| return int(val * 1024) |
| elif unit == 'M': |
| return int(val * 1024 * 1024) |
| elif unit == 'G': |
| return int(val * 1024 * 1024 * 1024) |
| return int(val) |
| |
| |
| def parse_vmmap_lines(lines): |
| """Parses lines of vmmap output and extracts IOAccelerator resident+swap. |
| |
| Only includes entries that have non-zero resident or swapped memory. |
| """ |
| sizes = [] |
| io_accelerator_re = re.compile( |
| r'\[\s*([0-9.a-zA-Z]+)\s+([0-9.a-zA-Z]+)\s+[0-9.a-zA-Z]+' |
| r'\s+([0-9.a-zA-Z]+)\]') |
| for line in lines: |
| if 'IOAccelerator' in line: |
| bracket_match = io_accelerator_re.search(line) |
| if bracket_match: |
| # Same names as the result headers from vmmap output. |
| vsize_str, rsdnt_str, swap_str = bracket_match.groups() |
| rsdnt = parse_size(rsdnt_str) |
| swap = parse_size(swap_str) |
| if rsdnt == 0 and swap == 0: |
| continue |
| sizes.append(rsdnt + swap) |
| return sizes |
| |
| |
| def main(): |
| logging.basicConfig(level=logging.INFO, |
| stream=sys.stderr, |
| format='%(message)s') |
| parser = argparse.ArgumentParser( |
| description= |
| 'Parse Mac vmmap output to group and count IOAccelerator allocations.') |
| group = parser.add_mutually_exclusive_group(required=True) |
| group.add_argument('--pid', |
| type=int, |
| help='PID of the process to call vmmap on.') |
| group.add_argument('--file', |
| type=str, |
| help='Path to a cached vmmap output file.') |
| |
| args = parser.parse_args() |
| |
| if args.file: |
| try: |
| with open(args.file, 'r', encoding='utf-8') as f: |
| lines = f.readlines() |
| except FileNotFoundError: |
| logging.error(f'Error: File \'{args.file}\' not found.') |
| sys.exit(1) |
| elif args.pid: |
| try: |
| logging.info(f'Running vmmap on PID {args.pid}...') |
| output = subprocess.check_output(['vmmap', str(args.pid)], |
| stderr=subprocess.STDOUT) |
| lines = output.decode('utf-8', errors='replace').splitlines() |
| except subprocess.CalledProcessError as e: |
| logging.error('Error running vmmap: %s', |
| e.output.decode('utf-8', errors='replace')) |
| sys.exit(1) |
| |
| sizes = parse_vmmap_lines(lines) |
| |
| if not sizes: |
| logging.error( |
| 'No active (resident or swapped) IOAccelerator regions found.') |
| return |
| |
| print(f'Total IOAccelerator entries: {len(sizes)}') |
| print(f'Total memory size: {sum(sizes) / (1024*1024):.2f} MB') |
| print('\nHistogram of total memory size per bucket:') |
| |
| counter = Counter(sizes) |
| max_bucket_mem = max(size * count for size, count in counter.items()) |
| |
| for size in sorted(counter.keys()): |
| count = counter[size] |
| bucket_mem = size * count |
| bucket_mem_mb = bucket_mem / (1024 * 1024) |
| size_kb = size / 1024 |
| |
| bar_len = int(60 * bucket_mem / max_bucket_mem) if max_bucket_mem > 0 else 0 |
| bar = '*' * bar_len |
| print(f'{size_kb:8.1f} KB | {count:4d} entries | {bucket_mem_mb:7.2f} MB ' |
| f'| {bar}') |
| |
| |
| if __name__ == '__main__': |
| main() |