blob: 568afab9fc78420eeb3b01763a355cc1903a5623 [file]
#!/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.
"""Maps the superpage occupancy, and the allocated object sizes on the heap.
To run this:
1. Build pa_dump_heap at a close-enough revision to the Chrome instance you're
interested in.
2. Run pa_dump_heap --pid=PID_OF_RUNNING_CHROME_PROCESS --json=output.json
3. Run plot_superpages.py --json=output.json --output=output.png
"""
import argparse
import bisect
import math
import json
import matplotlib
from matplotlib import pylab as plt
import numpy as np
# Valid for the "before slot" variant.
_BRP_OVERHEAD = 4
def ParseJson(filename: str) -> dict:
with open(filename, 'r') as f:
data = json.load(f)
return data
def PlotSuperPageData(data: dict, output_filename: str):
num_superpages = len(data)
num_partition_pages = len(data[0]['partition_pages'])
data_np = np.zeros((num_superpages, num_partition_pages, 3))
address_to_superpage = {superpage['address']: superpage for superpage in data}
addresses = sorted(address_to_superpage.keys())
BLACK = (0.0, 0.0, 0.0)
GRAY = (0.5, 0.5, 0.5)
RED = (1.0, 0.0, 0.0)
GREEN = (0.0, 0.8, 0.0)
WHITE = (1.0, 1.0, 1.0)
cmap = matplotlib.cm.get_cmap('coolwarm')
for superpage_index in range(len(addresses)):
superpage = address_to_superpage[addresses[superpage_index]]
for index, partition_page in enumerate(superpage['partition_pages']):
value = None
if partition_page['type'] == 'metadata':
value = BLACK
elif partition_page['type'] == 'guard':
value = GRAY
elif partition_page['all_zeros'] and not 'is_active' in partition_page:
# Otherwise it may be the subsequent partition page of a decommitted
# slot span.
if partition_page['page_index_in_span'] == 0:
value = WHITE
if value is not None:
data_np[superpage_index, index] = value
continue
assert partition_page['type'] == 'payload'
if partition_page['page_index_in_span'] == 0:
num_partition_pages_in_slot_span = math.ceil(
partition_page['num_system_pages_per_slot_span'] / 4
)
if partition_page['is_decommitted'] or partition_page['is_empty']:
value = GREEN
else:
fullness = (
partition_page['num_allocated_slots']
/ partition_page['slots_per_span']
)
value = cmap(fullness)[:3]
data_np[
superpage_index, index : index + num_partition_pages_in_slot_span, :
] = value
plt.figure(figsize=(20, len(address_to_superpage) / 2))
plt.imshow(data_np)
plt.title('Super page map')
plt.yticks(ticks=range(len(addresses)), labels=addresses)
plt.xlabel('PartitionPage index')
handles = [
matplotlib.patches.Patch(facecolor=BLACK, edgecolor='k', label='Metadata'),
matplotlib.patches.Patch(facecolor=GRAY, edgecolor='k', label='Guard'),
matplotlib.patches.Patch(facecolor=WHITE, edgecolor='k', label='Empty'),
matplotlib.patches.Patch(
facecolor=GREEN, edgecolor='k', label='Decommitted'
),
matplotlib.patches.Patch(
facecolor=cmap(0.0), edgecolor='k', label='Committed Empty'
),
matplotlib.patches.Patch(
facecolor=cmap(1.0), edgecolor='k', label='Committed Full'
),
]
plt.legend(handles=handles, loc='lower left', fontsize=12, framealpha=0.7)
plt.savefig(output_filename, bbox_inches='tight')
def PlotSuperPageCompressionData(data: dict, output_filename: str):
num_superpages = len(data)
num_pages = len(data[0]['page_sizes'])
data_np = np.zeros((num_superpages, num_pages, 3))
address_to_superpage = {superpage['address']: superpage for superpage in data}
addresses = sorted(address_to_superpage.keys())
WHITE = (1.0, 1.0, 1.0)
cmap = matplotlib.cm.get_cmap('coolwarm')
total_compressed_size = 0
total_uncompressed_size = 0
for superpage_index in range(len(addresses)):
superpage = address_to_superpage[addresses[superpage_index]]
for index, page in enumerate(superpage['page_sizes']):
total_uncompressed_size += page['uncompressed']
total_compressed_size += page['compressed']
if page['uncompressed'] == 0:
value = WHITE
else:
value = cmap(page['compressed'] / page['uncompressed'])[:3]
data_np[superpage_index, index] = value
overall_compression_ratio = (
(100.0 * total_compressed_size / (total_uncompressed_size))
if total_uncompressed_size
else 0
)
plt.figure(figsize=(20, len(address_to_superpage)))
plt.imshow(data_np, aspect=8)
plt.title(
'Super page compression ratio map - Ratio = %.1f%% '
'- Compressed Size = %.1fMiB'
% (overall_compression_ratio, total_compressed_size / (1 << 20))
)
plt.yticks(ticks=range(len(addresses)), labels=addresses)
plt.xlabel('Page index in SuperPage')
handles = [
matplotlib.patches.Patch(
facecolor=WHITE, edgecolor='k', label='Empty / Decommitted'
),
matplotlib.patches.Patch(
facecolor=cmap(0.0), edgecolor='k', label='Compressible'
),
matplotlib.patches.Patch(
facecolor=cmap(1.0), edgecolor='k', label='Incompressible'
),
]
plt.legend(handles=handles, loc='lower left', fontsize=12, framealpha=0.7)
plt.savefig(output_filename, bbox_inches='tight')
def _PlotFragmentationCommon(bucket_to_allocated: dict, output_filename: str):
slot_sizes = sorted(bucket_to_allocated.keys())
allocated = []
waste = []
for slot_size in slot_sizes:
slots = np.array(bucket_to_allocated[slot_size])
allocated.append(np.sum(slots))
waste.append(np.sum(slot_size - slots))
plt.figure(figsize=(18, 8))
indices = range(len(slot_sizes))
plt.bar(indices, allocated, label='Requested Memory')
b = plt.bar(
indices, waste, bottom=allocated, label='Waste due to padding + bucketing'
)
waste_percentage = [
('%d%%' % (int(100.0 * w / (w + a)))) if a else ''
for (w, a) in zip(waste, allocated)
]
plt.bar_label(b, labels=waste_percentage)
plt.xticks(indices, slot_sizes, rotation='vertical')
plt.xlim(left=-0.5, right=len(indices))
plt.xlabel('Bucket size')
plt.ylabel('Memory (bytes)')
plt.legend()
total_allocated = np.sum(allocated)
total_waste = np.sum(waste)
plt.title(
'Absolute and relative waste due to bucketing and padding'
' per bucket - Total Allocated = %dMiB, Waste = %d%%'
% (
total_allocated / (1 << 20),
int(100 * total_waste / (total_allocated + total_waste)),
)
)
plt.savefig(output_filename, bbox_inches='tight')
def _AdjustSizes(data: dict, bucket_sizes: list, adjustment_size: int) -> dict:
requested_sizes = []
for slot_span in data:
requested_sizes += slot_span['allocated_sizes']
# Assumes that all buckets have *some* live allocations.
bucket_sizes = sorted(bucket_sizes)
bucket_to_allocated = {slot_size: list() for slot_size in bucket_sizes}
# Map the requested sizes without paddingb to buckets.
for requested_size in requested_sizes:
adjusted_size = requested_size + adjustment_size
bucket_index = bisect.bisect_left(bucket_sizes, adjusted_size)
slot_size = bucket_sizes[bucket_index]
assert bucket_sizes[bucket_index] >= adjusted_size
bucket_to_allocated[slot_size].append(adjusted_size)
return bucket_to_allocated
def PlotSimulatedFragmentationData(
data: dict, bucket_sizes: list, output_filename: str
):
# No adjustment, want to check waste without any padding.
bucket_to_allocated = _AdjustSizes(data, bucket_sizes, 0)
_PlotFragmentationCommon(bucket_to_allocated, output_filename)
def PlotFragmentationData(data: dict, bucket_sizes: list, output_filename: str):
# "Before allocation" takes only 4 bytes, but the instrumentation added
# expands this to 8 bytes. To reconstruct what it would have been without it,
# take the requested size and add 4 bytes to it.
bucket_to_allocated = _AdjustSizes(data, bucket_sizes, _BRP_OVERHEAD)
_PlotFragmentationCommon(bucket_to_allocated, output_filename)
def PlotDelta(data: dict, bucket_sizes: list, output_filename: str):
bucket_to_allocated_brp = _AdjustSizes(data, bucket_sizes, _BRP_OVERHEAD)
bucket_to_allocated_no_brp = _AdjustSizes(data, bucket_sizes, 0)
slot_sizes = sorted(bucket_to_allocated_no_brp)
allocated_per_bucket_brp = {
slot_size: len(bucket_to_allocated_brp[slot_size]) * slot_size
for slot_size in bucket_to_allocated_brp
}
allocated_per_bucket_no_brp = {
slot_size: len(bucket_to_allocated_no_brp[slot_size]) * slot_size
for slot_size in bucket_to_allocated_no_brp
}
delta = [
allocated_per_bucket_brp[slot_size] - allocated_per_bucket_no_brp[slot_size]
for slot_size in slot_sizes
]
plt.figure(figsize=(18, 8))
indices = range(len(slot_sizes))
plt.bar(indices, delta, label='Delta')
plt.xticks(indices, slot_sizes, rotation='vertical')
plt.xlim(left=-0.5, right=len(indices))
plt.xlabel('Bucket size')
plt.ylabel('Memory delta (bytes)')
plt.title(
'Per-bucket size difference BRP/No-BRP - Total = %.1fMiB'
% (sum(delta) / (1 << 20))
)
plt.savefig(output_filename, bbox_inches='tight')
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--json', help='JSON dump from pa_tcache_inspect', required=True
)
parser.add_argument('--output', help='Output filename prefix', required=True)
args = parser.parse_args()
data = ParseJson(args.json)
PlotSuperPageData(data['superpages'], args.output + '_superpage.png')
PlotSuperPageCompressionData(
data['superpages'], args.output + '_compression.png'
)
# Allocated object data is not always available.
if 'allocated_sizes' in data:
bucket_sizes = [x['slot_size'] for x in data['buckets']]
PlotFragmentationData(
data['allocated_sizes'], bucket_sizes, args.output + '_waste.png'
)
PlotSimulatedFragmentationData(
data['allocated_sizes'],
bucket_sizes,
args.output + '_waste_simulated.png',
)
PlotDelta(data['allocated_sizes'], bucket_sizes, args.output + '_delta.png')
if __name__ == '__main__':
main()