stats: add --stats-root option to dump only under some SimObjects
[gem5.git] / src / python / m5 / stats / __init__.py
index 770749bf0d033acf3620b22b4ad74d3bfe3eef75..6c4a42cb8b4eb1dbc98ac08fea8bfa878dc79398 100644 (file)
@@ -1,3 +1,15 @@
+# Copyright (c) 2017-2020 ARM Limited
+# All rights reserved.
+#
+# The license below extends only to copyright in the software and shall
+# not be construed as granting a license to any other intellectual
+# property including but not limited to intellectual property relating
+# to a hardware implementation of the functionality of the software
+# licensed hereunder.  You may use the software subject to the license
+# terms below provided that you ensure that this notice is replicated
+# unmodified and in its entirety in all distributions of the software,
+# modified or unmodified, in source code or in binary form.
+#
 # Copyright (c) 2007 The Regents of The University of Michigan
 # Copyright (c) 2010 The Hewlett-Packard Development Company
 # All rights reserved.
 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# Authors: Nathan Binkert
+
+from __future__ import print_function
+from __future__ import absolute_import
 
 import m5
 
-from m5 import internal
-from m5.internal.stats import schedStatEvent as schedEvent
+import _m5.stats
 from m5.objects import Root
+from m5.params import isNullPointer
 from m5.util import attrdict, fatal
 
+# Stat exports
+from _m5.stats import schedStatEvent as schedEvent
+from _m5.stats import periodicStatDump
+
 outputList = []
-def initText(filename, desc=True):
-    output = internal.stats.initText(filename, desc)
-    outputList.append(output)
+
+# Dictionary of stat visitor factories populated by the _url_factory
+# visitor.
+factories = { }
+
+# List of all factories. Contains tuples of (factory, schemes,
+# enabled).
+all_factories = []
+
+def _url_factory(schemes, enable=True):
+    """Wrap a plain Python function with URL parsing helpers
+
+    Wrap a plain Python function f(fn, **kwargs) to expect a URL that
+    has been split using urlparse.urlsplit. First positional argument
+    is assumed to be a filename, this is created as the concatenation
+    of the netloc (~hostname) and path in the parsed URL. Keyword
+    arguments are derived from the query values in the URL.
+
+    Arguments:
+        schemes: A list of URL schemes to use for this function.
+
+    Keyword arguments:
+        enable: Enable/disable this factory. Typically used when the
+                presence of a function depends on some runtime property.
+
+    For example:
+        wrapped_f(urlparse.urlsplit("text://stats.txt?desc=False")) ->
+        f("stats.txt", desc=False)
+
+    """
+
+    from functools import wraps
+
+    def decorator(func):
+        @wraps(func)
+        def wrapper(url):
+            try:
+                from urllib.parse import parse_qs
+            except ImportError:
+                # Python 2 fallback
+                from urlparse import parse_qs
+            from ast import literal_eval
+
+            qs = parse_qs(url.query, keep_blank_values=True)
+
+            # parse_qs returns a list of values for each parameter. Only
+            # use the last value since kwargs don't allow multiple values
+            # per parameter. Use literal_eval to transform string param
+            # values into proper Python types.
+            def parse_value(key, values):
+                if len(values) == 0 or (len(values) == 1 and not values[0]):
+                    fatal("%s: '%s' doesn't have a value." % (
+                        url.geturl(), key))
+                elif len(values) > 1:
+                    fatal("%s: '%s' has multiple values." % (
+                        url.geturl(), key))
+                else:
+                    try:
+                        return key, literal_eval(values[0])
+                    except ValueError:
+                        fatal("%s: %s isn't a valid Python literal" \
+                              % (url.geturl(), values[0]))
+
+            kwargs = dict([ parse_value(k, v) for k, v in qs.items() ])
+
+            try:
+                return func("%s%s" % (url.netloc, url.path), **kwargs)
+            except TypeError:
+                fatal("Illegal stat visitor parameter specified")
+
+        all_factories.append((wrapper, schemes, enable))
+        for scheme in schemes:
+            assert scheme not in factories
+            factories[scheme] = wrapper if enable else None
+        return wrapper
+
+    return decorator
+
+@_url_factory([ None, "", "text", "file", ])
+def _textFactory(fn, desc=True, spaces=True):
+    """Output stats in text format.
+
+    Text stat files contain one stat per line with an optional
+    description. The description is enabled by default, but can be
+    disabled by setting the desc parameter to False.
+
+    Parameters:
+      * desc (bool): Output stat descriptions (default: True)
+      * spaces (bool): Output alignment spaces (default: True)
+
+    Example:
+      text://stats.txt?desc=False;spaces=False
+
+    """
+
+    return _m5.stats.initText(fn, desc, spaces)
+
+@_url_factory([ "h5", ], enable=hasattr(_m5.stats, "initHDF5"))
+def _hdf5Factory(fn, chunking=10, desc=True, formulas=True):
+    """Output stats in HDF5 format.
+
+    The HDF5 file format is a structured binary file format. It has
+    the multiple benefits over traditional text stat files:
+
+      * Efficient storage of time series (multiple stat dumps)
+      * Fast lookup of stats
+      * Plenty of existing tooling (e.g., Python libraries and graphical
+        viewers)
+      * File format can be used to store frame buffers together with
+        normal stats.
+
+    There are some drawbacks compared to the default text format:
+      * Large startup cost (single stat dump larger than text equivalent)
+      * Stat dumps are slower than text
+
+
+    Known limitations:
+      * Distributions and histograms currently unsupported.
+      * No support for forking.
+
+
+    Parameters:
+      * chunking (unsigned): Number of time steps to pre-allocate (default: 10)
+      * desc (bool): Output stat descriptions (default: True)
+      * formulas (bool): Output derived stats (default: True)
+
+    Example:
+      h5://stats.h5?desc=False;chunking=100;formulas=False
+
+    """
+
+    return _m5.stats.initHDF5(fn, chunking, desc, formulas)
+
+def addStatVisitor(url):
+    """Add a stat visitor specified using a URL string
+
+    Stat visitors are specified using URLs on the following format:
+    format://path[?param=value[;param=value]]
+
+    The available formats are listed in the factories list. Factories
+    are called with the path as the first positional parameter and the
+    parameters are keyword arguments. Parameter values must be valid
+    Python literals.
+
+    """
+
+    try:
+        from urllib.parse import urlsplit
+    except ImportError:
+        # Python 2 fallback
+        from urlparse import urlsplit
+
+    parsed = urlsplit(url)
+
+    try:
+        factory = factories[parsed.scheme]
+    except KeyError:
+        fatal("Illegal stat file type '%s' specified." % parsed.scheme)
+
+    if factory is None:
+        fatal("Stat type '%s' disabled at compile time" % parsed.scheme)
+
+    outputList.append(factory(parsed))
+
+def printStatVisitorTypes():
+    """List available stat visitors and their documentation"""
+
+    import inspect
+
+    def print_doc(doc):
+        for line in doc.splitlines():
+            print("| %s" % line)
+        print()
+
+    enabled_visitors = [ x for x in all_factories if x[2] ]
+    for factory, schemes, _ in enabled_visitors:
+        print("%s:" % ", ".join(filter(lambda x: x is not None, schemes)))
+
+        # Try to extract the factory doc string
+        print_doc(inspect.getdoc(factory))
 
 def initSimStats():
