Skip to content

Commit 26bdb52

Browse files
authored
Merge pull request #11 from mhkarsten/master
PicoLog CM3 + Self Measurement Implementation
2 parents 0758c4d + f7adba7 commit 26bdb52

File tree

23 files changed

+2595
-5
lines changed

23 files changed

+2595
-5
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.idea
22
.venv
3+
.vagrant
34
*/__pycache__/*
45
examples/linux-ps-profiling/primer
56
examples/linux-powerjoular-profiling/primer
@@ -13,3 +14,7 @@ tmp/
1314
_trial_temp/
1415
pycharm-interpreter.sh
1516
python
17+
18+
Session.vim
19+
20+
Vagrantfile
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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/measure-self-profiling/RunnerConfig.py
27+
```
28+
29+
## Results
30+
31+
The results are generated in the `examples/measure-self-profiling/experiments` folder, and are added to your run table model.
32+
A log file can be specified to additionally save the full energibridge logs to a separate file.
33+
34+
**!!! WARNING !!!**: COLUMNS IN THE `energibridge.log` FILES CAN BE DIFFERENT ACROSS MACHINES.
35+
ADJUST YOUR ANALYSIS OF THE RESULTS ACCORDINGLY.
36+
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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.RunnerContext import RunnerContext
5+
from ConfigValidator.Config.Models.OperationType import OperationType
6+
from ProgressManager.Output.OutputProcedure import OutputProcedure as output
7+
8+
from typing import Optional, Dict, Any
9+
from pathlib import Path
10+
from os.path import dirname, realpath
11+
12+
import time
13+
14+
class RunnerConfig:
15+
ROOT_DIR = Path(dirname(realpath(__file__)))
16+
17+
# ================================ USER SPECIFIC CONFIG ================================
18+
"""The name of the experiment."""
19+
name: str = "new_runner_experiment"
20+
21+
"""The path in which Experiment Runner will create a folder with the name `self.name`, in order to store the
22+
results from this experiment. (Path does not need to exist - it will be created if necessary.)
23+
Output path defaults to the config file's path, inside the folder 'experiments'"""
24+
results_output_path: Path = ROOT_DIR / 'experiments'
25+
26+
"""Experiment operation type. Unless you manually want to initiate each run, use `OperationType.AUTO`."""
27+
operation_type: OperationType = OperationType.AUTO
28+
29+
"""The time Experiment Runner will wait after a run completes.
30+
This can be essential to accommodate for cooldown periods on some systems."""
31+
time_between_runs_in_ms: int = 1000
32+
33+
"""
34+
Whether EnergiBridge should be used to measure the energy consumption of the entire system during
35+
the experiment.
36+
37+
This parameter is optional and defaults to False
38+
"""
39+
self_measure: bool = True
40+
41+
"""
42+
Where the EnergiBridge executable is located. As its not a package on linux distros, and must be
43+
installed manually, the install location can vary.
44+
45+
This parameter is optional and defaults to /usr/local/bin/energibridge
46+
"""
47+
self_measure_bin: Path = "/usr/local/bin/energibridge"
48+
49+
"""
50+
Where to save the full log files for energibridge. If specified, log files are saved to context.run_dir/<self_measure_logfile>.
51+
If self_measure_logfile is None, then no log file is generated.
52+
53+
This parameter is optional and defaults to None, where only summary results will be saved in the run table.
54+
"""
55+
self_measure_logfile: Path = "energibridge.log"
56+
57+
# Dynamic configurations can be one-time satisfied here before the program takes the config as-is
58+
# e.g. Setting some variable based on some criteria
59+
def __init__(self):
60+
"""Executes immediately after program start, on config load"""
61+
62+
EventSubscriptionController.subscribe_to_multiple_events([
63+
(RunnerEvents.BEFORE_EXPERIMENT, self.before_experiment),
64+
(RunnerEvents.BEFORE_RUN , self.before_run ),
65+
(RunnerEvents.START_RUN , self.start_run ),
66+
(RunnerEvents.START_MEASUREMENT, self.start_measurement),
67+
(RunnerEvents.INTERACT , self.interact ),
68+
(RunnerEvents.STOP_MEASUREMENT , self.stop_measurement ),
69+
(RunnerEvents.STOP_RUN , self.stop_run ),
70+
(RunnerEvents.POPULATE_RUN_DATA, self.populate_run_data),
71+
(RunnerEvents.AFTER_EXPERIMENT , self.after_experiment )
72+
])
73+
self.run_table_model = None # Initialized later
74+
output.console_log("Custom config loaded")
75+
76+
def create_run_table_model(self) -> RunTableModel:
77+
"""Create and return the run_table model here. A run_table is a List (rows) of tuples (columns),
78+
representing each run performed"""
79+
self.run_table_model = RunTableModel(
80+
factors = [],
81+
data_columns=['example_data']
82+
)
83+
return self.run_table_model
84+
85+
def before_experiment(self) -> None:
86+
"""Perform any activity required before starting the experiment here
87+
Invoked only once during the lifetime of the program."""
88+
pass
89+
90+
def before_run(self) -> None:
91+
"""Perform any activity required before starting a run.
92+
No context is available here as the run is not yet active (BEFORE RUN)"""
93+
pass
94+
95+
def start_run(self, context: RunnerContext) -> None:
96+
"""Perform any activity required for starting the run here.
97+
For example, starting the target system to measure.
98+
Activities after starting the run should also be performed here."""
99+
pass
100+
101+
def start_measurement(self, context: RunnerContext) -> None:
102+
"""Perform any activity required for starting measurements."""
103+
pass
104+
105+
def interact(self, context: RunnerContext) -> None:
106+
"""Perform any interaction with the running target system here, or block here until the target finishes."""
107+
108+
# No interaction. We just run it for XX seconds while EnergiBridge measures.
109+
output.console_log("Sleeping 5 seconds to measure system power consumption")
110+
time.sleep(5)
111+
112+
def stop_measurement(self, context: RunnerContext) -> None:
113+
"""Perform any activity here required for stopping measurements."""
114+
pass
115+
116+
def stop_run(self, context: RunnerContext) -> None:
117+
"""Perform any activity here required for stopping the run.
118+
Activities after stopping the run should also be performed here."""
119+
pass
120+
121+
def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
122+
"""Parse and process any measurement data here.
123+
You can also store the raw measurement data under `context.run_dir`
124+
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
125+
return {"example_data": "test value"}
126+
127+
def after_experiment(self) -> None:
128+
"""Perform any activity required after stopping the experiment here
129+
Invoked only once during the lifetime of the program."""
130+
pass
131+
132+
# ================================ DO NOT ALTER BELOW THIS LINE ================================
133+
experiment_path: Path = None
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
# `PicoLog CM3` profiler
3+
4+
A simple Linux example that uses stress-ng on a storage device and measures power consumption
5+
using a [PicoLog CM3](https://www.picotech.com/download/manuals/PicoLogCM3CurrentDataLoggerUsersGuide.pdf).
6+
7+
As an example program, a simple program is used that repeatedly checks
8+
if random numbers are prime or not.
9+
10+
## Requirements
11+
12+
A [PicoLog CM3](https://www.picotech.com/download/manuals/PicoLogCM3CurrentDataLoggerUsersGuide.pdf) device is required with a clamp of
13+
the correct variety for your task.
14+
15+
The [PicoLog CM3 Driver](https://www.picotech.com/downloads/linux) is assumed to be already installed.
16+
17+
Instructions to install the drivers for your operating system can be found [here](https://www.picotech.com/downloads/linux).
18+
19+
For this example program, [stress-ng](https://github.com/ColinIanKing/stress-ng) is additionally required as an example of something to measure, but this can be replaced with any other program, or simply a call to sleep if the device is not controlled through software.
20+
21+
Ensure you have the paremeters for stress-ng properly configured, such that an appropriate storage device is selected, and the PicoLog clamp has been correctly attached to the wire.
22+
23+
## Running
24+
25+
From the root directory of the repo, run the following commands:
26+
27+
```bash
28+
29+
# REQUIRED: Ensure this path matches where your PicoLog CM3 drivers are stored
30+
export LD_LIBRARY_PATH="/opt/picoscope/lib"
31+
32+
python experiment-runner/ examples/picocm3-profiling/RunnerConfig.py
33+
```
34+
35+
## Results
36+
37+
The results are generated in the `examples/picocm3-profiling/experiments` folder.
38+
There should be a unique log file for each variation in the experiment, as well as a run_table.csv file summarizing these log files.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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, Any, Optional
10+
from pathlib import Path
11+
from os.path import dirname, realpath
12+
13+
import subprocess
14+
import shlex
15+
from statistics import mean
16+
17+
from Plugins.Profilers.PicoCM3 import PicoCM3, CM3DataTypes, CM3Channels
18+
19+
class RunnerConfig:
20+
ROOT_DIR = Path(dirname(realpath(__file__)))
21+
22+
# ================================ USER SPECIFIC CONFIG ================================
23+
"""The name of the experiment."""
24+
name: str = "new_runner_experiment"
25+
26+
"""The path in which Experiment Runner will create a folder with the name `self.name`, in order to store the
27+
results from this experiment. (Path does not need to exist - it will be created if necessary.)
28+
Output path defaults to the config file's path, inside the folder 'experiments'"""
29+
results_output_path: Path = ROOT_DIR / 'experiments'
30+
31+
"""Experiment operation type. Unless you manually want to initiate each run, use `OperationType.AUTO`."""
32+
operation_type: OperationType = OperationType.AUTO
33+
34+
"""The time Experiment Runner will wait after a run completes.
35+
This can be essential to accommodate for cooldown periods on some systems."""
36+
time_between_runs_in_ms: int = 1000
37+
38+
# Dynamic configurations can be one-time satisfied here before the program takes the config as-is
39+
# e.g. Setting some variable based on some criteria
40+
def __init__(self):
41+
"""Executes immediately after program start, on config load"""
42+
43+
EventSubscriptionController.subscribe_to_multiple_events([
44+
(RunnerEvents.BEFORE_EXPERIMENT, self.before_experiment),
45+
(RunnerEvents.BEFORE_RUN , self.before_run ),
46+
(RunnerEvents.START_RUN , self.start_run ),
47+
(RunnerEvents.START_MEASUREMENT, self.start_measurement),
48+
(RunnerEvents.INTERACT , self.interact ),
49+
(RunnerEvents.STOP_MEASUREMENT , self.stop_measurement ),
50+
(RunnerEvents.STOP_RUN , self.stop_run ),
51+
(RunnerEvents.POPULATE_RUN_DATA, self.populate_run_data),
52+
(RunnerEvents.AFTER_EXPERIMENT , self.after_experiment )
53+
])
54+
55+
self.latest_log = None
56+
self.run_table_model = None # Initialized later
57+
output.console_log("Custom config loaded")
58+
59+
def create_run_table_model(self) -> RunTableModel:
60+
"""Create and return the run_table model here. A run_table is a List (rows) of tuples (columns),
61+
representing each run performed"""
62+
workers_factor = FactorModel("num_workers", [2, 4])
63+
write_factor = FactorModel("write_size", [1024, 4096])
64+
65+
self.run_table_model = RunTableModel(
66+
factors = [workers_factor, write_factor],
67+
data_columns=['timestamp', 'channel_1(avg)', 'channel_2(off)', 'channel_3(off)']) # Channel 1 is in Amps
68+
69+
return self.run_table_model
70+
71+
def before_experiment(self) -> None:
72+
"""Perform any activity required before starting the experiment here
73+
Invoked only once during the lifetime of the program."""
74+
75+
# Setup the picolog cm3 here (the parameters passed are also the default)
76+
self.meter = PicoCM3(sample_frequency = 1000, # Sample the CM3 every second
77+
mains_setting = 0, # Account for 50hz mains frequency
78+
channel_settings = { # Which channels are enabled in what mode
79+
CM3Channels.PLCM3_CHANNEL_1.value: CM3DataTypes.PLCM3_1_MILLIVOLT.value,
80+
CM3Channels.PLCM3_CHANNEL_2.value: CM3DataTypes.PLCM3_OFF.value,
81+
CM3Channels.PLCM3_CHANNEL_3.value: CM3DataTypes.PLCM3_OFF.value})
82+
# Open the device
83+
self.meter.open_device()
84+
85+
def before_run(self) -> None:
86+
"""Perform any activity required before starting a run.
87+
No context is available here as the run is not yet active (BEFORE RUN)"""
88+
pass
89+
90+
def start_run(self, context: RunnerContext) -> None:
91+
"""Perform any activity required for starting the run here.
92+
For example, starting the target system to measure.
93+
Activities after starting the run should also be performed here."""
94+
95+
num_workers = context.run_variation['num_workers']
96+
write_size = context.run_variation['write_size']
97+
98+
# Start stress-ng
99+
stress_cmd = f"sudo stress-ng \
100+
--hdd {num_workers} \
101+
--hdd-write-size {write_size} \
102+
--hdd-ops 1000000 \
103+
--hdd-dev /dev/sda1 \
104+
--timeout 60s \
105+
--metrics-brief"
106+
107+
stress_log = open(f'{context.run_dir}/stress-ng.log', 'w')
108+
self.stress_ng = subprocess.Popen(shlex.split(stress_cmd), stdout=stress_log)
109+
110+
def start_measurement(self, context: RunnerContext) -> None:
111+
"""Perform any activity required for starting measurements."""
112+
113+
# Start the picologs measurements here, create a unique log file for each (or pass the values through a variable)
114+
self.latest_log = str(context.run_dir.resolve() / 'picocm3.log')
115+
self.meter.log(finished_fn=lambda: self.stress_ng.poll() == None, logfile=self.latest_log)
116+
117+
def interact(self, context: RunnerContext) -> None:
118+
"""Perform any interaction with the running target system here, or block here until the target finishes."""
119+
120+
# Wait for stress-ng to finish or time.sleep(60)
121+
self.stress_ng.wait()
122+
123+
def stop_measurement(self, context: RunnerContext) -> None:
124+
"""Perform any activity here required for stopping measurements."""
125+
126+
# Wait for stress-ng to finish
127+
self.stress_ng.wait()
128+
129+
def stop_run(self, context: RunnerContext) -> None:
130+
"""Perform any activity here required for stopping the run.
131+
Activities after stopping the run should also be performed here."""
132+
pass
133+
134+
def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
135+
"""Parse and process any measurement data here.
136+
You can also store the raw measurement data under `context.run_dir`
137+
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
138+
139+
if self.latest_log == None:
140+
return {}
141+
142+
# Read data from the relavent CM3 log
143+
log_data = self.meter.parse_log(self.latest_log)
144+
145+
return {'timestamp': log_data['timestamp'][0] + " - " + log_data['timestamp'][-1],
146+
'channel_1(avg)': mean(log_data['channel_1']),
147+
'channel_2(off)': mean(log_data['channel_2']),
148+
'channel_3(off)': mean(log_data['channel_3'])}
149+
150+
def after_experiment(self) -> None:
151+
"""Perform any activity required after stopping the experiment here
152+
Invoked only once during the lifetime of the program."""
153+
154+
# This must always be run
155+
self.meter.close_device()
156+
157+
# ================================ DO NOT ALTER BELOW THIS LINE ================================
158+
experiment_path: Path = None

0 commit comments

Comments
 (0)