Skip to content

Commit 1680b57

Browse files
committed
Completed EnergiBridge self_measure integration
1 parent 9e8943c commit 1680b57

File tree

5 files changed

+270
-1
lines changed

5 files changed

+270
-1
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Self Measurement with `EnergiBridge`
2+
3+
A simple Linux example, of how to enable the self-measurement feature of experiment-runner.
4+
5+
We use the [EnergiBridge](https://github.com/tdurieux/EnergiBridge) program to measure power consuption
6+
of the entire system while performing any experiment.
7+
8+
As an example program, we simply sleep for a number of seconds while
9+
10+
## Requirements
11+
12+
[EnergiBridge](https://github.com/tdurieux/EnergiBridge) is assumed to be already installed.
13+
To install EnergiBridge, please follow the instructions on their GitHub repo.
14+
15+
By default we assume that (on Linux) your EnergiBridge binary executable is located at /usr/local/bin/energibridge.
16+
If you have installed it in a different location you can specify this in RunnerConfig.
17+
18+
## Running
19+
20+
Set RunnerConfig to True in your RunnerConfig.
21+
Optionally set self_measure_bin to the path of your executable.
22+
23+
From the root directory of the repo, run the following command:
24+
25+
```bash
26+
python experiment-runner/ examples/energibridge-profiling/RunnerConfig.py
27+
```
28+
29+
## Results
30+
31+
The results are generated in the `examples/energibridge-profiling/experiments` folder.
32+
33+
**!!! WARNING !!!**: COLUMNS IN THE `energibridge.csv` FILES CAN BE DIFFERENT ACROSS MACHINES.
34+
ADJUST THE DATAFRAME COLUMN NAMES ACCORDINGLY.
35+
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from EventManager.Models.RunnerEvents import RunnerEvents
2+
from EventManager.EventSubscriptionController import EventSubscriptionController
3+
from ConfigValidator.Config.Models.RunTableModel import RunTableModel
4+
from ConfigValidator.Config.Models.FactorModel import FactorModel
5+
from ConfigValidator.Config.Models.RunnerContext import RunnerContext
6+
from ConfigValidator.Config.Models.OperationType import OperationType
7+
from ProgressManager.Output.OutputProcedure import OutputProcedure as output
8+
9+
from typing import Dict, List, Any, Optional
10+
from pathlib import Path
11+
from os.path import dirname, realpath
12+
13+
import os
14+
import signal
15+
import pandas as pd
16+
import time
17+
import subprocess
18+
import shlex
19+
20+
class RunnerConfig:
21+
ROOT_DIR = Path(dirname(realpath(__file__)))
22+
23+
# ================================ USER SPECIFIC CONFIG ================================
24+
"""The name of the experiment."""
25+
name: str = "new_runner_experiment"
26+
27+
"""The path in which Experiment Runner will create a folder with the name `self.name`, in order to store the
28+
results from this experiment. (Path does not need to exist - it will be created if necessary.)
29+
Output path defaults to the config file's path, inside the folder 'experiments'"""
30+
results_output_path: Path = ROOT_DIR / 'experiments'
31+
32+
"""Experiment operation type. Unless you manually want to initiate each run, use `OperationType.AUTO`."""
33+
operation_type: OperationType = OperationType.AUTO
34+
35+
"""The time Experiment Runner will wait after a run completes.
36+
This can be essential to accommodate for cooldown periods on some systems."""
37+
time_between_runs_in_ms: int = 1000
38+
39+
"""
40+
Whether EnergiBridge should be used to measure the energy consumption of the entire system durring
41+
the experiment.
42+
43+
This parameter is optional and defaults to False
44+
"""
45+
self_measure: bool = True
46+
47+
"""
48+
Where the EnergiBridge executable is located. As its not a package on linux distros, and must be
49+
installed manually install location can vary.
50+
51+
This parameter is optional and defaults to /usr/local/bin/energibridge
52+
"""
53+
felf_measure_bin: Path = "/usr/local/bin/energibridge"
54+
55+
# Dynamic configurations can be one-time satisfied here before the program takes the config as-is
56+
# e.g. Setting some variable based on some criteria
57+
def __init__(self):
58+
"""Executes immediately after program start, on config load"""
59+
60+
EventSubscriptionController.subscribe_to_multiple_events([
61+
(RunnerEvents.BEFORE_EXPERIMENT, self.before_experiment),
62+
(RunnerEvents.BEFORE_RUN , self.before_run ),
63+
(RunnerEvents.START_RUN , self.start_run ),
64+
(RunnerEvents.START_MEASUREMENT, self.start_measurement),
65+
(RunnerEvents.INTERACT , self.interact ),
66+
(RunnerEvents.STOP_MEASUREMENT , self.stop_measurement ),
67+
(RunnerEvents.STOP_RUN , self.stop_run ),
68+
(RunnerEvents.POPULATE_RUN_DATA, self.populate_run_data),
69+
(RunnerEvents.AFTER_EXPERIMENT , self.after_experiment )
70+
])
71+
self.run_table_model = None # Initialized later
72+
output.console_log("Custom config loaded")
73+
74+
def create_run_table_model(self) -> RunTableModel:
75+
"""Create and return the run_table model here. A run_table is a List (rows) of tuples (columns),
76+
representing each run performed"""
77+
self.run_table_model = RunTableModel(
78+
factors = [],
79+
data_columns=['example_data']
80+
)
81+
return self.run_table_model
82+
83+
def before_experiment(self) -> None:
84+
"""Perform any activity required before starting the experiment here
85+
Invoked only once during the lifetime of the program."""
86+
pass
87+
88+
def before_run(self) -> None:
89+
"""Perform any activity required before starting a run.
90+
No context is available here as the run is not yet active (BEFORE RUN)"""
91+
pass
92+
93+
def start_run(self, context: RunnerContext) -> None:
94+
"""Perform any activity required for starting the run here.
95+
For example, starting the target system to measure.
96+
Activities after starting the run should also be performed here."""
97+
pass
98+
99+
def start_measurement(self, context: RunnerContext) -> None:
100+
"""Perform any activity required for starting measurements."""
101+
pass
102+
103+
def interact(self, context: RunnerContext) -> None:
104+
"""Perform any interaction with the running target system here, or block here until the target finishes."""
105+
106+
# No interaction. We just run it for XX seconds while EnergiBridge measures.
107+
output.console_log("Sleeping 5 seconds to measure system power consumption")
108+
time.sleep(5)
109+
110+
def stop_measurement(self, context: RunnerContext) -> None:
111+
"""Perform any activity here required for stopping measurements."""
112+
pass
113+
114+
def stop_run(self, context: RunnerContext) -> None:
115+
"""Perform any activity here required for stopping the run.
116+
Activities after stopping the run should also be performed here."""
117+
pass
118+
119+
def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
120+
"""Parse and process any measurement data here.
121+
You can also store the raw measurement data under `context.run_dir`
122+
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
123+
return {"example_data": "test value"}
124+
125+
def after_experiment(self) -> None:
126+
"""Perform any activity required after stopping the experiment here
127+
Invoked only once during the lifetime of the program."""
128+
pass
129+
130+
# ================================ DO NOT ALTER BELOW THIS LINE ================================
131+
experiment_path: Path = None

experiment-runner/ConfigValidator/Config/Validation/ConfigValidator.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from pathlib import Path
22
from tabulate import tabulate
3+
import os
4+
import subprocess
5+
import platform
36

47
from ExperimentOrchestrator.Misc.DictConversion import class_to_dict
58
from ExperimentOrchestrator.Misc.PathValidation import is_path_exists_or_creatable_portable
@@ -18,7 +21,47 @@ def __check_expression(name, value, expected, expression):
1821
.config_values_or_exception_dict[name] = str(ConfigValidator.config_values_or_exception_dict[name]) + \
1922
f"\n\n{ConfigAttributeInvalidError(name, value, expected)}"
2023
ConfigValidator.error_found = True
24+
25+
# Verifies that an energybridge executable is present, and can be executed without error
26+
@staticmethod
27+
def __validate_energibridge(measure_enabled, eb_path):
28+
# Do nothing if its not enabled
29+
if not measure_enabled:
30+
return
31+
32+
if not platform.system() == "Linux" \
33+
or not os.path.exists(eb_path) \
34+
or not os.access(eb_path, os.X_OK):
35+
36+
ConfigValidator.error_found = True
37+
ConfigValidator \
38+
.config_values_or_exception_dict["EnergiBridge"] = "EnergiBridge executable was not present or valid"
39+
40+
# Test run to see if energibridge works
41+
try:
42+
eb_args = [eb_path, "--summary", "-o", "/dev/null", "--", "sleep", "0.5"]
43+
p = subprocess.Popen(eb_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
44+
45+
stdout, stderr = p.communicate(timeout=5)
46+
47+
if stderr or not stdout:
48+
ConfigValidator.error_found = True
49+
ConfigValidator \
50+
.config_values_or_exception_dict["EnergiBridge"] = f"EnergiBridge error durring test:\n{stderr}"
51+
52+
53+
if "joules" not in stdout:
54+
ConfigValidator.error_found = True
55+
ConfigValidator \
56+
.config_values_or_exception_dict["EnergiBridge"] = f"Unexpected output durring EnergiBridge test:\n{stdout}"
2157

58+
59+
except Exception as e:
60+
ConfigValidator.error_found = True
61+
ConfigValidator \
62+
.config_values_or_exception_dict["EnergiBridge"] = f"Exception durring EnergiBridge test:\n{e}"
63+
64+
2265
@staticmethod
2366
def validate_config(config: RunnerConfig):
2467

@@ -27,6 +70,13 @@ def validate_config(config: RunnerConfig):
2770
if '~' in str(config.experiment_path):
2871
config.experiment_path = config.experiment_path.expanduser()
2972

73+
# Set defaults to support configs without the self_measure parameter
74+
if not hasattr(config, "self_measure"):
75+
config.self_measure = False
76+
77+
if config.self_measure and not hasattr(config, "self_measure_bin"):
78+
config.self_measure_bin = "/usr/local/bin" # This is spesific to linux, might work for osx as well
79+
3080
# Convert class to dictionary with utility method
3181
ConfigValidator.config_values_or_exception_dict = class_to_dict(config)
3282

@@ -51,6 +101,8 @@ def validate_config(config: RunnerConfig):
51101
"path must be valid and writable",
52102
(lambda a, b: is_path_exists_or_creatable_portable(a))
53103
)
104+
105+
ConfigValidator.__validate_energibridge(config.self_measure, config.self_measure_bin)
54106

55107
# Display config in user-friendly manner, including potential errors found
56108
print(

experiment-runner/ExperimentOrchestrator/Experiment/ExperimentController.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,17 @@ def __init__(self, config: RunnerConfig, metadata: Metadata):
3636

3737
self.csv_data_manager = CSVOutputManager(self.config.experiment_path)
3838
self.json_data_manager = JSONOutputManager(self.config.experiment_path)
39-
self.run_table = self.config.create_run_table_model().generate_experiment_run_table()
39+
run_tbl = self.config.create_run_table_model()
40+
41+
# Add in the proper data column for energibridge
42+
if self.config.self_measure:
43+
if "self-measure" in run_tbl._RunTableModel__data_columns:
44+
raise BaseError("Cannot use self-measure as data column name if self_measure is active")
4045

46+
run_tbl._RunTableModel__data_columns.append("self-measure")
47+
48+
self.run_table = run_tbl.generate_experiment_run_table()
49+
4150
# Create experiment output folder, and in case that it exists, check if we can resume
4251
self.restarted = False
4352
try:

experiment-runner/ExperimentOrchestrator/Experiment/Run/RunController.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import subprocess
2+
13
from ProgressManager.RunTable.Models.RunProgress import RunProgress
24
from EventManager.Models.RunnerEvents import RunnerEvents
35
from EventManager.EventSubscriptionController import EventSubscriptionController
@@ -6,8 +8,45 @@
68
from ProgressManager.Output.OutputProcedure import OutputProcedure as output
79

810
class RunController(IRunController):
11+
# Start EnergiBridge measurements
12+
def start_eb(self):
13+
if not self.config.self_measure:
14+
return
15+
16+
eb_args = [self.config.self_measure_bin, "--summary",
17+
"-o", "/dev/null", "--", "sleep", "1000000"]
18+
19+
try:
20+
self.eb_proc = subprocess.Popen(eb_args, stdout=subprocess.PIPE,
21+
stderr=subprocess.PIPE, text=True)
22+
except Exception as e:
23+
output.console_log_FAIL(f"Error while starting EnergiBridge:\n{e}")
24+
25+
# Stop EnergiBridge
26+
def stop_eb(self):
27+
if not self.config.self_measure or not self.eb_proc:
28+
return
29+
30+
try:
31+
self.eb_proc.terminate()
32+
stdout, stderr = self.eb_proc.communicate()
33+
34+
if stderr:
35+
output.console_log_FAIL(f"EnergiBridge error encountered:\n{stderr}")
36+
37+
if "joules:" not in stdout:
38+
output.console_log_FAIL("EnergiBridge error: Could not extract joules from output")
39+
40+
self.run_context.run_variation["self-measure"] = round(float(stdout.split(" ")[6]), 3)
41+
42+
except Exception as e:
43+
output.console_log_FAIL(f"Failed to stop EnergiBridge:\n{e}")
44+
945
@processify
1046
def do_run(self):
47+
# Start EnergiBridge
48+
self.start_eb()
49+
1150
# -- Start run
1251
output.console_log_WARNING("Calling start_run config hook")
1352
EventSubscriptionController.raise_event(RunnerEvents.START_RUN, self.run_context)
@@ -32,6 +71,9 @@ def do_run(self):
3271
# -- Collect data from measurements
3372
output.console_log_WARNING("Calling populate_run_data config hook")
3473
user_run_data = EventSubscriptionController.raise_event(RunnerEvents.POPULATE_RUN_DATA, self.run_context)
74+
75+
# Stop EnergiBridge
76+
self.stop_eb()
3577

3678
if user_run_data:
3779
# TODO: check if data columns exist and if yes, if they match

0 commit comments

Comments
 (0)