Skip to content

Commit 8a55eb5

Browse files
committed
Most of the plugin complete, as well as most of the Runner Config
1 parent d145b10 commit 8a55eb5

File tree

4 files changed

+267
-50
lines changed

4 files changed

+267
-50
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ tmp/
1313
_trial_temp/
1414
pycharm-interpreter.sh
1515
python
16+
17+
Session.vim
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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://github.com/tdurieux/EnergiBridge).
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://github.com/tdurieux/EnergiBridge) device is required with a clamp of
13+
the correct variety for your task
14+
15+
The [PicoLog CM3 Driver](https://github.com/tdurieux/EnergiBridge) is assumed to be already installed.
16+
17+
Instructions to install the drivers for your operating system can be found [here]().
18+
19+
## Running
20+
21+
From the root directory of the repo, run the following command:
22+
23+
```bash
24+
python experiment-runner/examples/picocm3-profiling/RunnerConfig.py
25+
```
26+
27+
## Results
28+
29+
The results are generated in the `examples/picocm3-profiling/experiments` folder.

examples/picocm3-profiling/RunnerConfig.py

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,28 @@
66
from ConfigValidator.Config.Models.OperationType import OperationType
77
from ProgressManager.Output.OutputProcedure import OutputProcedure as output
88

9-
from typing import Dict, List, Any, Optional
9+
from typing import Dict, Any, Optional
1010
from pathlib import Path
1111
from os.path import dirname, realpath
1212

13-
import os
14-
import signal
15-
import pandas as pd
1613
import time
1714
import subprocess
1815
import shlex
1916

17+
from Plugins.Profilers.PicoCM3 import PicoCM3, CM3DataTypes, CM3Channels
18+
19+
# TODO
20+
# Finish parsing / averaging the values to place in the results table
21+
# Finish the documentation in 2 places
22+
# Test the implementation
23+
# Write actual test code to test the implementaiton
24+
2025
class RunnerConfig:
2126
ROOT_DIR = Path(dirname(realpath(__file__)))
2227

2328
# ================================ USER SPECIFIC CONFIG ================================
2429
"""The name of the experiment."""
25-
name: str = "new_picocm3_experiment"
30+
name: str = "new_runner_experiment"
2631

2732
"""The path in which Experiment Runner will create a folder with the name `self.name`, in order to store the
2833
results from this experiment. (Path does not need to exist - it will be created if necessary.)
@@ -52,25 +57,34 @@ def __init__(self):
5257
(RunnerEvents.POPULATE_RUN_DATA, self.populate_run_data),
5358
(RunnerEvents.AFTER_EXPERIMENT , self.after_experiment )
5459
])
60+
61+
self.latest_measurement = None
5562
self.run_table_model = None # Initialized later
5663
output.console_log("Custom config loaded")
5764

5865
def create_run_table_model(self) -> RunTableModel:
5966
"""Create and return the run_table model here. A run_table is a List (rows) of tuples (columns),
6067
representing each run performed"""
61-
sampling_factor = FactorModel("sampling", [10, 50, 100, 200, 500, 1000])
68+
workers_factor = FactorModel("num_workers", [1, 2, 3, 4])
69+
write_factor = FactorModel("write_size", [256, 1024, 2048, 4096])
70+
6271
self.run_table_model = RunTableModel(
63-
factors = [sampling_factor],
64-
data_columns=['dram_energy', 'package_energy',
65-
'pp0_energy', 'pp1_energy']
72+
factors = [workers_factor, write_factor],
73+
data_columns=['timestamp', 'channel_1(A)', 'channel_2(off)', 'channel_3(off)']) # Channel 1 is in Amps
6674

67-
)
6875
return self.run_table_model
6976

7077
def before_experiment(self) -> None:
7178
"""Perform any activity required before starting the experiment here
7279
Invoked only once during the lifetime of the program."""
73-
pass
80+
81+
# Setup the picolog cm3 here (the parameters passed are also the default)
82+
self.meter = PicoCM3(sample_frequency = 1000, # Sample the CM3 every second
83+
mains_setting = 0, # Account for 50hz mains frequency
84+
# Which channels are enabled in what mode
85+
channel_settings = { CM3Channels.PLCM3_CHANNEL_1: CM3DataTypes.PLCM3_1_MILLIVOLT,
86+
CM3Channels.PLCM3_CHANNEL_2: CM3DataTypes.PLCM3_OFF,
87+
CM3Channels.PLCM3_CHANNEL_3: CM3DataTypes.PLCM3_OFF})
7488

7589
def before_run(self) -> None:
7690
"""Perform any activity required before starting a run.
@@ -81,34 +95,43 @@ def start_run(self, context: RunnerContext) -> None:
8195
"""Perform any activity required for starting the run here.
8296
For example, starting the target system to measure.
8397
Activities after starting the run should also be performed here."""
84-
pass
98+
99+
num_workers = context.run_variation['num_workers']
100+
write_size = context.run_variation['write_size']
101+
102+
# Start stress-ng
103+
stress_cmd = f"sudo stress-ng \
104+
--hdd {num_workers} \
105+
--hdd-write-size {write_size} \
106+
--hdd-ops 1000000 \
107+
--hdd-dev /dev/sda1 \
108+
--timeout 60s \
109+
--metrics-brief"
110+
111+
stress_log = open(f'{context.run_dir}/stress-ng-log.log', 'w')
112+
self.stress_ng = subprocess.Popen(shlex.split(stress_cmd), stdout=stress_log)
85113

86114
def start_measurement(self, context: RunnerContext) -> None:
87115
"""Perform any activity required for starting measurements."""
88-
sampling_interval = context.run_variation['sampling']
89-
90-
profiler_cmd = f'sudo energibridge \
91-
--interval {sampling_interval} \
92-
--max-execution 20 \
93-
--output {context.run_dir / "energibridge.csv"} \
94-
--summary \
95-
python3 examples/energibridge-profiling/primer.py'
96-
97-
#time.sleep(1) # allow the process to run a little before measuring
98-
energibridge_log = open(f'{context.run_dir}/energibridge.log', 'w')
99-
self.profiler = subprocess.Popen(shlex.split(profiler_cmd), stdout=energibridge_log)
100-
116+
117+
num_workers = context.run_variation['num_workers']
118+
write_size = context.run_variation['write_size']
119+
120+
# Start the picologs measurements here, create a unique log file for each
121+
self.latest_log = str(context.run_dir.resolve() / f'pico_run_{num_workers}_{write_size}.log')
122+
self.latest_measurement = self.meter.log(lambda: self.stress_ng.poll() != None, self.latest_log)
123+
101124
def interact(self, context: RunnerContext) -> None:
102125
"""Perform any interaction with the running target system here, or block here until the target finishes."""
103126

104-
# No interaction. We just run it for XX seconds.
105-
# Another example would be to wait for the target to finish, e.g. via `self.target.wait()`
106-
output.console_log("Running program for 20 seconds")
107-
time.sleep(20)
127+
# Wait the maximum timeout for stress-ng to finish or time.sleep(60)
128+
self.stress_ng.wait()
108129

109130
def stop_measurement(self, context: RunnerContext) -> None:
110131
"""Perform any activity here required for stopping measurements."""
111-
self.profiler.wait()
132+
133+
# Wait for stress-ng to finish
134+
self.stress_ng.wait()
112135

113136
def stop_run(self, context: RunnerContext) -> None:
114137
"""Perform any activity here required for stopping the run.
@@ -120,14 +143,29 @@ def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
120143
You can also store the raw measurement data under `context.run_dir`
121144
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
122145

123-
# energibridge.csv - Power consumption of the whole system
124-
df = pd.read_csv(context.run_dir / f"energibridge.csv")
125-
run_data = {
126-
'dram_energy' : round(df['DRAM_ENERGY (J)'].sum(), 3),
127-
'package_energy': round(df['PACKAGE_ENERGY (J)'].sum(), 3),
128-
'pp0_energy' : round(df['PP0_ENERGY (J)'].sum(), 3),
129-
'pp1_energy' : round(df['PP1_ENERGY (J)'].sum(), 3),
130-
}
146+
run_data = {k: [] for k in self.run_table_model.data_columns}
147+
148+
# Pass data through variables
149+
if self.latest_measurement != {}:
150+
for k, v in self.latest_measurement.items():
151+
run_data['timestamp'].append(k)
152+
run_data['channel_1(A)'].append(v[0][0])
153+
run_data['channel_2(off)'].append(v[1][0])
154+
run_data['channel_3(off)'].append(v[2][0])
155+
156+
# Or through a log file
157+
else:
158+
with open(self.latest_log) as f:
159+
lines = f.readlines()
160+
161+
for line in lines:
162+
channel_vals = line.split(",")
163+
164+
run_data['timestamp'].append(channel_vals[0])
165+
run_data['channel_1(A)'].append(channel_vals[1].split(" ")[0])
166+
run_data['channel_2(off)'].append(channel_vals[2].split(" ")[0])
167+
run_data['channel_3(off)'].append(channel_vals[3].split(" ")[0])
168+
131169
return run_data
132170

133171
def after_experiment(self) -> None:

experiment-runner/Plugins/Profilers/PicoCM3.py

Lines changed: 160 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,173 @@
1-
import os
21
import ctypes
2+
import datetime
3+
import time
4+
import enum
5+
from collections.abc import Callable
6+
37
from picosdk.plcm3 import plcm3
4-
from platform import uname
8+
from picosdk.functions import assert_pico_ok
9+
from picosdk.constants import PICO_STATUS
10+
11+
# Mapping of the PicoLog CM3 enums for convenience
12+
class CM3DataTypes(enum.Enum):
13+
PLCM3_OFF = 0
14+
PLCM3_1_MILLIVOLT = 1
15+
PLCM3_10_MILLIVOLTS = 2
16+
PLCM3_100_MILLIVOLTS = 3
17+
PLCM3_VOLTAGE = 4
18+
19+
class CM3Channels(enum.Enum):
20+
PLCM3_CHANNEL_1 = 1
21+
PLCM3_CHANNEL_2 = 2
22+
PLCM3_CHANNEL_3 = 3
523

24+
class CM3MainsTypes(enum.Enum):
25+
PLCM3_MAINS_50HZ = 0
26+
PLCM3_MAINS_60HZ = 1
627

728
class PicoCM3(object):
829
"""An integration of PicoTech CM3 current data logger (https://www.picotech.com/download/manuals/PicoLogCM3CurrentDataLoggerUsersGuide.pdf)"""
9-
SAMPLE_FREQUENCY = 1000 # in ms
10-
CHANNEL_SETTINGS = {
30+
def __init__(self, sample_frequency: int = None, mains_setting: int = None, channel_settings: dict[int, int] = None):
31+
# Check that the picolog driver is accessible
32+
if ctypes.util.find_library("plcm3") is None:
33+
print("No valid PicoLog CM3 driver could be found, please check LD_LIBRARY_PATH is set properly")
34+
raise RuntimeError("Driver not available")
1135

12-
}
36+
# Check if a CM3 device is present
37+
if self.list_devices() == "":
38+
print("No valid PicoLog CM3 device could be found, please ensure the device is connected")
39+
raise RuntimeError("Device not available")
40+
41+
# Some default settings
42+
self.sample_frequency = sample_frequency if sample_frequency != None else 1000 # In ms
43+
self.mains_setting = mains_setting if mains_setting != None else 0 # 50 Hz
44+
self.channel_settings = channel_settings if channel_settings != None else { # Which channels are enabled in what mode
45+
CM3Channels.PLCM3_CHANNEL_1: CM3DataTypes.PLCM3_1_MILLIVOLT,
46+
CM3Channels.PLCM3_CHANNEL_2: CM3DataTypes.PLCM3_OFF,
47+
CM3Channels.PLCM3_CHANNEL_3: CM3DataTypes.PLCM3_OFF}
1348

14-
def __init__(self):
15-
# Check if a CM3 is present
16-
pass
49+
# Apply channel and mains settings to the picolog
50+
def mode(self, handle):
51+
# Validate the channel settings
52+
if len(self.channel_settings.values()) < 3:
53+
print("All channels should have a setting")
54+
raise RuntimeError("Invalid PicoLog CM3 settings")
55+
56+
for ch, setting in self.channel_settings.items():
57+
if ch not in plcm3.PLCM3Channels.values() or \
58+
setting not in plcm3.PLCM3DataTypes.values():
59+
60+
print(f"Channel {ch} has invalid setting: {setting}")
61+
raise RuntimeError("Invalid PicoLog CM3 settings")
62+
63+
if self.mains_setting > 1 or self.mains_setting < 0:
64+
print(f"Channel {ch} has invalid setting {setting}")
65+
raise RuntimeError("Invalid PicoLog CM3 settings")
66+
67+
# Apply settings to channels
68+
for ch in range(plcm3.PLCM3Channels["PLCM3_MAX_CHANNELS"]):
69+
status = plcm3.PLCM3SetChannel(handle, ch+1, self.channel_settings[ch+1])
70+
assert_pico_ok(status)
71+
72+
# Apply mains setting
73+
status = plcm3.PLCM3SetMains(handle, ctypes.c_uint16(self.mains_setting))
74+
assert_pico_ok(status)
75+
76+
def log(self, logfile = None, dev = None, timeout: int = 60, finished_fn: Callable[[], bool] = None):
77+
log_data = {}
78+
if logfile:
79+
self.logfile = logfile
80+
81+
# Open the device
82+
handle = self.open_device()
83+
84+
# Apply channel and mains settings
85+
self.mode(handle)
86+
87+
print('Logging started successfully...')
88+
timeout_start = time.time()
89+
finished_checker = finished_fn if finished_fn != None else lambda: time.time() < timeout_start + timeout
90+
91+
# Ensure the PicoLog always gets closed
92+
try:
93+
while finished_checker():
94+
channel_data = {}
95+
# Poll every channel for data
96+
for ch in range(plcm3.PLCM3Channels["PLCM3_MAX_CHANNELS"]):
97+
ch_handle = ctypes.c_uint32(ch+1)
98+
data_handle = ctypes.c_uint32()
99+
status = plcm3.PLCM3GetValue(handle, ch_handle, ctypes.byref(data_handle))
100+
101+
if status == PICO_STATUS["PICO_NO_SAMPLES_AVAILABLE"]:
102+
channel_data[ch+1] = (0, "")
103+
else:
104+
assert_pico_ok(status)
105+
channel_data[ch+1] = self.apply_scaling(data_handle.value, self.channel_settings[ch+1])
106+
107+
log_data[datetime.datetime.now()] = [channel_data[1], channel_data[2], channel_data[3]]
108+
109+
# Ensure measurement frequency
110+
time.sleep(self.sample_frequency/1000)
111+
except:
112+
print("Error durring PicoLog CM3 data collection")
113+
finally:
114+
# Close the connection to the unit
115+
self.close_device(handle)
116+
117+
# Write all of the data to a log file (if requested)
118+
if self.logfile:
119+
with open(self.logfile,'w') as f:
120+
for t_stamp, data in log_data.items():
121+
f.write('%s, %.2f %s, %.2f %s, %.2f %s\n' %
122+
(t_stamp, data[0][0], data[0][1], data[1][0], data[1][1], data[2][0], data[2][1]))
123+
return log_data
124+
125+
def close_device(self, handle):
126+
status = plcm3.PLCM3CloseUnit(handle)
127+
assert_pico_ok(status)
128+
129+
def open_device(self, dev=None, verbose=True):
130+
if dev is not None:
131+
dev = ctypes.create_string_buffer(dev.encode("utf-8"))
132+
133+
# Open the device
134+
handle = ctypes.c_int16()
135+
status = plcm3.PLCM3OpenUnit(ctypes.byref(self.handle), dev)
136+
assert_pico_ok(status)
137+
138+
if verbose:
139+
print("Device opened: ")
140+
self.print_info(handle)
141+
142+
return handle
143+
144+
def enumerate_devices(self):
145+
details = ctypes.create_string_buffer(255)
146+
length = ctypes.c_uint32(255)
147+
com_type = ctypes.c_uint32(plcm3.PLCM3CommunicationType["PLCM3_CT_USB"])
148+
149+
status = plcm3.PLCM3Enumerate(details, ctypes.byref(length), com_type)
150+
assert_pico_ok(status)
151+
152+
return details.value.decode("utf-8")
153+
154+
def print_info(self):
155+
info_strings = ["Driver Version :",
156+
"USB Version :",
157+
"Hardware Version :",
158+
"Variant Info :",
159+
"Batch and Serial :",
160+
"Calibration Date :",
161+
"Kernel Driver Ver.:",
162+
"Mac Address :"]
17163

18-
def mode(self, runmode):
19-
pass
164+
for i, s in enumerate(info_strings):
165+
res_buf = ctypes.create_string_buffer(255)
166+
required_size = ctypes.c_int16()
20167

21-
def log(self,timeout, logfile = None):
22-
pass
168+
status = plcm3.PLCM3GetUnitInfo(self.handle, res_buf, ctypes.c_int16(255), ctypes.byref(required_size), ctypes.c_uint32(i))
169+
assert_pico_ok(status)
170+
print(f" {s}{repr(res_buf.value).decode("utf-8")}")
23171

24172
# Returns a tuple (scaled_value, unit_string)
25173
def apply_scaling(value, channel_mode):

0 commit comments

Comments
 (0)