stats: Add support for listing available formats

Add a command line option to list available stat formats and their
documentation.

Change-Id: I7f5f2272d9b0176639f59f2efedb9cab2f7da5b9
Signed-off-by: Andreas Sandberg <andreas.sandberg@arm.com>
Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/19670
Reviewed-by: Jason Lowe-Power <jason@lowepower.com>
Tested-by: kokoro <noreply+kokoro@google.com>
diff --git a/src/python/m5/main.py b/src/python/m5/main.py
index fd1f6a0..295108c 100644
--- a/src/python/m5/main.py
+++ b/src/python/m5/main.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 ARM Limited
+# Copyright (c) 2016, 2019 Arm Limited
 # All rights reserved.
 #
 # The license below extends only to copyright in the software and shall
@@ -53,6 +53,18 @@
 brief_copyright=\
     "gem5 is copyrighted software; use the --copyright option for details."
 
+def _stats_help(option, opt, value, parser):
+    import m5
+    print("A stat file can either be specified as a URI or a plain")
+    print("path. When specified as a path, gem5 uses the default text ")
+    print("format.")
+    print()
+    print("The following stat formats are supported:")
+    print()
+    m5.stats.printStatVisitorTypes()
+    sys.exit(0)
+
+
 def parse_options():
     from . import config
     from .options import OptionParser
@@ -105,6 +117,9 @@
     group("Statistics Options")
     option("--stats-file", metavar="FILE", default="stats.txt",
         help="Sets the output file for statistics [Default: %default]")
+    option("--stats-help",
+           action="callback", callback=_stats_help,
+           help="Display documentation for available stat visitors")
 
     # Configuration Options
     group("Configuration Options")
diff --git a/src/python/m5/stats/__init__.py b/src/python/m5/stats/__init__.py
index bca311d..77ed5e8 100644
--- a/src/python/m5/stats/__init__.py
+++ b/src/python/m5/stats/__init__.py
@@ -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 @@
 
 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 @@
     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 @@
 
     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
+    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)
+            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]))
+            # 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() ])
+            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")
+            try:
+                return func("%s%s" % (url.netloc, url.path), **kwargs)
+            except TypeError:
+                fatal("Illegal stat visitor parameter specified")
 
-    return wrapper
+        all_factories.append((wrapper, schemes, enable))
+        for scheme in schemes:
+            assert scheme not in factories
+            factories[scheme] = wrapper if enable else None
+        return wrapper
 
-@_url_factory
+    return decorator
+
+@_url_factory([ None, "", "text", "file", ])
 def _textFactory(fn, desc=True):
     """Output stats in text format.
 
@@ -113,13 +140,17 @@
     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)
 
-@_url_factory
+@_url_factory([ "h5", ], enable=hasattr(_m5.stats, "initHDF5"))
 def _hdf5Factory(fn, chunking=10, desc=True, formulas=True):
     """Output stats in HDF5 format.
 
@@ -153,19 +184,7 @@
 
     """
 
-    if hasattr(_m5.stats, "initHDF5"):
-        return _m5.stats.initHDF5(fn, chunking, desc, formulas)
-    else:
-        fatal("HDF5 support not enabled at compile time")
-
-factories = {
-    # Default to the text factory if we're given a naked path
-    "" : _textFactory,
-    "file" : _textFactory,
-    "text" : _textFactory,
-    "h5" : _hdf5Factory,
-}
-
+    return _m5.stats.initHDF5(fn, chunking, desc, formulas)
 
 def addStatVisitor(url):
     """Add a stat visitor specified using a URL string
@@ -191,10 +210,30 @@
     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()