mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-04-22 02:23:48 +00:00
781 lines
28 KiB
Python
781 lines
28 KiB
Python
"""
|
|
Provide saving, loading and presenting gathered `ClassTracker` statistics.
|
|
"""
|
|
|
|
from typing import (
|
|
Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING, Union
|
|
)
|
|
|
|
import os
|
|
import pickle
|
|
import sys
|
|
from copy import deepcopy
|
|
from pympler.util.stringutils import trunc, pp, pp_timestamp
|
|
|
|
from pympler.asizeof import Asized
|
|
|
|
if TYPE_CHECKING:
|
|
from .classtracker import TrackedObject, ClassTracker, Snapshot
|
|
|
|
|
|
__all__ = ["Stats", "ConsoleStats", "HtmlStats"]
|
|
|
|
|
|
def _ref2key(ref: Asized) -> str:
|
|
return ref.name.split(':')[0]
|
|
|
|
|
|
def _merge_asized(base: Asized, other: Asized, level: int = 0) -> None:
|
|
"""
|
|
Merge **Asized** instances `base` and `other` into `base`.
|
|
"""
|
|
base.size += other.size
|
|
base.flat += other.flat
|
|
if level > 0:
|
|
base.name = _ref2key(base)
|
|
# Add refs from other to base. Any new refs are appended.
|
|
base.refs = list(base.refs) # we may need to append items
|
|
refs = {}
|
|
for ref in base.refs:
|
|
refs[_ref2key(ref)] = ref
|
|
for ref in other.refs:
|
|
key = _ref2key(ref)
|
|
if key in refs:
|
|
_merge_asized(refs[key], ref, level=level + 1)
|
|
else:
|
|
# Don't modify existing Asized instances => deepcopy
|
|
base.refs.append(deepcopy(ref))
|
|
base.refs[-1].name = key
|
|
|
|
|
|
def _merge_objects(tref: float, merged: Asized, obj: 'TrackedObject') -> None:
|
|
"""
|
|
Merge the snapshot size information of multiple tracked objects. The
|
|
tracked object `obj` is scanned for size information at time `tref`.
|
|
The sizes are merged into **Asized** instance `merged`.
|
|
"""
|
|
size = None
|
|
for (timestamp, tsize) in obj.snapshots:
|
|
if timestamp == tref:
|
|
size = tsize
|
|
if size:
|
|
_merge_asized(merged, size)
|
|
|
|
|
|
def _format_trace(trace: List[Tuple]) -> str:
|
|
"""
|
|
Convert the (stripped) stack-trace to a nice readable format. The stack
|
|
trace `trace` is a list of frame records as returned by
|
|
**inspect.stack** but without the frame objects.
|
|
Returns a string.
|
|
"""
|
|
lines = []
|
|
for fname, lineno, func, src, _ in trace:
|
|
if src:
|
|
for line in src:
|
|
lines.append(' ' + line.strip() + '\n')
|
|
lines.append(' %s:%4d in %s\n' % (fname, lineno, func))
|
|
return ''.join(lines)
|
|
|
|
|
|
class Stats(object):
|
|
"""
|
|
Presents the memory statistics gathered by a `ClassTracker` based on user
|
|
preferences.
|
|
"""
|
|
|
|
def __init__(self, tracker: 'Optional[ClassTracker]' = None,
|
|
filename: Optional[str] = None,
|
|
stream: Optional[IO] = None):
|
|
"""
|
|
Initialize the data log structures either from a `ClassTracker`
|
|
instance (argument `tracker`) or a previously dumped file (argument
|
|
`filename`).
|
|
|
|
:param tracker: ClassTracker instance
|
|
:param filename: filename of previously dumped statistics
|
|
:param stream: where to print statistics, defaults to ``sys.stdout``
|
|
"""
|
|
if stream:
|
|
self.stream = stream
|
|
else:
|
|
self.stream = sys.stdout
|
|
self.tracker = tracker
|
|
self.index = {} # type: Dict[str, List[TrackedObject]]
|
|
self.snapshots = [] # type: List[Snapshot]
|
|
if tracker:
|
|
self.index = tracker.index
|
|
self.snapshots = tracker.snapshots
|
|
self.history = tracker.history
|
|
self.sorted = [] # type: List[TrackedObject]
|
|
if filename:
|
|
self.load_stats(filename)
|
|
|
|
def load_stats(self, fdump: Union[str, IO[bytes]]) -> None:
|
|
"""
|
|
Load the data from a dump file.
|
|
The argument `fdump` can be either a filename or an open file object
|
|
that requires read access.
|
|
"""
|
|
if isinstance(fdump, str):
|
|
fdump = open(fdump, 'rb')
|
|
self.index = pickle.load(fdump)
|
|
self.snapshots = pickle.load(fdump)
|
|
self.sorted = []
|
|
|
|
def dump_stats(self, fdump: Union[str, IO[bytes]], close: bool = True
|
|
) -> None:
|
|
"""
|
|
Dump the logged data to a file.
|
|
The argument `file` can be either a filename or an open file object
|
|
that requires write access. `close` controls if the file is closed
|
|
before leaving this method (the default behaviour).
|
|
"""
|
|
if self.tracker:
|
|
self.tracker.stop_periodic_snapshots()
|
|
|
|
if isinstance(fdump, str):
|
|
fdump = open(fdump, 'wb')
|
|
pickle.dump(self.index, fdump, protocol=pickle.HIGHEST_PROTOCOL)
|
|
pickle.dump(self.snapshots, fdump, protocol=pickle.HIGHEST_PROTOCOL)
|
|
if close:
|
|
fdump.close()
|
|
|
|
def _init_sort(self) -> None:
|
|
"""
|
|
Prepare the data to be sorted.
|
|
If not yet sorted, import all tracked objects from the tracked index.
|
|
Extend the tracking information by implicit information to make
|
|
sorting easier (DSU pattern).
|
|
"""
|
|
if not self.sorted:
|
|
# Identify the snapshot that tracked the largest amount of memory.
|
|
tmax = None
|
|
maxsize = 0
|
|
for snapshot in self.snapshots:
|
|
if snapshot.tracked_total > maxsize:
|
|
tmax = snapshot.timestamp
|
|
for key in list(self.index.keys()):
|
|
for tobj in self.index[key]:
|
|
tobj.classname = key # type: ignore
|
|
tobj.size = tobj.get_max_size() # type: ignore
|
|
tobj.tsize = tobj.get_size_at_time(tmax) # type: ignore
|
|
self.sorted.extend(self.index[key])
|
|
|
|
def sort_stats(self, *args: str) -> 'Stats':
|
|
"""
|
|
Sort the tracked objects according to the supplied criteria. The
|
|
argument is a string identifying the basis of a sort (example: 'size'
|
|
or 'classname'). When more than one key is provided, then additional
|
|
keys are used as secondary criteria when there is equality in all keys
|
|
selected before them. For example, ``sort_stats('name', 'size')`` will
|
|
sort all the entries according to their class name, and resolve all
|
|
ties (identical class names) by sorting by size. The criteria are
|
|
fields in the tracked object instances. Results are stored in the
|
|
``self.sorted`` list which is used by ``Stats.print_stats()`` and other
|
|
methods. The fields available for sorting are:
|
|
|
|
'classname'
|
|
the name with which the class was registered
|
|
'name'
|
|
the classname
|
|
'birth'
|
|
creation timestamp
|
|
'death'
|
|
destruction timestamp
|
|
'size'
|
|
the maximum measured size of the object
|
|
'tsize'
|
|
the measured size during the largest snapshot
|
|
'repr'
|
|
string representation of the object
|
|
|
|
Note that sorts on size are in descending order (placing most memory
|
|
consuming items first), whereas name, repr, and creation time searches
|
|
are in ascending order (alphabetical).
|
|
|
|
The function returns self to allow calling functions on the result::
|
|
|
|
stats.sort_stats('size').reverse_order().print_stats()
|
|
"""
|
|
|
|
criteria = ('classname', 'tsize', 'birth', 'death',
|
|
'name', 'repr', 'size')
|
|
|
|
if not set(criteria).issuperset(set(args)):
|
|
raise ValueError("Invalid sort criteria")
|
|
|
|
if not args:
|
|
args = criteria
|
|
|
|
def args_to_tuple(obj: 'TrackedObject') -> Tuple[str, ...]:
|
|
keys: List[str] = []
|
|
for attr in args:
|
|
attribute = getattr(obj, attr, '')
|
|
if attr in ('tsize', 'size'):
|
|
attribute = -int(attribute)
|
|
keys.append(attribute)
|
|
return tuple(keys)
|
|
|
|
self._init_sort()
|
|
self.sorted.sort(key=args_to_tuple)
|
|
|
|
return self
|
|
|
|
def reverse_order(self) -> 'Stats':
|
|
"""
|
|
Reverse the order of the tracked instance index `self.sorted`.
|
|
"""
|
|
self._init_sort()
|
|
self.sorted.reverse()
|
|
return self
|
|
|
|
def annotate(self) -> None:
|
|
"""
|
|
Annotate all snapshots with class-based summaries.
|
|
"""
|
|
for snapshot in self.snapshots:
|
|
self.annotate_snapshot(snapshot)
|
|
|
|
def annotate_snapshot(self, snapshot: 'Snapshot'
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
Store additional statistical data in snapshot.
|
|
"""
|
|
if snapshot.classes is not None:
|
|
return snapshot.classes
|
|
|
|
snapshot.classes = {}
|
|
|
|
for classname in list(self.index.keys()):
|
|
total = 0
|
|
active = 0
|
|
merged = Asized(0, 0)
|
|
for tobj in self.index[classname]:
|
|
_merge_objects(snapshot.timestamp, merged, tobj)
|
|
total += tobj.get_size_at_time(snapshot.timestamp)
|
|
if (tobj.birth < snapshot.timestamp and
|
|
(tobj.death is None or
|
|
tobj.death > snapshot.timestamp)):
|
|
active += 1
|
|
|
|
try:
|
|
pct = total * 100.0 / snapshot.total
|
|
except ZeroDivisionError: # pragma: no cover
|
|
pct = 0
|
|
try:
|
|
avg = total / active
|
|
except ZeroDivisionError:
|
|
avg = 0
|
|
|
|
snapshot.classes[classname] = dict(sum=total,
|
|
avg=avg,
|
|
pct=pct,
|
|
active=active)
|
|
snapshot.classes[classname]['merged'] = merged
|
|
|
|
return snapshot.classes
|
|
|
|
@property
|
|
def tracked_classes(self) -> List[str]:
|
|
"""Return a list of all tracked classes occurring in any snapshot."""
|
|
return sorted(list(self.index.keys()))
|
|
|
|
|
|
class ConsoleStats(Stats):
|
|
"""
|
|
Presentation layer for `Stats` to be used in text-based consoles.
|
|
"""
|
|
|
|
def _print_refs(self, refs: Iterable[Asized], total: int,
|
|
prefix: str = ' ', level: int = 1, minsize: int = 0,
|
|
minpct: float = 0.1) -> None:
|
|
"""
|
|
Print individual referents recursively.
|
|
"""
|
|
lrefs = list(refs)
|
|
lrefs.sort(key=lambda x: x.size)
|
|
lrefs.reverse()
|
|
for ref in lrefs:
|
|
if ref.size > minsize and (ref.size * 100.0 / total) > minpct:
|
|
self.stream.write('%-50s %-14s %3d%% [%d]\n' % (
|
|
trunc(prefix + str(ref.name), 50),
|
|
pp(ref.size),
|
|
int(ref.size * 100.0 / total),
|
|
level
|
|
))
|
|
self._print_refs(ref.refs, total, prefix=prefix + ' ',
|
|
level=level + 1)
|
|
|
|
def print_object(self, tobj: 'TrackedObject') -> None:
|
|
"""
|
|
Print the gathered information of object `tobj` in human-readable
|
|
format.
|
|
"""
|
|
if tobj.death:
|
|
self.stream.write('%-32s ( free ) %-35s\n' % (
|
|
trunc(tobj.name, 32, left=True), trunc(tobj.repr, 35)))
|
|
else:
|
|
self.stream.write('%-32s 0x%08x %-35s\n' % (
|
|
trunc(tobj.name, 32, left=True),
|
|
|
|
tobj.id,
|
|
trunc(tobj.repr, 35)
|
|
))
|
|
if tobj.trace:
|
|
self.stream.write(_format_trace(tobj.trace))
|
|
for (timestamp, size) in tobj.snapshots:
|
|
self.stream.write(' %-30s %s\n' % (
|
|
pp_timestamp(timestamp), pp(size.size)
|
|
))
|
|
self._print_refs(size.refs, size.size)
|
|
if tobj.death is not None:
|
|
self.stream.write(' %-30s finalize\n' % (
|
|
pp_timestamp(tobj.death),
|
|
))
|
|
|
|
def print_stats(self, clsname: Optional[str] = None, limit: float = 1.0
|
|
) -> None:
|
|
"""
|
|
Write tracked objects to stdout. The output can be filtered and
|
|
pruned. Only objects are printed whose classname contain the substring
|
|
supplied by the `clsname` argument. The output can be pruned by
|
|
passing a `limit` value.
|
|
|
|
:param clsname: Only print objects whose classname contain the given
|
|
substring.
|
|
:param limit: If `limit` is a float smaller than one, only the supplied
|
|
percentage of the total tracked data is printed. If `limit` is
|
|
bigger than one, this number of tracked objects are printed.
|
|
Tracked objects are first filtered, and then pruned (if specified).
|
|
"""
|
|
if self.tracker:
|
|
self.tracker.stop_periodic_snapshots()
|
|
|
|
if not self.sorted:
|
|
self.sort_stats()
|
|
|
|
_sorted = self.sorted
|
|
|
|
if clsname:
|
|
_sorted = [
|
|
to for to in _sorted
|
|
if clsname in to.classname # type: ignore
|
|
]
|
|
|
|
if limit < 1.0:
|
|
limit = max(1, int(len(self.sorted) * limit))
|
|
_sorted = _sorted[:int(limit)]
|
|
|
|
# Emit per-instance data
|
|
for tobj in _sorted:
|
|
self.print_object(tobj)
|
|
|
|
def print_summary(self) -> None:
|
|
"""
|
|
Print per-class summary for each snapshot.
|
|
"""
|
|
# Emit class summaries for each snapshot
|
|
classlist = self.tracked_classes
|
|
|
|
fobj = self.stream
|
|
|
|
fobj.write('---- SUMMARY ' + '-' * 66 + '\n')
|
|
for snapshot in self.snapshots:
|
|
classes = self.annotate_snapshot(snapshot)
|
|
fobj.write('%-35s %11s %12s %12s %5s\n' % (
|
|
trunc(snapshot.desc, 35),
|
|
'active',
|
|
pp(snapshot.asizeof_total),
|
|
'average',
|
|
'pct'
|
|
))
|
|
for classname in classlist:
|
|
info = classes[classname]
|
|
fobj.write(' %-33s %11d %12s %12s %4d%%\n' % (
|
|
trunc(classname, 33),
|
|
info['active'],
|
|
pp(info['sum']),
|
|
pp(info['avg']),
|
|
info['pct']
|
|
))
|
|
fobj.write('-' * 79 + '\n')
|
|
|
|
|
|
class HtmlStats(Stats):
|
|
"""
|
|
Output the `ClassTracker` statistics as HTML pages and graphs.
|
|
"""
|
|
|
|
style = """<style type="text/css">
|
|
table { width:100%; border:1px solid #000; border-spacing:0px; }
|
|
td, th { border:0px; }
|
|
div { width:200px; padding:10px; background-color:#FFEECC; }
|
|
#nb { border:0px; }
|
|
#tl { margin-top:5mm; margin-bottom:5mm; }
|
|
#p1 { padding-left: 5px; }
|
|
#p2 { padding-left: 50px; }
|
|
#p3 { padding-left: 100px; }
|
|
#p4 { padding-left: 150px; }
|
|
#p5 { padding-left: 200px; }
|
|
#p6 { padding-left: 210px; }
|
|
#p7 { padding-left: 220px; }
|
|
#hl { background-color:#FFFFCC; }
|
|
#r1 { background-color:#BBBBBB; }
|
|
#r2 { background-color:#CCCCCC; }
|
|
#r3 { background-color:#DDDDDD; }
|
|
#r4 { background-color:#EEEEEE; }
|
|
#r5,#r6,#r7 { background-color:#FFFFFF; }
|
|
#num { text-align:right; }
|
|
</style>
|
|
"""
|
|
|
|
nopylab_msg = """<div color="#FFCCCC">Could not generate %s chart!
|
|
Install <a href="http://matplotlib.sourceforge.net/">Matplotlib</a>
|
|
to generate charts.</div>\n"""
|
|
|
|
chart_tag = '<img src="%s">\n'
|
|
header = "<html><head><title>%s</title>%s</head><body>\n"
|
|
tableheader = '<table border="1">\n'
|
|
tablefooter = '</table>\n'
|
|
footer = '</body></html>\n'
|
|
|
|
refrow = """<tr id="r%(level)d">
|
|
<td id="p%(level)d">%(name)s</td>
|
|
<td id="num">%(size)s</td>
|
|
<td id="num">%(pct)3.1f%%</td></tr>"""
|
|
|
|
def _print_refs(self, fobj: IO, refs: Iterable[Asized], total: int,
|
|
level: int = 1, minsize: int = 0, minpct: float = 0.1
|
|
) -> None:
|
|
"""
|
|
Print individual referents recursively.
|
|
"""
|
|
lrefs = list(refs)
|
|
lrefs.sort(key=lambda x: x.size)
|
|
lrefs.reverse()
|
|
if level == 1:
|
|
fobj.write('<table>\n')
|
|
for ref in lrefs:
|
|
if ref.size > minsize and (ref.size * 100.0 / total) > minpct:
|
|
data = dict(level=level,
|
|
name=trunc(str(ref.name), 128),
|
|
size=pp(ref.size),
|
|
pct=ref.size * 100.0 / total)
|
|
fobj.write(self.refrow % data)
|
|
self._print_refs(fobj, ref.refs, total, level=level + 1)
|
|
if level == 1:
|
|
fobj.write("</table>\n")
|
|
|
|
class_summary = """<p>%(cnt)d instances of %(cls)s were registered. The
|
|
average size is %(avg)s, the minimal size is %(min)s, the maximum size
|
|
is %(max)s.</p>\n"""
|
|
class_snapshot = '''<h3>Snapshot: %(name)s, %(total)s occupied by instances
|
|
of class %(cls)s</h3>\n'''
|
|
|
|
def print_class_details(self, fname: str, classname: str) -> None:
|
|
"""
|
|
Print detailed statistics and instances for the class `classname`. All
|
|
data will be written to the file `fname`.
|
|
"""
|
|
fobj = open(fname, "w")
|
|
fobj.write(self.header % (classname, self.style))
|
|
|
|
fobj.write("<h1>%s</h1>\n" % (classname))
|
|
|
|
sizes = [tobj.get_max_size() for tobj in self.index[classname]]
|
|
total = 0
|
|
for s in sizes:
|
|
total += s
|
|
data = {'cnt': len(self.index[classname]), 'cls': classname}
|
|
data['avg'] = pp(total / len(sizes))
|
|
data['max'] = pp(max(sizes))
|
|
data['min'] = pp(min(sizes))
|
|
fobj.write(self.class_summary % data)
|
|
|
|
fobj.write(self.charts[classname])
|
|
|
|
fobj.write("<h2>Coalesced Referents per Snapshot</h2>\n")
|
|
for snapshot in self.snapshots:
|
|
if snapshot.classes and classname in snapshot.classes:
|
|
merged = snapshot.classes[classname]['merged']
|
|
fobj.write(self.class_snapshot % {
|
|
'name': snapshot.desc,
|
|
'cls': classname,
|
|
'total': pp(merged.size),
|
|
})
|
|
if merged.refs:
|
|
self._print_refs(fobj, merged.refs, merged.size)
|
|
else:
|
|
fobj.write('<p>No per-referent sizes recorded.</p>\n')
|
|
|
|
fobj.write("<h2>Instances</h2>\n")
|
|
for tobj in self.index[classname]:
|
|
fobj.write('<table id="tl" width="100%" rules="rows">\n')
|
|
fobj.write('<tr><td id="hl" width="140px">Instance</td>' +
|
|
'<td id="hl">%s at 0x%08x</td></tr>\n' %
|
|
(tobj.name, tobj.id))
|
|
if tobj.repr:
|
|
fobj.write("<tr><td>Representation</td>" +
|
|
"<td>%s </td></tr>\n" % tobj.repr)
|
|
fobj.write("<tr><td>Lifetime</td><td>%s - %s</td></tr>\n" %
|
|
(pp_timestamp(tobj.birth), pp_timestamp(tobj.death)))
|
|
if tobj.trace:
|
|
trace = "<pre>%s</pre>" % (_format_trace(tobj.trace))
|
|
fobj.write("<tr><td>Instantiation</td><td>%s</td></tr>\n" %
|
|
trace)
|
|
for (timestamp, size) in tobj.snapshots:
|
|
fobj.write("<tr><td>%s</td>" % pp_timestamp(timestamp))
|
|
if not size.refs:
|
|
fobj.write("<td>%s</td></tr>\n" % pp(size.size))
|
|
else:
|
|
fobj.write("<td>%s" % pp(size.size))
|
|
self._print_refs(fobj, size.refs, size.size)
|
|
fobj.write("</td></tr>\n")
|
|
fobj.write("</table>\n")
|
|
|
|
fobj.write(self.footer)
|
|
fobj.close()
|
|
|
|
snapshot_cls_header = """<tr>
|
|
<th id="hl">Class</th>
|
|
<th id="hl" align="right">Instance #</th>
|
|
<th id="hl" align="right">Total</th>
|
|
<th id="hl" align="right">Average size</th>
|
|
<th id="hl" align="right">Share</th></tr>\n"""
|
|
|
|
snapshot_cls = """<tr>
|
|
<td>%(cls)s</td>
|
|
<td align="right">%(active)d</td>
|
|
<td align="right">%(sum)s</td>
|
|
<td align="right">%(avg)s</td>
|
|
<td align="right">%(pct)3.2f%%</td></tr>\n"""
|
|
|
|
snapshot_summary = """<p>Total virtual memory assigned to the program
|
|
at that time was %(sys)s, which includes %(overhead)s profiling
|
|
overhead. The ClassTracker tracked %(tracked)s in total. The measurable
|
|
objects including code objects but excluding overhead have a total size
|
|
of %(asizeof)s.</p>\n"""
|
|
|
|
def relative_path(self, filepath: str, basepath: Optional[str] = None
|
|
) -> str:
|
|
"""
|
|
Convert the filepath path to a relative path against basepath. By
|
|
default basepath is self.basedir.
|
|
"""
|
|
if basepath is None:
|
|
basepath = self.basedir
|
|
if not basepath:
|
|
return filepath
|
|
if filepath.startswith(basepath):
|
|
filepath = filepath[len(basepath):]
|
|
if filepath and filepath[0] == os.sep:
|
|
filepath = filepath[1:]
|
|
return filepath
|
|
|
|
def create_title_page(self, filename: str, title: str = '') -> None:
|
|
"""
|
|
Output the title page.
|
|
"""
|
|
fobj = open(filename, "w")
|
|
fobj.write(self.header % (title, self.style))
|
|
|
|
fobj.write("<h1>%s</h1>\n" % title)
|
|
fobj.write("<h2>Memory distribution over time</h2>\n")
|
|
fobj.write(self.charts['snapshots'])
|
|
|
|
fobj.write("<h2>Snapshots statistics</h2>\n")
|
|
fobj.write('<table id="nb">\n')
|
|
|
|
classlist = list(self.index.keys())
|
|
classlist.sort()
|
|
|
|
for snapshot in self.snapshots:
|
|
fobj.write('<tr><td>\n')
|
|
fobj.write('<table id="tl" rules="rows">\n')
|
|
fobj.write("<h3>%s snapshot at %s</h3>\n" % (
|
|
snapshot.desc or 'Untitled',
|
|
pp_timestamp(snapshot.timestamp)
|
|
))
|
|
|
|
data = {}
|
|
data['sys'] = pp(snapshot.system_total.vsz)
|
|
data['tracked'] = pp(snapshot.tracked_total)
|
|
data['asizeof'] = pp(snapshot.asizeof_total)
|
|
data['overhead'] = pp(getattr(snapshot, 'overhead', 0))
|
|
|
|
fobj.write(self.snapshot_summary % data)
|
|
|
|
if snapshot.tracked_total:
|
|
fobj.write(self.snapshot_cls_header)
|
|
for classname in classlist:
|
|
if snapshot.classes:
|
|
info = snapshot.classes[classname].copy()
|
|
path = self.relative_path(self.links[classname])
|
|
info['cls'] = '<a href="%s">%s</a>' % (path, classname)
|
|
info['sum'] = pp(info['sum'])
|
|
info['avg'] = pp(info['avg'])
|
|
fobj.write(self.snapshot_cls % info)
|
|
fobj.write('</table>')
|
|
fobj.write('</td><td>\n')
|
|
if snapshot.tracked_total:
|
|
fobj.write(self.charts[snapshot])
|
|
fobj.write('</td></tr>\n')
|
|
|
|
fobj.write("</table>\n")
|
|
fobj.write(self.footer)
|
|
fobj.close()
|
|
|
|
def create_lifetime_chart(self, classname: str, filename: str = '') -> str:
|
|
"""
|
|
Create chart that depicts the lifetime of the instance registered with
|
|
`classname`. The output is written to `filename`.
|
|
"""
|
|
try:
|
|
from pylab import figure, title, xlabel, ylabel, plot, savefig
|
|
except ImportError:
|
|
return HtmlStats.nopylab_msg % (classname + " lifetime")
|
|
|
|
cnt = []
|
|
for tobj in self.index[classname]:
|
|
cnt.append([tobj.birth, 1])
|
|
if tobj.death:
|
|
cnt.append([tobj.death, -1])
|
|
cnt.sort()
|
|
for i in range(1, len(cnt)):
|
|
cnt[i][1] += cnt[i - 1][1]
|
|
|
|
x = [t for [t, c] in cnt]
|
|
y = [c for [t, c] in cnt]
|
|
|
|
figure()
|
|
xlabel("Execution time [s]")
|
|
ylabel("Instance #")
|
|
title("%s instances" % classname)
|
|
plot(x, y, 'o')
|
|
savefig(filename)
|
|
|
|
return self.chart_tag % (os.path.basename(filename))
|
|
|
|
def create_snapshot_chart(self, filename: str = '') -> str:
|
|
"""
|
|
Create chart that depicts the memory allocation over time apportioned
|
|
to the tracked classes.
|
|
"""
|
|
try:
|
|
from pylab import (figure, title, xlabel, ylabel, plot, fill,
|
|
legend, savefig)
|
|
import matplotlib.mlab as mlab
|
|
except ImportError:
|
|
return self.nopylab_msg % ("memory allocation")
|
|
|
|
classlist = self.tracked_classes
|
|
|
|
times = [snapshot.timestamp for snapshot in self.snapshots]
|
|
base = [0.0] * len(self.snapshots)
|
|
poly_labels = []
|
|
polys = []
|
|
for cn in classlist:
|
|
pct = [snapshot.classes[cn]['pct'] for snapshot in self.snapshots
|
|
if snapshot.classes is not None]
|
|
if pct and max(pct) > 3.0:
|
|
sz = [float(fp.classes[cn]['sum']) / (1024 * 1024)
|
|
for fp in self.snapshots
|
|
if fp.classes is not None]
|
|
sz = [sx + sy for sx, sy in zip(base, sz)]
|
|
xp, yp = mlab.poly_between(times, base, sz)
|
|
polys.append(((xp, yp), {'label': cn}))
|
|
poly_labels.append(cn)
|
|
base = sz
|
|
|
|
figure()
|
|
title("Snapshot Memory")
|
|
xlabel("Execution Time [s]")
|
|
ylabel("Virtual Memory [MiB]")
|
|
|
|
sizes = [float(fp.asizeof_total) / (1024 * 1024)
|
|
for fp in self.snapshots]
|
|
plot(times, sizes, 'r--', label='Total')
|
|
sizes = [float(fp.tracked_total) / (1024 * 1024)
|
|
for fp in self.snapshots]
|
|
plot(times, sizes, 'b--', label='Tracked total')
|
|
|
|
for (args, kwds) in polys:
|
|
fill(*args, **kwds)
|
|
legend(loc=2)
|
|
savefig(filename)
|
|
|
|
return self.chart_tag % (self.relative_path(filename))
|
|
|
|
def create_pie_chart(self, snapshot: 'Snapshot', filename: str = '') -> str:
|
|
"""
|
|
Create a pie chart that depicts the distribution of the allocated
|
|
memory for a given `snapshot`. The chart is saved to `filename`.
|
|
"""
|
|
try:
|
|
from pylab import figure, title, pie, axes, savefig
|
|
from pylab import sum as pylab_sum
|
|
except ImportError:
|
|
return self.nopylab_msg % ("pie_chart")
|
|
|
|
# Don't bother illustrating a pie without pieces.
|
|
if not snapshot.tracked_total or snapshot.classes is None:
|
|
return ''
|
|
|
|
classlist = []
|
|
sizelist = []
|
|
for k, v in list(snapshot.classes.items()):
|
|
if v['pct'] > 3.0:
|
|
classlist.append(k)
|
|
sizelist.append(v['sum'])
|
|
sizelist.insert(0, snapshot.asizeof_total - pylab_sum(sizelist))
|
|
classlist.insert(0, 'Other')
|
|
|
|
title("Snapshot (%s) Memory Distribution" % (snapshot.desc))
|
|
figure(figsize=(8, 8))
|
|
axes([0.1, 0.1, 0.8, 0.8])
|
|
pie(sizelist, labels=classlist)
|
|
savefig(filename, dpi=50)
|
|
|
|
return self.chart_tag % (self.relative_path(filename))
|
|
|
|
def create_html(self, fname: str, title: str = "ClassTracker Statistics"
|
|
) -> None:
|
|
"""
|
|
Create HTML page `fname` and additional files in a directory derived
|
|
from `fname`.
|
|
"""
|
|
# Create a folder to store the charts and additional HTML files.
|
|
self.basedir = os.path.dirname(os.path.abspath(fname))
|
|
self.filesdir = os.path.splitext(fname)[0] + '_files'
|
|
if not os.path.isdir(self.filesdir):
|
|
os.mkdir(self.filesdir)
|
|
self.filesdir = os.path.abspath(self.filesdir)
|
|
self.links = {} # type: Dict[str, str]
|
|
|
|
# Annotate all snapshots in advance
|
|
self.annotate()
|
|
|
|
# Create charts. The tags to show the images are returned and stored in
|
|
# the self.charts dictionary. This allows to return alternative text if
|
|
# the chart creation framework is not available.
|
|
self.charts = {} # type: Dict[Union[str, Snapshot], str]
|
|
fn = os.path.join(self.filesdir, 'timespace.png')
|
|
self.charts['snapshots'] = self.create_snapshot_chart(fn)
|
|
|
|
for fp, idx in zip(self.snapshots, list(range(len(self.snapshots)))):
|
|
fn = os.path.join(self.filesdir, 'fp%d.png' % (idx))
|
|
self.charts[fp] = self.create_pie_chart(fp, fn)
|
|
|
|
for cn in list(self.index.keys()):
|
|
fn = os.path.join(self.filesdir, cn.replace('.', '_') + '-lt.png')
|
|
self.charts[cn] = self.create_lifetime_chart(cn, fn)
|
|
|
|
# Create HTML pages first for each class and then the index page.
|
|
for cn in list(self.index.keys()):
|
|
fn = os.path.join(self.filesdir, cn.replace('.', '_') + '.html')
|
|
self.links[cn] = fn
|
|
self.print_class_details(fn, cn)
|
|
|
|
self.create_title_page(fname, title=title)
|