Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

--track-energy feature #125

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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;
}