blob: 0ffd17b3a4b85147016a299c6b0f52824a4494e5 [file] [log] [blame]
# Copyright 2017 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.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import codecs
import collections
import sys
import time
import six
from tracing.value import histogram
from tracing.value import histogram_set
from tracing.value.diagnostics import breakdown
from tracing.value.diagnostics import date_range
from tracing.value.diagnostics import generic_set
from tracing.value.diagnostics import related_name_map
from tracing.value.diagnostics import reserved_infos
from tracing_build import render_histograms_viewer
def _IsUserDefinedInstance(obj):
return str(type(obj)).startswith('<class ')
class _HeapProfiler(object):
__slots__ = '_diagnostics_callback', '_histograms', '_seen'
def __init__(self, diagnostics_callback=None):
self._diagnostics_callback = diagnostics_callback
self._histograms = None
self._seen = set()
def Profile(self, root):
self._histograms = histogram_set.HistogramSet()
total_hist = self._GetOrCreateHistogram('heap')
total_hist.diagnostics['types'] = related_name_map.RelatedNameMap()
total_breakdown = breakdown.Breakdown()
total_size = self._Recurse(
root, total_hist.diagnostics['types'], total_breakdown)
builtins_size = total_size - sum(subsize for _, subsize in total_breakdown)
if builtins_size:
total_breakdown.Set('(builtin types)', builtins_size)
total_hist.AddSample(total_size, dict(types=total_breakdown))
self._histograms.AddSharedDiagnosticToAllHistograms(
reserved_infos.TRACE_START.name,
date_range.DateRange(time.time() * 1000))
return self._histograms
def _GetOrCreateHistogram(self, name):
hs = self._histograms.GetHistogramsNamed(name)
if len(hs) > 1:
raise Exception('Too many Histograms named %s' % name)
if len(hs) == 1:
return hs[0]
hist = histogram.Histogram(name, 'sizeInBytes_smallerIsBetter')
hist.CustomizeSummaryOptions(dict(std=False, min=False, max=False))
self._histograms.AddHistogram(hist)
return hist
def _Recurse(self, obj, parent_related_names, parent_breakdown):
if id(obj) in self._seen:
return 0
self._seen.add(id(obj))
size = sys.getsizeof(obj)
related_names = parent_related_names
types_breakdown = parent_breakdown
hist = None
if _IsUserDefinedInstance(obj):
type_name = type(obj).__name__
hist = self._GetOrCreateHistogram('heap:' + type_name)
related_names = hist.diagnostics.get('types')
if related_names is None:
related_names = related_name_map.RelatedNameMap()
types_breakdown = breakdown.Breakdown()
if isinstance(obj, dict):
for objkey, objvalue in six.iteritems(obj):
size += self._Recurse(objkey, related_names, types_breakdown)
size += self._Recurse(objvalue, related_names, types_breakdown)
elif isinstance(obj, (tuple, list, set, frozenset, collections.deque)):
# Can't use collections.Iterable because strings are iterable, but
# sys.getsizeof() already handles strings, we don't need to iterate over
# them.
for elem in obj:
size += self._Recurse(elem, related_names, types_breakdown)
# It is possible to subclass builtin types like dict and add properties to
# them, so handle __dict__ and __slots__ even if obj is a dict/list/etc.
properties_breakdown = breakdown.Breakdown()
if hasattr(obj, '__dict__'):
size += sys.getsizeof(obj.__dict__)
for dkey, dvalue in six.iteritems(obj.__dict__):
size += self._Recurse(dkey, related_names, types_breakdown)
dsize = self._Recurse(dvalue, related_names, types_breakdown)
properties_breakdown.Set(dkey, dsize)
size += dsize
size += self._Recurse(obj.__dict__, related_names, types_breakdown)
# It is possible for a class to use both __slots__ and __dict__ by listing
# __dict__ as a slot.
if hasattr(obj.__class__, '__slots__'):
for slot in obj.__class__.__slots__:
if slot == '__dict__':
# obj.__dict__ was already handled
continue
if not hasattr(obj, slot):
continue
slot_size = self._Recurse(
getattr(obj, slot), related_names, types_breakdown)
properties_breakdown.Set(slot, slot_size)
size += slot_size
if hist:
if len(related_names):
hist.diagnostics['types'] = related_names
parent_related_names.Set(type_name, hist.name)
parent_breakdown.Set(type_name, parent_breakdown.Get(type_name) + size)
builtins_size = size - sum(subsize for _, subsize in types_breakdown)
if builtins_size:
types_breakdown.Set('(builtin types)', builtins_size)
sample_diagnostics = {'types': types_breakdown}
if len(properties_breakdown):
sample_diagnostics['properties'] = properties_breakdown
if self._diagnostics_callback:
sample_diagnostics.update(self._diagnostics_callback(obj))
hist.AddSample(size, sample_diagnostics)
return size
def Profile(root, label=None, html_filename=None, html_stream=None,
vulcanized_viewer=None, reset_results=False,
diagnostics_callback=None):
"""Profiles memory consumed by the root object.
Produces a HistogramSet containing 1 Histogram for each user-defined class
encountered when recursing through the root object's properties.
Each Histogram contains 1 sample for each instance of the class.
Each sample contains 2 Breakdowns:
- 'types' allows drilling down into memory profiles for other classes, and
- 'properties' breaks down the size of an instance by its properties.
Args:
label: string label to distinguish these results from those produced by
other Profile() calls.
html_filename: string filename to write HTML results.
html_stream: file-like string to write HTML results.
vulcanized_viewer: HTML string
reset_results: whether to delete pre-existing results in
html_filename/html_stream
diagnostics_callback: function that takes an instance of a class, and
returns a dictionary from strings to Diagnostic objects.
Returns:
HistogramSet
"""
# TODO(4068): Package this and its dependencies and a vulcanized viewer in
# order to remove the vulcanized_viewer parameter and simplify rendering the
# viewer.
profiler = _HeapProfiler(diagnostics_callback)
histograms = profiler.Profile(root)
if label:
histograms.AddSharedDiagnosticToAllHistograms(
reserved_infos.LABELS.name, generic_set.GenericSet([label]))
if html_filename and not html_stream:
open(html_filename, 'a').close() # Create file if it doesn't exist.
html_stream = codecs.open(html_filename, mode='r+', encoding='utf-8')
if html_stream:
# Vulcanizing the viewer requires a full catapult checkout, which is not
# available in some contexts such as appengine.
# Merely rendering the viewer requires a pre-vulcanized viewer HTML string.
# render_histograms_viewer does not require a full checkout, so it can run
# in restricted contexts such as appengine as long as a pre-vulcanized
# viewer is provided.
if vulcanized_viewer:
render_histograms_viewer.RenderHistogramsViewer(
histograms.AsDicts(), html_stream, reset_results, vulcanized_viewer)
else:
from tracing_build import vulcanize_histograms_viewer
vulcanize_histograms_viewer.VulcanizeAndRenderHistogramsViewer(
histograms.AsDicts(), html_stream)
return histograms