Skip to content
3 changes: 2 additions & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ Features of the ``pyperf`` module:
collect them.
* ``--track-memory`` and ``--tracemalloc`` :ref:`options <runner_cli>` to track
the memory usage of a benchmark.
* ``--track-energy`` :ref:`option <runner_cli>` to track the energy consumption of a benchmark. Based on the `Linux power capping framework <https://www.kernel.org/doc/html/latest/power/powercap/powercap.html>`_, but simple to extend to other energy APIs.
* :ref:`JSON format <json>` to store benchmark results.
* Support multiple units: seconds, bytes and integer.
* Support multiple units: seconds, bytes, integer and Joules.

Quick Links:

Expand Down
7 changes: 7 additions & 0 deletions doc/runner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <https://www.kernel.org/doc/html/latest/power/powercap/powercap.html>`_.
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
Expand Down
15 changes: 15 additions & 0 deletions pyperf/_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -108,6 +122,7 @@ def format_integers(numbers):
'second': format_timedeltas,
'byte': format_filesizes,
'integer': format_integers,
'joule': format_energies,
}


Expand Down
26 changes: 26 additions & 0 deletions pyperf/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion pyperf/_metadata.py
Original file line number Diff line number Diff line change
@@ -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)


Expand Down Expand Up @@ -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 = {
Expand Down
12 changes: 8 additions & 4 deletions pyperf/_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary spaces in an empty line.

Suggested change

args = self.parse_args()
try:
if args.worker:
Expand Down Expand Up @@ -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
Expand Down
20 changes: 17 additions & 3 deletions pyperf/_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Binary file added pyperf/libreaden.so
Binary file not shown.
29 changes: 29 additions & 0 deletions pyperf/read_file.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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;
}