910e62b5创建于 1月15日历史提交
#!/usr/bin/env python3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Parses allocation profiles from a trace and graphs the results.

This parses an allocation profile generated by PartitionAlloc in the thread
cache. This will only give data on Chrome instances where the thread cache is
enabled, and PA_THREAD_CACHE_ALLOC_STATS is defined, that is non-official
builds.

To collect a profile:
- Build a non-official chrome version (Should be a release build for accurate
  reports)
- Collect a trace with the memory-infra category enabled (in chrome://tracing)
- Save it as json.gz, and load it here.
"""

import argparse
import logging
import os

from matplotlib import pylab as plt
import numpy as np

from parse_trace import LoadTrace, GetAllocatorDumps, ProcessNamesAndLabels


def _ParseTrace(trace: dict) -> dict:
  """Parses a trace, and returns thread cache stats.

  Args:
    trace: As returned by LoadTrace()

  Returns:
    {pid  -> {'name': str, 'labels': str, 'data': np.array}.
    Where the data array contains 'size' and 'count' columns.
  """
  dumps = GetAllocatorDumps(trace)
  pid_to_name, pid_to_labels = ProcessNamesAndLabels(trace)

  result = {}
  for dump in dumps:
    pid = dump['pid']
    allocators = dump['args']['dumps']['allocators']

    # The browser process also has global dumps, we do not care about these.
    if 'global' in allocators:
      continue

    result[pid] = {
        'name': pid_to_name[pid],
        'labels': pid_to_labels.get(pid, '')
    }
    size_counts = []
    for allocator in allocators:
      if ('malloc/partitions/allocator/thread_cache/buckets_alloc/' not in
          allocator):
        continue
      size = int(allocator[allocator.rindex('/') + 1:])
      count = int(allocators[allocator]['attrs']['count']['value'], 16)
      size_counts.append((size, count))
      size_counts.sort()
      result[pid]['data'] = np.array(size_counts,
                                     dtype=[('size', np.int),
                                            ('count', np.int)])

  return result


def _PlotProcess(all_data: dict, pid: int, output_prefix: str):
  """Represents the allocation size distribution.

  Args:
    all_data: As returned by _ParseTrace().
    pid: PID to plot the data for.
    output_prefix: Prefix of the output file.
  """
  data = all_data[pid]
  logging.info('Plotting data for PID %d' % pid)

  # Allocations vs size.
  plt.figure(figsize=(16, 8))
  plt.title('Allocation count vs Size - %s - %s' %
            (data['name'], data['labels']))
  plt.xscale('log', base=2)
  plt.yscale('log', base=10)
  plt.stem(data['data']['size'], data['data']['count'])
  plt.xlabel('Size (log)')
  plt.ylabel('Allocations (log)')
  plt.savefig('%s_%d_count.png' % (output_prefix, pid), bbox_inches='tight')
  plt.close()

  # CDF.
  plt.figure(figsize=(16, 8))
  plt.title('CDF of allocation size - %s - %s' % (data['name'], data['labels']))
  cdf = np.cumsum(100. * data['data']['count']) / np.sum(data['data']['count'])

  for value in [512, 1024, 2048, 4096, 8192]:
    index = np.where(data['data']['size'] == value)[0]
    cdf_value = cdf[index]
    plt.axvline(x=value, ymin=0, ymax=cdf_value / 100., color='lightgrey')

  plt.step(data['data']['size'], cdf, color='black', where='post')
  plt.ylim(ymin=0, ymax=100)
  plt.xlim(xmin=10, xmax=1e6)
  plt.xscale('log', base=2)
  plt.xlabel('Size (log)')
  plt.ylabel('CDF (%)')
  plt.savefig('%s_%d_cdf.png' % (output_prefix, pid),
              bbox_inches='tight',
              dpi=300)
  plt.close()


def _CreateArgumentParser():
  parser = argparse.ArgumentParser()
  parser.add_argument(
      '--trace',
      type=str,
      required=True,
      help='Path to a trace.json[.gz] with memory-infra enabled.')
  parser.add_argument('--output-dir',
                      type=str,
                      required=True,
                      help='Output directory for graphs.')
  return parser


def main():
  logging.basicConfig(level=logging.INFO)
  parser = _CreateArgumentParser()
  args = parser.parse_args()

  logging.info('Loading the trace')
  trace = LoadTrace(args.trace)

  logging.info('Parsing the trace')
  stats_per_process = _ParseTrace(trace)

  logging.info('Plotting the results')
  for pid in stats_per_process:
    if 'data' in stats_per_process[pid]:
      _PlotProcess(stats_per_process, pid,
                   os.path.join(args.output_dir, 'result'))


if __name__ == '__main__':
  main()