Skip to content

Commit 5cc342d

Browse files
authored
Merge pull request #18 from mhkarsten/powermetrics_integration
Plugin Framework and PowerMetrics Integration
2 parents 26bdb52 + 1912211 commit 5cc342d

File tree

15 files changed

+1034
-58
lines changed

15 files changed

+1034
-58
lines changed

.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
.vagrant
2+
macos-sonoma.conf
3+
macos-sonoma/
4+
Vagrantfile
5+
Session.vim
16
.idea
27
.venv
38
.vagrant
@@ -14,7 +19,6 @@ tmp/
1419
_trial_temp/
1520
pycharm-interpreter.sh
1621
python
17-
1822
Session.vim
19-
2023
Vagrantfile
24+
scratch.py

examples/linux-powerjoular-profiling/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ pip install -r requirements.txt
2020
## Running
2121

2222
From the root directory of the repo, run the following command:
23+
NOTE: This program must be run as root, as powerjoular requires this for its use of Intel RAPL.
2324

2425
```bash
25-
python experiment-runner/ examples/linux-powerjoular-profiling/RunnerConfig.py
26+
sudo python experiment-runner/ examples/linux-powerjoular-profiling/RunnerConfig.py
2627
```
2728

2829
## Results

examples/linux-powerjoular-profiling/RunnerConfig.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
from ConfigValidator.Config.Models.OperationType import OperationType
77
from ProgressManager.Output.OutputProcedure import OutputProcedure as output
88

9+
from Plugins.Profilers.PowerJoular import PowerJoular
10+
911
from typing import Dict, List, Any, Optional
1012
from pathlib import Path
1113
from os.path import dirname, realpath
1214

13-
import os
14-
import signal
15-
import pandas as pd
1615
import time
1716
import subprocess
18-
import shlex
17+
import numpy as np
1918

2019
class RunnerConfig:
2120
ROOT_DIR = Path(dirname(realpath(__file__)))
@@ -88,16 +87,18 @@ def start_run(self, context: RunnerContext) -> None:
8887
)
8988

9089
# Configure the environment based on the current variation
91-
subprocess.check_call(shlex.split(f'cpulimit -b -p {self.target.pid} --limit {cpu_limit}'))
90+
subprocess.check_call(f'cpulimit -p {self.target.pid} --limit {cpu_limit} &', shell=True)
9291

92+
time.sleep(1) # allow the process to run a little before measuring
9393

9494
def start_measurement(self, context: RunnerContext) -> None:
9595
"""Perform any activity required for starting measurements."""
96-
97-
profiler_cmd = f'powerjoular -l -p {self.target.pid} -f {context.run_dir / "powerjoular.csv"}'
98-
99-
time.sleep(1) # allow the process to run a little before measuring
100-
self.profiler = subprocess.Popen(shlex.split(profiler_cmd))
96+
97+
# Set up the powerjoular object, provide an (optional) target and output file name
98+
self.meter = PowerJoular(target_pid=self.target.pid,
99+
out_file=context.run_dir / "powerjoular.csv")
100+
# Start measuring with powerjoular
101+
self.meter.start()
101102

102103
def interact(self, context: RunnerContext) -> None:
103104
"""Perform any interaction with the running target system here, or block here until the target finishes."""
@@ -109,30 +110,32 @@ def interact(self, context: RunnerContext) -> None:
109110

110111
def stop_measurement(self, context: RunnerContext) -> None:
111112
"""Perform any activity here required for stopping measurements."""
112-
113-
os.kill(self.profiler.pid, signal.SIGINT) # graceful shutdown of powerjoular
114-
self.profiler.wait()
113+
114+
# Stop the measurements
115+
stdout = self.meter.stop()
115116

116117
def stop_run(self, context: RunnerContext) -> None:
117118
"""Perform any activity here required for stopping the run.
118119
Activities after stopping the run should also be performed here."""
119-
120+
120121
self.target.kill()
121122
self.target.wait()
122123