-    internal.stats.initSimStats()
+    _m5.stats.initSimStats()
+    _m5.stats.registerPythonStatsHandlers()
+
+def _visit_groups(visitor, root=None):
+    if root is None:
+        root = Root.getInstance()
+    for group in root.getStatGroups().values():
+        visitor(group)
+        _visit_groups(visitor, root=group)
+
+def _visit_stats(visitor, root=None):
+    def for_each_stat(g):
+        for stat in g.getStats():
+            visitor(g, stat)
+    _visit_groups(for_each_stat, root=root)
+
+def _bindStatHierarchy(root):
+    def _bind_obj(name, obj):
+        if isNullPointer(obj):
+            return
+        if m5.SimObject.isSimObjectVector(obj):
+            if len(obj) == 1:
+                _bind_obj(name, obj[0])
+            else:
+                for idx, obj in enumerate(obj):
+                    _bind_obj("{}{}".format(name, idx), obj)
+        else:
+            # We need this check because not all obj.getCCObject() is an
+            # instance of Stat::Group. For example, sc_core::sc_module, the C++
+            # class of SystemC_ScModule, is not a subclass of Stat::Group. So
+            # it will cause a type error if obj is a SystemC_ScModule when
+            # calling addStatGroup().
+            if isinstance(obj.getCCObject(), _m5.stats.Group):
+                parent = root
+                while parent:
+                    if hasattr(parent, 'addStatGroup'):
+                        parent.addStatGroup(name, obj.getCCObject())
+                        break
+                    parent = parent.get_parent();
+
+            _bindStatHierarchy(obj)
+
+    for name, obj in root._children.items():
+        _bind_obj(name, obj)
 
 names = []
 stats_dict = {}
 stats_list = []
