11from pathlib import Path
22import pandas as pd
3+ import re
34from Plugins .Profilers .DataSource import CLISource , ParameterDict
45
56# Supported Paramters for the PowerJoular metrics plugin
1617class 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