blob: a250055dbae2b4a6a37f4abba6c41b928216210f [file]
#!/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()