Skip to content

Commit f087f96

Browse files
committed
Complete Fibonacci example
1 parent ecc9150 commit f087f96

File tree

9 files changed

+132
-41
lines changed

9 files changed

+132
-41
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: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11

2-
# Hello World
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.
38

4-
A simple example that just prints on each event. This examples serves as an equivalent of a "Hello World" program.
59

610
## Running
711

812
From the root directory of the repo, run the following command:
913

1014
```bash
11-
python experiment-runner/ examples/hello-world/RunnerConfig.py
15+
python experiment-runner/ examples/hello-world-fibonacci/RunnerConfig.py
1216
```
1317

1418
## Results
1519

16-
The results are generated in the `examples/hello-world/experiments` folder.
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.

examples/hello-world-fibonacci/RunnerConfig.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ def create_run_table_model(self) -> RunTableModel:
5555
"""Create and return the run_table model here. A run_table is a List (rows) of tuples (columns),
5656
representing each run performed"""
5757
factor1 = FactorModel("fib_type", ['iter', 'mem', 'rec'])
58-
factor2 = FactorModel("problem_size", [10, 100, 1000, 10000])
58+
factor2 = FactorModel("problem_size", [10, 20, 30])
5959
self.run_table_model = RunTableModel(
6060
factors=[factor1, factor2],
6161
repetitions = 3,
62-
data_columns=["total_power", "runtime", "avg_cpu", "avg_mem"]
62+
data_columns=["total_power (J)", "runtime (sec)", "avg_mem (bytes)"]
6363
)
6464
return self.run_table_model
6565

@@ -84,8 +84,10 @@ def start_measurement(self, context: RunnerContext) -> None:
8484
fib_type = context.run_variation["fib_type"]
8585
problem_size = context.run_variation["problem_size"]
8686

87-
self.profiler = EnergiBridge(target_program=f"./fibonacci_{fib_type}.py {problem_size}")
88-
87+
EnergiBridge.source_name = "../EnergiBridge/target/release/energibridge"
88+
self.profiler = EnergiBridge(target_program=f"python examples/hello-world-fibonacci/fibonacci_{fib_type}.py {problem_size}",
89+
out_file=context.run_dir / "energibridge.csv")
90+
8991
self.profiler.start()
9092

9193
def interact(self, context: RunnerContext) -> None:
@@ -106,13 +108,12 @@ def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
106108
You can also store the raw measurement data under `context.run_dir`
107109
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
108110

109-
eb_log = self.profiler.parse_log(context.run_dir / self.profiler.logfile)
110-
eb_summary = self.profiler.parse_log(context.run_dir / self.profiler.summaryfile)
111-
112-
return {"total_power": 0,
113-
"runtime": 0,
114-
"avg_cpu": 0,
115-
"avg_mem": 0}
111+
eb_log, eb_summary = self.profiler.parse_log(self.profiler.logfile,
112+
self.profiler.summary_logfile)
113+
114+
return {"total_power (J)": eb_summary["total_joules"],
115+
"runtime (sec)": eb_summary["runtime_seconds"],
116+
"total_mem (bytes)": list(eb_log["TOTAL_MEMORY"].values())[-1]}
116117

