stats: Add support for listing available formats
[gem5.git] / src / python / m5 / stats / __init__.py
index d9174d387a9453329533f042f2f40cef6749461b..77ed5e8deeb5f073c1118428c0a8e26e68ab2599 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2017, 2019 Arm Limited
+# Copyright (c) 2017-2019 ARM Limited
 # All rights reserved.
 #
 # The license below extends only to copyright in the software and shall
@@ -40,6 +40,9 @@
 # Authors: Nathan Binkert
 #          Andreas Sandberg
 
+from __future__ import print_function
+from __future__ import absolute_import
+
 import m5
 
 import _m5.stats
@@ -52,7 +55,15 @@ from _m5.stats import periodicStatDump
 
 outputList = []
 
-def _url_factory(func):
+# 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
@@ -61,6 +72,13 @@ def _url_factory(func):
     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)
@@ -69,43 +87,52 @@ def _url_factory(func):
 
     from functools import wraps
 
-    @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")
-
-    return wrapper
-
-@_url_factory
+    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):
     """Output stats in text format.
 
@@ -113,18 +140,51 @@ def _textFactory(fn, desc=True):
     description. The description is enabled by default, but can be
     disabled by setting the desc parameter to False.
 
-    Example: text://stats.txt?desc=False
+    Parameters:
+      * desc (bool): Output stat descriptions (default: True)
+
+    Example:
+      text://stats.txt?desc=False
 
     """
 
     return _m5.stats.initText(fn, desc)
 
-factories = {
-    # Default to the text factory if we're given a naked path
-    "" : _textFactory,
-    "file" : _textFactory,
-    "text" : _textFactory,
-}
+@_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
@@ -150,24 +210,46 @@ def addStatVisitor(url):
     try:
         factory = factories[parsed.scheme]
     except KeyError:
-        fatal("Illegal stat file type specified.")
+        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():
     _m5.stats.initSimStats()
     _m5.stats.registerPythonStatsHandlers()
 
-def _visit_groups(root, visitor):
+def _visit_groups(visitor, root=None):
+    if root is None:
+        root = Root.getInstance()
     for group in root.getStatGroups().values():
         visitor(group)
-        _visit_groups(group, visitor)
+        _visit_groups(visitor, root=group)
 
-def _visit_stats(root, visitor):
+def _visit_stats(visitor, root=None):
     def for_each_stat(g):
         for stat in g.getStats():
             visitor(g, stat)
-    _visit_groups(root, for_each_stat)
+    _visit_groups(for_each_stat, root=root)
 
 def _bindStatHierarchy(root):
     def _bind_obj(name, obj):
@@ -212,8 +294,8 @@ def enable():
 
 
     # New stats
-    _visit_stats(Root.getInstance(), check_stat)
-    _visit_stats(Root.getInstance(), lambda g, s: s.enable())
+    _visit_stats(check_stat)
+    _visit_stats(lambda g, s: s.enable())
 
     _m5.stats.enable();
 
@@ -226,14 +308,13 @@ def prepare():
         stat.prepare()
 
     # New stats
-    _visit_stats(Root.getInstance(), lambda g, s: s.prepare())
-
-lastDump = 0
+    _visit_stats(lambda g, s: s.prepare())
 
-def _dump_to_visitor(visitor):
+def _dump_to_visitor(visitor, root=None):
     # Legacy stats
-    for stat in stats_list:
-        stat.visit(visitor)
+    if root is None:
+        for stat in stats_list:
+            stat.visit(visitor)
 
     # New stats
     def dump_group(group):
@@ -245,28 +326,39 @@ def _dump_to_visitor(visitor):
             dump_group(g)
             visitor.endGroup()
 
-    dump_group(Root.getInstance())
+    if root is not None:
+        for p in root.path_list():
+            visitor.beginGroup(p)
+    dump_group(root if root is not None else Root.getInstance())
+    if root is not None:
+        for p in reversed(root.path_list()):
+            visitor.endGroup()
 
+lastDump = 0
 
-def dump():
+def dump(root=None):
     '''Dump all statistics data to the registered outputs'''
 
-    curTick = m5.curTick()
-
+    now = m5.curTick()
     global lastDump
-    assert lastDump <= curTick
-    if lastDump == curTick:
-        return
-    lastDump = curTick
+    assert lastDump <= now
+    new_dump = lastDump != now
+    lastDump = now
 
-    _m5.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 root is None:
+        return
 
-    prepare()
+    # Only prepare stats the first time we dump them in the same tick.
+    if new_dump:
+        _m5.stats.processDumpQueue()
+        prepare()
 
     for output in outputList:
         if output.valid():
             output.begin()
-            _dump_to_visitor(output)
+            _dump_to_visitor(output, root=root)
             output.end()
 
 def reset():