Skip to content

Commit c02ffb9

Browse files
authored
Merge pull request #19 from mhkarsten/hello_world_example
2 parents 5cc342d + 12fd70d commit c02ffb9

File tree

9 files changed

+335
-18
lines changed

9 files changed

+335
-18
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ python
2222
Session.vim
2323
Vagrantfile
2424
scratch.py
25+
26+
__MACOSX/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
# Hello World Fibonacci
3+
4+
A simple platform independent example that runs three different fibonacci implementations,
5+
and measures their power consumption, runtime, and memory usage using [EnergiBridge](https://github.com/tdurieux/EnergiBridge).
6+
7+
Note that admin permissions are needed to make use of EnergiBridge.
8+
9+
10+
## Running
11+
12+
From the root directory of the repo, run the following command:
13+
14+
```bash
15+
python experiment-runner/ examples/hello-world-fibonacci/RunnerConfig.py
16+
```
17+
18+
## Results
19+
20+
The results are generated in the `examples/hello-world-fibonacci/experiments` folder.
21+
22+
**!!! WARNING !!!**: COLUMNS IN THE `energibridge.csv` FILES CAN BE DIFFERENT ACROSS MACHINES.
23+
ADJUST THE DATAFRAME COLUMN NAMES ACCORDINGLY.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
from Plugins.Profilers.EnergiBridge import EnergiBridge
9+
10+
from typing import Dict, List, Any, Optional
11+
from pathlib import Path
12+
from os.path import dirname, realpath
13+
14+
15+
class RunnerConfig:
16+
ROOT_DIR = Path(dirname(realpath(__file__)))
17+
18+
# ================================ USER SPECIFIC CONFIG ================================
19+
"""The name of the experiment."""
20+
name: str = "new_runner_experiment"
21+
22+
"""The path in which Experiment Runner will create a folder with the name `self.name`, in order to store the
23+
results from this experiment. (Path does not need to exist - it will be created if necessary.)
24+
Output path defaults to the config file's path, inside the folder 'experiments'"""
25+
results_output_path: Path = ROOT_DIR / 'experiments'
26+
27+
"""Experiment operation type. Unless you manually want to initiate each run, use `OperationType.AUTO`."""
28+
operation_type: OperationType = OperationType.AUTO
29+
30+
"""The time Experiment Runner will wait after a run completes.
31+
This can be essential to accommodate for cooldown periods on some systems."""
32+
time_between_runs_in_ms: int = 1000
33+
34+
# Dynamic configurations can be one-time satisfied here before the program takes the config as-is
35+
# e.g. Setting some variable based on some criteria
36+
def __init__(self):
37+
"""Executes immediately after program start, on config load"""
38+
39+
EventSubscriptionController.subscribe_to_multiple_events([
40+
(RunnerEvents.BEFORE_EXPERIMENT, self.before_experiment),
41+
(RunnerEvents.BEFORE_RUN , self.before_run ),
42+
(RunnerEvents.START_RUN , self.start_run ),
43+
(RunnerEvents.START_MEASUREMENT, self.start_measurement),
44+
(RunnerEvents.INTERACT , self.interact ),
45+
(RunnerEvents.STOP_MEASUREMENT , self.stop_measurement ),
46+
(RunnerEvents.STOP_RUN , self.stop_run ),
47+
(RunnerEvents.POPULATE_RUN_DATA, self.populate_run_data),
48+
(RunnerEvents.AFTER_EXPERIMENT , self.after_experiment )
49+
])
50+
self.run_table_model = None # Initialized later
51+
52+
output.console_log("Custom config loaded")
53+
54+
def create_run_table_model(self) -> RunTableModel:
55+
"""Create and return the run_table model here. A run_table is a List (rows) of tuples (columns),
56+
representing each run performed"""
57+
factor1 = FactorModel("fib_type", ['iter', 'mem', 'rec'])
58+
factor2 = FactorModel("problem_size", [10, 20, 30])
59+
self.run_table_model = RunTableModel(
60+
factors=[factor1, factor2],
61+
exclude_variations=[
62+
{factor2: [10]}, # all runs having treatment "10" will be excluded
63+
{factor1: ['iter'], factor2: [30]}, # all runs having the combination ("iter", 30) will be excluded
64+
],
65+
repetitions = 3,
66+
data_columns=["total_power (J)", "runtime (sec)", "avg_mem (bytes)"]
67+
)
68+
return self.run_table_model
69+
70+
def before_experiment(self) -> None:
71+
"""Perform any activity required before starting the experiment here
72+
Invoked only once during the lifetime of the program."""
73+
pass
74+
75+
def before_run(self) -> None:
76+
"""Perform any activity required before starting a run.
77+
No context is available here as the run is not yet active (BEFORE RUN)"""
78+
pass
79+
80+
def start_run(self, context: RunnerContext) -> None:
81+
"""Perform any activity required for starting the run here.
82+
For example, starting the target system to measure.
83+
Activities after starting the run should also be performed here."""
84+
pass
85+
86+
def start_measurement(self, context: RunnerContext) -> None:
87+
"""Perform any activity required for starting measurements."""
88+
fib_type = context.run_variation["fib_type"]
89+
problem_size = context.run_variation["problem_size"]
90+
91+
self.profiler = EnergiBridge(target_program=f"python examples/hello-world-fibonacci/fibonacci_{fib_type}.py {problem_size}",
92+
out_file=context.run_dir / "energibridge.csv")
93+
94+
self.profiler.start()
95+
96+
def interact(self, context: RunnerContext) -> None:
97+
"""Perform any interaction with the running target system here, or block here until the target finishes."""
98+
pass
99+
100+
def stop_measurement(self, context: RunnerContext) -> None:
101+
"""Perform any activity here required for stopping measurements."""
102+
stdout = self.profiler.stop(wait=True)
103+
104+
def stop_run(self, context: RunnerContext) -> None:
105+
"""Perform any activity here required for stopping the run.
106+
Activities after stopping the run should also be performed here."""
107+
pass
108+
109+
def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
110+
"""Parse and process any measurement data here.
111+
You can also store the raw measurement data under `context.run_dir`
112+
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
113+
114+
eb_log, eb_summary = self.profiler.parse_log(self.profiler.logfile,
115+
self.profiler.summary_logfile)
116+
117+
return {"total_power (J)": eb_summary["total_joules"],
118+
"runtime (sec)": eb_summary["runtime_seconds"],
119+
"total_mem (bytes)": list(eb_log["TOTAL_MEMORY"].values())[-1]}
120+
121+
def after_experiment(self) -> None:
122+
"""Perform any activity required after stopping the experiment here
123+
Invoked only once during the lifetime of the program."""
124+
pass
125+
126+
# ================================ DO NOT ALTER BELOW THIS LINE ================================
127+
experiment_path: Path = None
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
3+
4+
def fib(n):
5+
a, b = 0, 1
6+
for i in range(0, n):
7+
a, b = b, a + b
8+
return a
9+
10+
for n in range(int(sys.argv[1])):
11+
print(fib(n))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
3+
4+
cache = {0: 0, 1: 1}
5+
6+
def fib(n):
7+
if n in cache: # Base case
8+
return cache[n]
9+
# Compute and cache the Fibonacci number
10+
cache[n] = fib(n - 1) + fib(n - 2) # Recursive case
11+
return cache[n]
12+
13+
for n in range(int(sys.argv[1])):
14+
print(fib(n))
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
3+
4+
def fib(n):
5+
if n in {0, 1}: # Base case
6+
return n
7+
return fib(n - 1) + fib(n - 2) # Recursive case
8+
9+
for n in range(int(sys.argv[1])):
10+
print(fib(n))

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,28 @@ def __check_expression(name, value, expected, expression):
2424

2525
# Verifies that an energybridge executable is present, and can be executed without error
2626
@staticmethod
27-
def __validate_energibridge(measure_enabled, eb_path, eb_logfile):
27+
def __validate_energibridge(config):
2828
# Do nothing if its not enabled
29-
if not measure_enabled:
29+
if not config.self_measure:
3030
return
3131

3232
if not platform.system() == "Linux" \
33-
or not os.path.exists(eb_path) \
34-
or not os.access(eb_path, os.X_OK):
33+
or not os.path.exists(config.self_measure_bin) \
34+
or not os.access(config.self_measure_bin, os.X_OK):
3535

3636
ConfigValidator.error_found = True
3737
ConfigValidator \
3838
.config_values_or_exception_dict["EnergiBridge"] = "EnergiBridge executable was not present or valid"
3939

40-
if eb_logfile \
41-
and not is_path_exists_or_creatable_portable(eb_logfile):
40+
if config.self_measure_logfile \
41+
and not is_path_exists_or_creatable_portable(config.self_measure_logfile):
4242
ConfigValidator.error_found = True
4343
ConfigValidator \
44-
.config_values_or_exception_dict["EnergiBridge"] = f"EnergiBridge logfile ({eb_logfile}) was not a valid path"
44+
.config_values_or_exception_dict["EnergiBridge"] = f"EnergiBridge logfile ({config.self_measure_logfile}) was not a valid path"
4545

4646
# Test run to see if energibridge works
4747
try:
48-
eb_args = [eb_path, "--summary", "-o", "/dev/null", "--", "sleep", "0.5"]
48+
eb_args = [config.self_measure_bin, "--summary", "-o", "/dev/null", "--", "sleep", "0.5"]
4949
p = subprocess.Popen(eb_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
5050

5151
stdout, stderr = p.communicate(timeout=5)
@@ -112,10 +112,7 @@ def validate_config(config: RunnerConfig):
112112
(lambda a, b: is_path_exists_or_creatable_portable(a))
113113
)
114114

115-
ConfigValidator.__validate_energibridge(config.self_measure,
116-
config.self_measure_bin,
117-
config.self_measure_logfile
118-
)
115+
ConfigValidator.__validate_energibridge(config)
119116

120117
# Display config in user-friendly manner, including potential errors found
121118
print(

experiment-runner/Plugins/Profilers/DataSource.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import shlex
88
from enum import StrEnum
99
import shutil
10+
import ctypes
11+
import os
1012
import subprocess
1113

1214
class ParameterDict(UserDict):
@@ -73,6 +75,12 @@ def _validate_platform(self):
7375

7476
raise RuntimeError(f"One of: {self.supported_platforms} is required for this plugin")
7577

78+
def is_admin(self):
79+
try:
80+
return os.getuid() == 0
81+
except:
82+
return ctypes.windll.shell32.IsUserAdmin() == 1
83+
7684
@property
7785
@abstractmethod
7886
def supported_platforms(self) -> list[str]:
@@ -89,14 +97,15 @@ def __del__(self):
8997

9098
@staticmethod
9199
@abstractmethod
92-
def parse_log():
100+
def parse_log(logfile):
93101
pass
94102

95103

96104
class CLISource(DataSource):
97105
def __init__(self):
98106
super().__init__()
99-
107+
108+
self.requires_admin = False
100109
self.process = None
101110
self.args = None
102111

@@ -112,7 +121,8 @@ def parameters(self) -> ParameterDict:
112121
def _validate_platform(self):
113122
super()._validate_platform()
114123

115-
if shutil.which(self.source_name) is None:
124+
if shutil.which(self.source_name) is None \
125+
and not os.access(self.source_name, os.X_OK):
116126
raise RuntimeError(f"The {self.source_name} cli tool is required for this plugin")
117127

118128
def _validate_start(self):
@@ -150,7 +160,11 @@ def _validate_parameters(self, parameters: dict):
150160

151161
def _format_cmd(self):
152162
self._validate_parameters(self.args)
163+
153164
cmd = self.source_name
165+
166+
if self.requires_admin:
167+
cmd = f"sudo {cmd}"
154168

155169
# Transform the parameter dict into string format to be parsed by shlex
156170
for p, v in self.args.items():
@@ -188,13 +202,15 @@ def start(self):
188202

189203
self._validate_start()
190204

191-
def stop(self):
205+
def stop(self, wait=False):
192206
if not self.process:
193207
return
194208

195209
try:
196-
self.process.terminate()
197-
stdout, stderr = self.process.communicate(timeout=5)
210+
if not wait:
211+
self.process.terminate()
212+
213+
stdout, stderr = self.process.communicate(timeout=None if wait else 5)
198214

199215
except Exception as e:
200216
self.process.kill()

0 commit comments

Comments
 (0)