summaryrefslogtreecommitdiffstats
path: root/venv/lib/python3.9/site-packages/pympler/classtracker_stats.py
blob: 191f1fbaff23b022090006b9f3514401533fe0ef (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
"""
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&nbsp;</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)