117118
def after_experiment(self) -> None:
118119
"""Perform any activity required after stopping the experiment here
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
23

34
def fib(n):
45
a, b = 0, 1
56
for i in range(0, n):
67
a, b = b, a + b
78
return a
89

9-
for n in range(10000):
10-
print(fib(n))
10+
for n in range(int(sys.argv[1])):
11+
print(fib(n))

examples/hello-world-fibonacci/fibonacci_mem.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
23

34
cache = {0: 0, 1: 1}
45

@@ -9,5 +10,5 @@ def fib(n):
910
cache[n] = fib(n - 1) + fib(n - 2) # Recursive case
1011
return cache[n]
1112

12-
for n in range(10000):
13-
print(fib(n))
13+
for n in range(int(sys.argv[1])):
14+
print(fib(n))
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
23

34
def fib(n):
45
if n in {0, 1}: # Base case
56
return n
67
return fib(n - 1) + fib(n - 2) # Recursive case
78

8-
for n in range(10000):
9-
print(fib(n))
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: 18 additions & 3 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]:
@@ -96,7 +104,8 @@ def parse_log(logfile):
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():
@@ -195,7 +209,8 @@ def stop(self, wait=False):
195209
try:
196210
if not wait:
197211
self.process.terminate()
198-
stdout, stderr = self.process.communicate(timeout=5)
212+
213+
stdout, stderr = self.process.communicate(timeout=None if wait else 5)
199214

200215
except Exception as e:
201216
self.process.kill()

experiment-runner/Plugins/Profilers/EnergiBridge.py

100644100755
Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pathlib import Path
22
import pandas as pd
3+
import re
34
from Plugins.Profilers.DataSource import CLISource, ParameterDict
45

56
# Supported Paramters for the PowerJoular metrics plugin
@@ -16,18 +17,19 @@
1617
class EnergiBridge(CLISource):
1718
parameters = ParameterDict(ENERGIBRIDGE_PARAMETERS)
1819
source_name = "energibridge"
19-
supported_platforms = ["Linux"]
20+
supported_platforms = ["Linux", "Darwin", "Windows"]
2021

2122
"""An integration of PowerJoular into experiment-runner as a data source plugin"""
2223
def __init__(self,
23-
sample_frequency: int = 5000,
24+
sample_frequency: int = 200,
2425
out_file: Path = "energibridge.csv",
2526
summary: bool = True,
2627
target_program: str = "sleep 1000000",
2728
additional_args: dict = {}):
2829

2930
super().__init__()
30-
31+
32+
self.requires_admin = True
3133
self.target_program = target_program
3234
self.logfile = out_file
3335
self.args = {
@@ -39,13 +41,77 @@ def __init__(self,
3941
self.update_parameters(add={"--summary": None})
4042

4143
self.update_parameters(add=additional_args)
44+
45+
@property
46+
def summary(self):
47+
return "--summary" in self.args.keys()
48+
49+
@property
50+
def summary_logfile(self):
51+
if not self.logfile \
52+
or not any(map(lambda x: x in self.args.keys(), ["-o", "--output"])):
53+
54+
return None
55+
56+
return self.logfile.parent / Path(self.logfile.name.split(".")[0] + "-summary.txt")
57+
58+
def _stat_delta(self, data, stat):
59+
return list(data[stat].values())[-1] - list(data[stat].values())[0]
60+
61+
# Less accurate than the summary from EB, but better than nothing
62+
# TODO: EnergiBridge calculates this differently in a system dependent way,
63+
# this approximates using available data
64+
def generate_summary(self):
65+
log_data = self.parse_log(self.logfile)
66+
67+
elapsed_time = self._stat_delta(log_data, "Time") / 1000
68+
total_joules = self._stat_delta(log_data, "PACKAGE_ENERGY (J)")
69+
70+
return f"Energy consumption in joules: {total_joules} for {elapsed_time} sec of execution"
71+
72+
# We also want to save the summary of EnergiBridge if present
73+
def stop(self, wait=False):
74+
75+
stdout = super().stop(wait)
76+
77+
if self.summary and self.summary_logfile:
78+
with open(self.summary_logfile, "w") as f:
79+
# The last line is the summary, if present
80+
last_line = stdout.splitlines()[-1]
81+
82+
# If runtime was too short, energibridge doesnt provide a summary
83+
# Approximate this instead
84+
if not last_line.startswith("Energy consumption"):
85+
last_line = self.generate_summary()
86+
87+
f.write(last_line)
88+
89+
return stdout
4290

4391
def _format_cmd(self):
4492
cmd = super()._format_cmd()
4593

4694
return cmd + f" -- {self.target_program}"
4795

4896
@staticmethod
49-
def parse_log(logfile: Path):
97+
def parse_log(logfile: Path, summary_logfile: Path|None=None):
5098
# Things are already in csv format here, no checks needed
51-
return pd.read_csv(logfile).to_dict()
99+
log_data = pd.read_csv(logfile).to_dict()
100+
101+
if not summary_logfile:
102+
return log_data
103+
104+
with open(summary_logfile, "r") as f:
105+
summary_data = f.read()
106+
107+
# Extract the floats from the string, we expect always positive X.X
108+
values = re.findall("[0-9]+[.]?[0-9]*", summary_data)
109+
110+
if len(values) == 2:
111+
summary_data = {
112+
"total_joules": float(values[0]),
113+
"runtime_seconds": float(values[1])
114+
}
115+
116+
return (log_data, summary_data)
117+

0 commit comments

Comments
 (0)