-raw_stats_list = []
 def enable():
     '''Enable the statistics package.  Before the statistics package is
     enabled, all statistics must be created and initialized and once
     the package is enabled, no more statistics can be created.'''
-    __dynamic_cast = []
-    for k, v in internal.stats.__dict__.iteritems():
-        if k.startswith('dynamic_'):
-            __dynamic_cast.append(v)
-
-    for stat in internal.stats.statsList():
-        for cast in __dynamic_cast:
-            val = cast(stat)
-            if val is not None:
-                stats_list.append(val)
-                raw_stats_list.append(val)
-                break
-        else:
-            fatal("unknown stat type %s", stat)
 
-    for stat in stats_list:
+    def check_stat(group, stat):
         if not stat.check() or not stat.baseCheck():
             fatal("statistic '%s' (%d) was not properly initialized " \
                   "by a regStats() function\n", stat.name, stat.id)
@@ -73,46 +295,100 @@ def enable():
         if not (stat.flags & flags.display):
             stat.name = "__Stat%06d" % stat.id
 
-    def less(stat1, stat2):
-        v1 = stat1.name.split('.')
-        v2 = stat2.name.split('.')
-        return v1 < v2
 
-    stats_list.sort(less)
+    # Legacy stat
+    global stats_list
+    stats_list = list(_m5.stats.statsList())
+
+    for stat in stats_list:
+        check_stat(None, stat)
+
+    stats_list.sort(key=lambda s: s.name.split('.'))
     for stat in stats_list:
         stats_dict[stat.name] = stat
         stat.enable()
 
-    internal.stats.enable();
+
+    # New stats
+    _visit_stats(check_stat)
+    _visit_stats(lambda g, s: s.enable())
+
+    _m5.stats.enable();
 
 def prepare():
     '''Prepare all stats for data access.  This must be done before
     dumping and serialization.'''
 
+    # Legacy stats
     for stat in stats_list:
         stat.prepare()
 
+    # New stats
+    _visit_stats(lambda g, s: s.prepare())
+
+def _dump_to_visitor(visitor, roots=None):
+    # New stats
+    def dump_group(group):
+        for stat in group.getStats():
+            stat.visit(visitor)
+        for n, g in group.getStatGroups().items():
+            visitor.beginGroup(n)
+            dump_group(g)
+            visitor.endGroup()
+
+    if roots:
+        # New stats from selected subroots.
+        for root in roots:
+            for p in root.path_list():
+                visitor.beginGroup(p)
+            dump_group(root)
+            for p in reversed(root.path_list()):
+                visitor.endGroup()
+    else:
+        # Legacy stats
+        for stat in stats_list:
+            stat.visit(visitor)
+
+        # New stats starting from root.
+        dump_group(Root.getInstance())
+
 lastDump = 0
-def dump():
+# List[SimObject].
+global_dump_roots = []
+
+def dump(roots=None):
     '''Dump all statistics data to the registered outputs'''
 
-    curTick = m5.curTick()
+    all_roots = []
+    if roots is not None:
+        all_roots.extend(roots)
+    global global_dump_roots
+    all_roots.extend(global_dump_roots)
 
+    now = m5.curTick()
     global lastDump
-    assert lastDump <= curTick
-    if lastDump == curTick:
-        return
-    lastDump = curTick
+    assert lastDump <= now
+    new_dump = lastDump != now
+    lastDump = now
 
-    internal.stats.processDumpQueue()
+    # Don't allow multiple global stat dumps in the same tick. It's
+    # still possible to dump a multiple sub-trees.
+    if not new_dump and not all_roots:
+        return
 
-    prepare()
+    # Only prepare stats the first time we dump them in the same tick.
+    if new_dump:
+        _m5.stats.processDumpQueue()
+        # Notify new-style stats group that we are about to dump stats.
+        sim_root = Root.getInstance()
+        if sim_root:
+            sim_root.preDumpStats();
+        prepare()
 
     for output in outputList:
         if output.valid():
             output.begin()
-            for stat in stats_list:
-                output.visit(stat)
+            _dump_to_visitor(output, roots=all_roots)
             output.end()
 
 def reset():
@@ -121,13 +397,13 @@ def reset():
     # call reset stats on all SimObjects
     root = Root.getInstance()
     if root:
-        for obj in root.descendants(): obj.resetStats()
+        root.resetStats()
 
-    # call any other registered stats reset callbacks
+    # call any other registered legacy stats reset callbacks
     for stat in stats_list:
         stat.reset()
 
-    internal.stats.processResetQueue()
+    _m5.stats.processResetQueue()
 
 flags = attrdict({
     'none'    : 0x0000,