123124
def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
124125
"""Parse and process any measurement data here.
125126
You can also store the raw measurement data under `context.run_dir`
126127
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
127-
128-
# powerjoular.csv - Power consumption of the whole system
129-
# powerjoular.csv-PID.csv - Power consumption of that specific process
130-
df = pd.read_csv(context.run_dir / f"powerjoular.csv-{self.target.pid}.csv")
131-
run_data = {
132-
'avg_cpu': round(df['CPU Utilization'].sum(), 3),
133-
'total_energy': round(df['CPU Power'].sum(), 3),
128+
129+
out_file = context.run_dir / "powerjoular.csv"
130+
131+
results_global = self.meter.parse_log(out_file)
132+
# If you specified a target_pid or used the -p paramter
133+
# a second csv for that target will be generated
134+
# results_process = self.meter.parse_log(self.meter.target_logfile)
135+
return {
136+
'avg_cpu': round(np.mean(list(results_global['CPU Utilization'].values())), 3),
137+
'total_energy': round(sum(list(results_global['CPU Power'].values())), 3),
134138
}
135-
return run_data
136139

137140
def after_experiment(self) -> None:
138141
"""Perform any activity required after stopping the experiment here

examples/linux-ps-profiling/RunnerConfig.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
from ConfigValidator.Config.Models.RunnerContext import RunnerContext
66
from ConfigValidator.Config.Models.OperationType import OperationType
77
from ProgressManager.Output.OutputProcedure import OutputProcedure as output
8+
from Plugins.Profilers.Ps import Ps
89

910
from typing import Dict, List, Any, Optional
1011
from pathlib import Path
1112
from os.path import dirname, realpath
1213

13-
import pandas as pd
14+
import numpy as np
1415
import time
1516
import subprocess
1617
import shlex
@@ -64,14 +65,16 @@ def create_run_table_model(self) -> RunTableModel:
6465
exclude_variations = [
6566
{cpu_limit_factor: [70], pin_core_factor: [False]} # all runs having the combination <'70', 'False'> will be excluded
6667
],
67-
data_columns=['avg_cpu']
68+
data_columns=["avg_cpu", "avg_mem"]
6869
)
6970
return self.run_table_model
7071

7172
def before_experiment(self) -> None:
7273
"""Perform any activity required before starting the experiment here
7374
Invoked only once during the lifetime of the program."""
74-
subprocess.check_call(['make'], cwd=self.ROOT_DIR) # compile
75+
76+
# compile the target program
77+
subprocess.check_call(['make'], cwd=self.ROOT_DIR)
7578

7679
def before_run(self) -> None:
7780
"""Perform any activity required before starting a run.
@@ -93,27 +96,21 @@ def start_run(self, context: RunnerContext) -> None:
9396

9497
# Configure the environment based on the current variation
9598
if pin_core:
96-
subprocess.check_call(shlex.split(f'taskset -cp 0 {self.target.pid}'))
97-
subprocess.check_call(shlex.split(f'cpulimit -b -p {self.target.pid} --limit {cpu_limit}'))
99+
subprocess.check_call(shlex.split(f'taskset -cp 0 {self.target.pid}'))
100+
101+
# Limit the targets cputime
102+
subprocess.check_call(f'cpulimit --limit={cpu_limit} -p {self.target.pid} &', shell=True)
98103

104+
time.sleep(1) # allow the process to run a little before measuring
99105

100106
def start_measurement(self, context: RunnerContext) -> None:
101107
"""Perform any activity required for starting measurements."""
102-
103-
# man 1 ps
104-
# %cpu:
105-
# cpu utilization of the process in "##.#" format. Currently, it is the CPU time used
106-
# divided by the time the process has been running (cputime/realtime ratio), expressed
107-
# as a percentage. It will not add up to 100% unless you are lucky. (alias pcpu).
108-
profiler_cmd = f'ps -p {self.target.pid} --noheader -o %cpu'
109-
wrapper_script = f'''
110-
while true; do {profiler_cmd}; sleep 1; done
111-
'''
112-
113-
time.sleep(1) # allow the process to run a little before measuring
114-
self.profiler = subprocess.Popen(['sh', '-c', wrapper_script],
115-
stdout=subprocess.PIPE, stderr=subprocess.PIPE
116-
)
108+
109+
# Set up the ps object, provide an (optional) target and output file name
110+
self.meter = Ps(out_file=context.run_dir / "ps.csv",
111+
target_pid=[self.target.pid])
112+
# Start measuring with ps
113+
self.meter.start()
117114

118115
def interact(self, context: RunnerContext) -> None:
119116
"""Perform any interaction with the running target system here, or block here until the target finishes."""
@@ -126,8 +123,8 @@ def interact(self, context: RunnerContext) -> None:
126123
def stop_measurement(self, context: RunnerContext) -> None:
127124
"""Perform any activity here required for stopping measurements."""
128125

129-
self.profiler.kill()
130-
self.profiler.wait()
126+
# Stop the measurements
127+
stdout = self.meter.stop()
131128

132129
def stop_run(self, context: RunnerContext) -> None:
133130
"""Perform any activity here required for stopping the run.
@@ -141,17 +138,13 @@ def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
141138
You can also store the raw measurement data under `context.run_dir`
142139
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
143140

