diff --git a/doc/index.rst b/doc/index.rst index 459a16e5..4b6c1df4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -36,8 +36,9 @@ Features of the ``pyperf`` module: collect them. * ``--track-memory`` and ``--tracemalloc`` :ref:`options ` to track the memory usage of a benchmark. +* ``--track-energy`` :ref:`option ` to track the energy consumption of a benchmark. Based on the `Linux power capping framework `_, but simple to extend to other energy APIs. * :ref:`JSON format ` to store benchmark results. -* Support multiple units: seconds, bytes and integer. +* Support multiple units: seconds, bytes, integer and Joules. Quick Links: diff --git a/doc/runner.rst b/doc/runner.rst index 6aa480b3..648d2408 100644 --- a/doc/runner.rst +++ b/doc/runner.rst @@ -100,6 +100,7 @@ Option:: --no-locale --track-memory --tracemalloc + --track-energy * ``--python=PYTHON``: Python executable. By default, use the running Python (``sys.executable``). The Python executable must have the ``pyperf`` module @@ -150,6 +151,12 @@ Option:: ``/proc/self/smaps``. On Windows, get ``PeakPagefileUsage`` of ``GetProcessMemoryInfo()`` (of the current process): the peak value of the Commit Charge during the lifetime of this process. +* ``--track-energy``: get the energy consumption. Implementation based on the `Linux + power capping framework `_. + User needs to export 2 environment variables prior to invoking ``pyperf`` with this option; ``ENFILE``, + the absolute path to a file containing the energy consumed by the component of interest (e.g. DRAM), and + ``READEN``, the absolute path to a shared C library containing a function ``readen`` for probing the aforementioned + file. A sample implementation is provided in ``pyperf/read_file.c``. Internal usage only diff --git a/pyperf/_formatter.py b/pyperf/_formatter.py index 767faf2d..a5b752c8 100644 --- a/pyperf/_formatter.py +++ b/pyperf/_formatter.py @@ -39,6 +39,20 @@ def format_filesizes(sizes): return tuple(format_filesize(size) for size in sizes) +def format_energy(en): + if en < 10 * 1000: + return '%.0f uJ' % en + + if en > 10 * 1000 * 1000: + return '%.1f J' % (en / (1000.0 * 1000.0)) + + return '%.1f mJ' % (en / 1000.0) + + +def format_energies(ens): + return tuple(format_energy(en) for en in ens) + + def format_seconds(seconds): # Coarse but human readable duration if not seconds: @@ -108,6 +122,7 @@ def format_integers(numbers): 'second': format_timedeltas, 'byte': format_filesizes, 'integer': format_integers, + 'joule': format_energies, } diff --git a/pyperf/_manager.py b/pyperf/_manager.py index f29af1fa..fe5a1695 100644 --- a/pyperf/_manager.py +++ b/pyperf/_manager.py @@ -25,6 +25,30 @@ class Manager(object): def __init__(self, runner, python=None): self.runner = runner self.args = runner.args + + # If --track-energy is used, check for and + # inherit READEN, ENFILE without explicit + # input from the user. + if self.args.track_energy: + if self.args.inherit_environ is None: + self.args.inherit_environ = [] + from os import environ as curr_env + try: + lib = curr_env['READEN'] + f = curr_env['ENFILE'] + ld = curr_env['LD_LIBRARY_PATH'] + # pyperf could have been invoked by pyperformance + # and then the inheritance stuff would already be + # addressed. + if 'READEN' not in self.args.inherit_environ: + self.args.inherit_environ.append('READEN') + if 'ENFILE' not in self.args.inherit_environ: + self.args.inherit_environ.append('ENFILE') + if 'LD_LIBRARY_PATH' not in self.args.inherit_environ: + self.args.inherit_environ.append('LD_LIBRARY_PATH') + except: + raise OSError('--track-energy needs READEN, ENFILE, LD_LIBRARY_PATH to function') + if python: self.python = python else: @@ -65,6 +89,8 @@ def worker_cmd(self, calibrate_loops, calibrate_warmups, wpipe): cmd.append('--tracemalloc') if args.track_memory: cmd.append('--track-memory') + if args.track_energy: + cmd.append('--track-energy') if self.runner._add_cmdline_args: self.runner._add_cmdline_args(cmd, args) diff --git a/pyperf/_metadata.py b/pyperf/_metadata.py index c8cdabdc..8feb162a 100644 --- a/pyperf/_metadata.py +++ b/pyperf/_metadata.py @@ -1,6 +1,6 @@ import collections -from pyperf._formatter import (format_number, format_seconds, format_filesize, +from pyperf._formatter import (format_number, format_energy, format_seconds, format_filesize, UNIT_FORMATTERS) @@ -62,6 +62,7 @@ def format_noop(value): LOOPS = _MetadataInfo(format_number, (int,), is_strictly_positive, 'integer') WARMUPS = _MetadataInfo(format_number, (int,), is_positive, 'integer') SECONDS = _MetadataInfo(format_seconds, NUMBER_TYPES, is_positive, 'second') +JOULES = _MetadataInfo(format_energy, NUMBER_TYPES, is_positive, 'joule') # Registry of metadata keys METADATA = { diff --git a/pyperf/_runner.py b/pyperf/_runner.py index b70e3e52..64e1f9bc 100644 --- a/pyperf/_runner.py +++ b/pyperf/_runner.py @@ -104,9 +104,6 @@ def __init__(self, values=None, warmups=None, processes=None, # Set used to check that benchmark names are unique self._bench_names = set() - # result of argparser.parse_args() - self.args = None - # callback used to prepare command line arguments to spawn a worker # child process. The callback is called with prepare(runner.args, cmd). # args must be modified in-place. @@ -221,6 +218,9 @@ def __init__(self, values=None, warmups=None, processes=None, help='option used with --compare-to to name ' 'PYTHON as CHANGED_NAME ' 'and REF_PYTHON as REF_NAME in results') + parser.add_argument("--track-energy", + action="store_true", + help="Measure energy instead of wall clock time.") memory = parser.add_mutually_exclusive_group() memory.add_argument('--tracemalloc', action="store_true", @@ -230,6 +230,9 @@ def __init__(self, values=None, warmups=None, processes=None, self.argparser = parser + # result of argparser.parse_args() + self.args = None + def _multiline_output(self): return self.args.verbose or multiline_output(self.args) @@ -420,7 +423,7 @@ def _main(self, task): if task.name in self._bench_names: raise ValueError("duplicated benchmark name: %r" % task.name) self._bench_names.add(task.name) - + args = self.parse_args() try: if args.worker: @@ -491,6 +494,7 @@ def task_func(task, loops): dt = local_timer() - t0 return dt + task = WorkerProcessTask(self, name, task_func, metadata) task.inner_loops = inner_loops diff --git a/pyperf/_worker.py b/pyperf/_worker.py index 2950aa3a..1104f349 100644 --- a/pyperf/_worker.py +++ b/pyperf/_worker.py @@ -15,6 +15,10 @@ MAX_WARMUP_VALUES = 300 WARMUP_SAMPLE_SIZE = 20 +# To invoke C in the context of --track-energy. +import ctypes +import os + class WorkerTask: def __init__(self, runner, name, task_func, func_metadata): @@ -35,6 +39,8 @@ def __init__(self, runner, name, task_func, func_metadata): if 'unit' not in self.metadata: # Set default unit to seconds self.metadata['unit'] = 'second' + if args.track_energy: + self.metadata['unit'] = 'joule' self.inner_loops = None self.warmups = None @@ -63,9 +69,17 @@ def _compute_values(self, values, nvalue, while True: if index > nvalue: break - - raw_value = self.task_func(self, self.loops) - raw_value = float(raw_value) + if self.args.track_energy: + # Use environment variable for where the readings are stored. + c_lib = ctypes.CDLL(os.environ.get("READEN")) + # Energy value is the difference between recorded energies + # before and after executing task function. + e_0 = ctypes.c_ulonglong(c_lib.readen(os.environ.get("ENFILE").encode('utf-8'))) + self.task_func(self, self.loops) + e_1 = ctypes.c_ulonglong(c_lib.readen(os.environ.get("ENFILE").encode('utf-8'))) + raw_value = float(e_1.value) - float(e_0.value) + else: + raw_value = float(self.task_func(self, self.loops)) value = raw_value / (self.loops * inner_loops) if not value and not calibrate_loops: diff --git a/pyperf/libreaden.so b/pyperf/libreaden.so new file mode 100755 index 00000000..ac1c99c2 Binary files /dev/null and b/pyperf/libreaden.so differ diff --git a/pyperf/read_file.c b/pyperf/read_file.c new file mode 100644 index 00000000..35a8a50f --- /dev/null +++ b/pyperf/read_file.c @@ -0,0 +1,29 @@ +#define _GNU_SOURCE +#include +#include +#include + +unsigned long long int readen(char *path) { + char *line = NULL; + size_t len = 0; + ssize_t read; + unsigned long long int data; + + FILE *fd = fopen(path, "r"); + + if (fd == NULL) + exit(EXIT_FAILURE); + + while ((read = getline(&line, &len, fd)) != -1) { + //Do nothing. + } + + data = strtoull(line, NULL, 10); + + if (line) + free(line); + + fclose(fd); + + return data; +}