144-
df = pd.DataFrame(columns=['cpu_usage'])
145-
for i, l in enumerate(self.profiler.stdout.readlines()):
146-
cpu_usage=float(l.decode('ascii').strip())
147-
df.loc[i] = [cpu_usage]
148-
149-
df.to_csv(context.run_dir / 'raw_data.csv', index=False)
141+
results = self.meter.parse_log(context.run_dir / "ps.csv",
142+
column_names=["cpu_usage", "memory_usage"])
150143

151-
run_data = {
152-
'avg_cpu': round(df['cpu_usage'].mean(), 3)
144+
return {
145+
"avg_cpu": round(np.mean(list(results['cpu_usage'].values())), 3),
146+
"avg_mem": round(np.mean(list(results['memory_usage'].values())), 3)
153147
}
154-
return run_data
155148

156149
def after_experiment(self) -> None:
157150
"""Perform any activity required after stopping the experiment here
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
# `powermetrics` profiler
3+
4+
A simple example using the OS X [powermetrics](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW10) cli tool to measure the ambient power consumtption of the system.
5+
6+
## Requirements
7+
8+
Install the requirements to run:
9+
10+
```bash
11+
pip install -r requirements.txt
12+
```
13+
14+
## Running
15+
16+
From the root directory of the repo, run the following command:
17+
NOTE: This program must be run as root, as powermetrics requires this
18+
19+
```bash
20+
sudo python experiment-runner/ examples/powermetrics-profiling/RunnerConfig.py
21+
```
22+
23+
## Results
24+
25+
The results are generated in the `examples/powermetrics-profiling/experiments` folder.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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.PowerMetrics import PowerMetrics
9+
10+
from typing import Dict, List, Any, Optional
11+
from pathlib import Path
12+
from os.path import dirname, realpath
13+
import time
14+
import numpy as np
15+
16+
class RunnerConfig:
17+
ROOT_DIR = Path(dirname(realpath(__file__)))
18+
19+
# ================================ USER SPECIFIC CONFIG ================================
20+
"""The name of the experiment."""
21+
name: str = "new_runner_experiment"
22+
23+
"""The path in which Experiment Runner will create a folder with the name `self.name`, in order to store the
24+
results from this experiment. (Path does not need to exist - it will be created if necessary.)
25+
Output path defaults to the config file's path, inside the folder 'experiments'"""
26+
results_output_path: Path = ROOT_DIR / 'experiments'
27+
28+
"""Experiment operation type. Unless you manually want to initiate each run, use `OperationType.AUTO`."""
29+
operation_type: OperationType = OperationType.AUTO
30+
31+
"""The time Experiment Runner will wait after a run completes.
32+
This can be essential to accommodate for cooldown periods on some systems."""
33+
time_between_runs_in_ms: int = 1000
34+
35+
# Dynamic configurations can be one-time satisfied here before the program takes the config as-is
36+
# e.g. Setting some variable based on some criteria
37+
def __init__(self):
38+
"""Executes immediately after program start, on config load"""
39+
40+
EventSubscriptionController.subscribe_to_multiple_events([
41+
(RunnerEvents.BEFORE_EXPERIMENT, self.before_experiment),
42+
(RunnerEvents.BEFORE_RUN , self.before_run ),
43+
(RunnerEvents.START_RUN , self.start_run ),
44+
(RunnerEvents.START_MEASUREMENT, self.start_measurement),
45+
(RunnerEvents.INTERACT , self.interact ),
46+
(RunnerEvents.STOP_MEASUREMENT , self.stop_measurement ),
47+
(RunnerEvents.STOP_RUN , self.stop_run ),
48+
(RunnerEvents.POPULATE_RUN_DATA, self.populate_run_data),
49+
(RunnerEvents.AFTER_EXPERIMENT , self.after_experiment )
50+
])
51+
self.run_table_model = None # Initialized later
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+
58+
# Create the experiment run table with factors, and desired data columns
59+
factor1 = FactorModel("test_factor", [1, 2])
60+
self.run_table_model = RunTableModel(
61+
factors = [factor1],
62+
data_columns=["joules", "avg_cpu", "avg_gpu"])
63+
64+
return self.run_table_model
65+
66+
def before_experiment(self) -> None:
67+
"""Perform any activity required before starting the experiment here
68+
Invoked only once during the lifetime of the program."""
69+
pass
70+
71+
def before_run(self) -> None:
72+
"""Perform any activity required before starting a run.
73+
No context is available here as the run is not yet active (BEFORE RUN)"""
74+
pass
75+
76+
def start_run(self, context: RunnerContext) -> None:
77+
"""Perform any activity required for starting the run here.
78+
For example, starting the target system to measure.
79+
Activities after starting the run should also be performed here."""
80+
pass
81+
82+
def start_measurement(self, context: RunnerContext) -> None:
83+
"""Perform any activity required for starting measurements."""
84+
85+
# Create the powermetrics object we will use to collect data
86+
self.meter = PowerMetrics(out_file=context.run_dir / "powermetrics.plist")
87+
# Start measuring useing powermetrics
88+
self.meter.start()
89+
90+
def interact(self, context: RunnerContext) -> None:
91+
"""Perform any interaction with the running target system here, or block here until the target finishes."""
92+
93+
# Wait (block) for a bit to collect some data
94+
time.sleep(20)
95+
96+
def stop_measurement(self, context: RunnerContext) -> None:
97+
"""Perform any activity here required for stopping measurements."""
98+
99+
# Stop measuring at the end of a run
100+
stdout = self.meter.stop()
101+
102+
def stop_run(self, context: RunnerContext) -> None:
103+
"""Perform any activity here required for stopping the run.
104+
Activities after stopping the run should also be performed here."""
105+
pass
106+
107+
def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
108+
"""Parse and process any measurement data here.
109+
You can also store the raw measurement data under `context.run_dir`
110+
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
111+
112+
# Retrieve data from run
113+
run_results = self.meter.parse_log(context.run_dir / "powermetrics.plist")
114+
115+
# Parse it as required for your experiment and add it to the run table
116+
return {
117+
"joules": sum(map(lambda x: x["processor"]["package_joules"], run_results)),
118+
"avg_cpu": np.mean(list(map(lambda x: x["processor"]["packages"][0]["cores_active_ratio"], run_results))),
119+
"avg_gpu": np.mean(list(map(lambda x: x["processor"]["packages"][0]["gpu_active_ratio"], run_results))),
120+
}
121+
122+
def after_experiment(self) -> None:
123+
"""Perform any activity required after stopping the experiment here
124+
Invoked only once during the lifetime of the program."""
125+
pass
126+
127+
# ================================ DO NOT ALTER BELOW THIS LINE ================================
128+
experiment_path: Path = None

0 commit comments

Comments
 (0)